From 68abb3e73405d3565e46d9fa9e059c3191249559 Mon Sep 17 00:00:00 2001 From: grabowski Date: Wed, 8 Apr 2026 09:47:51 +0700 Subject: [PATCH] Add checklist editing: rename, edit items, reorder with arrows Templates page (/checklists): - Edit template name/description inline (pencil icon) - Edit item text and unit inline (pencil icon on hover) - Move items up/down with arrow buttons - Reorder swaps sort order values in the database Device checklists: - Rename checklist title inline (pencil icon) - Edit item text and unit inline (pencil icon on hover) - Move items up/down with arrow buttons - All item types (checkbox and input) support editing Co-Authored-By: Claude Opus 4.6 (1M context) --- src/routes/(app)/checklists/+page.server.ts | 55 ++++++ src/routes/(app)/checklists/+page.svelte | 156 ++++++++++++---- src/routes/(app)/devices/[id]/+page.server.ts | 53 ++++++ src/routes/(app)/devices/[id]/+page.svelte | 170 ++++++++++++------ 4 files changed, 341 insertions(+), 93 deletions(-) diff --git a/src/routes/(app)/checklists/+page.server.ts b/src/routes/(app)/checklists/+page.server.ts index 0842847..049e5ef 100644 --- a/src/routes/(app)/checklists/+page.server.ts +++ b/src/routes/(app)/checklists/+page.server.ts @@ -89,6 +89,61 @@ export const actions: Actions = { return { itemAdded: true }; }, + renameTemplate: async ({ request }) => { + const formData = await request.formData(); + const templateId = formData.get('templateId') as string; + const title = (formData.get('title') as string)?.trim(); + const description = (formData.get('description') as string)?.trim(); + if (!title) return fail(400, { error: 'Title is required' }); + + await db + .update(checklistTemplates) + .set({ title, description: description || null, updatedAt: new Date() }) + .where(eq(checklistTemplates.id, templateId)); + return { renamed: true }; + }, + + editItem: async ({ request }) => { + const formData = await request.formData(); + const itemId = formData.get('itemId') as string; + const text = (formData.get('text') as string)?.trim(); + const unit = (formData.get('unit') as string)?.trim(); + if (!text) return fail(400, { error: 'Item text is required' }); + + await db + .update(templateItems) + .set({ text, unit: unit || null }) + .where(eq(templateItems.id, itemId)); + return { itemEdited: true }; + }, + + moveItem: async ({ request }) => { + const formData = await request.formData(); + const itemId = formData.get('itemId') as string; + const direction = formData.get('direction') as string; // 'up' or 'down' + const templateId = formData.get('templateId') as string; + + const items = await db + .select({ id: templateItems.id, sortOrder: templateItems.sortOrder }) + .from(templateItems) + .where(eq(templateItems.templateId, templateId)) + .orderBy(templateItems.sortOrder); + + const idx = items.findIndex((i) => i.id === itemId); + if (idx < 0) return fail(400, { error: 'Item not found' }); + + const swapIdx = direction === 'up' ? idx - 1 : idx + 1; + if (swapIdx < 0 || swapIdx >= items.length) return { moved: true }; + + // Swap sort orders + const a = items[idx]; + const b = items[swapIdx]; + await db.update(templateItems).set({ sortOrder: b.sortOrder }).where(eq(templateItems.id, a.id)); + await db.update(templateItems).set({ sortOrder: a.sortOrder }).where(eq(templateItems.id, b.id)); + + return { moved: true }; + }, + deleteItem: async ({ request }) => { const formData = await request.formData(); const itemId = formData.get('itemId') as string; diff --git a/src/routes/(app)/checklists/+page.svelte b/src/routes/(app)/checklists/+page.svelte index 4c3c6de..0802cc4 100644 --- a/src/routes/(app)/checklists/+page.svelte +++ b/src/routes/(app)/checklists/+page.svelte @@ -3,6 +3,8 @@ let { data, form } = $props(); let showNewTemplate = $state(false); + let editingTemplateId = $state(null); + let editingItemId = $state(null); function resetForm(form: HTMLFormElement) { const input = form.querySelector('input[name="text"]') as HTMLInputElement; const unit = form.querySelector('input[name="unit"]') as HTMLInputElement; @@ -59,48 +61,132 @@
{#each data.templates as template}
-
-
-

{template.title}

- {#if template.description} -

{template.description}

- {/if} -
-
- - {template.items.length} item{template.items.length !== 1 ? 's' : ''} - -
- - + +
+ {:else} +
+
+

{template.title}

+ {#if template.description} +

{template.description}

+ {/if} +
+
+ + {template.items.length} item{template.items.length !== 1 ? 's' : ''} + + - -
-
- - -
- {#each template.items as item} -
- {item.sortOrder + 1}. - {item.text} - {#if item.itemType === 'input'} - - input{#if item.unit}: {item.unit}{/if} - - {/if} -
- -
+
+ {/if} + + +
+ {#each template.items as item, idx} + {#if editingItemId === item.id} + + { + return async ({ update, result }) => { + await update(); + if (result.type === 'success') editingItemId = null; + }; + }} class="flex items-center gap-2 rounded-md bg-gray-50 p-2 dark:bg-gray-700/30"> + + + {#if item.itemType === 'input'} + + {/if} + + + + {:else} +
+ +
+ {#if idx > 0} +
+ + + + +
+ {:else} +
+ {/if} + {#if idx < template.items.length - 1} +
+ + + + +
+ {:else} +
+ {/if} +
+ {item.text} + {#if item.itemType === 'input'} + + input{#if item.unit}: {item.unit}{/if} + + {/if} + +
+ + +
+
+ {/if} {/each}
diff --git a/src/routes/(app)/devices/[id]/+page.server.ts b/src/routes/(app)/devices/[id]/+page.server.ts index bef46df..a48db7f 100644 --- a/src/routes/(app)/devices/[id]/+page.server.ts +++ b/src/routes/(app)/devices/[id]/+page.server.ts @@ -351,6 +351,59 @@ export const actions: Actions = { return { valueSaved: true }; }, + editChecklistItem: async ({ request }) => { + const formData = await request.formData(); + const itemId = formData.get('itemId') as string; + const text = (formData.get('text') as string)?.trim(); + const unit = (formData.get('unit') as string)?.trim(); + if (!text) return fail(400, { error: 'Item text is required' }); + + await db + .update(checklistItems) + .set({ text, unit: unit || null }) + .where(eq(checklistItems.id, itemId)); + return { itemEdited: true }; + }, + + moveChecklistItem: async ({ request }) => { + const formData = await request.formData(); + const itemId = formData.get('itemId') as string; + const direction = formData.get('direction') as string; + const checklistId = formData.get('checklistId') as string; + + const items = await db + .select({ id: checklistItems.id, sortOrder: checklistItems.sortOrder }) + .from(checklistItems) + .where(eq(checklistItems.checklistId, checklistId)) + .orderBy(checklistItems.sortOrder); + + const idx = items.findIndex((i) => i.id === itemId); + if (idx < 0) return fail(400, { error: 'Item not found' }); + + const swapIdx = direction === 'up' ? idx - 1 : idx + 1; + if (swapIdx < 0 || swapIdx >= items.length) return { moved: true }; + + const a = items[idx]; + const b = items[swapIdx]; + await db.update(checklistItems).set({ sortOrder: b.sortOrder }).where(eq(checklistItems.id, a.id)); + await db.update(checklistItems).set({ sortOrder: a.sortOrder }).where(eq(checklistItems.id, b.id)); + + return { moved: true }; + }, + + renameChecklist: async ({ request }) => { + const formData = await request.formData(); + const checklistId = formData.get('checklistId') as string; + const title = (formData.get('title') as string)?.trim(); + if (!title) return fail(400, { error: 'Title is required' }); + + await db + .update(deviceChecklists) + .set({ title }) + .where(eq(deviceChecklists.id, checklistId)); + return { checklistRenamed: true }; + }, + deleteChecklistItem: async ({ request }) => { const formData = await request.formData(); const itemId = formData.get('itemId') as string; diff --git a/src/routes/(app)/devices/[id]/+page.svelte b/src/routes/(app)/devices/[id]/+page.svelte index 67c34f7..d146303 100644 --- a/src/routes/(app)/devices/[id]/+page.svelte +++ b/src/routes/(app)/devices/[id]/+page.svelte @@ -11,6 +11,8 @@ let showLogForm = $state(false); let showNewChecklist = $state(false); let showImportChecklist = $state(false); + let editingChecklistId = $state(null); + let editingChecklistItemId = $state(null); function resetInput(formEl: HTMLFormElement) { const input = formEl.querySelector('input[name="text"]') as HTMLInputElement; const unit = formEl.querySelector('input[name="unit"]') as HTMLInputElement; @@ -390,11 +392,33 @@ {#each data.checklists as checklist}
-

{checklist.title}

+ {#if editingChecklistId === checklist.id} +
{ + return async ({ update, result }) => { + await update(); + if (result.type === 'success') editingChecklistId = null; + }; + }} class="flex flex-1 items-center gap-2"> + + + + +
+ {:else} +

{checklist.title}

+ {/if}
{checklist.items.filter((i: any) => i.itemType === 'input' ? !!i.value : i.checked).length}/{checklist.items.length} + {#if editingChecklistId !== checklist.id} + + {/if}
-
- - {:else} - -
- - - -
- - {item.text} - - {/if} -
+ {#each checklist.items as item, idx} + {#if editingChecklistItemId === item.id} + + { + return async ({ update, result }) => { + await update(); + if (result.type === 'success') editingChecklistItemId = null; + }; + }} class="flex items-center gap-2 rounded-md bg-gray-50 p-2 dark:bg-gray-700/30"> - + + {#if item.itemType === 'input'} + + {/if} + +
-
+ {:else} +
+ +
+ {#if idx > 0} +
+ + + + +
+ {:else}
{/if} + {#if idx < checklist.items.length - 1} +
+ + + + +
+ {:else}
{/if} +
+ + {#if item.itemType === 'input'} + {@const filled = !!item.value} + + {#if filled}{/if} + +
+ + {item.text} +
+ + {#if item.unit}{item.unit}{/if} + +
+
+ {:else} +
+ + + +
+ {item.text} + {/if} + + +
+ +
+ + +
+
+
+ {/if} {/each}