From 5c4041ded91e6fd295be8fdff4f653f45d2a8b92 Mon Sep 17 00:00:00 2001 From: Stephen Adamson Date: Fri, 8 May 2026 17:54:05 +0100 Subject: [PATCH] refactor: move discounts into admin panel as component, rebuild modal with proper field handling - DiscountsManagement component integrated into admin/+page.svelte - Removed standalone /admin/discounts route - Modal rebuilt with toggle buttons for campaign type, conditional field groups - Milestone unit auto-restricted based on milestone type (bookings-only for count types, months/years for anniversary) - Mobile-first: card layout on mobile, table on desktop (md: breakpoint) - Native select elements instead of broken bits-ui Select components --- .../admin/DiscountsManagement.svelte | 622 +++++++++++ frontend/src/routes/admin/+page.svelte | 44 + .../src/routes/admin/discounts/+page.svelte | 978 ------------------ 3 files changed, 666 insertions(+), 978 deletions(-) create mode 100644 frontend/src/lib/components/admin/DiscountsManagement.svelte delete mode 100644 frontend/src/routes/admin/discounts/+page.svelte diff --git a/frontend/src/lib/components/admin/DiscountsManagement.svelte b/frontend/src/lib/components/admin/DiscountsManagement.svelte new file mode 100644 index 0000000..d4be52e --- /dev/null +++ b/frontend/src/lib/components/admin/DiscountsManagement.svelte @@ -0,0 +1,622 @@ + + + + +
+
+ Discount Campaigns + Manage loyalty discounts, sales, and milestone campaigns +
+ +
+
+ + {#if loading} +
+ {#each Array(3) as _, i (i)} + + {/each} +
+ {:else if campaigns.length === 0} +
+ No campaigns yet. Create one to get started. +
+ {:else} + + + + +
+ {#each campaigns as c (c.id)} +
+
+
{c.name}
+ {@html statusBadge(c.status)} +
+
+
Type: {typeLabel(c)}
+
Discount: {c.discount_percent}%
+
Redeemed: {redemptionDisplay(c)}
+
+
+ {#if c.status === 'draft'} + + {/if} + {#if c.status === 'active'} + + {/if} + {#if c.status !== 'cancelled'} + + {/if} + {#if c.status === 'active' || c.status === 'completed'} + + {/if} +
+
+ {/each} +
+ {/if} +
+
+ + + + + + {editingCampaign ? 'Edit Campaign' : 'Create Campaign'} + Configure discount rules and campaign parameters + + +
+ +
+ Name * + + {#if errors.name}

{errors.name}

{/if} +
+ + +
+ Description + +
+ + +
+ Campaign Type * +
+ + +
+
+ + +
+ Discount Percent * +
+ + % +
+ {#if errors.discount_percent}

{errors.discount_percent}

{/if} +
+ + + {#if form.campaign_type === 'time_based'} +
+

TIME-BASED SETTINGS

+ + +
+ Scope + +
+ + +
+
+ Start Date * + + {#if errors.start_date}

{errors.start_date}

{/if} +
+
+ End Date * + + {#if errors.end_date}

{errors.end_date}

{/if} +
+
+
+ {/if} + + + {#if form.campaign_type === 'milestone'} +
+

MILESTONE SETTINGS

+ + +
+ Milestone Type * + +
+ + +
+
+ Value * + + {#if errors.milestone_value}

{errors.milestone_value}

{/if} +
+
+ Unit + {#if form.milestone_type === 'anniversary'} + + {:else} + + {/if} +
+
+ +

+ {#if form.milestone_type === 'per_user_booking_count'} + Discount applies when a user completes their {form.milestone_value}th booking + {:else if form.milestone_type === 'global_booking_count'} + Discount applies on the {form.milestone_value}th completed booking across all users + {:else} + Discount applies on a user's first booking after {form.milestone_value} {form.milestone_unit} since their first visit + {/if} +

+
+ {/if} + + +
+ Max Redemptions + +

0 = unlimited

+
+
+ + + + + +
+
+ + + + + + Campaign Stats + + {#if statsLoading} +
+ + +
+ {:else if statsData} +
+
+

Total Discount Given

+

£{statsData.total_discount_amount.toFixed(2)}

+
+
+

Bookings Discounted

+

{statsData.booking_count}

+
+
+ {/if} + + + +
+
diff --git a/frontend/src/routes/admin/+page.svelte b/frontend/src/routes/admin/+page.svelte index 9296246..2047854 100644 --- a/frontend/src/routes/admin/+page.svelte +++ b/frontend/src/routes/admin/+page.svelte @@ -11,6 +11,7 @@ import HolidayHours from '$lib/components/admin/HolidayHours.svelte'; import WeeklySchedule from '$lib/components/admin/WeeklySchedule.svelte'; import ServicesManagement from '$lib/components/admin/ServicesManagement.svelte'; + import DiscountsManagement from '$lib/components/admin/DiscountsManagement.svelte'; import UserModal from '$lib/components/admin/UserModal.svelte'; import BookingModal from '$lib/components/admin/BookingModal.svelte'; @@ -209,6 +210,48 @@ + + +
+
+
+ + +
+ +
+ +
{:else if pageState === 'authorized'}
@@ -227,6 +270,7 @@ +
diff --git a/frontend/src/routes/admin/discounts/+page.svelte b/frontend/src/routes/admin/discounts/+page.svelte deleted file mode 100644 index 466a6d8..0000000 --- a/frontend/src/routes/admin/discounts/+page.svelte +++ /dev/null @@ -1,978 +0,0 @@ - - -{#if pageState === 'loading'} - -
-
-
- - -
- -
- - - - - - - - - - - - - - - - - - - {#each Array(3) as _, i (i)} - - - - - - - - - {/each} - -
-
- - -
-
-
-
-
-{:else if pageState === 'authorized'} -
- -
-
-

Discount Campaigns

-

Create and manage discount campaigns for your customers

-
- -
- - - - - All Campaigns - View and manage your discount campaigns - - - - - - -
- {#if campaignsLoading} - {#each Array(3) as _, i (i)} -
-
- - -
- - -
-
-
- {/each} - {:else} - {#each campaigns as campaign (campaign.id)} -
-
-
-
-

{campaign.name}

- {#if campaign.description} -

{campaign.description}

- {/if} -
- - {campaign.status} - -
- -
- {campaign.discount_percent}% off - - - {getCampaignTypeLabel( - campaign.campaign_type, - campaign.milestone_type as MilestoneType - )} - - - - {campaign.times_redeemed} - {campaign.max_redemptions ? `/${campaign.max_redemptions}` : '/∞'} - redeemed - -
- -
- {#if campaign.status === 'draft'} - - {:else if campaign.status === 'active'} - - {/if} - - {#if campaign.status !== 'cancelled'} - - {/if} -
-
-
- {/each} - {/if} -
- - {#if !campaignsLoading && campaigns.length === 0} -
- No campaigns found. Click "Create Campaign" to create your first campaign. -
- {/if} -
-
-
- - - - - - Create Campaign - Create a new discount campaign for your customers. - - -
- -
- Campaign Name * - - {#if formErrors.name} -

{formErrors.name}

- {/if} -
- - -
- Milestone Unit - { newCampaign.milestone_unit = v as MilestoneUnit; }}> - - - Bookings - Months - Years - - -
- - -
- Campaign Type * - { newCampaign.campaign_type = v as CampaignType; }}> - - - Time-based - Milestone - - -
- - -
- Discount Percent * - -

Percentage off the booking total

-
- - - {#if newCampaign.campaign_type === 'time_based'} - -
- Scope - { newCampaign.scope = v as DiscountCampaignScope; }}> - - - All bookings - First booking only - New customers only - - -
- - -
- Start Date * - -
- - -
- End Date * - -
- {/if} - - - {#if newCampaign.campaign_type === 'milestone'} - -
- Milestone Type * - { - newCampaign.milestone_type = v as MilestoneType; - if (v === 'anniversary') { - newCampaign.milestone_unit = 'years'; - } else if (v === 'global_booking_count') { - newCampaign.milestone_unit = 'bookings'; - } else { - newCampaign.milestone_unit = 'bookings'; - } - }}> - - - Per-user booking count - Global booking count - Anniversary - - -
- - -
- Milestone Value * - -
- - -
- Milestone Unit - { newCampaign.milestone_unit = v as MilestoneUnit; }}> - - - Bookings - Months - Years - - -
- {/if} - - -
- Max Redemptions - -

Leave at 0 for unlimited

-
-
- - - - - -
-
- - - - - - - Campaign Stats - - - {selectedCampaign?.name || 'Campaign'} statistics - - - -
- {#if statsLoading} -
- - -
- {:else if campaignStats} -
- -
-
Total Discount Given
-
- £{campaignStats.total_discount_amount.toFixed(2)} -
-
- - -
-
Bookings Discounted
-
- {campaignStats.booking_count} -
-
-
- - -
-
Campaign Details
-
-
- Discount: - - {campaignStats.campaign.discount_percent}% - -
-
- Status: - - {campaignStats.campaign.status} - -
-
- Redeemed: - - {campaignStats.campaign.times_redeemed} - {campaignStats.campaign.max_redemptions - ? `/${campaignStats.campaign.max_redemptions}` - : '/∞'} - -
-
-
- {:else} -
- No stats available for this campaign. -
- {/if} -
- - - - -
-
-{/if} \ No newline at end of file