feat: Complete Customer Notifications section

##  Customer Notifications - Complete!

### Files Created

**Customer.tsx:**
- Main Customer Notifications page
- Tabs: Channels, Events, Templates
- Back button to main Notifications page

**Customer/Events.tsx:**
- Uses `/notifications/customer/events` endpoint
- Query key: `notification-customer-events`
- Shows customer-specific events (order_processing, order_completed, etc.)
- Per-channel toggles
- Recipient display

**Customer/Channels.tsx:**
- Email channel (active, built-in)
- Push notifications (requires customer opt-in)
- SMS channel (coming soon, addon)
- Customer preferences information
- Informative descriptions

### App.tsx Updates

-  Added CustomerNotifications import
-  Registered `/settings/notifications/customer` route

### Structure Complete

```
Settings → Notifications
├── Staff Notifications 
│   ├── Channels (Email, Push)
│   ├── Events (Orders, Products, Customers)
│   └── Templates
└── Customer Notifications 
    ├── Channels (Email, Push, SMS)
    ├── Events (Orders, Account)
    └── Templates
```

---

**Status:** Both Staff and Customer sections complete! 🎉
**Next:** Test navigation and functionality
This commit is contained in:
dwindown
2025-11-11 20:12:53 +07:00
parent 031829ace4
commit 24307a0fc9
4 changed files with 386 additions and 0 deletions

View File

@@ -202,6 +202,7 @@ import SettingsCustomers from '@/routes/Settings/Customers';
import SettingsLocalPickup from '@/routes/Settings/LocalPickup'; import SettingsLocalPickup from '@/routes/Settings/LocalPickup';
import SettingsNotifications from '@/routes/Settings/Notifications'; import SettingsNotifications from '@/routes/Settings/Notifications';
import StaffNotifications from '@/routes/Settings/Notifications/Staff'; import StaffNotifications from '@/routes/Settings/Notifications/Staff';
import CustomerNotifications from '@/routes/Settings/Notifications/Customer';
import SettingsDeveloper from '@/routes/Settings/Developer'; import SettingsDeveloper from '@/routes/Settings/Developer';
import MorePage from '@/routes/More'; import MorePage from '@/routes/More';
@@ -490,6 +491,7 @@ function AppRoutes() {
<Route path="/settings/checkout" element={<SettingsIndex />} /> <Route path="/settings/checkout" element={<SettingsIndex />} />
<Route path="/settings/notifications" element={<SettingsNotifications />} /> <Route path="/settings/notifications" element={<SettingsNotifications />} />
<Route path="/settings/notifications/staff" element={<StaffNotifications />} /> <Route path="/settings/notifications/staff" element={<StaffNotifications />} />
<Route path="/settings/notifications/customer" element={<CustomerNotifications />} />
<Route path="/settings/brand" element={<SettingsIndex />} /> <Route path="/settings/brand" element={<SettingsIndex />} />
<Route path="/settings/developer" element={<SettingsDeveloper />} /> <Route path="/settings/developer" element={<SettingsDeveloper />} />

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 CustomerChannels from './Customer/Channels';
import CustomerEvents from './Customer/Events';
import NotificationTemplates from './Templates';
export default function CustomerNotifications() {
const [activeTab, setActiveTab] = useState('channels');
return (
<SettingsLayout
title={__('Customer Notifications')}
description={__('Configure notifications sent to customers')}
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">
<CustomerChannels />
</TabsContent>
<TabsContent value="events" className="space-y-4">
<CustomerEvents />
</TabsContent>
<TabsContent value="templates" className="space-y-4">
<NotificationTemplates />
</TabsContent>
</Tabs>
</SettingsLayout>
);
}

View File

@@ -0,0 +1,160 @@
import React from 'react';
import { useQuery } 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 { Alert, AlertDescription } from '@/components/ui/alert';
import { RefreshCw, Mail, Bell, MessageSquare, Info } from 'lucide-react';
import { __ } from '@/lib/i18n';
interface NotificationChannel {
id: string;
label: string;
icon: string;
enabled: boolean;
builtin?: boolean;
addon?: string;
}
export default function CustomerChannels() {
// Fetch channels
const { data: channels, isLoading } = useQuery({
queryKey: ['notification-channels'],
queryFn: () => api.get('/notifications/channels'),
});
const getChannelIcon = (channelId: string) => {
switch (channelId) {
case 'email':
return <Mail className="h-6 w-6" />;
case 'push':
return <Bell className="h-6 w-6" />;
case 'sms':
return <MessageSquare className="h-6 w-6" />;
default:
return <Bell className="h-6 w-6" />;
}
};
if (isLoading) {
return (
<div className="flex items-center justify-center p-8">
<RefreshCw className="h-6 w-6 animate-spin text-muted-foreground" />
</div>
);
}
const builtinChannels = (channels || []).filter((c: NotificationChannel) => c.builtin);
return (
<div className="space-y-6">
<Alert>
<Info className="h-4 w-4" />
<AlertDescription>
{__('Customer notifications are sent based on order status, account activities, and customer preferences. Customers can manage their notification preferences from their account page.')}
</AlertDescription>
</Alert>
<SettingsCard
title={__('Channels')}
description={__('Available notification channels for customers')}
>
<div className="space-y-4">
{/* Email Channel */}
<div className="flex items-start gap-4 p-4 border rounded-lg">
<div className="p-2 bg-primary/10 rounded-lg text-primary">
<Mail className="h-6 w-6" />
</div>
<div className="flex-1">
<div className="flex items-center gap-2 mb-1">
<h4 className="font-medium">{__('Email')}</h4>
<Badge variant="outline" className="text-xs">
{__('Built-in')}
</Badge>
<Badge variant="default" className="text-xs bg-green-600">
{__('Active')}
</Badge>
</div>
<p className="text-sm text-muted-foreground mb-3">
{__('Transactional emails sent to customers for order updates, account activities, and more.')}
</p>
<div className="text-xs text-muted-foreground">
{__('Powered by WordPress email system')}
</div>
</div>
</div>
{/* Push Notifications */}
<div className="flex items-start gap-4 p-4 border rounded-lg bg-muted/30">
<div className="p-2 bg-blue-500/10 rounded-lg text-blue-500">
<Bell className="h-6 w-6" />
</div>
<div className="flex-1">
<div className="flex items-center gap-2 mb-1">
<h4 className="font-medium">{__('Push Notifications')}</h4>
<Badge variant="outline" className="text-xs">
{__('Built-in')}
</Badge>
<Badge variant="secondary" className="text-xs">
{__('Requires opt-in')}
</Badge>
</div>
<p className="text-sm text-muted-foreground mb-3">
{__('Browser push notifications for real-time order updates. Customers must enable push notifications in their account.')}
</p>
<div className="text-xs text-muted-foreground">
{__('Customer-controlled from their account preferences')}
</div>
</div>
</div>
{/* SMS Channel (Coming Soon) */}
<div className="flex items-start gap-4 p-4 border rounded-lg bg-muted/30 opacity-60">
<div className="p-2 bg-purple-500/10 rounded-lg text-purple-500">
<MessageSquare className="h-6 w-6" />
</div>
<div className="flex-1">
<div className="flex items-center gap-2 mb-1">
<h4 className="font-medium">{__('SMS Notifications')}</h4>
<Badge variant="outline" className="text-xs">
{__('Addon')}
</Badge>
<Badge variant="secondary" className="text-xs">
{__('Coming Soon')}
</Badge>
</div>
<p className="text-sm text-muted-foreground mb-3">
{__('Send SMS notifications for critical order updates and delivery notifications.')}
</p>
<Button variant="outline" size="sm" disabled>
{__('Install SMS Addon')}
</Button>
</div>
</div>
</div>
</SettingsCard>
<SettingsCard
title={__('Customer Preferences')}
description={__('How customers control their notifications')}
>
<div className="space-y-3 text-sm">
<p className="text-muted-foreground">
{__('Customers can manage their notification preferences from:')}
</p>
<ul className="list-disc list-inside space-y-2 text-muted-foreground pl-4">
<li>{__('My Account → Notification Preferences')}</li>
<li>{__('Unsubscribe links in emails')}</li>
<li>{__('Browser push notification settings')}</li>
</ul>
<div className="pt-3 border-t">
<p className="text-xs text-muted-foreground">
{__('Note: Transactional emails (order confirmations, shipping updates) cannot be disabled by customers as they are required for order fulfillment.')}
</p>
</div>
</div>
</SettingsCard>
</div>
);
}

View File

@@ -0,0 +1,175 @@
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 CustomerEvents() {
const queryClient = useQueryClient();
// Fetch customer events
const { data: eventsData, isLoading: eventsLoading } = useQuery({
queryKey: ['notification-customer-events'],
queryFn: () => api.get('/notifications/customer/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-customer-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 'push':
return <Bell 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 handleToggle = (eventId: string, channelId: string, currentEnabled: boolean, recipient: string) => {
updateMutation.mutate({
eventId,
channelId,
enabled: !currentEnabled,
recipient,
});
};
if (eventsLoading || channelsLoading) {
return (
<div className="flex items-center justify-center p-8">
<RefreshCw className="h-6 w-6 animate-spin text-muted-foreground" />
</div>
);
}
const events = eventsData || {};
const enabledChannels = (channels || []).filter((c: NotificationChannel) => c.enabled);
return (
<div className="space-y-6">
{Object.entries(events).map(([category, categoryEvents]: [string, any]) => (
<SettingsCard
key={category}
title={category.charAt(0).toUpperCase() + category.slice(1)}
description={__(`Manage ${category} notification events for customers`)}
>
<div className="space-y-4">
{(categoryEvents as NotificationEvent[]).map((event) => (
<div key={event.id} className="border-b last:border-0 pb-4 last:pb-0">
<div className="flex items-start justify-between mb-3">
<div>
<h4 className="font-medium">{event.label}</h4>
<p className="text-sm text-muted-foreground">{event.description}</p>
</div>
</div>
<div className="space-y-2 pl-4">
{enabledChannels.map((channel: NotificationChannel) => {
const channelSettings = event.channels[channel.id];
const isEnabled = channelSettings?.enabled || false;
const recipient = channelSettings?.recipient || 'customer';
return (
<div key={channel.id} className="flex items-center justify-between py-2">
<div className="flex items-center gap-3">
<div className={isEnabled ? 'text-green-600' : '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="outline" className="text-xs">
{__('Built-in')}
</Badge>
)}
</div>
<div className="text-xs text-muted-foreground">
{__('Recipient')}: {recipient}
</div>
</div>
</div>
<Switch
checked={isEnabled}
onCheckedChange={() => handleToggle(event.id, channel.id, isEnabled, recipient)}
disabled={updateMutation.isPending}
/>
</div>
);
})}
</div>
</div>
))}
</div>
</SettingsCard>
))}
{Object.keys(events).length === 0 && (
<SettingsCard
title={__('No Events')}
description={__('No customer notification events configured')}
>
<p className="text-sm text-muted-foreground">
{__('Customer notification events will appear here once configured.')}
</p>
</SettingsCard>
)}
</div>
);
}