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