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:
dwindown
2025-11-11 19:00:52 +07:00
parent 90407dcfc8
commit 7c0605d379
5 changed files with 1001 additions and 25 deletions

View File

@@ -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 { 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 NotificationEvents from './Notifications/Events';
import NotificationChannels from './Notifications/Channels';
import NotificationTemplates from './Notifications/Templates';
import { Bell, Users, ChevronRight, Activity } from 'lucide-react';
export default function NotificationsSettings() {
const [activeTab, setActiveTab] = useState('events');
return (
<SettingsLayout
title={__('Notifications')}
description={__('Manage notification events, channels, and templates')}
description={__('Manage staff and customer notifications')}
action={<div />} // Empty action to trigger contextual header
>
<Tabs value={activeTab} onValueChange={setActiveTab} className="space-y-6">
<TabsList className="grid w-full grid-cols-3">
<TabsTrigger value="events">{__('Events')}</TabsTrigger>
<TabsTrigger value="channels">{__('Channels')}</TabsTrigger>
<TabsTrigger value="templates">{__('Templates')}</TabsTrigger>
</TabsList>
<div className="grid gap-6 md:grid-cols-2">
{/* Staff Notifications */}
<Card className="hover:shadow-md transition-shadow">
<CardHeader>
<div className="flex items-center gap-3">
<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">
<NotificationEvents />
</TabsContent>
{/* Customer Notifications */}
<Card className="hover:shadow-md transition-shadow">
<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">
<NotificationChannels />
</TabsContent>
<TabsContent value="templates" className="space-y-4">
<NotificationTemplates />
</TabsContent>
</Tabs>
{/* Activity Log */}
<Card className="hover:shadow-md transition-shadow">
<CardHeader>
<div className="flex items-center gap-3">
<div className="p-2 bg-purple-500/10 rounded-lg">
<Activity className="h-6 w-6 text-purple-500" />
</div>
<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>
);
}

View 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>
);
}

View 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>
);
}

View 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>
);
}

View File

@@ -147,6 +147,24 @@ class NotificationsController {
'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);
}
/**
* 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
*