diff --git a/src/lib/server/db/schema.ts b/src/lib/server/db/schema.ts index c43be12..b22de1f 100644 --- a/src/lib/server/db/schema.ts +++ b/src/lib/server/db/schema.ts @@ -195,6 +195,36 @@ export const installationLog = pgTable( ] ); +// ─── Device Checklists ────────────────────────────────────────────── + +export const deviceChecklists = pgTable( + 'device_checklists', + { + id: uuid('id').defaultRandom().primaryKey(), + deviceId: uuid('device_id') + .notNull() + .references(() => devices.id, { onDelete: 'cascade' }), + title: text('title').notNull(), + createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull() + }, + (table) => [index('device_checklists_device_idx').on(table.deviceId)] +); + +export const checklistItems = pgTable( + 'checklist_items', + { + id: uuid('id').defaultRandom().primaryKey(), + checklistId: uuid('checklist_id') + .notNull() + .references(() => deviceChecklists.id, { onDelete: 'cascade' }), + text: text('text').notNull(), + checked: boolean('checked').default(false).notNull(), + sortOrder: integer('sort_order').default(0).notNull(), + createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull() + }, + (table) => [index('checklist_items_checklist_idx').on(table.checklistId)] +); + // ─── Device Log (append-only repair/operation history) ─────────────── export const deviceLog = pgTable( diff --git a/src/routes/(app)/devices/[id]/+page.server.ts b/src/routes/(app)/devices/[id]/+page.server.ts index cad987b..399a0c1 100644 --- a/src/routes/(app)/devices/[id]/+page.server.ts +++ b/src/routes/(app)/devices/[id]/+page.server.ts @@ -8,9 +8,11 @@ import { components, installationLog, deviceLog, + deviceChecklists, + checklistItems, locations } from '$lib/server/db/schema.js'; -import { eq, desc } from 'drizzle-orm'; +import { eq, desc, sql } from 'drizzle-orm'; import { error, fail, redirect } from '@sveltejs/kit'; import { saveImage, saveDocument, deleteFile } from '$lib/server/uploads.js'; @@ -102,6 +104,30 @@ export const load: PageServerLoad = async ({ params }) => { .where(eq(deviceLog.deviceId, params.id)) .orderBy(desc(deviceLog.performedAt)); + // Checklists + const checklists = await db + .select() + .from(deviceChecklists) + .where(eq(deviceChecklists.deviceId, params.id)) + .orderBy(deviceChecklists.createdAt); + + const checklistIds = checklists.map((c) => c.id); + let itemsByChecklist: Record = {}; + let allItems: Array<{ id: string; checklistId: string; text: string; checked: boolean; sortOrder: number; createdAt: Date }> = []; + + if (checklistIds.length > 0) { + allItems = await db + .select() + .from(checklistItems) + .where(sql`${checklistItems.checklistId} IN ${checklistIds}`) + .orderBy(checklistItems.sortOrder, checklistItems.createdAt); + + for (const item of allItems) { + if (!itemsByChecklist[item.checklistId]) itemsByChecklist[item.checklistId] = []; + itemsByChecklist[item.checklistId].push(item); + } + } + return { device, computerDetails: compDetails, @@ -109,7 +135,11 @@ export const load: PageServerLoad = async ({ params }) => { documents, installedComponents, history, - operationLog + operationLog, + checklists: checklists.map((c) => ({ + ...c, + items: itemsByChecklist[c.id] ?? [] + })) }; }; @@ -219,5 +249,70 @@ export const actions: Actions = { .where(eq(devices.id, params.id)); redirect(303, '/devices'); + }, + + createChecklist: async ({ request, params }) => { + const formData = await request.formData(); + const title = (formData.get('title') as string)?.trim(); + if (!title) return fail(400, { error: 'Checklist title is required' }); + + await db.insert(deviceChecklists).values({ + deviceId: params.id, + title + }); + + return { checklistCreated: true }; + }, + + deleteChecklist: async ({ request }) => { + const formData = await request.formData(); + const checklistId = formData.get('checklistId') as string; + await db.delete(deviceChecklists).where(eq(deviceChecklists.id, checklistId)); + return { checklistDeleted: true }; + }, + + addChecklistItem: async ({ request }) => { + const formData = await request.formData(); + const checklistId = formData.get('checklistId') as string; + const text = (formData.get('text') as string)?.trim(); + if (!text) return fail(400, { error: 'Item text is required' }); + + // Get next sort order + const existing = await db + .select({ sortOrder: checklistItems.sortOrder }) + .from(checklistItems) + .where(eq(checklistItems.checklistId, checklistId)) + .orderBy(desc(checklistItems.sortOrder)) + .limit(1); + + const nextOrder = (existing[0]?.sortOrder ?? -1) + 1; + + await db.insert(checklistItems).values({ + checklistId, + text, + sortOrder: nextOrder + }); + + return { itemAdded: true }; + }, + + toggleChecklistItem: async ({ request }) => { + const formData = await request.formData(); + const itemId = formData.get('itemId') as string; + const checked = formData.get('checked') === 'true'; + + await db + .update(checklistItems) + .set({ checked: !checked }) + .where(eq(checklistItems.id, itemId)); + + return { itemToggled: true }; + }, + + deleteChecklistItem: async ({ request }) => { + const formData = await request.formData(); + const itemId = formData.get('itemId') as string; + await db.delete(checklistItems).where(eq(checklistItems.id, itemId)); + return { itemDeleted: true }; } }; diff --git a/src/routes/(app)/devices/[id]/+page.svelte b/src/routes/(app)/devices/[id]/+page.svelte index f5a9753..a201996 100644 --- a/src/routes/(app)/devices/[id]/+page.svelte +++ b/src/routes/(app)/devices/[id]/+page.svelte @@ -9,6 +9,8 @@ let showDocForm = $state(false); let showDeleteConfirm = $state(false); let showLogForm = $state(false); + let showNewChecklist = $state(false); + let newItemText: Record = $state({}); @@ -327,6 +329,109 @@ {/if} + + +
+
+

Checklists

+ +
+ + {#if showNewChecklist} +
+ + +
+ {/if} + + {#if data.checklists.length === 0} +

No checklists yet.

+ {:else} +
+ {#each data.checklists as checklist} +
+
+

{checklist.title}

+
+ + {checklist.items.filter((i: any) => i.checked).length}/{checklist.items.length} + +
+ + +
+
+
+ + + {#if checklist.items.length > 0} + {@const pct = Math.round((checklist.items.filter((i: any) => i.checked).length / checklist.items.length) * 100)} +
+
+
+ {/if} + + +
+ {#each checklist.items as item} +
+
+ + + +
+ + {item.text} + +
+ + +
+
+ {/each} +
+ + +
{ newItemText[checklist.id] = ''; }} + class="mt-2 flex gap-2"> + + + +
+
+ {/each} +
+ {/if} +
diff --git a/static/favicon.png b/static/favicon.png new file mode 100644 index 0000000..8570073 Binary files /dev/null and b/static/favicon.png differ