Files
WooNooW/admin-spa/src/routes/Settings/Notifications/Staff/Events.tsx
dwindown 7c0605d379 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
2025-11-11 19:00:52 +07:00

304 lines
12 KiB
TypeScript

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