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 <clio-agent@sisyphuslabs.ai>
This commit is contained in:
2026-07-05 22:26:08 +01:00
co-authored by Sisyphus
parent 42c3b65a27
commit 56eda0ead5
2 changed files with 57 additions and 17 deletions
@@ -23,6 +23,12 @@
service_ids: string[];
};
interface Props {
services?: Service[];
}
let { services: servicesProp }: Props = $props();
let patchTests = $state<PatchTest[]>([]);
let availableServices = $state<Service[]>([]);
let loading = $state(true);
@@ -99,25 +105,29 @@
async function fetchData() {
loading = true;
try {
const [ptRes, svcRes] = await Promise.all([
fetch('/api/admin/patch-tests', {
const ptRes = await fetch('/api/admin/patch-tests', {
headers: { Authorization: `Bearer ${authStore.currentToken}` }
}),
fetch('/api/admin/services', {
headers: { Authorization: `Bearer ${authStore.currentToken}` }
})
]);
});
if (ptRes.ok && svcRes.ok) {
if (ptRes.ok) {
patchTests = await ptRes.json();
} 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<string, Service>();
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');
}
}
} catch (err) {
console.error(err);
@@ -241,6 +251,13 @@
}
}
// Reactively update availableServices when the parent provides services via prop
$effect(() => {
if (servicesProp !== undefined) {
availableServices = servicesProp;
}
});
$effect(() => {
fetchData();
});
@@ -24,6 +24,13 @@
created_by?: string;
};
interface Props {
services?: Service[];
onRefresh?: () => Promise<void>;
}
let { services: servicesProp, onRefresh }: Props = $props();
const durationOptions = Array.from({ length: 32 }, (_, i) => (i + 1) * 15);
let services = $state<Service[]>([]);
@@ -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(() => {
if (servicesProp !== undefined) {
if (servicesProp.length > 0) {
services = servicesProp;
}
servicesLoading = false;
} else {
fetchServices();
}
});
</script>
@@ -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'
: ''}"
>