From 05ebd0f2bbe8c6a82700e870534624542e818d62 Mon Sep 17 00:00:00 2001 From: poduck Date: Tue, 2 Dec 2025 01:42:38 -0500 Subject: [PATCH] feat: Email templates, bulk delete, communication credits, plan features MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add email template presets for Browse Templates tab (12 templates) - Add bulk selection and deletion for My Templates tab - Add communication credits system with Twilio integration - Add payment views for credit purchases and auto-reload - Add SMS reminder and masked calling plan permissions - Fix appointment status mapping (frontend/backend mismatch) - Clear masquerade stack on login/logout for session hygiene - Update platform settings with credit configuration - Add new migrations for Twilio and Stripe payment fields 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- .gemini/tmp/update_email_template.py | 125 ++ .idea/inspectionProfiles/Project_Default.xml | 17 + .../inspectionProfiles/profiles_settings.xml | 6 + .idea/smoothschedule2.iml | 18 + .idea/vcs.xml | 6 + email_templates/README.md | 34 + email_templates/confirmation/bold_dark.html | 72 + .../confirmation/classic_serif.html | 82 + email_templates/confirmation/modern_blue.html | 93 + .../marketing/minimalist_promo.html | 57 + .../marketing/newsletter_grid.html | 88 + .../marketing/welcome_vibrant.html | 77 + email_templates/reminder/personal_note.html | 38 + email_templates/reminder/soft_clean.html | 56 + email_templates/reminder/urgent_bold.html | 47 + email_templates/reports/monthly_data.html | 74 + .../reports/staff_leaderboard.html | 99 + email_templates/reports/weekly_cards.html | 73 + frontend/.env.development | 1 + frontend/NAVIGATION_REDESIGN_PLAN.md | 201 ++ frontend/package-lock.json | 25 + frontend/package.json | 2 + frontend/src/App.tsx | 39 +- frontend/src/components/CreditPaymentForm.tsx | 339 +++ frontend/src/components/DevQuickLogin.tsx | 3 + frontend/src/components/EmailTemplateForm.tsx | 93 +- .../EmailTemplatePresetSelector.tsx | 292 +++ frontend/src/components/Sidebar.tsx | 374 ++-- .../navigation/SidebarComponents.tsx | 281 +++ frontend/src/hooks/useAuth.ts | 6 + frontend/src/hooks/useCommunicationCredits.ts | 195 ++ frontend/src/hooks/usePlatformSettings.ts | 22 + frontend/src/layouts/SettingsLayout.tsx | 154 ++ frontend/src/pages/EmailTemplates.tsx | 567 ++++- frontend/src/pages/Settings.tsx | 981 ++++++++- .../src/pages/platform/PlatformSettings.tsx | 575 +++++- frontend/src/pages/settings/ApiSettings.tsx | 52 + .../pages/settings/AuthenticationSettings.tsx | 427 ++++ .../src/pages/settings/BillingSettings.tsx | 288 +++ .../src/pages/settings/BrandingSettings.tsx | 321 +++ .../pages/settings/CommunicationSettings.tsx | 727 +++++++ .../src/pages/settings/DomainsSettings.tsx | 333 +++ frontend/src/pages/settings/EmailSettings.tsx | 52 + .../src/pages/settings/GeneralSettings.tsx | 163 ++ .../pages/settings/ResourceTypesSettings.tsx | 292 +++ frontend/src/pages/settings/index.tsx | 24 + smoothschedule/config/settings/base.py | 1 + .../config/settings/multitenancy.py | 2 +- smoothschedule/config/urls.py | 2 + ...011_tenant_twilio_phone_number_and_more.py | 28 + .../0012_tenant_can_use_sms_reminders.py | 18 + .../migrations/0013_stripe_payment_fields.py | 83 + smoothschedule/core/models.py | 119 ++ smoothschedule/core/permissions.py | 26 +- smoothschedule/payments/urls.py | 46 + smoothschedule/payments/views.py | 1047 +++++++++- .../platform_admin/management/__init__.py | 0 .../management/commands/__init__.py | 0 .../commands/seed_subscription_plans.py | 429 ++++ ...fault_auto_reload_amount_cents_and_more.py | 58 + smoothschedule/platform_admin/models.py | 44 + .../schedule/email_template_presets.py | 1106 ++++++++++ .../commands/seed_email_templates.py | 1829 +++++++++++++++++ smoothschedule/schedule/serializers.py | 46 +- smoothschedule/schedule/views.py | 65 +- .../smoothschedule/comms_credits/__init__.py | 0 .../smoothschedule/comms_credits/admin.py | 83 + .../smoothschedule/comms_credits/apps.py | 7 + .../comms_credits/migrations/0001_initial.py | 107 + .../migrations/0002_add_stripe_customer_id.py | 18 + .../comms_credits/migrations/__init__.py | 0 .../smoothschedule/comms_credits/models.py | 532 +++++ .../smoothschedule/comms_credits/tasks.py | 418 ++++ .../smoothschedule/comms_credits/urls.py | 43 + .../smoothschedule/comms_credits/views.py | 575 ++++++ .../commands/seed_email_templates.py | 940 --------- .../0013_delete_ticketemailsettings.py | 16 + 77 files changed, 14185 insertions(+), 1394 deletions(-) create mode 100644 .gemini/tmp/update_email_template.py create mode 100644 .idea/inspectionProfiles/Project_Default.xml create mode 100644 .idea/inspectionProfiles/profiles_settings.xml create mode 100644 .idea/smoothschedule2.iml create mode 100644 .idea/vcs.xml create mode 100644 email_templates/README.md create mode 100644 email_templates/confirmation/bold_dark.html create mode 100644 email_templates/confirmation/classic_serif.html create mode 100644 email_templates/confirmation/modern_blue.html create mode 100644 email_templates/marketing/minimalist_promo.html create mode 100644 email_templates/marketing/newsletter_grid.html create mode 100644 email_templates/marketing/welcome_vibrant.html create mode 100644 email_templates/reminder/personal_note.html create mode 100644 email_templates/reminder/soft_clean.html create mode 100644 email_templates/reminder/urgent_bold.html create mode 100644 email_templates/reports/monthly_data.html create mode 100644 email_templates/reports/staff_leaderboard.html create mode 100644 email_templates/reports/weekly_cards.html create mode 100644 frontend/NAVIGATION_REDESIGN_PLAN.md create mode 100644 frontend/src/components/CreditPaymentForm.tsx create mode 100644 frontend/src/components/EmailTemplatePresetSelector.tsx create mode 100644 frontend/src/components/navigation/SidebarComponents.tsx create mode 100644 frontend/src/hooks/useCommunicationCredits.ts create mode 100644 frontend/src/layouts/SettingsLayout.tsx create mode 100644 frontend/src/pages/settings/ApiSettings.tsx create mode 100644 frontend/src/pages/settings/AuthenticationSettings.tsx create mode 100644 frontend/src/pages/settings/BillingSettings.tsx create mode 100644 frontend/src/pages/settings/BrandingSettings.tsx create mode 100644 frontend/src/pages/settings/CommunicationSettings.tsx create mode 100644 frontend/src/pages/settings/DomainsSettings.tsx create mode 100644 frontend/src/pages/settings/EmailSettings.tsx create mode 100644 frontend/src/pages/settings/GeneralSettings.tsx create mode 100644 frontend/src/pages/settings/ResourceTypesSettings.tsx create mode 100644 frontend/src/pages/settings/index.tsx create mode 100644 smoothschedule/core/migrations/0011_tenant_twilio_phone_number_and_more.py create mode 100644 smoothschedule/core/migrations/0012_tenant_can_use_sms_reminders.py create mode 100644 smoothschedule/core/migrations/0013_stripe_payment_fields.py create mode 100644 smoothschedule/platform_admin/management/__init__.py create mode 100644 smoothschedule/platform_admin/management/commands/__init__.py create mode 100644 smoothschedule/platform_admin/management/commands/seed_subscription_plans.py create mode 100644 smoothschedule/platform_admin/migrations/0010_subscriptionplan_default_auto_reload_amount_cents_and_more.py create mode 100644 smoothschedule/schedule/email_template_presets.py create mode 100644 smoothschedule/schedule/management/commands/seed_email_templates.py create mode 100644 smoothschedule/smoothschedule/comms_credits/__init__.py create mode 100644 smoothschedule/smoothschedule/comms_credits/admin.py create mode 100644 smoothschedule/smoothschedule/comms_credits/apps.py create mode 100644 smoothschedule/smoothschedule/comms_credits/migrations/0001_initial.py create mode 100644 smoothschedule/smoothschedule/comms_credits/migrations/0002_add_stripe_customer_id.py create mode 100644 smoothschedule/smoothschedule/comms_credits/migrations/__init__.py create mode 100644 smoothschedule/smoothschedule/comms_credits/models.py create mode 100644 smoothschedule/smoothschedule/comms_credits/tasks.py create mode 100644 smoothschedule/smoothschedule/comms_credits/urls.py create mode 100644 smoothschedule/smoothschedule/comms_credits/views.py delete mode 100644 smoothschedule/smoothschedule/schedule/management/commands/seed_email_templates.py create mode 100644 smoothschedule/tickets/migrations/0013_delete_ticketemailsettings.py diff --git a/.gemini/tmp/update_email_template.py b/.gemini/tmp/update_email_template.py new file mode 100644 index 0000000..bb6bddf --- /dev/null +++ b/.gemini/tmp/update_email_template.py @@ -0,0 +1,125 @@ +from schedule.models import EmailTemplate +import json + +html_content = """ + + + + + +
+ + + + + + + + + + + + + + + +
+

Appointment Confirmed

+
+

+ Hello {{CUSTOMER_NAME}}, +

+ +

+ Your appointment has been confirmed. We look forward to seeing you! +

+ + + + + + +
+ + + + + + + + + + + + + + + + + +
Service:{{SERVICE_NAME}}
Date & Time:{{EVENT_START_DATETIME}}
Duration:{{SERVICE_DURATION}} minutes
With:{{STAFF_NAME}}
+
+ + + + + + +
+ + + + +
+ View My Appointment +
+
+ +

+ If you need to reschedule or cancel, please contact us at least 24 hours in advance. +

+
+

+ {{BUSINESS_NAME}}
+ {{BUSINESS_EMAIL}} | {{BUSINESS_PHONE}} +

+
+
+ + +""" + +text_content = """ +Hello {{CUSTOMER_NAME}}, + +Your appointment has been confirmed. We look forward to seeing you! + +--- +Appointment Details: +Service: {{SERVICE_NAME}} +Date & Time: {{EVENT_START_DATETIME}} +Duration: {{SERVICE_DURATION}} minutes +With: {{STAFF_NAME}} +--- + +If you need to reschedule or cancel, please contact us at least 24 hours in advance. + +View your appointment: {{VIEW_APPOINTMENT_LINK}} + +--- +{{BUSINESS_NAME}} +{{BUSINESS_EMAIL}} | {{BUSINESS_PHONE}} +""" + +template_name = "Appointment Confirmed" # Assuming this is the name of the template to update + +try: + template = EmailTemplate.objects.get(name=template_name) + template.html_content = html_content + template.text_content = text_content + template.save() + print(f"Successfully updated template '{template_name}'.") +except EmailTemplate.DoesNotExist: + print(f"Error: Template '{template_name}' not found.") +except Exception as e: + print(f"An error occurred: {e}") diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml new file mode 100644 index 0000000..288fd26 --- /dev/null +++ b/.idea/inspectionProfiles/Project_Default.xml @@ -0,0 +1,17 @@ + + + + \ No newline at end of file diff --git a/.idea/inspectionProfiles/profiles_settings.xml b/.idea/inspectionProfiles/profiles_settings.xml new file mode 100644 index 0000000..105ce2d --- /dev/null +++ b/.idea/inspectionProfiles/profiles_settings.xml @@ -0,0 +1,6 @@ + + + + \ No newline at end of file diff --git a/.idea/smoothschedule2.iml b/.idea/smoothschedule2.iml new file mode 100644 index 0000000..f3596ce --- /dev/null +++ b/.idea/smoothschedule2.iml @@ -0,0 +1,18 @@ + + + + + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..35eb1dd --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/email_templates/README.md b/email_templates/README.md new file mode 100644 index 0000000..c87f776 --- /dev/null +++ b/email_templates/README.md @@ -0,0 +1,34 @@ +# Email Template Pack - Styles & Variations + +This directory contains a set of uniquely styled email templates. Each category offers multiple design aesthetics to suit different business brands. + +## Categories & Styles + +### 1. Appointment Confirmation (`/confirmation`) +- **Modern Blue (`modern_blue.html`)**: Clean, corporate, uses `Segoe UI`, rounded corners, and a blue hero header. Ideal for medical, tech, or professional services. +- **Classic Serif (`classic_serif.html`)**: Elegant, uses `Georgia/Times`, borders instead of shadows, warm beige background (`#faf9f6`). Perfect for law firms, salons, or luxury brands. +- **Bold Dark (`bold_dark.html`)**: High contrast, dark mode aesthetic (`#111111` background), bold typography (`Helvetica Neue`), vibrant pink accents. Great for gyms, modern barbershops, or nightlife venues. + +### 2. Appointment Reminder (`/reminder`) +- **Soft & Clean (`soft_clean.html`)**: Minimalist, uses circle imagery, ample whitespace, soft pink/rose color palette. Friendly and non-intrusive. +- **Urgent Bold (`urgent_bold.html`)**: Uses red accents and bold `Arial Black` fonts to convey urgency. "Action Required" styling to reduce no-shows. +- **Personal Note (`personal_note.html`)**: A simple, letter-style layout using serif fonts on a cream background. Feels like a handwritten note from the owner. + +### 3. Marketing / Welcome (`/marketing`) +- **Vibrant (`welcome_vibrant.html`)**: Uses a gradient top bar, bold typography, and image collages. High energy, designed to excite new customers. +- **Minimalist Promo (`minimalist_promo.html`)**: Monochromatic, fashion-forward design with a large hero image and a prominent discount code box. High impact. +- **Newsletter Grid (`newsletter_grid.html`)**: A classic multi-column layout for monthly updates, featuring a main story and secondary news items. Clean and readable. + +### 4. Reports (`/reports`) +- **Monthly Data (`monthly_data.html`)**: A utility-focused layout with a data grid, performance chart placeholder, and clean typography. Designed for clarity and readability. +- **Weekly Snapshot (`weekly_cards.html`)**: A dashboard-style dark mode email with card-based statistics (Revenue, Bookings, etc.) for quick scanning. +- **Staff Leaderboard (`staff_leaderboard.html`)**: A ranked list view with avatars and performance metrics to highlight top employees. Motivating and clear. + +## Image Assets +Templates use `https://placehold.co` for dynamic image generation to ensure immediate previewability without requiring local asset hosting. +- **Banners**: 600x200px +- **Icons**: 80x80px or 120x120px +- **Colors**: Matched to the template theme (e.g., `#4f46e5` for modern blue). + +## Usage +Copy the HTML code from the desired style file into your email sending service or SmoothSchedule template editor. Ensure all `{{TAGS}}` are replaced with actual data. diff --git a/email_templates/confirmation/bold_dark.html b/email_templates/confirmation/bold_dark.html new file mode 100644 index 0000000..03b1788 --- /dev/null +++ b/email_templates/confirmation/bold_dark.html @@ -0,0 +1,72 @@ + + + + + + Appointment Confirmed - Bold + + + + + + +
+ + + + + + + + + + + + + + + +
+ Confirmed +
+

+ Ready for you, {{CUSTOMER_NAME}}. +

+

+ Your slot is locked in. We've got everything prepared for your upcoming visit. +

+ + + + + + + + +
+

Service

+

{{APPOINTMENT_SERVICE}}

+
+

When

+

{{APPOINTMENT_DATE}}
{{APPOINTMENT_TIME}}

+
+ + + + + + +
+
+ Check-in QR +
+

Scan at front desk to check in

+
+
+

+ {{BUSINESS_NAME}} • {{BUSINESS_PHONE}} +

+
+
+ + \ No newline at end of file diff --git a/email_templates/confirmation/classic_serif.html b/email_templates/confirmation/classic_serif.html new file mode 100644 index 0000000..4842c34 --- /dev/null +++ b/email_templates/confirmation/classic_serif.html @@ -0,0 +1,82 @@ + + + + + + Appointment Confirmed - Classic + + + + + + +
+ + + + + + + + + + + + + + + +
+ Logo +
+

+ Appointment Confirmation +

+

+ Dear {{CUSTOMER_NAME}},

+ We are pleased to confirm your appointment with {{BUSINESS_NAME}}. Please review the details below. +

+ + + + + + +
+ + + + + + + + + + + + + + + + + + + + + +
Service{{APPOINTMENT_SERVICE}}
Date{{APPOINTMENT_DATE}}
Time{{APPOINTMENT_TIME}}
+ + +
+ Location +
+ +
+

+ {{BUSINESS_NAME}}
+ {{BUSINESS_PHONE}} | {{BUSINESS_EMAIL}} +

+
+
+ + \ No newline at end of file diff --git a/email_templates/confirmation/modern_blue.html b/email_templates/confirmation/modern_blue.html new file mode 100644 index 0000000..ae991b8 --- /dev/null +++ b/email_templates/confirmation/modern_blue.html @@ -0,0 +1,93 @@ + + + + + + Appointment Confirmed - Modern + + + + + + +
+ + + + + + + + + + + + + + + +
+ + Appointment Confirmed +
+

+ You're All Set, {{CUSTOMER_NAME}}! +

+

+ We are excited to see you at {{BUSINESS_NAME}}. Your appointment has been confirmed for the following time: +

+ + + + + + +
+ + + + + + + +
+

Service

+

{{APPOINTMENT_SERVICE}}

+
+ + + + + +
+

Date

+

{{APPOINTMENT_DATE}}

+
+

Time

+

{{APPOINTMENT_TIME}}

+
+
+
+ + + + + + +
+ + Manage Appointment + +
+
+ Logo +

+ {{BUSINESS_NAME}} +

+

+ {{BUSINESS_PHONE}} • {{BUSINESS_EMAIL}} +

+
+
+ + \ No newline at end of file diff --git a/email_templates/marketing/minimalist_promo.html b/email_templates/marketing/minimalist_promo.html new file mode 100644 index 0000000..ad0cc87 --- /dev/null +++ b/email_templates/marketing/minimalist_promo.html @@ -0,0 +1,57 @@ + + + + + + Marketing - Minimalist Promo + + + + + + +
+ + + + + + + + + + + + + + + +
+ Flash Sale +
+

+ limited time only +

+

+ Treat yourself to something special. For the next 48 hours, get exclusive access to our VIP booking slots and a special discount. +

+ + +
+

USE CODE:

+ VIP20 +
+ +
+ + + Claim Offer + +
+

+ {{BUSINESS_NAME}} +

+
+
+ + \ No newline at end of file diff --git a/email_templates/marketing/newsletter_grid.html b/email_templates/marketing/newsletter_grid.html new file mode 100644 index 0000000..f5cc241 --- /dev/null +++ b/email_templates/marketing/newsletter_grid.html @@ -0,0 +1,88 @@ + + + + + + Marketing - Newsletter Grid + + + + + + + + + + + +
+ View this email in your browser +
+ + + + + +
+

{{BUSINESS_NAME}} MONTHLY

+
+ + + + + + + + + +
+ Feature +
+

Introducing Our New Premium Service

+

+ We've been listening to your feedback and are excited to announce a brand new way to experience {{BUSINESS_NAME}}. Our new premium tier offers extended hours and dedicated support. +

+ Read more → +
+ + + + + + + + +
+ Staff +
+

Employee of the Month

+

+ Meet Sarah, our lead specialist who has gone above and beyond this month. +

+
+
+ Community +
+

Community Events

+

+ Join us this weekend for our local charity drive. +

+
+
+ + + + + + +
+

+ © {{TODAY}} {{BUSINESS_NAME}}. All rights reserved. +

+

+ {{BUSINESS_ADDRESS}} +

+
+
+ + \ No newline at end of file diff --git a/email_templates/marketing/welcome_vibrant.html b/email_templates/marketing/welcome_vibrant.html new file mode 100644 index 0000000..4f1dceb --- /dev/null +++ b/email_templates/marketing/welcome_vibrant.html @@ -0,0 +1,77 @@ + + + + + + Marketing - Vibrant + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + +
+ SmoothSchedule +
+

+ Welcome to the family. +

+

+ Thanks for joining {{BUSINESS_NAME}}! We're thrilled to have you on board. +

+
+ Lifestyle +
+ + + + + + +
+

Expert Staff

+

Top-tier professionals ready to serve.

+
+

Easy Booking

+

Schedule anytime, anywhere.

+
+

Best Value

+

Premium service at great rates.

+
+
+ Book Your First Visit +
+
+ + \ No newline at end of file diff --git a/email_templates/reminder/personal_note.html b/email_templates/reminder/personal_note.html new file mode 100644 index 0000000..50dac2e --- /dev/null +++ b/email_templates/reminder/personal_note.html @@ -0,0 +1,38 @@ + + + + + + Reminder - Personal Note + + + + + + +
+ + + + +
+

+ Dear {{CUSTOMER_NAME}}, +

+

+ I'm writing to confirm that we're still on for your {{APPOINTMENT_SERVICE}} tomorrow, {{APPOINTMENT_DATE}} at {{APPOINTMENT_TIME}}. +

+

+ Looking forward to our session. +

+

+ Warmly,

+ {{BUSINESS_NAME}} +

+
+

+ Reschedule or Cancel +

+
+ + \ No newline at end of file diff --git a/email_templates/reminder/soft_clean.html b/email_templates/reminder/soft_clean.html new file mode 100644 index 0000000..bd36718 --- /dev/null +++ b/email_templates/reminder/soft_clean.html @@ -0,0 +1,56 @@ + + + + + + Reminder - Soft & Clean + + + + + + +
+ + + + + + + + + + +
+ Soon +
+

+ Just a Friendly Reminder +

+

+ Hi {{CUSTOMER_NAME}}, your appointment with {{BUSINESS_NAME}} is coming up soon! +

+ +
+

{{APPOINTMENT_DATE}}

+

{{APPOINTMENT_TIME}}

+
+ +

+ Need to make changes? Reschedule here +

+
+ + + + + + +
+

+ {{BUSINESS_NAME}} +

+
+
+ + \ No newline at end of file diff --git a/email_templates/reminder/urgent_bold.html b/email_templates/reminder/urgent_bold.html new file mode 100644 index 0000000..40628e1 --- /dev/null +++ b/email_templates/reminder/urgent_bold.html @@ -0,0 +1,47 @@ + + + + + + Reminder - Urgent + + + + + + +
+ + + + + + + +
+

Action Required

+

+ Don't Forget
Your Visit. +

+ + Tomorrow + +

+ {{CUSTOMER_NAME}}, we're holding your spot for {{APPOINTMENT_SERVICE}}. +

+ + + + + +
+

{{APPOINTMENT_DATE}} @ {{APPOINTMENT_TIME}}

+
+
+

+ {{BUSINESS_NAME}} - {{BUSINESS_PHONE}} +

+
+
+ + \ No newline at end of file diff --git a/email_templates/reports/monthly_data.html b/email_templates/reports/monthly_data.html new file mode 100644 index 0000000..00e76dd --- /dev/null +++ b/email_templates/reports/monthly_data.html @@ -0,0 +1,74 @@ + + + + + + Monthly Report - Data Heavy + + + + + + +
+ + + + + + +
+

+ SmoothSchedule Report +

+
+

{{TODAY}}

+
+ + + + + + + + + +
+

Performance Summary

+ + + Chart + + + + + + + + + + + + + + + + + + + + + + + +
MetricValueChange
Total Revenue$12,450+12% ▲
Appointments142+5% ▲
New Customers28-2% ▼
+ +

+ This report was automatically generated for {{BUSINESS_NAME}}. +

+
+ View Full Report in Dashboard → +
+
+ + \ No newline at end of file diff --git a/email_templates/reports/staff_leaderboard.html b/email_templates/reports/staff_leaderboard.html new file mode 100644 index 0000000..faae9f6 --- /dev/null +++ b/email_templates/reports/staff_leaderboard.html @@ -0,0 +1,99 @@ + + + + + + Report - Staff Leaderboard + + + + + + +
+ + + + + + + + + + + + + + + +
+

Staff Performance

+

Top performers for {{TODAY}}

+
+ + + + + + + + + + + + + +
+ + + + + + +
+ Rank 1 + +

Sarah Johnson

+

32 Appointments

+
+

$3,200

+
+
+ + + + + + +
+ Rank 2 + +

Mike Chen

+

28 Appointments

+
+

$2,850

+
+
+ + + + + + +
+ Rank 3 + +

Jessica Williams

+

25 Appointments

+
+

$2,100

+
+
+
+

+ Great work team! 🚀 +

+
+
+ + \ No newline at end of file diff --git a/email_templates/reports/weekly_cards.html b/email_templates/reports/weekly_cards.html new file mode 100644 index 0000000..3ac5fbc --- /dev/null +++ b/email_templates/reports/weekly_cards.html @@ -0,0 +1,73 @@ + + + + + + Report - Weekly Snapshot + + + + + + +
+ + + + + + + + + + + + + + + +
+

Weekly Snapshot

+

Week of {{TODAY}}

+
+ + + + + + + + + + + + + + + + +
+ Revenue +

Revenue

+

$4,250

+

↑ 15% vs last week

+
+ Bookings +

Bookings

+

84

+

→ Stable

+
+ Rating +

Avg Rating

+

4.9

+
+ Cancellations +

Cancellations

+

3

+
+
+ View detailed analytics +
+
+ + \ No newline at end of file diff --git a/frontend/.env.development b/frontend/.env.development index 45351bd..65ecb62 100644 --- a/frontend/.env.development +++ b/frontend/.env.development @@ -1,2 +1,3 @@ VITE_DEV_MODE=true VITE_API_URL=http://api.lvh.me:8000 +VITE_STRIPE_PUBLISHABLE_KEY=pk_test_51SYttT4pb5kWPtNt8n52NRQLyBGbFQ52tnG1O5o11V06m3TPUyIzf6AHOpFNQErBj4m7pOwM6VzltePrdL16IFn0004YqWhRpA diff --git a/frontend/NAVIGATION_REDESIGN_PLAN.md b/frontend/NAVIGATION_REDESIGN_PLAN.md new file mode 100644 index 0000000..fbdc183 --- /dev/null +++ b/frontend/NAVIGATION_REDESIGN_PLAN.md @@ -0,0 +1,201 @@ +# Navigation Redesign Plan + +## Overview + +Redesigning both the main sidebar and settings page navigation to be more organized and scalable. + +## Current Issues + +### Main Sidebar +- 15+ items in a flat list with no grouping +- Dropdowns (Plugins, Help) hide important items +- No visual hierarchy or section headers +- Settings isolated at bottom + +### Settings Page +- 7 horizontal tabs getting crowded +- Not scalable for new settings +- No logical grouping + +--- + +## Phase 1: Refactor Main Sidebar (COMPLETED) + +### New Structure with Grouped Sections + +``` +[Logo] Business Name +subdomain.smoothschedule + +○ Dashboard +○ Scheduler +○ Tasks + +MANAGE +○ Customers +○ Services +○ Resources +○ Staff + +COMMUNICATE +○ Messages +○ Tickets + +MONEY +○ Payments + +EXTEND +○ Plugins +○ Email Templates + +────────── +○ Settings +○ Help & Docs +────────── +[User] Sign Out +``` + +### Files Created/Modified +- `src/components/navigation/SidebarComponents.tsx` - Shared components (DONE) +- `src/components/Sidebar.tsx` - Refactor to use new components (TODO) + +--- + +## Phase 2: Settings Sidebar Layout + +### New Settings Structure + +Settings becomes a sub-application with its own sidebar: + +``` +/settings +├── /general - Business name, timezone, etc. +├── /branding - Logo, colors, display mode +├── /resource-types - Resource type management +├── /domains - Custom domains +├── /api-tokens - API access tokens +├── /authentication - OAuth, social login +├── /email - Email addresses for tickets +├── /communication - SMS & calling credits +├── /billing - Subscription, credits, invoices (future) +``` + +### Settings Sidebar Layout + +``` +┌──────────────────┬──────────────────────────────────────────┐ +│ ← Back to App │ │ +│ │ [Page Title] │ +│ BUSINESS │ [Page Description] │ +│ ○ General │ │ +│ ○ Branding │ [Content Area] │ +│ ○ Resource Types │ │ +│ │ │ +│ INTEGRATIONS │ │ +│ ○ Domains │ │ +│ ○ API & Webhooks │ │ +│ │ │ +│ ACCESS │ │ +│ ○ Authentication │ │ +│ │ │ +│ COMMUNICATION │ │ +│ ○ Email Setup │ │ +│ ○ SMS & Calling │ │ +│ │ │ +│ BILLING │ │ +│ ○ Credits │ │ +│ │ │ +└──────────────────┴──────────────────────────────────────────┘ +``` + +### Files to Create +1. `src/components/navigation/SettingsSidebar.tsx` - Settings-specific sidebar +2. `src/layouts/SettingsLayout.tsx` - Layout wrapper with sidebar + content +3. Split `src/pages/Settings.tsx` into: + - `src/pages/settings/GeneralSettings.tsx` + - `src/pages/settings/BrandingSettings.tsx` + - `src/pages/settings/ResourceTypesSettings.tsx` + - `src/pages/settings/DomainsSettings.tsx` + - `src/pages/settings/ApiTokensSettings.tsx` + - `src/pages/settings/AuthenticationSettings.tsx` + - `src/pages/settings/EmailSettings.tsx` + - `src/pages/settings/CommunicationSettings.tsx` + +### Route Updates (in App.tsx or routes file) + +```tsx +}> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + +``` + +--- + +## Implementation Order + +1. ✅ Create shared sidebar components (`SidebarComponents.tsx`) +2. ✅ Refactor main `Sidebar.tsx` to use grouped sections +3. ✅ Create `SettingsLayout.tsx` (includes sidebar) +4. ⏳ Split Settings.tsx into sub-pages +5. ⬜ Update routes in App.tsx +6. ⬜ Test all navigation flows + +## Files Created So Far + +- `src/components/navigation/SidebarComponents.tsx` - Shared nav components +- `src/components/Sidebar.tsx` - Refactored with grouped sections +- `src/layouts/SettingsLayout.tsx` - Settings page wrapper with sidebar +- `src/pages/settings/` - Directory for settings sub-pages (in progress) + +--- + +## Component APIs + +### SidebarSection +```tsx + + + +``` + +### SidebarItem +```tsx + +``` + +### SettingsSidebarSection / SettingsSidebarItem +```tsx + + + +``` + +--- + +## Notes + +- Main sidebar uses white/transparent colors (gradient background) +- Settings sidebar uses gray/brand colors (white background) +- Both support collapsed state on main sidebar +- Settings sidebar is always expanded (no collapse) +- Mobile: Main sidebar becomes drawer, Settings sidebar becomes sheet/drawer diff --git a/frontend/package-lock.json b/frontend/package-lock.json index df49962..96db25a 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -12,6 +12,8 @@ "@dnd-kit/utilities": "^3.2.2", "@stripe/connect-js": "^3.3.31", "@stripe/react-connect-js": "^3.3.31", + "@stripe/react-stripe-js": "^5.4.1", + "@stripe/stripe-js": "^8.5.3", "@tanstack/react-query": "^5.90.10", "@types/react-syntax-highlighter": "^15.5.13", "axios": "^1.13.2", @@ -1474,6 +1476,29 @@ "react-dom": ">=16.8.0" } }, + "node_modules/@stripe/react-stripe-js": { + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/@stripe/react-stripe-js/-/react-stripe-js-5.4.1.tgz", + "integrity": "sha512-ipeYcAHa4EPmjwfv0lFE+YDVkOQ0TMKkFWamW+BqmnSkEln/hO8rmxGPPWcd9WjqABx6Ro8Xg4pAS7evCcR9cw==", + "license": "MIT", + "dependencies": { + "prop-types": "^15.7.2" + }, + "peerDependencies": { + "@stripe/stripe-js": ">=8.0.0 <9.0.0", + "react": ">=16.8.0 <20.0.0", + "react-dom": ">=16.8.0 <20.0.0" + } + }, + "node_modules/@stripe/stripe-js": { + "version": "8.5.3", + "resolved": "https://registry.npmjs.org/@stripe/stripe-js/-/stripe-js-8.5.3.tgz", + "integrity": "sha512-UM0GHAxlTN7v0lCK2P6t0VOlvBIdApIQxhnM3yZ2kupQ4PpSrLsK/n/NyYKtw2NJGMaNRRD1IicWS7fSL2sFtA==", + "license": "MIT", + "engines": { + "node": ">=12.16" + } + }, "node_modules/@tailwindcss/node": { "version": "4.1.17", "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.1.17.tgz", diff --git a/frontend/package.json b/frontend/package.json index 6d39ddc..5483988 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -8,6 +8,8 @@ "@dnd-kit/utilities": "^3.2.2", "@stripe/connect-js": "^3.3.31", "@stripe/react-connect-js": "^3.3.31", + "@stripe/react-stripe-js": "^5.4.1", + "@stripe/stripe-js": "^8.5.3", "@tanstack/react-query": "^5.90.10", "@types/react-syntax-highlighter": "^15.5.13", "axios": "^1.13.2", diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index e283810..c9ea1a2 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -72,6 +72,19 @@ const PluginMarketplace = React.lazy(() => import('./pages/PluginMarketplace')); const MyPlugins = React.lazy(() => import('./pages/MyPlugins')); // Import My Plugins 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 + +// Settings pages +const SettingsLayout = React.lazy(() => import('./layouts/SettingsLayout')); +const GeneralSettings = React.lazy(() => import('./pages/settings/GeneralSettings')); +const BrandingSettings = React.lazy(() => import('./pages/settings/BrandingSettings')); +const ResourceTypesSettings = React.lazy(() => import('./pages/settings/ResourceTypesSettings')); +const DomainsSettings = React.lazy(() => import('./pages/settings/DomainsSettings')); +const ApiSettings = React.lazy(() => import('./pages/settings/ApiSettings')); +const AuthenticationSettings = React.lazy(() => import('./pages/settings/AuthenticationSettings')); +const EmailSettings = React.lazy(() => import('./pages/settings/EmailSettings')); +const CommunicationSettings = React.lazy(() => import('./pages/settings/CommunicationSettings')); +const BillingSettings = React.lazy(() => import('./pages/settings/BillingSettings')); + import { Toaster } from 'react-hot-toast'; // Import Toaster for notifications const queryClient = new QueryClient({ @@ -537,9 +550,10 @@ const AppContent: React.FC = () => { } /> } /> } /> + {/* Trial-expired users can access billing settings to upgrade */} : } + path="/settings/*" + element={hasAccess(['owner']) ? : } /> } /> @@ -678,10 +692,23 @@ const AppContent: React.FC = () => { ) } /> - : } - /> + {/* Settings Routes with Nested Layout */} + {hasAccess(['owner']) ? ( + }> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + + ) : ( + } /> + )} } /> } /> } /> diff --git a/frontend/src/components/CreditPaymentForm.tsx b/frontend/src/components/CreditPaymentForm.tsx new file mode 100644 index 0000000..d75b761 --- /dev/null +++ b/frontend/src/components/CreditPaymentForm.tsx @@ -0,0 +1,339 @@ +/** + * Credit Payment Form Component + * + * Uses Stripe Elements for secure card collection when purchasing + * communication credits. + */ + +import React, { useState, useEffect } from 'react'; +import { loadStripe } from '@stripe/stripe-js'; +import { + Elements, + PaymentElement, + useStripe, + useElements, +} from '@stripe/react-stripe-js'; +import { CreditCard, Loader2, X, CheckCircle, AlertCircle } from 'lucide-react'; +import { useCreatePaymentIntent, useConfirmPayment } from '../hooks/useCommunicationCredits'; + +// Initialize Stripe +const stripePromise = loadStripe(import.meta.env.VITE_STRIPE_PUBLISHABLE_KEY || ''); + +interface PaymentFormProps { + amountCents: number; + onSuccess: () => void; + onCancel: () => void; + savePaymentMethod?: boolean; +} + +const PaymentFormInner: React.FC = ({ + amountCents, + onSuccess, + onCancel, + savePaymentMethod = false, +}) => { + const stripe = useStripe(); + const elements = useElements(); + const [isProcessing, setIsProcessing] = useState(false); + const [errorMessage, setErrorMessage] = useState(null); + const [isComplete, setIsComplete] = useState(false); + const confirmPayment = useConfirmPayment(); + + const formatCurrency = (cents: number) => `$${(cents / 100).toFixed(2)}`; + + const handleSubmit = async (event: React.FormEvent) => { + event.preventDefault(); + + if (!stripe || !elements) { + return; + } + + setIsProcessing(true); + setErrorMessage(null); + + try { + // Confirm the payment with Stripe + const { error, paymentIntent } = await stripe.confirmPayment({ + elements, + confirmParams: { + return_url: window.location.href, + }, + redirect: 'if_required', + }); + + if (error) { + setErrorMessage(error.message || 'Payment failed. Please try again.'); + setIsProcessing(false); + return; + } + + if (paymentIntent && paymentIntent.status === 'succeeded') { + // Confirm the payment on the backend + await confirmPayment.mutateAsync({ + payment_intent_id: paymentIntent.id, + save_payment_method: savePaymentMethod, + }); + setIsComplete(true); + setTimeout(() => { + onSuccess(); + }, 1500); + } + } catch (err: any) { + setErrorMessage(err.message || 'An unexpected error occurred.'); + setIsProcessing(false); + } + }; + + if (isComplete) { + return ( +
+ +

+ Payment Successful! +

+

+ {formatCurrency(amountCents)} has been added to your credits. +

+
+ ); + } + + return ( +
+
+
+ Amount + + {formatCurrency(amountCents)} + +
+
+ +
+ +
+ + {errorMessage && ( +
+ +

{errorMessage}

+
+ )} + +
+ + +
+ +

+ Your payment is securely processed by Stripe +

+
+ ); +}; + +interface CreditPaymentModalProps { + isOpen: boolean; + onClose: () => void; + onSuccess: () => void; + amountCents: number; + onAmountChange: (cents: number) => void; + savePaymentMethod?: boolean; +} + +export const CreditPaymentModal: React.FC = ({ + isOpen, + onClose, + onSuccess, + amountCents, + onAmountChange, + savePaymentMethod = false, +}) => { + const [clientSecret, setClientSecret] = useState(null); + const [isLoadingIntent, setIsLoadingIntent] = useState(false); + const [error, setError] = useState(null); + const [showPaymentForm, setShowPaymentForm] = useState(false); + const createPaymentIntent = useCreatePaymentIntent(); + + const formatCurrency = (cents: number) => `$${(cents / 100).toFixed(2)}`; + + useEffect(() => { + if (!isOpen) { + setClientSecret(null); + setShowPaymentForm(false); + setError(null); + } + }, [isOpen]); + + const handleContinueToPayment = async () => { + setIsLoadingIntent(true); + setError(null); + + try { + const result = await createPaymentIntent.mutateAsync(amountCents); + setClientSecret(result.client_secret); + setShowPaymentForm(true); + } catch (err: any) { + setError(err.response?.data?.error || 'Failed to initialize payment. Please try again.'); + } finally { + setIsLoadingIntent(false); + } + }; + + if (!isOpen) return null; + + return ( +
+
+
+

+ Add Credits +

+ +
+ + {!showPaymentForm ? ( +
+
+ {[1000, 2500, 5000].map((amount) => ( + + ))} +
+ +
+ +
+ $ + { + const val = e.target.value.replace(/[^0-9]/g, ''); + onAmountChange(Math.max(5, parseInt(val) || 5) * 100); + }} + onKeyDown={(e) => { + if (!/[0-9]/.test(e.key) && !['Backspace', 'Delete', 'ArrowLeft', 'ArrowRight', 'Tab'].includes(e.key)) { + e.preventDefault(); + } + }} + className="w-full pl-8 pr-12 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-white" + /> + .00 +
+
+ + {error && ( +
+ +

{error}

+
+ )} + +
+ + +
+
+ ) : clientSecret ? ( + + { + setShowPaymentForm(false); + setClientSecret(null); + }} + savePaymentMethod={savePaymentMethod} + /> + + ) : null} +
+
+ ); +}; + +export default CreditPaymentModal; diff --git a/frontend/src/components/DevQuickLogin.tsx b/frontend/src/components/DevQuickLogin.tsx index c8fcd7a..41bc759 100644 --- a/frontend/src/components/DevQuickLogin.tsx +++ b/frontend/src/components/DevQuickLogin.tsx @@ -97,6 +97,9 @@ export function DevQuickLogin({ embedded = false }: DevQuickLoginProps) { // Store token in cookie (use 'access_token' to match what client.ts expects) setCookie('access_token', response.data.token, 7); + // Clear any existing masquerade stack - this is a fresh login + localStorage.removeItem('masquerade_stack'); + // Fetch user data to determine redirect const userResponse = await apiClient.get('/auth/me/'); const userData = userResponse.data; diff --git a/frontend/src/components/EmailTemplateForm.tsx b/frontend/src/components/EmailTemplateForm.tsx index 2bf0f2c..01a2d12 100644 --- a/frontend/src/components/EmailTemplateForm.tsx +++ b/frontend/src/components/EmailTemplateForm.tsx @@ -11,10 +11,13 @@ import { Smartphone, Plus, AlertTriangle, - ChevronDown + 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; @@ -44,6 +47,15 @@ const EmailTemplateForm: React.FC = ({ 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[] }>({ @@ -105,6 +117,24 @@ const EmailTemplateForm: React.FC = ({ } }; + 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') }, @@ -137,6 +167,23 @@ const EmailTemplateForm: React.FC = ({ {/* 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 */}
@@ -245,12 +292,37 @@ const EmailTemplateForm: React.FC = ({ {/* 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.')} +

+ +
+
+
+ )} +
@@ -448,6 +526,15 @@ const EmailTemplateForm: React.FC = ({ )}
+ + {/* Preset Selector Modal */} + {showPresetSelector && ( + setShowPresetSelector(false)} + /> + )}
); diff --git a/frontend/src/components/EmailTemplatePresetSelector.tsx b/frontend/src/components/EmailTemplatePresetSelector.tsx new file mode 100644 index 0000000..f2fcbb7 --- /dev/null +++ b/frontend/src/components/EmailTemplatePresetSelector.tsx @@ -0,0 +1,292 @@ +import React, { useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { useQuery } from '@tanstack/react-query'; +import { + X, + Search, + Eye, + Check, + Sparkles, + Smile, + Minus, + ChevronRight +} from 'lucide-react'; +import api from '../api/client'; +import { EmailTemplateCategory } from '../types'; + +interface TemplatePreset { + name: string; + description: string; + style: string; + subject: string; + html_content: string; + text_content: string; +} + +interface PresetsResponse { + presets: Record; +} + +interface EmailTemplatePresetSelectorProps { + category: EmailTemplateCategory; + onSelect: (preset: TemplatePreset) => void; + onClose: () => void; +} + +const styleIcons: Record = { + professional: , + friendly: , + minimalist: , +}; + +const styleColors: Record = { + professional: 'bg-blue-100 text-blue-700 dark:bg-blue-900/30 dark:text-blue-300', + friendly: 'bg-amber-100 text-amber-700 dark:bg-amber-900/30 dark:text-amber-300', + minimalist: 'bg-gray-100 text-gray-700 dark:bg-gray-700 dark:text-gray-300', +}; + +const EmailTemplatePresetSelector: React.FC = ({ + category, + onSelect, + onClose, +}) => { + const { t } = useTranslation(); + const [searchQuery, setSearchQuery] = useState(''); + const [selectedPreview, setSelectedPreview] = useState(null); + const [selectedStyle, setSelectedStyle] = useState('all'); + + // Fetch presets + const { data: presetsData, isLoading } = useQuery({ + queryKey: ['email-template-presets'], + queryFn: async () => { + const { data } = await api.get('/email-templates/presets/'); + return data; + }, + }); + + const presets = presetsData?.presets[category] || []; + + // Filter presets + const filteredPresets = presets.filter(preset => { + const matchesSearch = searchQuery.trim() === '' || + preset.name.toLowerCase().includes(searchQuery.toLowerCase()) || + preset.description.toLowerCase().includes(searchQuery.toLowerCase()); + + const matchesStyle = selectedStyle === 'all' || preset.style === selectedStyle; + + return matchesSearch && matchesStyle; + }); + + // Get unique styles from presets + const availableStyles = Array.from(new Set(presets.map(p => p.style))); + + const handleSelectPreset = (preset: TemplatePreset) => { + onSelect(preset); + onClose(); + }; + + return ( +
+
+ {/* Header */} +
+
+

+ {t('emailTemplates.selectPreset', 'Choose a Template')} +

+

+ {t('emailTemplates.presetDescription', 'Select a pre-designed template to customize')} +

+
+ +
+ + {/* Search and Filters */} +
+
+ {/* Search */} +
+ + setSearchQuery(e.target.value)} + placeholder={t('emailTemplates.searchPresets', 'Search templates...')} + className="w-full pl-9 pr-4 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-white text-sm focus:ring-2 focus:ring-brand-500 focus:border-brand-500" + /> +
+ + {/* Style Filter */} +
+ + {availableStyles.map(style => ( + + ))} +
+
+
+ + {/* Content */} +
+ {isLoading ? ( +
+
+
+ ) : filteredPresets.length === 0 ? ( +
+

+ {t('emailTemplates.noPresets', 'No templates found matching your criteria')} +

+
+ ) : ( +
+ {filteredPresets.map((preset, index) => ( +
+ {/* Preview Image Placeholder */} +
+
+