feat: migrate to dedicated patch test management

This commit is contained in:
2026-05-29 17:19:02 +01:00
parent 9d8015f1b8
commit f36497090a
8 changed files with 450 additions and 124 deletions
@@ -20,55 +20,27 @@
patch_test_duration_hours: number;
minimum_age_required: number;
created_at: string;
updated_at?: string;
created_by?: string;
updated_by?: string;
};
// =============== State ===============
let services = $state<Service[]>([]);
let servicesLoading = $state(true);
let servicesUpdating = $state<Record<string, boolean>>({});
// Service Creation State
let showServiceModal = $state(false);
let creatingService = $state(false);
let newService = $state({
let newService = $state<Omit<Service, 'id' | 'created_at' | 'created_by' | 'patch_test_duration_hours'>>({
name: '',
description: '',
price: '',
duration_minutes: 60,
patch_test_duration_hours: 0,
minimum_age_required: 0
price: 0,
duration_minutes: 0,
is_active: true,
minimum_age_required: 0,
});
let editingService = $state<Service | null>(null);
let loading = $state(true);
let serviceErrors = $state<Record<string, string>>({});
let serviceErrors = $state({
name: '',
price: '',
duration_minutes: '',
patch_test_duration_hours: '',
minimum_age_required: ''
});
function validateDuration(val: any, field: string) {
if (typeof val !== 'number' || val < 0) return 'Duration must be a positive number';
return '';
}
// =============== Validation ===============
let isFormValid = $derived(
newService.name.trim() !== '' &&
/^\d+(\.\d{1,2})?$/.test(newService.price) &&
parseFloat(newService.price) > 0 &&
Number.isInteger(newService.duration_minutes) &&
newService.duration_minutes > 0 &&
Number.isInteger(newService.patch_test_duration_hours) &&
newService.patch_test_duration_hours >= 0 &&
Number.isInteger(newService.minimum_age_required) &&
newService.minimum_age_required >= 0 &&
newService.minimum_age_required <= 100
);
function validatePrice(price: string): string {
const validFormat = /^\d*\.?\d*$/.test(price);
if (!validFormat) {
return 'Price must be a valid number (e.g., 4.50)';
}
const numPrice = parseFloat(price);
if (isNaN(numPrice)) {
@@ -143,12 +115,97 @@
}
function validateDurationField() {
serviceErrors.duration_minutes = validateDuration(
newService.duration_minutes,
'duration_minutes'
);
serviceErrors.duration_minutes = validateDuration(newService.duration_minutes, 'duration_minutes');
}
function validateMinimumAgeField() {
serviceErrors.minimum_age_required = validateDuration(newService.minimum_age_required, 'minimum_age_required');
}
function validateNameField() {
if (!newService.name) serviceErrors.name = 'Name is required';
else serviceErrors.name = '';
}
function validatePriceField() {
if (typeof newService.price !== 'number' || newService.price <= 0) serviceErrors.price = 'Price must be greater than 0';
else serviceErrors.price = '';
}
let isFormValid = $derived(
newService.name !== '' &&
!serviceErrors.name &&
!serviceErrors.price &&
!serviceErrors.duration_minutes &&
!serviceErrors.minimum_age_required
);
let creatingService = $state(false);
async function createService() {
creatingService = true;
const loadingToast = toast.loading('Creating service...');
const payload = {
...newService,
price: Number(newService.price),
duration_minutes: Number(newService.duration_minutes),
minimum_age_required: Number(newService.minimum_age_required)
};
try {
const response = await fetch('/api/admin/services', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${authStore.currentToken}`
},
body: JSON.stringify(payload)
});
if (response.ok) {
await response.json();
toast.success('Service created successfully!', { id: loadingToast });
resetServiceForm();
showServiceModal = false;
await fetchServices();
} else if (response.status === 409) {
toast.error('A service with this name already exists', { id: loadingToast });
} else if (response.status === 400) {
const errorText = await response.text();
toast.error(`Validation error: ${errorText}`, { id: loadingToast });
} else {
const errorText = await response.text();
toast.error(`Failed to create service: ${errorText}`, { id: loadingToast });
}
} catch (err) {
console.error('Error creating service:', err);
toast.error('Network error creating service', { id: loadingToast });
} finally {
creatingService = false;
}
}
function resetServiceForm() {
newService = {
name: '',
description: '',
price: 0,
duration_minutes: 60,
is_active: true,
minimum_age_required: 0
};
serviceErrors = {
name: '',
price: '',
duration_minutes: '',
minimum_age_required: ''
};
}
function validatePatchTestField() {
serviceErrors.patch_test_duration_hours = validateDuration(
newService.patch_test_duration_hours,
@@ -618,27 +675,6 @@
<!-- Patch Test and Minimum Age - Side by side on desktop -->
<div class="grid grid-cols-1 gap-4 md:grid-cols-2">
<!-- Patch Test Duration -->
<div class="space-y-2">
<label for="patch-test-duration" class="text-sm font-medium"
>Patch Test Duration (hours)</label
>
<Input
id="patch-test-duration"
type="number"
min="0"
step="1"
placeholder="0"
bind:value={newService.patch_test_duration_hours}
onblur={validatePatchTestField}
class="w-full border-red-500={serviceErrors.patch_test_duration_hours}"
/>
{#if serviceErrors.patch_test_duration_hours}
<p class="text-sm text-red-600">{serviceErrors.patch_test_duration_hours}</p>
{/if}
<p class="text-xs text-gray-500">Hours required before service (0 for none)</p>
</div>
<!-- Minimum Age -->
<div class="space-y-2">
<label for="minimum-age" class="text-sm font-medium">Minimum Age</label>