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:
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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user