feat: Restructure notifications - Staff and Customer separation (WIP)
## 🎯 Phase 1: Backend + Frontend Structure ### Backend Changes **NotificationsController.php:** - ✅ Added `/notifications/staff/events` endpoint - ✅ Added `/notifications/customer/events` endpoint - ✅ Created `get_all_events()` helper method - ✅ Added `recipient_type` field to all events - ✅ Filter events by recipient (staff vs customer) ### Frontend Changes **Main Notifications Page:** - ✅ Restructured to show cards for Staff, Customer, Activity Log - ✅ Entry point with clear separation - ✅ Modern card-based UI **Staff Notifications:** - ✅ Created `/settings/notifications/staff` route - ✅ Moved Channels.tsx → Staff/Channels.tsx - ✅ Moved Events.tsx → Staff/Events.tsx - ✅ Updated Staff/Events to use `/notifications/staff/events` - ✅ Fixed import paths ### Structure ``` Settings → Notifications ├── Staff Notifications (admin alerts) │ ├── Channels (Email, Push) │ ├── Events (Orders, Products, Customers) │ └── Templates └── Customer Notifications (customer emails) ├── Channels (Email, Push, SMS) ├── Events (Orders, Shipping, Account) └── Templates ``` --- **Next:** Customer notifications page + routes
This commit is contained in:
@@ -1,39 +1,116 @@
|
|||||||
import React, { useState } from 'react';
|
import React from 'react';
|
||||||
|
import { Link } from 'react-router-dom';
|
||||||
import { SettingsLayout } from './components/SettingsLayout';
|
import { SettingsLayout } from './components/SettingsLayout';
|
||||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
|
||||||
|
import { Button } from '@/components/ui/button';
|
||||||
import { __ } from '@/lib/i18n';
|
import { __ } from '@/lib/i18n';
|
||||||
import NotificationEvents from './Notifications/Events';
|
import { Bell, Users, ChevronRight, Activity } from 'lucide-react';
|
||||||
import NotificationChannels from './Notifications/Channels';
|
|
||||||
import NotificationTemplates from './Notifications/Templates';
|
|
||||||
|
|
||||||
export default function NotificationsSettings() {
|
export default function NotificationsSettings() {
|
||||||
const [activeTab, setActiveTab] = useState('events');
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SettingsLayout
|
<SettingsLayout
|
||||||
title={__('Notifications')}
|
title={__('Notifications')}
|
||||||
description={__('Manage notification events, channels, and templates')}
|
description={__('Manage staff and customer notifications')}
|
||||||
action={<div />} // Empty action to trigger contextual header
|
action={<div />} // Empty action to trigger contextual header
|
||||||
>
|
>
|
||||||
<Tabs value={activeTab} onValueChange={setActiveTab} className="space-y-6">
|
<div className="grid gap-6 md:grid-cols-2">
|
||||||
<TabsList className="grid w-full grid-cols-3">
|
{/* Staff Notifications */}
|
||||||
<TabsTrigger value="events">{__('Events')}</TabsTrigger>
|
<Card className="hover:shadow-md transition-shadow">
|
||||||
<TabsTrigger value="channels">{__('Channels')}</TabsTrigger>
|
<CardHeader>
|
||||||
<TabsTrigger value="templates">{__('Templates')}</TabsTrigger>
|
<div className="flex items-center gap-3">
|
||||||
</TabsList>
|
<div className="p-2 bg-primary/10 rounded-lg">
|
||||||
|
<Bell className="h-6 w-6 text-primary" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<CardTitle>{__('Staff Notifications')}</CardTitle>
|
||||||
|
<CardDescription>
|
||||||
|
{__('Alerts for admins and staff members')}
|
||||||
|
</CardDescription>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="space-y-4">
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
{__('Get notified about orders, low stock, new customers, and more. Configure email and push notifications for your team.')}
|
||||||
|
</p>
|
||||||
|
<div className="flex items-center justify-between pt-2">
|
||||||
|
<div className="text-sm text-muted-foreground">
|
||||||
|
{__('Orders, Products, Customers')}
|
||||||
|
</div>
|
||||||
|
<Link to="/settings/notifications/staff">
|
||||||
|
<Button variant="outline" size="sm">
|
||||||
|
{__('Configure')}
|
||||||
|
<ChevronRight className="ml-2 h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
<TabsContent value="events" className="space-y-4">
|
{/* Customer Notifications */}
|
||||||
<NotificationEvents />
|
<Card className="hover:shadow-md transition-shadow">
|
||||||
</TabsContent>
|
<CardHeader>
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<div className="p-2 bg-blue-500/10 rounded-lg">
|
||||||
|
<Users className="h-6 w-6 text-blue-500" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<CardTitle>{__('Customer Notifications')}</CardTitle>
|
||||||
|
<CardDescription>
|
||||||
|
{__('Transactional emails and updates for customers')}
|
||||||
|
</CardDescription>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="space-y-4">
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
{__('Manage order confirmations, shipping updates, account emails, and marketing messages sent to your customers.')}
|
||||||
|
</p>
|
||||||
|
<div className="flex items-center justify-between pt-2">
|
||||||
|
<div className="text-sm text-muted-foreground">
|
||||||
|
{__('Orders, Shipping, Account')}
|
||||||
|
</div>
|
||||||
|
<Link to="/settings/notifications/customer">
|
||||||
|
<Button variant="outline" size="sm">
|
||||||
|
{__('Configure')}
|
||||||
|
<ChevronRight className="ml-2 h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
<TabsContent value="channels" className="space-y-4">
|
{/* Activity Log */}
|
||||||
<NotificationChannels />
|
<Card className="hover:shadow-md transition-shadow">
|
||||||
</TabsContent>
|
<CardHeader>
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
<TabsContent value="templates" className="space-y-4">
|
<div className="p-2 bg-purple-500/10 rounded-lg">
|
||||||
<NotificationTemplates />
|
<Activity className="h-6 w-6 text-purple-500" />
|
||||||
</TabsContent>
|
</div>
|
||||||
</Tabs>
|
<div>
|
||||||
|
<CardTitle>{__('Activity Log')}</CardTitle>
|
||||||
|
<CardDescription>
|
||||||
|
{__('View notification history and activities')}
|
||||||
|
</CardDescription>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="space-y-4">
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
{__('Track all notification activities, view delivery status, and monitor system events.')}
|
||||||
|
</p>
|
||||||
|
<div className="flex items-center justify-between pt-2">
|
||||||
|
<div className="text-sm text-muted-foreground">
|
||||||
|
{__('Coming soon')}
|
||||||
|
</div>
|
||||||
|
<Button variant="outline" size="sm" disabled>
|
||||||
|
{__('View Log')}
|
||||||
|
<ChevronRight className="ml-2 h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
</SettingsLayout>
|
</SettingsLayout>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
49
admin-spa/src/routes/Settings/Notifications/Staff.tsx
Normal file
49
admin-spa/src/routes/Settings/Notifications/Staff.tsx
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
import React, { useState } from 'react';
|
||||||
|
import { Link } from 'react-router-dom';
|
||||||
|
import { SettingsLayout } from '../components/SettingsLayout';
|
||||||
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
||||||
|
import { Button } from '@/components/ui/button';
|
||||||
|
import { __ } from '@/lib/i18n';
|
||||||
|
import { ChevronLeft } from 'lucide-react';
|
||||||
|
import StaffChannels from './Staff/Channels';
|
||||||
|
import StaffEvents from './Staff/Events';
|
||||||
|
import NotificationTemplates from './Templates';
|
||||||
|
|
||||||
|
export default function StaffNotifications() {
|
||||||
|
const [activeTab, setActiveTab] = useState('channels');
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SettingsLayout
|
||||||
|
title={__('Staff Notifications')}
|
||||||
|
description={__('Configure notifications for admins and staff members')}
|
||||||
|
action={
|
||||||
|
<Link to="/settings/notifications">
|
||||||
|
<Button variant="ghost" size="sm">
|
||||||
|
<ChevronLeft className="mr-2 h-4 w-4" />
|
||||||
|
{__('Back to Notifications')}
|
||||||
|
</Button>
|
||||||
|
</Link>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Tabs value={activeTab} onValueChange={setActiveTab} className="space-y-6">
|
||||||
|
<TabsList className="grid w-full grid-cols-3">
|
||||||
|
<TabsTrigger value="channels">{__('Channels')}</TabsTrigger>
|
||||||
|
<TabsTrigger value="events">{__('Events')}</TabsTrigger>
|
||||||
|
<TabsTrigger value="templates">{__('Templates')}</TabsTrigger>
|
||||||
|
</TabsList>
|
||||||
|
|
||||||
|
<TabsContent value="channels" className="space-y-4">
|
||||||
|
<StaffChannels />
|
||||||
|
</TabsContent>
|
||||||
|
|
||||||
|
<TabsContent value="events" className="space-y-4">
|
||||||
|
<StaffEvents />
|
||||||
|
</TabsContent>
|
||||||
|
|
||||||
|
<TabsContent value="templates" className="space-y-4">
|
||||||
|
<NotificationTemplates recipientType="staff" />
|
||||||
|
</TabsContent>
|
||||||
|
</Tabs>
|
||||||
|
</SettingsLayout>
|
||||||
|
);
|
||||||
|
}
|
||||||
366
admin-spa/src/routes/Settings/Notifications/Staff/Channels.tsx
Normal file
366
admin-spa/src/routes/Settings/Notifications/Staff/Channels.tsx
Normal file
@@ -0,0 +1,366 @@
|
|||||||
|
import React, { useState, useEffect } from 'react';
|
||||||
|
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||||
|
import { api } from '@/lib/api';
|
||||||
|
import { SettingsCard } from '../components/SettingsCard';
|
||||||
|
import { Badge } from '@/components/ui/badge';
|
||||||
|
import { Button } from '@/components/ui/button';
|
||||||
|
import { Switch } from '@/components/ui/switch';
|
||||||
|
import { RefreshCw, Mail, MessageCircle, Send, Bell, ExternalLink, Settings, Check, X } from 'lucide-react';
|
||||||
|
import { toast } from 'sonner';
|
||||||
|
import { __ } from '@/lib/i18n';
|
||||||
|
import ChannelConfig from './ChannelConfig';
|
||||||
|
|
||||||
|
interface NotificationChannel {
|
||||||
|
id: string;
|
||||||
|
label: string;
|
||||||
|
icon: string;
|
||||||
|
enabled: boolean;
|
||||||
|
builtin?: boolean;
|
||||||
|
addon?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper function to convert VAPID key
|
||||||
|
function urlBase64ToUint8Array(base64String: string) {
|
||||||
|
const padding = '='.repeat((4 - (base64String.length % 4)) % 4);
|
||||||
|
const base64 = (base64String + padding).replace(/\-/g, '+').replace(/_/g, '/');
|
||||||
|
const rawData = window.atob(base64);
|
||||||
|
const outputArray = new Uint8Array(rawData.length);
|
||||||
|
for (let i = 0; i < rawData.length; ++i) {
|
||||||
|
outputArray[i] = rawData.charCodeAt(i);
|
||||||
|
}
|
||||||
|
return outputArray;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function NotificationChannels() {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
const [pushSubscribed, setPushSubscribed] = useState(false);
|
||||||
|
const [pushSupported, setPushSupported] = useState(false);
|
||||||
|
const [configOpen, setConfigOpen] = useState(false);
|
||||||
|
const [selectedChannel, setSelectedChannel] = useState<NotificationChannel | null>(null);
|
||||||
|
|
||||||
|
// Fetch channels
|
||||||
|
const { data: channels, isLoading } = useQuery({
|
||||||
|
queryKey: ['notification-channels'],
|
||||||
|
queryFn: () => api.get('/notifications/channels'),
|
||||||
|
});
|
||||||
|
|
||||||
|
// Toggle channel mutation
|
||||||
|
const toggleChannelMutation = useMutation({
|
||||||
|
mutationFn: async ({ channelId, enabled }: { channelId: string; enabled: boolean }) => {
|
||||||
|
const response = await api.post('/notifications/channels/toggle', { channelId, enabled });
|
||||||
|
return response;
|
||||||
|
},
|
||||||
|
onSuccess: (data, variables) => {
|
||||||
|
// Update cache with server response
|
||||||
|
queryClient.setQueryData(['notification-channels'], (old: any) => {
|
||||||
|
if (!old) return old;
|
||||||
|
return old.map((channel: any) =>
|
||||||
|
channel.id === variables.channelId
|
||||||
|
? { ...channel, enabled: data.enabled }
|
||||||
|
: channel
|
||||||
|
);
|
||||||
|
});
|
||||||
|
toast.success(__('Channel updated'));
|
||||||
|
},
|
||||||
|
onError: (error: any) => {
|
||||||
|
// Refetch on error to sync with server
|
||||||
|
queryClient.invalidateQueries({ queryKey: ['notification-channels'] });
|
||||||
|
toast.error(error?.message || __('Failed to update channel'));
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Check push notification support
|
||||||
|
useEffect(() => {
|
||||||
|
if ('Notification' in window && 'serviceWorker' in navigator && 'PushManager' in window) {
|
||||||
|
setPushSupported(true);
|
||||||
|
// Check if already subscribed
|
||||||
|
checkPushSubscription();
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const checkPushSubscription = async () => {
|
||||||
|
try {
|
||||||
|
const registration = await navigator.serviceWorker.ready;
|
||||||
|
const subscription = await registration.pushManager.getSubscription();
|
||||||
|
setPushSubscribed(!!subscription);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error checking push subscription:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const subscribeToPush = useMutation({
|
||||||
|
mutationFn: async () => {
|
||||||
|
// Request notification permission
|
||||||
|
const permission = await Notification.requestPermission();
|
||||||
|
if (permission !== 'granted') {
|
||||||
|
throw new Error('Notification permission denied');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get VAPID public key
|
||||||
|
const { publicKey } = await api.get('/notifications/push/vapid-key');
|
||||||
|
|
||||||
|
// Register service worker if not already registered
|
||||||
|
let registration = await navigator.serviceWorker.getRegistration();
|
||||||
|
if (!registration) {
|
||||||
|
// For now, we'll wait for service worker to be registered elsewhere
|
||||||
|
// In production, you'd register it here
|
||||||
|
registration = await navigator.serviceWorker.ready;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Subscribe to push
|
||||||
|
const subscription = await registration.pushManager.subscribe({
|
||||||
|
userVisibleOnly: true,
|
||||||
|
applicationServerKey: urlBase64ToUint8Array(publicKey),
|
||||||
|
});
|
||||||
|
|
||||||
|
// Send subscription to server
|
||||||
|
await api.post('/notifications/push/subscribe', {
|
||||||
|
subscription: subscription.toJSON(),
|
||||||
|
});
|
||||||
|
|
||||||
|
return subscription;
|
||||||
|
},
|
||||||
|
onSuccess: () => {
|
||||||
|
setPushSubscribed(true);
|
||||||
|
toast.success(__('Push notifications enabled'));
|
||||||
|
},
|
||||||
|
onError: (error: any) => {
|
||||||
|
console.error('Push subscription error:', error);
|
||||||
|
toast.error(error?.message || __('Failed to enable push notifications'));
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const unsubscribeFromPush = useMutation({
|
||||||
|
mutationFn: async () => {
|
||||||
|
const registration = await navigator.serviceWorker.ready;
|
||||||
|
const subscription = await registration.pushManager.getSubscription();
|
||||||
|
if (subscription) {
|
||||||
|
await subscription.unsubscribe();
|
||||||
|
// Notify server
|
||||||
|
await api.post('/notifications/push/unsubscribe', {
|
||||||
|
subscriptionId: btoa(JSON.stringify(subscription.toJSON())),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onSuccess: () => {
|
||||||
|
setPushSubscribed(false);
|
||||||
|
toast.success(__('Push notifications disabled'));
|
||||||
|
},
|
||||||
|
onError: (error: any) => {
|
||||||
|
toast.error(error?.message || __('Failed to disable push notifications'));
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const getChannelIcon = (channelId: string) => {
|
||||||
|
switch (channelId) {
|
||||||
|
case 'email':
|
||||||
|
return <Mail className="h-5 w-5" />;
|
||||||
|
case 'whatsapp':
|
||||||
|
return <MessageCircle className="h-5 w-5" />;
|
||||||
|
case 'telegram':
|
||||||
|
return <Send className="h-5 w-5" />;
|
||||||
|
default:
|
||||||
|
return <Bell className="h-5 w-5" />;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (isLoading) {
|
||||||
|
return (
|
||||||
|
<div className="flex items-center justify-center py-12">
|
||||||
|
<RefreshCw className="h-6 w-6 animate-spin text-muted-foreground" />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const builtinChannels = channels?.filter((c: NotificationChannel) => c.builtin) || [];
|
||||||
|
const addonChannels = channels?.filter((c: NotificationChannel) => !c.builtin) || [];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-6">
|
||||||
|
{/* Info Card */}
|
||||||
|
<SettingsCard
|
||||||
|
title={__('Notification Channels')}
|
||||||
|
description={__('Configure how notifications are sent')}
|
||||||
|
>
|
||||||
|
<div className="text-sm space-y-3">
|
||||||
|
<p className="text-muted-foreground">
|
||||||
|
{__(
|
||||||
|
'Channels are the different ways notifications can be sent. Email is built-in and always available. Install addons to enable additional channels like WhatsApp, Telegram, SMS, and more.'
|
||||||
|
)}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</SettingsCard>
|
||||||
|
|
||||||
|
{/* All Channels */}
|
||||||
|
<SettingsCard title={__('Channels')} description={__('Manage notification delivery channels')}>
|
||||||
|
<div className="space-y-4">
|
||||||
|
{builtinChannels.map((channel: NotificationChannel) => (
|
||||||
|
<div key={channel.id} className="flex flex-col sm:flex-row sm:items-center gap-4 p-4 rounded-lg border bg-card">
|
||||||
|
<div className="flex items-start gap-3 flex-1 min-w-0">
|
||||||
|
<div className={`p-3 rounded-lg shrink-0 ${channel.enabled ? 'bg-green-500/20 text-green-600' : 'bg-muted text-muted-foreground'}`}>
|
||||||
|
{getChannelIcon(channel.id)}
|
||||||
|
</div>
|
||||||
|
<div className="flex-1 min-w-0">
|
||||||
|
<div className="flex items-center gap-2 mb-2">
|
||||||
|
<h3 className="font-medium">{channel.label}</h3>
|
||||||
|
<Badge variant="secondary" className="text-xs">
|
||||||
|
{__('Built-in')}
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
{channel.id === 'email' &&
|
||||||
|
__('Email notifications powered by WooCommerce. Configure templates and SMTP settings.')}
|
||||||
|
{channel.id === 'push' &&
|
||||||
|
__('Browser push notifications for real-time updates. Perfect for PWA.')}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col sm:flex-row items-stretch sm:items-center gap-2 sm:gap-2">
|
||||||
|
{/* Channel Enable/Disable Toggle */}
|
||||||
|
<div className="flex items-center justify-between sm:justify-start gap-2 p-2 sm:p-0 rounded-lg sm:rounded-none border sm:border-0">
|
||||||
|
<span className="text-sm text-muted-foreground">
|
||||||
|
{channel.enabled ? __('Enabled') : __('Disabled')}
|
||||||
|
</span>
|
||||||
|
<Switch
|
||||||
|
checked={channel.enabled}
|
||||||
|
onCheckedChange={(checked) => {
|
||||||
|
toggleChannelMutation.mutate({ channelId: channel.id, enabled: checked });
|
||||||
|
// If enabling push, also subscribe
|
||||||
|
if (channel.id === 'push' && checked && pushSupported && !pushSubscribed) {
|
||||||
|
subscribeToPush.mutate();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
disabled={toggleChannelMutation.isPending}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => {
|
||||||
|
setSelectedChannel(channel);
|
||||||
|
setConfigOpen(true);
|
||||||
|
}}
|
||||||
|
className="w-full sm:w-auto"
|
||||||
|
>
|
||||||
|
<Settings className="h-4 w-4 sm:mr-2" />
|
||||||
|
<span className="sm:inline">{__('Configure')}</span>
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
{channel.id === 'push' && !pushSupported && (
|
||||||
|
<Badge variant="destructive" className="text-xs w-full sm:w-auto justify-center">
|
||||||
|
{__('Not Supported')}
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</SettingsCard>
|
||||||
|
|
||||||
|
{/* Addon Channels */}
|
||||||
|
{addonChannels.length > 0 ? (
|
||||||
|
<SettingsCard title={__('Addon Channels')} description={__('Channels provided by installed addons')}>
|
||||||
|
<div className="space-y-4">
|
||||||
|
{addonChannels.map((channel: NotificationChannel) => (
|
||||||
|
<div key={channel.id} className="flex items-center justify-between p-4 rounded-lg border bg-card">
|
||||||
|
<div className="flex items-center gap-4">
|
||||||
|
<div className="p-3 rounded-lg bg-primary/10">{getChannelIcon(channel.id)}</div>
|
||||||
|
<div>
|
||||||
|
<div className="flex items-center gap-2 mb-1">
|
||||||
|
<h3 className="font-medium">{channel.label}</h3>
|
||||||
|
<Badge variant="outline" className="text-xs">
|
||||||
|
{__('Addon')}
|
||||||
|
</Badge>
|
||||||
|
<Badge variant={channel.enabled ? 'default' : 'secondary'} className="text-xs">
|
||||||
|
{channel.enabled ? __('Active') : __('Inactive')}
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
{__('Provided by')} {channel.addon}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Button variant="outline" size="sm">
|
||||||
|
<Settings className="h-4 w-4 mr-2" />
|
||||||
|
{__('Configure')}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</SettingsCard>
|
||||||
|
) : (
|
||||||
|
<SettingsCard
|
||||||
|
title={__('Extend with Addons')}
|
||||||
|
description={__('Add more notification channels to your store')}
|
||||||
|
>
|
||||||
|
<div className="space-y-4">
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
{__(
|
||||||
|
'Install notification addons to send notifications via WhatsApp, Telegram, SMS, and more.'
|
||||||
|
)}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div className="grid gap-4 md:grid-cols-2">
|
||||||
|
{/* Example addon cards */}
|
||||||
|
<div className="p-4 rounded-lg border bg-card">
|
||||||
|
<div className="flex items-center gap-3 mb-2">
|
||||||
|
<MessageCircle className="h-5 w-5 text-green-600" />
|
||||||
|
<h4 className="font-medium">{__('WhatsApp Notifications')}</h4>
|
||||||
|
</div>
|
||||||
|
<p className="text-sm text-muted-foreground mb-3">
|
||||||
|
{__('Send order updates and notifications via WhatsApp Business API')}
|
||||||
|
</p>
|
||||||
|
<Button variant="outline" size="sm" className="w-full">
|
||||||
|
<ExternalLink className="h-4 w-4 mr-2" />
|
||||||
|
{__('View Addon')}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="p-4 rounded-lg border bg-card">
|
||||||
|
<div className="flex items-center gap-3 mb-2">
|
||||||
|
<Send className="h-5 w-5 text-blue-600" />
|
||||||
|
<h4 className="font-medium">{__('Telegram Notifications')}</h4>
|
||||||
|
</div>
|
||||||
|
<p className="text-sm text-muted-foreground mb-3">
|
||||||
|
{__('Get instant notifications in your Telegram channel or group')}
|
||||||
|
</p>
|
||||||
|
<Button variant="outline" size="sm" className="w-full">
|
||||||
|
<ExternalLink className="h-4 w-4 mr-2" />
|
||||||
|
{__('View Addon')}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="p-4 rounded-lg border bg-card">
|
||||||
|
<div className="flex items-center gap-3 mb-2">
|
||||||
|
<Bell className="h-5 w-5 text-purple-600" />
|
||||||
|
<h4 className="font-medium">{__('SMS Notifications')}</h4>
|
||||||
|
</div>
|
||||||
|
<p className="text-sm text-muted-foreground mb-3">
|
||||||
|
{__('Send SMS notifications via Twilio, Nexmo, or other providers')}
|
||||||
|
</p>
|
||||||
|
<Button variant="outline" size="sm" className="w-full">
|
||||||
|
<ExternalLink className="h-4 w-4 mr-2" />
|
||||||
|
{__('View Addon')}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</SettingsCard>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Channel Configuration Dialog */}
|
||||||
|
{selectedChannel && (
|
||||||
|
<ChannelConfig
|
||||||
|
open={configOpen}
|
||||||
|
onClose={() => {
|
||||||
|
setConfigOpen(false);
|
||||||
|
setSelectedChannel(null);
|
||||||
|
}}
|
||||||
|
channelId={selectedChannel.id}
|
||||||
|
channelLabel={selectedChannel.label}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
303
admin-spa/src/routes/Settings/Notifications/Staff/Events.tsx
Normal file
303
admin-spa/src/routes/Settings/Notifications/Staff/Events.tsx
Normal file
@@ -0,0 +1,303 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||||
|
import { api } from '@/lib/api';
|
||||||
|
import { SettingsCard } from '../../components/SettingsCard';
|
||||||
|
import { Switch } from '@/components/ui/switch';
|
||||||
|
import { Button } from '@/components/ui/button';
|
||||||
|
import { Badge } from '@/components/ui/badge';
|
||||||
|
import { RefreshCw, Mail, MessageCircle, Send, Bell } from 'lucide-react';
|
||||||
|
import { toast } from 'sonner';
|
||||||
|
import { __ } from '@/lib/i18n';
|
||||||
|
|
||||||
|
interface NotificationEvent {
|
||||||
|
id: string;
|
||||||
|
label: string;
|
||||||
|
description: string;
|
||||||
|
category: string;
|
||||||
|
enabled: boolean;
|
||||||
|
channels: {
|
||||||
|
[channelId: string]: {
|
||||||
|
enabled: boolean;
|
||||||
|
recipient: 'admin' | 'customer' | 'both';
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
interface NotificationChannel {
|
||||||
|
id: string;
|
||||||
|
label: string;
|
||||||
|
icon: string;
|
||||||
|
enabled: boolean;
|
||||||
|
builtin?: boolean;
|
||||||
|
addon?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function NotificationEvents() {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
|
// Fetch staff events
|
||||||
|
const { data: eventsData, isLoading: eventsLoading } = useQuery({
|
||||||
|
queryKey: ['notification-staff-events'],
|
||||||
|
queryFn: () => api.get('/notifications/staff/events'),
|
||||||
|
});
|
||||||
|
|
||||||
|
// Fetch channels
|
||||||
|
const { data: channels, isLoading: channelsLoading } = useQuery({
|
||||||
|
queryKey: ['notification-channels'],
|
||||||
|
queryFn: () => api.get('/notifications/channels'),
|
||||||
|
});
|
||||||
|
|
||||||
|
// Update event mutation
|
||||||
|
const updateMutation = useMutation({
|
||||||
|
mutationFn: async ({ eventId, channelId, enabled, recipient }: any) => {
|
||||||
|
return api.post('/notifications/events/update', {
|
||||||
|
eventId,
|
||||||
|
channelId,
|
||||||
|
enabled,
|
||||||
|
recipient,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
onSuccess: () => {
|
||||||
|
queryClient.invalidateQueries({ queryKey: ['notification-staff-events'] });
|
||||||
|
toast.success(__('Event settings updated'));
|
||||||
|
},
|
||||||
|
onError: (error: any) => {
|
||||||
|
toast.error(error?.message || __('Failed to update event'));
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const getChannelIcon = (channelId: string) => {
|
||||||
|
switch (channelId) {
|
||||||
|
case 'email':
|
||||||
|
return <Mail className="h-4 w-4" />;
|
||||||
|
case 'whatsapp':
|
||||||
|
return <MessageCircle className="h-4 w-4" />;
|
||||||
|
case 'telegram':
|
||||||
|
return <Send className="h-4 w-4" />;
|
||||||
|
default:
|
||||||
|
return <Bell className="h-4 w-4" />;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const toggleChannel = (eventId: string, channelId: string, currentlyEnabled: boolean) => {
|
||||||
|
updateMutation.mutate({
|
||||||
|
eventId,
|
||||||
|
channelId,
|
||||||
|
enabled: !currentlyEnabled,
|
||||||
|
recipient: 'admin', // Default recipient
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
if (eventsLoading || channelsLoading) {
|
||||||
|
return (
|
||||||
|
<div className="flex items-center justify-center py-12">
|
||||||
|
<RefreshCw className="h-6 w-6 animate-spin text-muted-foreground" />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const orderEvents = eventsData?.orders || [];
|
||||||
|
const productEvents = eventsData?.products || [];
|
||||||
|
const customerEvents = eventsData?.customers || [];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-6">
|
||||||
|
{/* Info Card */}
|
||||||
|
<SettingsCard
|
||||||
|
title={__('Notification Events')}
|
||||||
|
description={__('Configure which channels to use for each notification event')}
|
||||||
|
>
|
||||||
|
<div className="text-sm space-y-3">
|
||||||
|
<p className="text-muted-foreground">
|
||||||
|
{__(
|
||||||
|
'Choose which notification channels (Email, WhatsApp, Telegram, etc.) should be used for each event. Enable multiple channels to send notifications through different mediums.'
|
||||||
|
)}
|
||||||
|
</p>
|
||||||
|
<div className="bg-muted/50 rounded-lg p-4">
|
||||||
|
<p className="text-xs text-muted-foreground">
|
||||||
|
💡 {__('Tip: Email is always available. Install addons to enable WhatsApp, Telegram, SMS, and other channels.')}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</SettingsCard>
|
||||||
|
|
||||||
|
{/* Order Events */}
|
||||||
|
{orderEvents.length > 0 && (
|
||||||
|
<SettingsCard
|
||||||
|
title={__('Order Events')}
|
||||||
|
description={__('Notifications triggered by order status changes')}
|
||||||
|
>
|
||||||
|
<div className="space-y-6">
|
||||||
|
{orderEvents.map((event: NotificationEvent) => (
|
||||||
|
<div key={event.id} className="pb-6 border-b last:border-0">
|
||||||
|
<div className="mb-4">
|
||||||
|
<h3 className="font-medium text-sm">{event.label}</h3>
|
||||||
|
<p className="text-xs text-muted-foreground mt-1">{event.description}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Channel Selection */}
|
||||||
|
<div className="space-y-3">
|
||||||
|
{channels?.map((channel: NotificationChannel) => {
|
||||||
|
const channelEnabled = event.channels?.[channel.id]?.enabled || false;
|
||||||
|
const recipient = event.channels?.[channel.id]?.recipient || 'admin';
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={channel.id}
|
||||||
|
className="flex items-center justify-between p-3 rounded-lg border bg-card"
|
||||||
|
>
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<div className={`p-2 rounded-lg ${channelEnabled ? 'bg-green-500/20 text-green-600' : 'bg-muted text-muted-foreground'}`}>
|
||||||
|
{getChannelIcon(channel.id)}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<span className="text-sm font-medium">{channel.label}</span>
|
||||||
|
{channel.builtin && (
|
||||||
|
<Badge variant="secondary" className="text-xs">
|
||||||
|
{__('Built-in')}
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{channelEnabled && (
|
||||||
|
<span className="text-xs text-muted-foreground">
|
||||||
|
{__('Send to')}: {recipient === 'admin' ? __('Admin') : recipient === 'customer' ? __('Customer') : __('Both')}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Switch
|
||||||
|
checked={channelEnabled}
|
||||||
|
onCheckedChange={() => toggleChannel(event.id, channel.id, channelEnabled)}
|
||||||
|
disabled={!channel.enabled || updateMutation.isPending}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</SettingsCard>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Product Events */}
|
||||||
|
{productEvents.length > 0 && (
|
||||||
|
<SettingsCard
|
||||||
|
title={__('Product Events')}
|
||||||
|
description={__('Notifications about product stock levels')}
|
||||||
|
>
|
||||||
|
<div className="space-y-6">
|
||||||
|
{productEvents.map((event: NotificationEvent) => (
|
||||||
|
<div key={event.id} className="pb-6 border-b last:border-0">
|
||||||
|
<div className="mb-4">
|
||||||
|
<h3 className="font-medium text-sm">{event.label}</h3>
|
||||||
|
<p className="text-xs text-muted-foreground mt-1">{event.description}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-3">
|
||||||
|
{channels?.map((channel: NotificationChannel) => {
|
||||||
|
const channelEnabled = event.channels?.[channel.id]?.enabled || false;
|
||||||
|
const recipient = event.channels?.[channel.id]?.recipient || 'admin';
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={channel.id}
|
||||||
|
className="flex items-center justify-between p-3 rounded-lg border bg-card"
|
||||||
|
>
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<div className={`p-2 rounded-lg ${channelEnabled ? 'bg-green-500/20 text-green-600' : 'bg-muted text-muted-foreground'}`}>
|
||||||
|
{getChannelIcon(channel.id)}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<span className="text-sm font-medium">{channel.label}</span>
|
||||||
|
{channel.builtin && (
|
||||||
|
<Badge variant="secondary" className="text-xs">
|
||||||
|
{__('Built-in')}
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{channelEnabled && (
|
||||||
|
<span className="text-xs text-muted-foreground">
|
||||||
|
{__('Send to')}: {recipient === 'admin' ? __('Admin') : recipient === 'customer' ? __('Customer') : __('Both')}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Switch
|
||||||
|
checked={channelEnabled}
|
||||||
|
onCheckedChange={() => toggleChannel(event.id, channel.id, channelEnabled)}
|
||||||
|
disabled={!channel.enabled || updateMutation.isPending}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</SettingsCard>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Customer Events */}
|
||||||
|
{customerEvents.length > 0 && (
|
||||||
|
<SettingsCard
|
||||||
|
title={__('Customer Events')}
|
||||||
|
description={__('Notifications about customer activities')}
|
||||||
|
>
|
||||||
|
<div className="space-y-6">
|
||||||
|
{customerEvents.map((event: NotificationEvent) => (
|
||||||
|
<div key={event.id} className="pb-6 border-b last:border-0">
|
||||||
|
<div className="mb-4">
|
||||||
|
<h3 className="font-medium text-sm">{event.label}</h3>
|
||||||
|
<p className="text-xs text-muted-foreground mt-1">{event.description}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-3">
|
||||||
|
{channels?.map((channel: NotificationChannel) => {
|
||||||
|
const channelEnabled = event.channels?.[channel.id]?.enabled || false;
|
||||||
|
const recipient = event.channels?.[channel.id]?.recipient || 'admin';
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={channel.id}
|
||||||
|
className="flex items-center justify-between p-3 rounded-lg border bg-card"
|
||||||
|
>
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<div className={`p-2 rounded-lg ${channelEnabled ? 'bg-green-500/20 text-green-600' : 'bg-muted text-muted-foreground'}`}>
|
||||||
|
{getChannelIcon(channel.id)}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<span className="text-sm font-medium">{channel.label}</span>
|
||||||
|
{channel.builtin && (
|
||||||
|
<Badge variant="secondary" className="text-xs">
|
||||||
|
{__('Built-in')}
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{channelEnabled && (
|
||||||
|
<span className="text-xs text-muted-foreground">
|
||||||
|
{__('Send to')}: {recipient === 'admin' ? __('Admin') : recipient === 'customer' ? __('Customer') : __('Both')}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Switch
|
||||||
|
checked={channelEnabled}
|
||||||
|
onCheckedChange={() => toggleChannel(event.id, channel.id, channelEnabled)}
|
||||||
|
disabled={!channel.enabled || updateMutation.isPending}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</SettingsCard>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -147,6 +147,24 @@ class NotificationsController {
|
|||||||
'permission_callback' => [$this, 'check_permission'],
|
'permission_callback' => [$this, 'check_permission'],
|
||||||
],
|
],
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
// GET /woonoow/v1/notifications/staff/events
|
||||||
|
register_rest_route($this->namespace, '/' . $this->rest_base . '/staff/events', [
|
||||||
|
[
|
||||||
|
'methods' => 'GET',
|
||||||
|
'callback' => [$this, 'get_staff_events'],
|
||||||
|
'permission_callback' => [$this, 'check_permission'],
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
|
||||||
|
// GET /woonoow/v1/notifications/customer/events
|
||||||
|
register_rest_route($this->namespace, '/' . $this->rest_base . '/customer/events', [
|
||||||
|
[
|
||||||
|
'methods' => 'GET',
|
||||||
|
'callback' => [$this, 'get_customer_events'],
|
||||||
|
'permission_callback' => [$this, 'check_permission'],
|
||||||
|
],
|
||||||
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -290,6 +308,169 @@ class NotificationsController {
|
|||||||
return new WP_REST_Response($events, 200);
|
return new WP_REST_Response($events, 200);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get staff notification events (admin/staff recipient)
|
||||||
|
*
|
||||||
|
* @param WP_REST_Request $request Request object
|
||||||
|
* @return WP_REST_Response
|
||||||
|
*/
|
||||||
|
public function get_staff_events(WP_REST_Request $request) {
|
||||||
|
$all_events = $this->get_all_events();
|
||||||
|
|
||||||
|
// Filter events where default recipient is 'admin' or 'staff'
|
||||||
|
$staff_events = [];
|
||||||
|
foreach ($all_events as $category => $events) {
|
||||||
|
$filtered = array_filter($events, function($event) {
|
||||||
|
$first_channel = reset($event['channels']);
|
||||||
|
return in_array($first_channel['recipient'] ?? 'admin', ['admin', 'staff']);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!empty($filtered)) {
|
||||||
|
$staff_events[$category] = array_values($filtered);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return new WP_REST_Response($staff_events, 200);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get customer notification events (customer recipient)
|
||||||
|
*
|
||||||
|
* @param WP_REST_Request $request Request object
|
||||||
|
* @return WP_REST_Response
|
||||||
|
*/
|
||||||
|
public function get_customer_events(WP_REST_Request $request) {
|
||||||
|
$all_events = $this->get_all_events();
|
||||||
|
|
||||||
|
// Filter events where default recipient is 'customer'
|
||||||
|
$customer_events = [];
|
||||||
|
foreach ($all_events as $category => $events) {
|
||||||
|
$filtered = array_filter($events, function($event) {
|
||||||
|
$first_channel = reset($event['channels']);
|
||||||
|
return ($first_channel['recipient'] ?? 'admin') === 'customer';
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!empty($filtered)) {
|
||||||
|
$customer_events[$category] = array_values($filtered);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return new WP_REST_Response($customer_events, 200);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all events (internal helper)
|
||||||
|
*
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
private function get_all_events() {
|
||||||
|
// Get saved settings
|
||||||
|
$settings = get_option('woonoow_notification_settings', []);
|
||||||
|
|
||||||
|
// Define all events
|
||||||
|
$events = [
|
||||||
|
'orders' => [
|
||||||
|
[
|
||||||
|
'id' => 'order_placed',
|
||||||
|
'label' => __('Order Placed', 'woonoow'),
|
||||||
|
'description' => __('When a new order is placed', 'woonoow'),
|
||||||
|
'category' => 'orders',
|
||||||
|
'wc_email' => 'new_order',
|
||||||
|
'enabled' => true,
|
||||||
|
'recipient_type' => 'staff',
|
||||||
|
'channels' => $settings['order_placed']['channels'] ?? ['email' => ['enabled' => false, 'recipient' => 'admin'], 'push' => ['enabled' => false, 'recipient' => 'admin']],
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'id' => 'order_processing',
|
||||||
|
'label' => __('Order Processing', 'woonoow'),
|
||||||
|
'description' => __('When order status changes to processing', 'woonoow'),
|
||||||
|
'category' => 'orders',
|
||||||
|
'wc_email' => 'customer_processing_order',
|
||||||
|
'enabled' => true,
|
||||||
|
'recipient_type' => 'customer',
|
||||||
|
'channels' => $settings['order_processing']['channels'] ?? ['email' => ['enabled' => false, 'recipient' => 'customer'], 'push' => ['enabled' => false, 'recipient' => 'customer']],
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'id' => 'order_completed',
|
||||||
|
'label' => __('Order Completed', 'woonoow'),
|
||||||
|
'description' => __('When order is marked as completed', 'woonoow'),
|
||||||
|
'category' => 'orders',
|
||||||
|
'wc_email' => 'customer_completed_order',
|
||||||
|
'enabled' => true,
|
||||||
|
'recipient_type' => 'customer',
|
||||||
|
'channels' => $settings['order_completed']['channels'] ?? ['email' => ['enabled' => false, 'recipient' => 'customer'], 'push' => ['enabled' => false, 'recipient' => 'customer']],
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'id' => 'order_cancelled',
|
||||||
|
'label' => __('Order Cancelled', 'woonoow'),
|
||||||
|
'description' => __('When order is cancelled', 'woonoow'),
|
||||||
|
'category' => 'orders',
|
||||||
|
'wc_email' => 'cancelled_order',
|
||||||
|
'enabled' => true,
|
||||||
|
'recipient_type' => 'staff',
|
||||||
|
'channels' => $settings['order_cancelled']['channels'] ?? ['email' => ['enabled' => false, 'recipient' => 'admin'], 'push' => ['enabled' => false, 'recipient' => 'admin']],
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'id' => 'order_refunded',
|
||||||
|
'label' => __('Order Refunded', 'woonoow'),
|
||||||
|
'description' => __('When order is refunded', 'woonoow'),
|
||||||
|
'category' => 'orders',
|
||||||
|
'wc_email' => 'customer_refunded_order',
|
||||||
|
'enabled' => true,
|
||||||
|
'recipient_type' => 'customer',
|
||||||
|
'channels' => $settings['order_refunded']['channels'] ?? ['email' => ['enabled' => false, 'recipient' => 'customer'], 'push' => ['enabled' => false, 'recipient' => 'customer']],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
'products' => [
|
||||||
|
[
|
||||||
|
'id' => 'low_stock',
|
||||||
|
'label' => __('Low Stock Alert', 'woonoow'),
|
||||||
|
'description' => __('When product stock is low', 'woonoow'),
|
||||||
|
'category' => 'products',
|
||||||
|
'wc_email' => 'low_stock',
|
||||||
|
'enabled' => true,
|
||||||
|
'recipient_type' => 'staff',
|
||||||
|
'channels' => $settings['low_stock']['channels'] ?? ['email' => ['enabled' => false, 'recipient' => 'admin'], 'push' => ['enabled' => false, 'recipient' => 'admin']],
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'id' => 'out_of_stock',
|
||||||
|
'label' => __('Out of Stock Alert', 'woonoow'),
|
||||||
|
'description' => __('When product is out of stock', 'woonoow'),
|
||||||
|
'category' => 'products',
|
||||||
|
'wc_email' => 'no_stock',
|
||||||
|
'enabled' => true,
|
||||||
|
'recipient_type' => 'staff',
|
||||||
|
'channels' => $settings['out_of_stock']['channels'] ?? ['email' => ['enabled' => false, 'recipient' => 'admin'], 'push' => ['enabled' => false, 'recipient' => 'admin']],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
'customers' => [
|
||||||
|
[
|
||||||
|
'id' => 'new_customer',
|
||||||
|
'label' => __('New Customer', 'woonoow'),
|
||||||
|
'description' => __('When a new customer registers', 'woonoow'),
|
||||||
|
'category' => 'customers',
|
||||||
|
'wc_email' => 'customer_new_account',
|
||||||
|
'enabled' => true,
|
||||||
|
'recipient_type' => 'customer',
|
||||||
|
'channels' => $settings['new_customer']['channels'] ?? ['email' => ['enabled' => false, 'recipient' => 'customer'], 'push' => ['enabled' => false, 'recipient' => 'customer']],
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'id' => 'customer_note',
|
||||||
|
'label' => __('Customer Note Added', 'woonoow'),
|
||||||
|
'description' => __('When a note is added to customer order', 'woonoow'),
|
||||||
|
'category' => 'customers',
|
||||||
|
'wc_email' => 'customer_note',
|
||||||
|
'enabled' => true,
|
||||||
|
'recipient_type' => 'customer',
|
||||||
|
'channels' => $settings['customer_note']['channels'] ?? ['email' => ['enabled' => false, 'recipient' => 'customer'], 'push' => ['enabled' => false, 'recipient' => 'customer']],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
];
|
||||||
|
|
||||||
|
// Allow addons to add custom events
|
||||||
|
return apply_filters('woonoow_notification_events', $events);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Update event settings
|
* Update event settings
|
||||||
*
|
*
|
||||||
|
|||||||
Reference in New Issue
Block a user