Distinguish picked-up from removed shopping list items

Soft-remove items via RemovedAt/RemovedByUserId instead of hard
deleting so the row survives for undo and future history reporting.
DELETE now sets the removal fields; a new POST .../restore clears
them. Active list reads (summary, detail, check toggle) filter to
RemovedAt IS NULL. Frontend surfaces an Undo toast on remove and
handles a new ItemRestored SignalR event.
This commit is contained in:
Josh Rogers
2026-05-08 20:07:41 -05:00
parent 9b2db931ee
commit 7fcae09afb
8 changed files with 682 additions and 15 deletions
@@ -4,6 +4,7 @@
import { goto } from '$app/navigation';
import { api } from '$lib/api';
import { startConnection, stopConnection } from '$lib/signalr';
import { toast } from '$lib/toast.svelte';
import type { HubConnection } from '@microsoft/signalr';
interface ListItem {
@@ -70,6 +71,22 @@
items = items.filter((i) => i.id !== data.id);
});
connection.on(
'ItemRestored',
(data: {
id: number;
name: string;
isChecked: boolean;
checkedByUserName: string | null;
sortOrder: number;
recipeTitle: string | null;
}) => {
if (!items.find((i) => i.id === data.id)) {
items = [...items, data];
}
}
);
connection.on('ListUpdated', (data: { id: number; name: string; storeId: number }) => {
if (list) list = { ...list, name: data.name };
});
@@ -98,8 +115,17 @@
await api(`/api/lists/${listId}/items/${itemId}/check`, { method: 'PATCH' });
}
async function removeItem(itemId: number) {
async function removeItem(itemId: number, itemName: string) {
await api(`/api/lists/${listId}/items/${itemId}`, { method: 'DELETE' });
toast.info(`Removed "${itemName}"`, {
duration: 5000,
action: {
label: 'Undo',
onClick: async () => {
await api(`/api/lists/${listId}/items/${itemId}/restore`, { method: 'POST' });
}
}
});
}
async function archiveList() {
@@ -157,7 +183,7 @@
{/if}
</div>
<button
onclick={() => removeItem(item.id)}
onclick={() => removeItem(item.id, item.name)}
class="shrink-0 p-1 text-gray-300 active:text-danger"
aria-label="Remove {item.name}"
>
@@ -192,7 +218,7 @@
{/if}
</div>
<button
onclick={() => removeItem(item.id)}
onclick={() => removeItem(item.id, item.name)}
class="shrink-0 p-1 text-gray-300 active:text-danger"
aria-label="Remove {item.name}"
>