From 97e76a837b58013af581b4883c6fcdc2e7f4c495 Mon Sep 17 00:00:00 2001 From: dwindown Date: Tue, 11 Nov 2025 13:09:33 +0700 Subject: [PATCH] feat: Add template editor and push notifications infrastructure MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## ✅ Template Editor + Push Notifications ### Backend (PHP) **1. TemplateProvider** (`includes/Core/Notifications/TemplateProvider.php`) - Manages notification templates in wp_options - Default templates for all events x channels - Variable system (order, product, customer, store) - Template CRUD operations - Variable replacement engine **2. PushNotificationHandler** (`includes/Core/Notifications/PushNotificationHandler.php`) - VAPID keys generation and storage - Push subscription management - Queue system for push notifications - User-specific subscriptions - Service worker integration ready **3. NotificationsController** - Extended with: - GET /notifications/templates - List all templates - GET /notifications/templates/:eventId/:channelId - Get template - POST /notifications/templates - Save template - DELETE /notifications/templates/:eventId/:channelId - Reset to default - GET /notifications/push/vapid-key - Get VAPID public key - POST /notifications/push/subscribe - Subscribe to push - POST /notifications/push/unsubscribe - Unsubscribe **4. Push channel added to built-in channels** ### Frontend (React) **1. TemplateEditor Component** (`TemplateEditor.tsx`) - Modal dialog for editing templates - Subject + Body text editors - Variable insertion with dropdown - Click-to-insert variables - Live preview - Save and reset to default - Per event + channel customization **2. Templates Page** - Completely rewritten: - Lists all events x channels - Shows "Custom" badge for customized templates - Edit button opens template editor - Fetches templates from API - Variable reference guide - Organized by channel ### Key Features ✅ **Simple Text Editor** (not HTML builder) - Subject line - Body text with variables - Variable picker - Preview mode ✅ **Variable System** - Order variables: {order_number}, {order_total}, etc. - Customer variables: {customer_name}, {customer_email}, etc. - Product variables: {product_name}, {stock_quantity}, etc. - Store variables: {store_name}, {store_url}, etc. - Click to insert at cursor position ✅ **Push Notifications Ready** - VAPID key generation - Subscription management - Queue system - PWA integration ready - Built-in channel (alongside email) ✅ **Template Management** - Default templates for all events - Per-event, per-channel customization - Reset to default functionality - Custom badge indicator ### Default Templates Included **Email:** - Order Placed, Processing, Completed, Cancelled, Refunded - Low Stock, Out of Stock - New Customer, Customer Note **Push:** - Order Placed, Processing, Completed - Low Stock Alert ### Next Steps 1. ✅ Service worker for push notifications 2. ✅ Push subscription UI in Channels page 3. ✅ Test push notifications 4. ✅ Addon integration examples --- **Ready for testing!** 🚀 --- .../Settings/Notifications/TemplateEditor.tsx | 203 +++++++++++++ .../Settings/Notifications/Templates.tsx | 206 +++++++------ includes/Api/NotificationsController.php | 228 +++++++++++++++ includes/Core/Bootstrap.php | 2 + .../Notifications/PushNotificationHandler.php | 213 ++++++++++++++ .../Core/Notifications/TemplateProvider.php | 276 ++++++++++++++++++ 6 files changed, 1020 insertions(+), 108 deletions(-) create mode 100644 admin-spa/src/routes/Settings/Notifications/TemplateEditor.tsx create mode 100644 includes/Core/Notifications/PushNotificationHandler.php create mode 100644 includes/Core/Notifications/TemplateProvider.php diff --git a/admin-spa/src/routes/Settings/Notifications/TemplateEditor.tsx b/admin-spa/src/routes/Settings/Notifications/TemplateEditor.tsx new file mode 100644 index 0000000..07ba254 --- /dev/null +++ b/admin-spa/src/routes/Settings/Notifications/TemplateEditor.tsx @@ -0,0 +1,203 @@ +import React, { useState, useEffect } from 'react'; +import { useMutation, useQueryClient } from '@tanstack/react-query'; +import { api } from '@/lib/api'; +import { Button } from '@/components/ui/button'; +import { Input } from '@/components/ui/input'; +import { Textarea } from '@/components/ui/textarea'; +import { Label } from '@/components/ui/label'; +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from '@/components/ui/dialog'; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from '@/components/ui/select'; +import { Badge } from '@/components/ui/badge'; +import { X, Plus } from 'lucide-react'; +import { toast } from 'sonner'; +import { __ } from '@/lib/i18n'; + +interface TemplateEditorProps { + open: boolean; + onClose: () => void; + eventId: string; + channelId: string; + eventLabel: string; + channelLabel: string; + template?: any; +} + +export default function TemplateEditor({ + open, + onClose, + eventId, + channelId, + eventLabel, + channelLabel, + template: initialTemplate, +}: TemplateEditorProps) { + const queryClient = useQueryClient(); + const [subject, setSubject] = useState(''); + const [body, setBody] = useState(''); + const [variables, setVariables] = useState<{ [key: string]: string }>({}); + + useEffect(() => { + if (initialTemplate) { + setSubject(initialTemplate.subject || ''); + setBody(initialTemplate.body || ''); + setVariables(initialTemplate.variables || {}); + } + }, [initialTemplate]); + + const saveMutation = useMutation({ + mutationFn: async () => { + return api.post('/notifications/templates', { + eventId, + channelId, + subject, + body, + }); + }, + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ['notification-templates'] }); + toast.success(__('Template saved successfully')); + onClose(); + }, + onError: (error: any) => { + toast.error(error?.message || __('Failed to save template')); + }, + }); + + const resetMutation = useMutation({ + mutationFn: async () => { + return api.del(`/notifications/templates/${eventId}/${channelId}`); + }, + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ['notification-templates'] }); + toast.success(__('Template reset to default')); + onClose(); + }, + onError: (error: any) => { + toast.error(error?.message || __('Failed to reset template')); + }, + }); + + const insertVariable = (variable: string) => { + const textarea = document.querySelector('textarea[name="body"]') as HTMLTextAreaElement; + if (textarea) { + const start = textarea.selectionStart; + const end = textarea.selectionEnd; + const text = body; + const before = text.substring(0, start); + const after = text.substring(end); + const newText = before + `{${variable}}` + after; + setBody(newText); + + // Set cursor position after inserted variable + setTimeout(() => { + textarea.focus(); + textarea.setSelectionRange(start + variable.length + 2, start + variable.length + 2); + }, 0); + } + }; + + return ( + + + + + {__('Edit Template')}: {eventLabel} - {channelLabel} + + + {__('Customize the notification template. Use variables like {customer_name} to personalize messages.')} + + + +
+ {/* Subject */} +
+ + setSubject(e.target.value)} + placeholder={__('Enter notification subject')} + /> +

+ {channelId === 'email' + ? __('Email subject line') + : __('Push notification title')} +

+
+ + {/* Body */} +
+ +