From 57e1d3fcc7444da0c079db151a9409b6d76cb9c5 Mon Sep 17 00:00:00 2001 From: grabowski Date: Wed, 8 Apr 2026 17:27:49 +0700 Subject: [PATCH] Redesign checklists: grid overview + dedicated edit page - /checklists now shows templates in a responsive grid (cards with title, description, item count, last updated) - Clicking a card opens /checklists/[id] dedicated edit page - "New Template" creates and redirects to the edit page - Edit page has: rename title/description, add/edit/delete/reorder items, delete template (redirects back to grid) - Breadcrumb navigation back to grid Co-Authored-By: Claude Opus 4.6 (1M context) --- src/routes/(app)/checklists/+page.server.ts | 129 ++---------- src/routes/(app)/checklists/+page.svelte | 197 ++---------------- .../(app)/checklists/[id]/+page.server.ts | 113 ++++++++++ src/routes/(app)/checklists/[id]/+page.svelte | 188 +++++++++++++++++ 4 files changed, 340 insertions(+), 287 deletions(-) create mode 100644 src/routes/(app)/checklists/[id]/+page.server.ts create mode 100644 src/routes/(app)/checklists/[id]/+page.svelte diff --git a/src/routes/(app)/checklists/+page.server.ts b/src/routes/(app)/checklists/+page.server.ts index 049e5ef..adae777 100644 --- a/src/routes/(app)/checklists/+page.server.ts +++ b/src/routes/(app)/checklists/+page.server.ts @@ -1,8 +1,8 @@ import type { PageServerLoad, Actions } from './$types'; import { db } from '$lib/server/db/index.js'; import { checklistTemplates, templateItems } from '$lib/server/db/schema.js'; -import { eq, desc, sql } from 'drizzle-orm'; -import { fail } from '@sveltejs/kit'; +import { eq, count, sql } from 'drizzle-orm'; +import { fail, redirect } from '@sveltejs/kit'; export const load: PageServerLoad = async () => { const templates = await db @@ -10,144 +10,49 @@ export const load: PageServerLoad = async () => { .from(checklistTemplates) .orderBy(checklistTemplates.title); + // Get item counts per template const templateIds = templates.map((t) => t.id); - let itemsByTemplate: Record> = {}; + let itemCounts: Record = {}; if (templateIds.length > 0) { - const allItems = await db - .select({ - id: templateItems.id, - templateId: templateItems.templateId, - text: templateItems.text, - itemType: templateItems.itemType, - unit: templateItems.unit, - sortOrder: templateItems.sortOrder - }) + const counts = await db + .select({ templateId: templateItems.templateId, count: count() }) .from(templateItems) .where(sql`${templateItems.templateId} IN ${templateIds}`) - .orderBy(templateItems.sortOrder); + .groupBy(templateItems.templateId); - for (const item of allItems) { - if (!itemsByTemplate[item.templateId]) itemsByTemplate[item.templateId] = []; - itemsByTemplate[item.templateId].push(item); + for (const c of counts) { + itemCounts[c.templateId] = c.count; } } return { templates: templates.map((t) => ({ ...t, - items: itemsByTemplate[t.id] ?? [] + itemCount: itemCounts[t.id] ?? 0 })) }; }; export const actions: Actions = { - createTemplate: async ({ request }) => { + create: async ({ request }) => { const formData = await request.formData(); 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.insert(checklistTemplates).values({ - title, - description: description || null - }); + const [template] = await db + .insert(checklistTemplates) + .values({ title, description: description || null }) + .returning({ id: checklistTemplates.id }); - return { created: true }; + redirect(303, `/checklists/${template!.id}`); }, - deleteTemplate: async ({ request }) => { + delete: async ({ request }) => { const formData = await request.formData(); const templateId = formData.get('templateId') as string; await db.delete(checklistTemplates).where(eq(checklistTemplates.id, templateId)); return { deleted: true }; - }, - - addItem: async ({ request }) => { - const formData = await request.formData(); - const templateId = formData.get('templateId') as string; - const text = (formData.get('text') as string)?.trim(); - const itemType = (formData.get('itemType') as string) || 'checkbox'; - const unit = (formData.get('unit') as string)?.trim(); - if (!text) return fail(400, { error: 'Item text is required' }); - - const existing = await db - .select({ sortOrder: templateItems.sortOrder }) - .from(templateItems) - .where(eq(templateItems.templateId, templateId)) - .orderBy(desc(templateItems.sortOrder)) - .limit(1); - - await db.insert(templateItems).values({ - templateId, - text, - itemType, - unit: unit || null, - sortOrder: (existing[0]?.sortOrder ?? -1) + 1 - }); - - 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; - await db.delete(templateItems).where(eq(templateItems.id, itemId)); - return { itemDeleted: true }; } }; diff --git a/src/routes/(app)/checklists/+page.svelte b/src/routes/(app)/checklists/+page.svelte index 0802cc4..fa463ba 100644 --- a/src/routes/(app)/checklists/+page.svelte +++ b/src/routes/(app)/checklists/+page.svelte @@ -1,16 +1,9 @@ @@ -23,9 +16,9 @@

Checklist Templates

Create reusable checklists to import into devices.

- @@ -33,9 +26,9 @@
{form.error}
{/if} - {#if showNewTemplate} + {#if showNewForm}
-
+
- +
{/if} - {#if data.templates.length === 0} + {#if data.templates.length === 0 && !showNewForm}

No templates yet. Create your first checklist template to reuse across devices.

- {:else} -
+ {:else if data.templates.length > 0} +
{#each data.templates as template} -
- - {#if editingTemplateId === template.id} -
{ - return async ({ update, result }) => { - await update(); - if (result.type === 'success') editingTemplateId = null; - }; - }} class="mb-3 flex flex-wrap items-end gap-2"> - -
- - -
-
- - -
- - -
- {:else} -
-
-

{template.title}

- {#if template.description} -

{template.description}

- {/if} -
-
- - {template.items.length} item{template.items.length !== 1 ? 's' : ''} - - -
- - -
-
-
- {/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} + +
+

{template.title}

+ + {template.itemCount} item{template.itemCount !== 1 ? 's' : ''} +
- - -
{ - return async ({ update, result }) => { - await update(); - if (result.type === 'success') { - const form = document.querySelector(`form[data-template="${template.id}"]`) as HTMLFormElement; - if (form) resetForm(form); - } - }; - }} - data-template={template.id} - class="mt-2 space-y-2"> - -
- - - - -
-
-
+ {#if template.description} +

{template.description}

+ {/if} +

Updated {formatDate(template.updatedAt)}

+
{/each}
{/if} diff --git a/src/routes/(app)/checklists/[id]/+page.server.ts b/src/routes/(app)/checklists/[id]/+page.server.ts new file mode 100644 index 0000000..c91e9fa --- /dev/null +++ b/src/routes/(app)/checklists/[id]/+page.server.ts @@ -0,0 +1,113 @@ +import type { PageServerLoad, Actions } from './$types'; +import { db } from '$lib/server/db/index.js'; +import { checklistTemplates, templateItems } from '$lib/server/db/schema.js'; +import { eq, desc } from 'drizzle-orm'; +import { error, fail, redirect } from '@sveltejs/kit'; + +export const load: PageServerLoad = async ({ params }) => { + const [template] = await db + .select() + .from(checklistTemplates) + .where(eq(checklistTemplates.id, params.id)); + + if (!template) error(404, 'Template not found'); + + const items = await db + .select() + .from(templateItems) + .where(eq(templateItems.templateId, params.id)) + .orderBy(templateItems.sortOrder); + + return { template, items }; +}; + +export const actions: Actions = { + rename: async ({ request, params }) => { + const formData = await request.formData(); + 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, params.id)); + return { renamed: true }; + }, + + delete: async ({ params }) => { + await db.delete(checklistTemplates).where(eq(checklistTemplates.id, params.id)); + redirect(303, '/checklists'); + }, + + addItem: async ({ request, params }) => { + const formData = await request.formData(); + const text = (formData.get('text') as string)?.trim(); + const itemType = (formData.get('itemType') as string) || 'checkbox'; + const unit = (formData.get('unit') as string)?.trim(); + if (!text) return fail(400, { error: 'Item text is required' }); + + const existing = await db + .select({ sortOrder: templateItems.sortOrder }) + .from(templateItems) + .where(eq(templateItems.templateId, params.id)) + .orderBy(desc(templateItems.sortOrder)) + .limit(1); + + await db.insert(templateItems).values({ + templateId: params.id, + text, + itemType, + unit: unit || null, + sortOrder: (existing[0]?.sortOrder ?? -1) + 1 + }); + + return { itemAdded: 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, params }) => { + const formData = await request.formData(); + const itemId = formData.get('itemId') as string; + const direction = formData.get('direction') as string; + + const items = await db + .select({ id: templateItems.id, sortOrder: templateItems.sortOrder }) + .from(templateItems) + .where(eq(templateItems.templateId, params.id)) + .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 }; + + 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; + await db.delete(templateItems).where(eq(templateItems.id, itemId)); + return { itemDeleted: true }; + } +}; diff --git a/src/routes/(app)/checklists/[id]/+page.svelte b/src/routes/(app)/checklists/[id]/+page.svelte new file mode 100644 index 0000000..1f16aab --- /dev/null +++ b/src/routes/(app)/checklists/[id]/+page.svelte @@ -0,0 +1,188 @@ + + + + {data.template.title} - Checklists - B4L Repair + + +
+ +
+ Checklists + +
+ + + {#if editingTitle} +
{ + return async ({ update, result }) => { + await update(); + if (result.type === 'success') editingTitle = false; + }; + }} class="mb-6 rounded-lg border border-gray-200 bg-white p-5 dark:border-gray-700 dark:bg-gray-800"> +
+
+ + +
+
+ + +
+
+
+ + +
+
+ {:else} +
+
+

{data.template.title}

+ {#if data.template.description} +

{data.template.description}

+ {/if} +
+
+ +
+ +
+
+
+ {/if} + + {#if form?.error} +
{form.error}
+ {/if} + + +
+

+ Items ({data.items.length}) +

+ + {#if data.items.length === 0} +

No items yet. Add your first item below.

+ {:else} +
+ {#each data.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 < data.items.length - 1} +
+ + + +
+ {:else}
{/if} +
+ + + {idx + 1}. + + + {item.text} + + + {#if item.itemType === 'input'} + + input{#if item.unit}: {item.unit}{/if} + + {/if} + + + + + +
+ + +
+
+ {/if} + {/each} +
+ {/if} + + +
{ + return async ({ update, result }) => { + await update(); + if (result.type === 'success') resetAddForm(); + }; + }} data-add-item class="border-t border-gray-100 pt-3 dark:border-gray-700"> +
+ + + + +
+
+
+