feat(frontend): add former name display, referral discount type, and refunds to bookings
Update frontend types, stores, and components to support name history display and new API fields. - Add previousFirstName/previousLastName to User type and BookingUser type - Add referral discount_source type and refunds array to Booking type - Create nameDisplay.ts utility for rendering '(formerly ...)' labels - Create booking.ts utility for booking-related helpers - Update 25+ admin, payments, today, and account components to display former names on booking cards, modals, appointment views, and user lists Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
This commit is contained in:
@@ -7,6 +7,7 @@
|
||||
import { EmailInput } from '$lib/components/ui/email-input';
|
||||
import * as Modal from '$lib/components/ui/dialog';
|
||||
import { Skeleton } from '$lib/components/ui/skeleton';
|
||||
import { formatUserName } from '$lib/utils/nameDisplay';
|
||||
|
||||
|
||||
interface GiftCard {
|
||||
@@ -27,6 +28,7 @@
|
||||
email: string;
|
||||
balance: number;
|
||||
updated_at: string;
|
||||
/* TODO: add previousFirstName/previousLastName when backend sends them */
|
||||
}
|
||||
|
||||
interface GiftCardSummary {
|
||||
@@ -80,7 +82,7 @@
|
||||
let generateAmount = $state('');
|
||||
let generateUserQuery = $state('');
|
||||
let generateUsers = $state<
|
||||
Array<{ id: string; fullName: string; email?: string; phone?: string }>
|
||||
Array<{ id: string; fullName: string; email?: string; phone?: string; previousFirstName?: string | null; previousLastName?: string | null }>
|
||||
>([]);
|
||||
let generateLoadingUsers = $state(false);
|
||||
let topUpAmount = $state('');
|
||||
@@ -103,11 +105,11 @@
|
||||
|
||||
// Page 2: Customer selection state
|
||||
let generateCustomerTab = $state<'current' | 'member' | 'guest'>('current');
|
||||
let currentCustomerInfo = $state<{ id: string; name: string; email?: string; phone?: string } | null>(null);
|
||||
let currentCustomerInfo = $state<{ id: string; name: string; email?: string; phone?: string; previousFirstName?: string | null; previousLastName?: string | null } | null>(null);
|
||||
let loadingCurrentCustomer = $state(false);
|
||||
|
||||
// Selection from Page 2
|
||||
let selectedCustomer = $state<{ id: string; name: string; email?: string } | null>(null);
|
||||
let selectedCustomer = $state<{ id: string; name: string; email?: string; previousFirstName?: string | null; previousLastName?: string | null } | null>(null);
|
||||
let isGuestSelected = $state(false);
|
||||
|
||||
// Page 3: Recipient email input
|
||||
@@ -333,7 +335,9 @@
|
||||
id: appointment.user.id,
|
||||
name: appointment.user.full_name || appointment.user.name || 'Current Customer',
|
||||
email: appointment.user.email,
|
||||
phone: appointment.user.phone
|
||||
phone: appointment.user.phone,
|
||||
previousFirstName: appointment.user.previous_first_name,
|
||||
previousLastName: appointment.user.previous_last_name
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -993,28 +997,17 @@
|
||||
<th class="py-3 text-center font-medium">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{#if loading || loadingSearch}
|
||||
{#each Array(3) as _, i (i)}
|
||||
<tr class="border-b">
|
||||
<td class="py-3"><Skeleton class="h-4 w-32" /></td>
|
||||
<td class="py-3"><Skeleton class="h-4 w-16" /></td>
|
||||
<td class="py-3"><Skeleton class="h-4 w-16" /></td>
|
||||
<td class="py-3"><Skeleton class="h-4 w-24" /></td>
|
||||
<td class="py-3"><Skeleton class="h-4 w-20" /></td>
|
||||
<td class="py-3 text-center"><Skeleton class="mx-auto h-4 w-24" /></td>
|
||||
<tbody class={sortedCards.length > 0 && (loading || loadingSearch) ? 'opacity-60' : ''}>
|
||||
{#if sortedCards.length === 0 && !loading && !loadingSearch}
|
||||
<tr>
|
||||
<td colspan="8" class="py-8 text-center text-gray-500">
|
||||
{activeSection === 'expired_cards'
|
||||
? 'No expired gift cards found.'
|
||||
: 'No gift cards generated yet. Click "Generate Gift Card" to create one.'}
|
||||
</td>
|
||||
</tr>
|
||||
{/each}
|
||||
{:else if sortedCards.length === 0}
|
||||
<tr>
|
||||
<td colspan="8" class="py-8 text-center text-gray-500">
|
||||
{activeSection === 'expired_cards'
|
||||
? 'No expired gift cards found.'
|
||||
: 'No gift cards generated yet. Click "Generate Gift Card" to create one.'}
|
||||
</td>
|
||||
</tr>
|
||||
{:else}
|
||||
{#each sortedCards as gc (gc.id)}
|
||||
{:else if sortedCards.length > 0}
|
||||
{#each sortedCards as gc (gc.id)}
|
||||
<tr class="border-b hover:bg-gray-50">
|
||||
<td class="py-3 font-mono font-bold text-gray-900">{formatCardCode(gc.id)}</td>
|
||||
<td class="py-3 font-medium text-gray-600"
|
||||
@@ -1101,22 +1094,15 @@
|
||||
|
||||
<!-- Mobile view - Cards -->
|
||||
<div class="grid gap-4 md:hidden">
|
||||
{#if loading || loadingSearch}
|
||||
{#each Array(2) as _, i (i)}
|
||||
<div class="space-y-3 rounded-lg border p-4">
|
||||
<Skeleton class="h-4 w-32" />
|
||||
<Skeleton class="h-4 w-full" />
|
||||
<Skeleton class="h-4 w-24" />
|
||||
</div>
|
||||
{/each}
|
||||
{:else if sortedCards.length === 0}
|
||||
{#if sortedCards.length === 0 && !loading && !loadingSearch}
|
||||
<div class="rounded-lg border border-dashed py-8 text-center text-gray-500">
|
||||
{activeSection === 'expired_cards'
|
||||
? 'No expired gift cards found.'
|
||||
: 'No gift cards generated yet.'}
|
||||
</div>
|
||||
{:else}
|
||||
{#each sortedCards as gc (gc.id)}
|
||||
{:else if sortedCards.length > 0}
|
||||
<div class={loading || loadingSearch ? 'opacity-60' : ''}>
|
||||
{#each sortedCards as gc (gc.id)}
|
||||
<div class="space-y-3 rounded-lg border p-4 hover:bg-gray-50">
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="font-mono font-bold text-gray-900">{formatCardCode(gc.id)}</span>
|
||||
@@ -1204,6 +1190,7 @@
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
@@ -1279,7 +1266,7 @@
|
||||
{:else}
|
||||
{#each sortedBalances as ub (ub.user_id)}
|
||||
<tr class="border-b hover:bg-gray-50">
|
||||
<td class="py-3 font-medium text-gray-900">{ub.name}</td>
|
||||
<td class="py-3 font-medium text-gray-900">{ub.name}<!-- TODO: add formerly name when previous name data is available --></td>
|
||||
<td class="py-3 text-gray-600">{ub.email}</td>
|
||||
<td class="py-3 font-semibold text-primary">{formatCurrency(ub.balance)}</td>
|
||||
<td class="py-3 text-gray-600">{formatDate(ub.updated_at)}</td>
|
||||
@@ -1308,7 +1295,7 @@
|
||||
{#each sortedBalances as ub (ub.user_id)}
|
||||
<div class="space-y-3 rounded-lg border p-4 hover:bg-gray-50">
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="font-medium text-gray-900">{ub.name}</span>
|
||||
<span class="font-medium text-gray-900">{ub.name}<!-- TODO: add formerly name when previous name data is available --></span>
|
||||
</div>
|
||||
<div class="grid grid-cols-2 gap-2 border-t border-b py-2 text-xs text-gray-600">
|
||||
<div class="col-span-2">
|
||||
@@ -1593,14 +1580,16 @@
|
||||
selectedCustomer = {
|
||||
id: currentCustomerInfo.id,
|
||||
name: currentCustomerInfo.name,
|
||||
email: currentCustomerInfo.email
|
||||
email: currentCustomerInfo.email,
|
||||
previousFirstName: currentCustomerInfo.previousFirstName,
|
||||
previousLastName: currentCustomerInfo.previousLastName
|
||||
};
|
||||
isGuestSelected = false;
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div>
|
||||
<div class="text-sm font-semibold">{currentCustomerInfo.name}</div>
|
||||
<div class="text-sm font-semibold">{formatUserName(currentCustomerInfo.name, currentCustomerInfo.previousFirstName, currentCustomerInfo.previousLastName)}</div>
|
||||
{#if currentCustomerInfo.email}
|
||||
<div class="text-xs text-gray-500">{currentCustomerInfo.email}</div>
|
||||
{/if}
|
||||
@@ -1641,17 +1630,12 @@
|
||||
</div>
|
||||
|
||||
<div class="max-h-[220px] overflow-y-auto rounded-md border border-gray-200">
|
||||
{#if generateLoadingUsers}
|
||||
<div class="space-y-2 p-2">
|
||||
<Skeleton class="h-10 w-full" />
|
||||
<Skeleton class="h-10 w-full" />
|
||||
</div>
|
||||
{:else if generateUsers.length === 0}
|
||||
{#if generateUsers.length === 0 && !generateLoadingUsers}
|
||||
<div class="flex items-center justify-center p-8 text-xs text-gray-500">
|
||||
{generateUserQuery ? 'No members found.' : 'Search for a member above.'}
|
||||
</div>
|
||||
{:else}
|
||||
<ul class="divide-y divide-gray-200">
|
||||
{:else if generateUsers.length > 0}
|
||||
<ul class="divide-y divide-gray-200 {generateLoadingUsers ? 'opacity-60' : ''}">
|
||||
{#each generateUsers.slice(0, 5) as user (user.id)}
|
||||
<li>
|
||||
<button
|
||||
@@ -1660,12 +1644,12 @@
|
||||
? 'bg-fuchsia-100 font-medium'
|
||||
: 'hover:bg-fuchsia-50/40'}"
|
||||
onclick={() => {
|
||||
selectedCustomer = { id: user.id, name: user.fullName, email: user.email };
|
||||
selectedCustomer = { id: user.id, name: user.fullName, email: user.email, previousFirstName: user.previousFirstName, previousLastName: user.previousLastName };
|
||||
isGuestSelected = false;
|
||||
}}
|
||||
>
|
||||
<div>
|
||||
<div class="text-sm font-semibold">{user.fullName}</div>
|
||||
<div class="text-sm font-semibold">{formatUserName(user.fullName, user.previousFirstName, user.previousLastName)}</div>
|
||||
<div class="text-xs text-gray-500">
|
||||
{#if user.email && user.phone}
|
||||
{user.email} • {user.phone}
|
||||
@@ -1736,7 +1720,7 @@
|
||||
<span class="text-xs text-gray-400 font-semibold uppercase tracking-wider block">Customer</span>
|
||||
<p class="mt-0.5 font-medium text-gray-900">
|
||||
{#if selectedCustomer}
|
||||
{selectedCustomer.name} (Member)
|
||||
{formatUserName(selectedCustomer.name, selectedCustomer.previousFirstName, selectedCustomer.previousLastName)} (Member)
|
||||
{:else if isGuestSelected}
|
||||
Walk-in Guest
|
||||
{/if}
|
||||
@@ -1865,22 +1849,6 @@
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-4 gap-2">
|
||||
{#each [Number(generateAmount), 10, 20, 50] as val}
|
||||
<button
|
||||
type="button"
|
||||
class="rounded border border-gray-200 py-1.5 text-center text-xs font-semibold hover:bg-gray-50"
|
||||
onclick={() => (cashAmount = val.toFixed(2))}
|
||||
>
|
||||
{#if val === Number(generateAmount)}
|
||||
Exact
|
||||
{:else}
|
||||
£{val}
|
||||
{/if}
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
{#if Number(cashAmount) > Number(generateAmount)}
|
||||
<div class="rounded-md bg-green-50 p-3 text-xs text-green-800">
|
||||
Change due: <span class="font-bold">{formatCurrency(Number(cashAmount) - Number(generateAmount))}</span>
|
||||
@@ -2141,22 +2109,6 @@
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-4 gap-2">
|
||||
{#each [Number(topUpAmount), 10, 20, 50] as val}
|
||||
<button
|
||||
type="button"
|
||||
class="rounded border border-gray-200 py-1.5 text-center text-xs font-semibold hover:bg-gray-50"
|
||||
onclick={() => (cashAmount = val.toFixed(2))}
|
||||
>
|
||||
{#if val === Number(topUpAmount)}
|
||||
Exact
|
||||
{:else}
|
||||
£{val}
|
||||
{/if}
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
{#if Number(cashAmount) > Number(topUpAmount)}
|
||||
<div class="rounded-md bg-green-50 p-3 text-xs text-green-800">
|
||||
Change due: <span class="font-bold">{formatCurrency(Number(cashAmount) - Number(topUpAmount))}</span>
|
||||
|
||||
Reference in New Issue
Block a user