## 🎯 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
304 lines
12 KiB
TypeScript
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>
|
|
);
|
|
}
|