diff --git a/frontend/playwright-report/data/5ffa88b026d5419ae4ab64937dbb748dff0649bd.png b/frontend/playwright-report/data/5ffa88b026d5419ae4ab64937dbb748dff0649bd.png deleted file mode 100644 index 6fc8483..0000000 Binary files a/frontend/playwright-report/data/5ffa88b026d5419ae4ab64937dbb748dff0649bd.png and /dev/null differ diff --git a/frontend/playwright-report/data/bbb7e32ecf71376161c96bc2259c44bb2a19be2a.md b/frontend/playwright-report/data/bbb7e32ecf71376161c96bc2259c44bb2a19be2a.md deleted file mode 100644 index d6dfb6c..0000000 --- a/frontend/playwright-report/data/bbb7e32ecf71376161c96bc2259c44bb2a19be2a.md +++ /dev/null @@ -1,71 +0,0 @@ -# Page snapshot - -```yaml -- generic [ref=e1]: - - generic [ref=e3]: - - generic [ref=e5]: - - button "Collapse sidebar" [ref=e6]: - - img [ref=e7] - - generic [ref=e13]: - - heading "Smooth Schedule" [level=1] [ref=e14] - - paragraph [ref=e15]: superuser - - navigation [ref=e16]: - - paragraph [ref=e17]: Operations - - link "Dashboard" [ref=e18] [cursor=pointer]: - - /url: /platform/dashboard - - img [ref=e19] - - generic [ref=e24]: Dashboard - - link "Businesses" [ref=e25] [cursor=pointer]: - - /url: /platform/businesses - - img [ref=e26] - - generic [ref=e30]: Businesses - - link "Users" [ref=e31] [cursor=pointer]: - - /url: /platform/users - - img [ref=e32] - - generic [ref=e37]: Users - - link "Support" [active] [ref=e38] [cursor=pointer]: - - /url: /platform/support - - img [ref=e39] - - generic [ref=e41]: Support - - paragraph [ref=e42]: System - - link "Staff" [ref=e43] [cursor=pointer]: - - /url: /platform/staff - - img [ref=e44] - - generic [ref=e46]: Staff - - link "Platform Settings" [ref=e47] [cursor=pointer]: - - /url: /platform/settings - - img [ref=e48] - - generic [ref=e51]: Platform Settings - - generic [ref=e52]: - - link "Help" [ref=e53] [cursor=pointer]: - - /url: /help/ticketing - - img [ref=e54] - - generic [ref=e57]: Help - - link "API Docs" [ref=e58] [cursor=pointer]: - - /url: /help/api - - img [ref=e59] - - generic [ref=e62]: API Docs - - generic [ref=e63]: - - banner [ref=e64]: - - generic [ref=e66]: - - img [ref=e67] - - generic [ref=e70]: smoothschedule.com - - generic [ref=e71]: / - - generic [ref=e72]: Admin Console - - generic [ref=e73]: - - button [ref=e74]: - - img [ref=e75] - - button "Open notifications" [ref=e78]: - - img [ref=e79] - - button "Super User Superuser SU" [ref=e83]: - - generic [ref=e84]: - - paragraph [ref=e85]: Super User - - paragraph [ref=e86]: Superuser - - generic [ref=e87]: SU - - img [ref=e88] - - main [ref=e90]: - - generic [ref=e91]: - - img [ref=e92] - - paragraph [ref=e94]: Error loading tickets - - generic [ref=e95]: $0k -``` \ No newline at end of file diff --git a/frontend/playwright-report/index.html b/frontend/playwright-report/index.html index 18ccdb3..6ebf236 100644 --- a/frontend/playwright-report/index.html +++ b/frontend/playwright-report/index.html @@ -82,4 +82,4 @@ Error generating stack: `+n.message+`
- \ No newline at end of file + \ No newline at end of file diff --git a/frontend/public/logo-branding.png b/frontend/public/logo-branding.png new file mode 100644 index 0000000..66b4bcc Binary files /dev/null and b/frontend/public/logo-branding.png differ diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index b87bab8..90cac9c 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -108,11 +108,12 @@ const PluginMarketplace = React.lazy(() => import('./pages/PluginMarketplace')); const MyPlugins = React.lazy(() => import('./pages/MyPlugins')); // Import My Plugins page const CreatePlugin = React.lazy(() => import('./pages/CreatePlugin')); // Import Create Plugin page const Tasks = React.lazy(() => import('./pages/Tasks')); // Import Tasks page for scheduled plugin executions -const EmailTemplates = React.lazy(() => import('./pages/EmailTemplates')); // Import Email Templates page +const SystemEmailTemplates = React.lazy(() => import('./pages/settings/SystemEmailTemplates')); // System email templates (Puck-based) const Contracts = React.lazy(() => import('./pages/Contracts')); // Import Contracts page const ContractTemplates = React.lazy(() => import('./pages/ContractTemplates')); // Import Contract Templates page const ContractSigning = React.lazy(() => import('./pages/ContractSigning')); // Import Contract Signing page (public) const PageEditor = React.lazy(() => import('./pages/PageEditor')); // Import PageEditor +const EmailTemplateEditor = React.lazy(() => import('./pages/EmailTemplateEditor')); // Import Email Template Editor const PublicPage = React.lazy(() => import('./pages/PublicPage')); // Import PublicPage const BookingFlow = React.lazy(() => import('./pages/BookingFlow')); // Import Booking Flow const Locations = React.lazy(() => import('./pages/Locations')); // Import Locations management page @@ -802,16 +803,7 @@ const AppContent: React.FC = () => { ) } /> - - ) : ( - - ) - } - /> + {/* Email templates are now accessed via Settings > Email Templates */} } /> { ) } /> + + ) : ( + + ) + } + /> { } /> } /> } /> - } /> + } /> } /> } /> } /> diff --git a/frontend/src/components/DevQuickLogin.tsx b/frontend/src/components/DevQuickLogin.tsx index 4b61c3e..6c99933 100644 --- a/frontend/src/components/DevQuickLogin.tsx +++ b/frontend/src/components/DevQuickLogin.tsx @@ -145,12 +145,12 @@ export function DevQuickLogin({ embedded = false, filter = 'all' }: DevQuickLogi if (needsRedirect) { // Redirect to the correct subdomain - window.location.href = buildSubdomainUrl(targetSubdomain, '/'); + window.location.href = buildSubdomainUrl(targetSubdomain, '/dashboard'); return; } - // Already on correct subdomain - just reload to update auth state - window.location.reload(); + // Already on correct subdomain - navigate to dashboard + window.location.href = '/dashboard'; } catch (error: any) { console.error('Quick login failed:', error); alert(`Failed to login as ${user.label}: ${error.message || 'Unknown error'}`); diff --git a/frontend/src/components/EmailTemplateForm.tsx b/frontend/src/components/EmailTemplateForm.tsx deleted file mode 100644 index 01a2d12..0000000 --- a/frontend/src/components/EmailTemplateForm.tsx +++ /dev/null @@ -1,543 +0,0 @@ -import React, { useState, useEffect } from 'react'; -import { useTranslation } from 'react-i18next'; -import { useMutation, useQuery } from '@tanstack/react-query'; -import { - X, - Save, - Eye, - Code, - FileText, - Monitor, - Smartphone, - Plus, - AlertTriangle, - ChevronDown, - Sparkles, - Check -} from 'lucide-react'; -import api from '../api/client'; -import { EmailTemplate, EmailTemplateCategory, EmailTemplateVariableGroup } from '../types'; -import EmailTemplatePresetSelector from './EmailTemplatePresetSelector'; - -interface EmailTemplateFormProps { - template?: EmailTemplate | null; - onClose: () => void; - onSuccess: () => void; -} - -const EmailTemplateForm: React.FC = ({ - template, - onClose, - onSuccess, -}) => { - const { t } = useTranslation(); - const isEditing = !!template; - - // Form state - const [name, setName] = useState(template?.name || ''); - const [description, setDescription] = useState(template?.description || ''); - const [subject, setSubject] = useState(template?.subject || ''); - const [htmlContent, setHtmlContent] = useState(template?.htmlContent || ''); - const [textContent, setTextContent] = useState(template?.textContent || ''); - const [category, setCategory] = useState(template?.category || 'OTHER'); - - // UI state - const [activeTab, setActiveTab] = useState<'html' | 'text'>('html'); - const [editorMode, setEditorMode] = useState<'visual' | 'code'>('code'); - const [previewDevice, setPreviewDevice] = useState<'desktop' | 'mobile'>('desktop'); - const [showPreview, setShowPreview] = useState(false); - const [showVariables, setShowVariables] = useState(false); - const [showPresetSelector, setShowPresetSelector] = useState(false); - const [showTwoVersionsWarning, setShowTwoVersionsWarning] = useState(() => { - // Check localStorage to see if user has dismissed the warning - try { - return localStorage.getItem('emailTemplates_twoVersionsWarning_dismissed') !== 'true'; - } catch { - return true; - } - }); - - // Fetch available variables - const { data: variablesData } = useQuery<{ variables: EmailTemplateVariableGroup[] }>({ - queryKey: ['email-template-variables'], - queryFn: async () => { - const { data } = await api.get('/email-templates/variables/'); - return data; - }, - }); - - // Preview mutation - const previewMutation = useMutation({ - mutationFn: async () => { - const { data } = await api.post('/email-templates/preview/', { - subject, - html_content: htmlContent, - text_content: textContent, - }); - return data; - }, - }); - - // Create/Update mutation - const saveMutation = useMutation({ - mutationFn: async () => { - const payload = { - name, - description, - subject, - html_content: htmlContent, - text_content: textContent, - category, - scope: 'BUSINESS', // Business users only create business templates - }; - - if (isEditing && template) { - const { data } = await api.patch(`/email-templates/${template.id}/`, payload); - return data; - } else { - const { data } = await api.post('/email-templates/', payload); - return data; - } - }, - onSuccess: () => { - onSuccess(); - }, - }); - - const handlePreview = () => { - previewMutation.mutate(); - setShowPreview(true); - }; - - const insertVariable = (code: string) => { - if (activeTab === 'html') { - setHtmlContent(prev => prev + code); - } else if (activeTab === 'text') { - setTextContent(prev => prev + code); - } - }; - - const handlePresetSelect = (preset: any) => { - setName(preset.name); - setDescription(preset.description); - setSubject(preset.subject); - setHtmlContent(preset.html_content); - setTextContent(preset.text_content); - setShowPresetSelector(false); - }; - - const handleDismissTwoVersionsWarning = () => { - setShowTwoVersionsWarning(false); - try { - localStorage.setItem('emailTemplates_twoVersionsWarning_dismissed', 'true'); - } catch { - // Ignore localStorage errors - } - }; - - const categories: { value: EmailTemplateCategory; label: string }[] = [ - { value: 'APPOINTMENT', label: t('emailTemplates.categoryAppointment', 'Appointment') }, - { value: 'REMINDER', label: t('emailTemplates.categoryReminder', 'Reminder') }, - { value: 'CONFIRMATION', label: t('emailTemplates.categoryConfirmation', 'Confirmation') }, - { value: 'MARKETING', label: t('emailTemplates.categoryMarketing', 'Marketing') }, - { value: 'NOTIFICATION', label: t('emailTemplates.categoryNotification', 'Notification') }, - { value: 'REPORT', label: t('emailTemplates.categoryReport', 'Report') }, - { value: 'OTHER', label: t('emailTemplates.categoryOther', 'Other') }, - ]; - - const isValid = name.trim() && subject.trim() && (htmlContent.trim() || textContent.trim()); - - return ( -
-
- {/* Modal Header */} -
-

- {isEditing - ? t('emailTemplates.edit', 'Edit Template') - : t('emailTemplates.create', 'Create Template')} -

- -
- - {/* Modal Body */} -
- {/* Choose from Preset Button */} - {!isEditing && ( -
- -

- {t('emailTemplates.presetHint', 'Start with a professionally designed template and customize it to your needs')} -

-
- )} - -
- {/* Left Column - Form */} -
- {/* Name */} -
- - setName(e.target.value)} - placeholder={t('emailTemplates.namePlaceholder', 'e.g., Appointment Confirmation')} - className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-white focus:ring-2 focus:ring-brand-500 focus:border-brand-500" - /> -
- - {/* Category */} -
- - -
- - {/* Description */} -
- - setDescription(e.target.value)} - placeholder={t('emailTemplates.descriptionPlaceholder', 'Brief description of when this template is used')} - className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-white focus:ring-2 focus:ring-brand-500 focus:border-brand-500" - /> -
- - {/* Subject */} -
- - setSubject(e.target.value)} - placeholder={t('emailTemplates.subjectPlaceholder', 'e.g., Your appointment is confirmed!')} - className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-white focus:ring-2 focus:ring-brand-500 focus:border-brand-500" - /> -
- - {/* Variables Dropdown */} -
- - - {showVariables && variablesData?.variables && ( -
- {variablesData.variables.map((group) => ( -
-

- {group.category} -

-
- {group.items.map((variable) => ( - - ))} -
-
- ))} -
- )} -
- - {/* Content Tabs */} -
- {/* Info callout about HTML and Text versions */} - {showTwoVersionsWarning && ( -
-
- -
-

- {t('emailTemplates.twoVersionsRequired', 'Please edit both email versions')} -

-

- {t('emailTemplates.twoVersionsExplanation', 'Your customers will receive one of two versions of this email depending on their email client. Edit both the HTML version (rich formatting) and the Plain Text version (simple text) below. Make sure both versions include the same information so all your customers get the complete message.')} -

- -
-
-
- )} - -
-
- - -
- - {/* Editor Mode Toggle (for HTML only) */} - {activeTab === 'html' && ( -
- - -
- )} -
- - {/* Content Editor */} - {activeTab === 'html' && ( -