feat: Newsletter system improvements and validation framework

- Fix: Marketing events now display in Staff notifications tab
- Reorganize: Move Coupons to Marketing/Coupons for better organization
- Add: Comprehensive email/phone validation with extensible filter hooks
  - Email validation with regex pattern (xxxx@xxxx.xx)
  - Phone validation with WhatsApp verification support
  - Filter hooks for external API integration (QuickEmailVerification, etc.)
- Fix: Newsletter template routes now use centralized notification email builder
- Add: Validation.php class for reusable validation logic
- Add: VALIDATION_HOOKS.md documentation with integration examples
- Add: NEWSLETTER_CAMPAIGN_PLAN.md architecture for future campaign system
- Fix: API delete method call in Newsletter.tsx (delete -> del)
- Remove: Duplicate EmailTemplates.tsx (using notification system instead)
- Update: Newsletter controller to use centralized Validation class

Breaking changes:
- Coupons routes moved from /routes/Coupons to /routes/Marketing/Coupons
- Legacy /coupons routes maintained for backward compatibility
This commit is contained in:
Dwindi Ramadhana
2025-12-26 10:59:48 +07:00
parent 0b08ddefa1
commit 0b2c8a56d6
23 changed files with 1132 additions and 232 deletions

View File

@@ -105,6 +105,7 @@ export default function NotificationEvents() {
const orderEvents = eventsData?.orders || [];
const productEvents = eventsData?.products || [];
const customerEvents = eventsData?.customers || [];
const marketingEvents = eventsData?.marketing || [];
return (
<div className="space-y-6">
@@ -340,6 +341,77 @@ export default function NotificationEvents() {
</div>
</SettingsCard>
)}
{/* Marketing Events */}
{marketingEvents.length > 0 && (
<SettingsCard
title={__('Marketing Events')}
description={__('Notifications about newsletter and marketing activities')}
>
<div className="space-y-6">
{marketingEvents.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>
<div className="flex items-center gap-2">
{channelEnabled && channel.enabled && (
<Button
variant="ghost"
size="sm"
onClick={() => openTemplateEditor(event.id, channel.id)}
className="h-8 w-8 p-0"
>
<Settings className="h-4 w-4" />
</Button>
)}
<Switch
checked={channelEnabled}
onCheckedChange={() => toggleChannel(event.id, channel.id, channelEnabled)}
disabled={!channel.enabled || updateMutation.isPending}
/>
</div>
</div>
);
})}
</div>
</div>
))}
</div>
</SettingsCard>
)}
</div>
);
}