From 56eda0ead5979e42017c559b99ffec55f6c53f6a Mon Sep 17 00:00:00 2001 From: Stephen Adamson Date: Sun, 5 Jul 2026 22:26:08 +0100 Subject: [PATCH] refactor: accept services prop in PatchTestsManagement and ServicesManagement Add optional services prop so the page can pass pre-fetched data, avoiding duplicate API calls. Fall back to self-fetching when prop is not provided. Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus --- .../admin/PatchTestsManagement.svelte | 47 +++++++++++++------ .../admin/ServicesManagement.svelte | 27 ++++++++++- 2 files changed, 57 insertions(+), 17 deletions(-) diff --git a/frontend/src/lib/components/admin/PatchTestsManagement.svelte b/frontend/src/lib/components/admin/PatchTestsManagement.svelte index 0443ce5..4402c61 100644 --- a/frontend/src/lib/components/admin/PatchTestsManagement.svelte +++ b/frontend/src/lib/components/admin/PatchTestsManagement.svelte @@ -23,6 +23,12 @@ service_ids: string[]; }; + interface Props { + services?: Service[]; + } + + let { services: servicesProp }: Props = $props(); + let patchTests = $state([]); let availableServices = $state([]); let loading = $state(true); @@ -99,26 +105,30 @@ async function fetchData() { loading = true; try { - const [ptRes, svcRes] = await Promise.all([ - fetch('/api/admin/patch-tests', { - headers: { Authorization: `Bearer ${authStore.currentToken}` } - }), - fetch('/api/admin/services', { - headers: { Authorization: `Bearer ${authStore.currentToken}` } - }) - ]); + const ptRes = await fetch('/api/admin/patch-tests', { + headers: { Authorization: `Bearer ${authStore.currentToken}` } + }); - if (ptRes.ok && svcRes.ok) { + if (ptRes.ok) { patchTests = await ptRes.json(); - const svcData = await svcRes.json(); - const uniqueSvc = new SvelteMap(); - svcData.forEach((s: Service) => { - if (s.id) uniqueSvc.set(s.id, s); - }); - availableServices = Array.from(uniqueSvc.values()); } else { toast.error('Failed to load patch test data'); } + + // Only fetch services if not provided via prop + if (!servicesProp) { + const svcRes = await fetch('/api/admin/services', { + headers: { Authorization: `Bearer ${authStore.currentToken}` } + }); + if (svcRes.ok) { + const svcData = await svcRes.json(); + const uniqueSvc = new SvelteMap(); + svcData.forEach((s: Service) => { + if (s.id) uniqueSvc.set(s.id, s); + }); + availableServices = Array.from(uniqueSvc.values()); + } + } } catch (err) { console.error(err); toast.error('Network error loading data'); @@ -241,6 +251,13 @@ } } + // Reactively update availableServices when the parent provides services via prop + $effect(() => { + if (servicesProp !== undefined) { + availableServices = servicesProp; + } + }); + $effect(() => { fetchData(); }); diff --git a/frontend/src/lib/components/admin/ServicesManagement.svelte b/frontend/src/lib/components/admin/ServicesManagement.svelte index e28dbc5..ac5e944 100644 --- a/frontend/src/lib/components/admin/ServicesManagement.svelte +++ b/frontend/src/lib/components/admin/ServicesManagement.svelte @@ -24,6 +24,13 @@ created_by?: string; }; + interface Props { + services?: Service[]; + onRefresh?: () => Promise; + } + + let { services: servicesProp, onRefresh }: Props = $props(); + const durationOptions = Array.from({ length: 32 }, (_, i) => (i + 1) * 15); let services = $state([]); @@ -139,6 +146,12 @@ // =============== API Functions =============== async function fetchServices() { + // If the parent provides services via prop, delegate refresh to parent + if (servicesProp && onRefresh) { + await onRefresh(); + return; + } + servicesLoading = true; try { const response = await fetch('/api/admin/services', { @@ -306,8 +319,18 @@ } // =============== Lifecycle =============== + // Single effect: sync from prop when provided, self-fetch when not. + // Combined into one effect to prevent the reactive loop where + // fetchServices() → onRefresh() → parent updates → servicesProp changes → effect re-runs. $effect(() => { - fetchServices(); + if (servicesProp !== undefined) { + if (servicesProp.length > 0) { + services = servicesProp; + } + servicesLoading = false; + } else { + fetchServices(); + } }); @@ -570,7 +593,7 @@ id="service-duration" bind:value={newService.duration_minutes} onblur={validateDurationField} - class="flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:outline-none {serviceErrors.duration_minutes + class="flex h-9 w-full rounded-md border border-input bg-background px-3 py-1 text-sm shadow-xs ring-offset-background transition-[color,box-shadow] outline-none focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:opacity-50 {serviceErrors.duration_minutes ? 'border-red-500' : ''}" >