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:
@@ -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 />} />
|
||||||
|
|
||||||
|
|||||||
49
admin-spa/src/routes/Settings/Notifications/Customer.tsx
Normal file
49
admin-spa/src/routes/Settings/Notifications/Customer.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
175
admin-spa/src/routes/Settings/Notifications/Customer/Events.tsx
Normal file
175
admin-spa/src/routes/Settings/Notifications/Customer/Events.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user