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:
@@ -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();
|
||||
}}
|
||||
|
||||
Reference in New Issue
Block a user