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:
2026-06-20 16:59:31 +01:00
co-authored by Sisyphus
parent 1cf7083bcc
commit 1a3829b4d9
27 changed files with 597 additions and 300 deletions
@@ -5,6 +5,7 @@
import * as Modal from '$lib/components/ui/dialog';
import { Button } from '$lib/components/ui/button';
import PatchTestModal from './PatchTestModal.svelte';
import { formatUserName } from '$lib/utils/nameDisplay';
interface Props {
open: boolean;
@@ -42,6 +43,8 @@
dataRetentionConsent: boolean;
dataConsentUpdatedAt?: string;
socialLogins?: SocialLogin[];
previousFirstName?: string;
previousLastName?: string;
};
type Booking = {
@@ -84,6 +87,8 @@
let selectedUser = $state<AdminUserDetail | null>(null);
let bookingUserHistory = $state<Booking[]>([]);
let totalBookings = $state(0);
let cursors = $state<string[]>(['']);
let nextCursor = $state<string | null>(null);
let currentBookingPage = $state(1);
let totalBookingPages = $state(1);
let loadingBookings = $state(false);
@@ -144,15 +149,18 @@
}
});
async function fetchUserBookings(page: number = 1) {
async function fetchUserBookings(pageIdx: number = 0) {
if (!userId) return;
loadingBookings = true;
try {
const params = new URLSearchParams({
page: page.toString(),
per_page: '4'
});
const cursor = cursors[pageIdx];
if (cursor) {
params.set('cursor', cursor);
}
const response = await fetch(`/api/admin/bookings/user/${userId}?${params}`, {
method: 'GET',
@@ -164,10 +172,27 @@
if (response.ok) {
const data = await response.json();
if (!data.bookings || data.bookings.length === 0) {
bookingUserHistory = [];
totalBookings = 0;
totalBookingPages = 1;
currentBookingPage = 1;
cursors = [''];
nextCursor = null;
loadingBookings = false;
return;
}
bookingUserHistory = data.bookings || [];
totalBookings = data.total || 0;
currentBookingPage = data.page || 1;
totalBookingPages = data.totalPages || 1;
currentBookingPage = pageIdx + 1;
nextCursor = data.next_cursor ?? null;
if (nextCursor && cursors.length <= pageIdx + 1) {
cursors = [...cursors, nextCursor];
}
} else {
const text = await response.text();
toast.error('Failed to load user bookings: ' + text);
@@ -181,15 +206,13 @@
}
function nextBookingPage() {
if (currentBookingPage < totalBookingPages) {
fetchUserBookings(currentBookingPage + 1);
}
if (!nextCursor || currentBookingPage >= totalBookingPages) return;
fetchUserBookings(currentBookingPage);
}
function previousBookingPage() {
if (currentBookingPage > 1) {
fetchUserBookings(currentBookingPage - 1);
}
if (currentBookingPage <= 1) return;
fetchUserBookings(currentBookingPage - 2);
}
async function fetchCustomerRelationship() {
@@ -277,15 +300,15 @@
{#if selectedUser}
<div class="space-y-6 px-4 pb-4">
<!-- Personal Information -->
<div class="rounded-lg border border-gray-200 bg-gray-50 p-4">
<h3 class="mb-3 text-sm font-semibold tracking-wide text-gray-600 uppercase">
Personal Information
</h3>
<div class="grid gap-3 md:grid-cols-2">
<div>
<div class="text-xs text-gray-500">Full Name</div>
<div class="font-medium">{selectedUser.fullName}</div>
</div>
<div class="rounded-lg border border-gray-200 bg-gray-50 p-4">
<h3 class="mb-3 text-sm font-semibold tracking-wide text-gray-600 uppercase">
Personal Information
</h3>
<div class="grid gap-3 md:grid-cols-2">
<div>
<div class="text-xs text-gray-500">Full Name</div>
<div class="font-medium">{formatUserName(selectedUser.fullName, selectedUser.previousFirstName, selectedUser.previousLastName)}</div>
</div>
<div>
<div class="text-xs text-gray-500">Email</div>
<div class="font-medium break-words" title={selectedUser.email}>
@@ -371,16 +394,10 @@
<h3 class="mb-3 text-sm font-semibold tracking-wide text-gray-600 uppercase">
Booking History ({totalBookings})
</h3>
{#if loadingBookings}
<div class="space-y-2">
{#each Array(3) as _, i (i)}
<div class="h-20 animate-pulse rounded-md bg-gray-200"></div>
{/each}
</div>
{:else if bookingUserHistory.length === 0}
{#if bookingUserHistory.length === 0 && !loadingBookings}
<div class="text-center text-sm text-gray-500">No bookings found</div>
{:else}
<div class="space-y-2">
{:else if bookingUserHistory.length > 0}
<div class="space-y-2 {loadingBookings ? 'opacity-60' : ''}">
{#each bookingUserHistory as booking (booking.id)}
<div class="rounded-md border border-gray-300 bg-white p-3">
<div class="flex items-start justify-between">
@@ -619,7 +636,7 @@
<PatchTestModal
bind:open={showPatchTestModal}
userId={selectedUser.id}
userName={selectedUser.fullName}
userName={formatUserName(selectedUser.fullName, selectedUser.previousFirstName, selectedUser.previousLastName)}
onPatchTestAdded={() => {
fetchUserDetails();
}}