Polish Store endpoints: 409 conflicts and confirm-before-delete

Backend pre-checks duplicate names on POST/PUT and returns 409
Conflict (replacing the previous 500 from the unique constraint).
DELETE-with-active-lists also returns 409 instead of 400 for
semantic accuracy.

Frontend addStore and saveEdit now surface API errors via toast
instead of failing silently. Delete is now gated by a confirmation
modal so accidental clicks no longer destroy data.
This commit is contained in:
Josh Rogers
2026-05-08 21:07:57 -05:00
parent 7fcae09afb
commit d4db819e72
3 changed files with 114 additions and 19 deletions
+64 -16
View File
@@ -14,6 +14,7 @@
let editingId = $state<number | null>(null);
let editName = $state('');
let loading = $state(true);
let pendingDelete = $state<Store | null>(null);
onMount(async () => {
stores = await api<Store[]>('/api/stores');
@@ -22,12 +23,16 @@
async function addStore() {
if (!newName.trim()) return;
await api('/api/stores', {
method: 'POST',
body: JSON.stringify({ name: newName, sortOrder: stores.length })
});
newName = '';
stores = await api<Store[]>('/api/stores');
try {
await api('/api/stores', {
method: 'POST',
body: JSON.stringify({ name: newName, sortOrder: stores.length })
});
newName = '';
stores = await api<Store[]>('/api/stores');
} catch (e) {
toast.error(e instanceof Error ? e.message : 'Failed to add store');
}
}
function startEdit(store: Store) {
@@ -38,20 +43,31 @@
async function saveEdit() {
if (!editName.trim() || !editingId) return;
const store = stores.find((s) => s.id === editingId)!;
await api(`/api/stores/${editingId}`, {
method: 'PUT',
body: JSON.stringify({ name: editName, sortOrder: store.sortOrder })
});
editingId = null;
stores = await api<Store[]>('/api/stores');
try {
await api(`/api/stores/${editingId}`, {
method: 'PUT',
body: JSON.stringify({ name: editName, sortOrder: store.sortOrder })
});
editingId = null;
stores = await api<Store[]>('/api/stores');
} catch (e) {
toast.error(e instanceof Error ? e.message : 'Failed to update store');
}
}
async function deleteStore(id: number) {
function requestDelete(store: Store) {
pendingDelete = store;
}
async function confirmDelete() {
if (!pendingDelete) return;
const id = pendingDelete.id;
pendingDelete = null;
try {
await api(`/api/stores/${id}`, { method: 'DELETE' });
stores = stores.filter((s) => s.id !== id);
} catch (e: any) {
toast.error(e.message);
} catch (e) {
toast.error(e instanceof Error ? e.message : 'Failed to delete store');
}
}
</script>
@@ -93,10 +109,42 @@
{:else}
<span class="flex-1 font-medium">{store.name}</span>
<button onclick={() => startEdit(store)} class="text-sm text-gray-400">Edit</button>
<button onclick={() => deleteStore(store.id)} class="text-sm text-danger">Delete</button>
<button onclick={() => requestDelete(store)} class="text-sm text-danger">Delete</button>
{/if}
</li>
{/each}
</ul>
{/if}
</div>
{#if pendingDelete}
<div
class="fixed inset-0 z-40 flex items-center justify-center bg-black/40 px-4"
role="dialog"
aria-modal="true"
aria-labelledby="delete-store-title"
>
<div class="w-full max-w-sm rounded-lg bg-white p-5 shadow-xl">
<h3 id="delete-store-title" class="mb-2 text-lg font-semibold">Delete store?</h3>
<p class="mb-5 text-sm text-gray-600">
Delete <span class="font-medium">{pendingDelete.name}</span>? This can't be undone.
</p>
<div class="flex justify-end gap-2">
<button
type="button"
onclick={() => (pendingDelete = null)}
class="rounded-lg border border-gray-300 px-4 py-2 text-sm font-medium text-gray-700"
>
Cancel
</button>
<button
type="button"
onclick={confirmDelete}
class="rounded-lg bg-danger px-4 py-2 text-sm font-semibold text-white"
>
Delete
</button>
</div>
</div>
</div>
{/if}