diff --git a/src/routes/admin/+page.svelte b/src/routes/admin/+page.svelte index 03fbed3..0ea8801 100644 --- a/src/routes/admin/+page.svelte +++ b/src/routes/admin/+page.svelte @@ -18,10 +18,14 @@ // Unit form state let unitForm = $state>({}); let unitFormNew = $state(false); + let unitFormSnapshot = $state>({}); + let unitFieldError = $state(null); // which field has error // Group form state let groupForm = $state>({}); let groupFormNew = $state(false); + let groupFormSnapshot = $state>({}); + let groupFieldError = $state(null); // which field has error // ── Load data ──────────────────────────────────────────────────────────────── async function loadData() { @@ -58,8 +62,11 @@ function selectUnit(unit: Unit) { selectedItem = { type: 'unit', id: unit.id }; unitForm = { ...unit }; + unitFormSnapshot = { ...unit }; unitFormNew = false; groupFormNew = false; + unitFieldError = null; + groupFieldError = null; formError = null; formSuccess = null; } @@ -67,26 +74,147 @@ function selectGroup(group: Group) { selectedItem = { type: 'group', id: group.id }; groupForm = { ...group }; + groupFormSnapshot = { ...group }; groupFormNew = false; unitFormNew = false; + unitFieldError = null; + groupFieldError = null; formError = null; formSuccess = null; } - function startNewUnit(groupId?: string) { + // ── Dirty checks ───────────────────────────────────────────────────────────── + function isUnitDirty(): boolean { + if (!unitFormNew && selectedItem === null) return false; + const s = unitFormSnapshot; + return ( + unitForm.label !== s.label || + unitForm.labelPlural !== s.labelPlural || + unitForm.symbol !== s.symbol || + unitForm.toBase !== s.toBase || + unitForm.group !== s.group || + (unitForm.description ?? '') !== (s.description ?? '') + ); + } + + function isGroupDirty(): boolean { + if (!groupFormNew && selectedItem === null) return false; + const s = groupFormSnapshot; + return ( + groupForm.label !== s.label || + groupForm.id !== s.id + ); + } + + // ── Validate unit form ─────────────────────────────────────────────────────── + function validateUnitForm(): { valid: boolean; firstError: string | null } { + const f = unitForm; + if (!f.id || !/^[a-z][a-z0-9-]*$/.test(f.id)) return { valid: false, firstError: 'unit-id' }; + if (!f.label) return { valid: false, firstError: 'unit-label' }; + if (!f.labelPlural) return { valid: false, firstError: 'unit-labelPlural' }; + if (!f.symbol) return { valid: false, firstError: 'unit-symbol' }; + if (typeof f.toBase !== 'number' || f.toBase <= 0) return { valid: false, firstError: 'unit-toBase' }; + return { valid: true, firstError: null }; + } + + function validateGroupForm(): { valid: boolean; firstError: string | null } { + const f = groupForm; + if (!f.id || !/^[a-z][a-z0-9-]*$/.test(f.id)) return { valid: false, firstError: 'group-id' }; + if (!f.label) return { valid: false, firstError: 'group-label' }; + return { valid: true, firstError: null }; + } + + // ── Auto-save current form if dirty ────────────────────────────────────────── + async function autoSaveCurrentForm(): Promise { + if (unitFormNew || (selectedItem?.type === 'unit')) { + if (!isUnitDirty()) return true; + const v = validateUnitForm(); + if (!v.valid) { + formError = 'Please complete the current form before opening a new one.'; + unitFieldError = v.firstError; + return false; + } + try { + const res = unitFormNew + ? await fetch('/admin/api/units', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(unitForm) + }) + : await fetch(`/admin/api/units/${selectedItem?.id}`, { + method: 'PUT', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(unitForm) + }); + if (!res.ok) { + formError = await extractError(res, 'Auto-save failed'); + return false; + } + await loadData(); + return true; + } catch { + formError = 'Network error during auto-save'; + return false; + } + } + if (groupFormNew || (selectedItem?.type === 'group')) { + if (!isGroupDirty()) return true; + const v = validateGroupForm(); + if (!v.valid) { + formError = 'Please complete the current form before opening a new one.'; + groupFieldError = v.firstError; + return false; + } + try { + const res = groupFormNew + ? await fetch('/admin/api/groups', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(groupForm) + }) + : await fetch(`/admin/api/groups/${selectedItem?.id}`, { + method: 'PUT', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(groupForm) + }); + if (!res.ok) { + formError = await extractError(res, 'Auto-save failed'); + return false; + } + await loadData(); + return true; + } catch { + formError = 'Network error during auto-save'; + return false; + } + } + return true; + } + + async function startNewUnit(groupId?: string) { + const ok = await autoSaveCurrentForm(); + if (!ok) return; selectedItem = null; unitForm = { group: groupId ?? null, toBase: 1, symbol: '', label: '', labelPlural: '', id: '' }; + unitFormSnapshot = { group: groupId ?? null, toBase: 1, symbol: '', label: '', labelPlural: '', id: '' }; unitFormNew = true; groupFormNew = false; + unitFieldError = null; + groupFieldError = null; formError = null; formSuccess = null; } - function startNewGroup() { + async function startNewGroup() { + const ok = await autoSaveCurrentForm(); + if (!ok) return; selectedItem = null; groupForm = { id: '', label: '', baseUnitId: '', toUniversal: 1 }; + groupFormSnapshot = { id: '', label: '', baseUnitId: '', toUniversal: 1 }; groupFormNew = true; unitFormNew = false; + unitFieldError = null; + groupFieldError = null; formError = null; formSuccess = null; } @@ -149,6 +277,13 @@ async function saveUnit() { formError = null; formSuccess = null; + unitFieldError = null; + const v = validateUnitForm(); + if (!v.valid) { + formError = 'Please fix the highlighted field.'; + unitFieldError = v.firstError; + return; + } try { const res = unitFormNew ? await fetch('/admin/api/units', { @@ -167,6 +302,10 @@ const created = (await res.json()) as Unit; unitFormNew = false; selectedItem = { type: 'unit', id: created.id ?? '' }; + unitForm = { ...created }; + unitFormSnapshot = { ...created }; + } else { + unitFormSnapshot = { ...unitForm }; } await loadData(); } catch { formError = 'Network error'; } @@ -188,6 +327,13 @@ async function saveGroup() { formError = null; formSuccess = null; + groupFieldError = null; + const v = validateGroupForm(); + if (!v.valid) { + formError = 'Please fix the highlighted field.'; + groupFieldError = v.firstError; + return; + } try { const res = groupFormNew ? await fetch('/admin/api/groups', { @@ -206,6 +352,10 @@ const created = (await res.json()) as Group; groupFormNew = false; selectedItem = { type: 'group', id: created.id ?? '' }; + groupForm = { ...created }; + groupFormSnapshot = { ...created }; + } else { + groupFormSnapshot = { ...groupForm }; } await loadData(); } catch { formError = 'Network error'; } @@ -434,19 +584,23 @@

{unitFormNew ? 'New Unit' : 'Edit Unit'}