diff --git a/src/lib/components/layout/Sidebar.svelte b/src/lib/components/layout/Sidebar.svelte index ab812db..e0bfb31 100644 --- a/src/lib/components/layout/Sidebar.svelte +++ b/src/lib/components/layout/Sidebar.svelte @@ -37,6 +37,11 @@ label: 'Locations', icon: 'M17.657 16.657L13.414 20.9a1.998 1.998 0 01-2.827 0l-4.244-4.243a8 8 0 1111.314 0z M15 11a3 3 0 11-6 0 3 3 0 016 0z' }, + { + href: '/checklists', + label: 'Checklists', + icon: 'M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4' + }, { href: '/gallery', label: 'Gallery', diff --git a/src/lib/server/db/schema.ts b/src/lib/server/db/schema.ts index b22de1f..34042b4 100644 --- a/src/lib/server/db/schema.ts +++ b/src/lib/server/db/schema.ts @@ -195,6 +195,29 @@ export const installationLog = pgTable( ] ); +// ─── Checklist Templates ──────────────────────────────────────────── + +export const checklistTemplates = pgTable('checklist_templates', { + id: uuid('id').defaultRandom().primaryKey(), + title: text('title').notNull(), + description: text('description'), + createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(), + updatedAt: timestamp('updated_at', { withTimezone: true }).defaultNow().notNull() +}); + +export const templateItems = pgTable( + 'template_items', + { + id: uuid('id').defaultRandom().primaryKey(), + templateId: uuid('template_id') + .notNull() + .references(() => checklistTemplates.id, { onDelete: 'cascade' }), + text: text('text').notNull(), + sortOrder: integer('sort_order').default(0).notNull() + }, + (table) => [index('template_items_template_idx').on(table.templateId)] +); + // ─── Device Checklists ────────────────────────────────────────────── export const deviceChecklists = pgTable( diff --git a/src/routes/(app)/checklists/+page.server.ts b/src/routes/(app)/checklists/+page.server.ts new file mode 100644 index 0000000..1f2fed5 --- /dev/null +++ b/src/routes/(app)/checklists/+page.server.ts @@ -0,0 +1,92 @@ +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'; + +export const load: PageServerLoad = async () => { + const templates = await db + .select() + .from(checklistTemplates) + .orderBy(checklistTemplates.title); + + const templateIds = templates.map((t) => t.id); + let itemsByTemplate: Record> = {}; + + if (templateIds.length > 0) { + const allItems = await db + .select({ + id: templateItems.id, + templateId: templateItems.templateId, + text: templateItems.text, + sortOrder: templateItems.sortOrder + }) + .from(templateItems) + .where(sql`${templateItems.templateId} IN ${templateIds}`) + .orderBy(templateItems.sortOrder); + + for (const item of allItems) { + if (!itemsByTemplate[item.templateId]) itemsByTemplate[item.templateId] = []; + itemsByTemplate[item.templateId].push(item); + } + } + + return { + templates: templates.map((t) => ({ + ...t, + items: itemsByTemplate[t.id] ?? [] + })) + }; +}; + +export const actions: Actions = { + createTemplate: 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 + }); + + return { created: true }; + }, + + deleteTemplate: 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(); + 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, + sortOrder: (existing[0]?.sortOrder ?? -1) + 1 + }); + + return { itemAdded: 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 new file mode 100644 index 0000000..84c4851 --- /dev/null +++ b/src/routes/(app)/checklists/+page.svelte @@ -0,0 +1,111 @@ + + + + Checklist Templates - B4L Repair + + +
+
+
+

Checklist Templates

+

Create reusable checklists to import into devices.

+
+ +
+ + {#if form?.error} +
{form.error}
+ {/if} + + {#if showNewTemplate} +
+
+
+ + +
+
+ + +
+ +
+
+ {/if} + + {#if data.templates.length === 0} +
+

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

+
+ {:else} +
+ {#each data.templates as template} +
+
+
+

{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} +
+ + +
+
+ {/each} +
+ + +
{ newItemText[template.id] = ''; }} + class="mt-2 flex gap-2"> + + + +
+
+ {/each} +
+ {/if} +
diff --git a/src/routes/(app)/devices/[id]/+page.server.ts b/src/routes/(app)/devices/[id]/+page.server.ts index 399a0c1..0d2bf2c 100644 --- a/src/routes/(app)/devices/[id]/+page.server.ts +++ b/src/routes/(app)/devices/[id]/+page.server.ts @@ -10,6 +10,8 @@ import { deviceLog, deviceChecklists, checklistItems, + checklistTemplates, + templateItems, locations } from '$lib/server/db/schema.js'; import { eq, desc, sql } from 'drizzle-orm'; @@ -128,6 +130,12 @@ export const load: PageServerLoad = async ({ params }) => { } } + // Available templates for import + const templates = await db + .select({ id: checklistTemplates.id, title: checklistTemplates.title }) + .from(checklistTemplates) + .orderBy(checklistTemplates.title); + return { device, computerDetails: compDetails, @@ -139,7 +147,8 @@ export const load: PageServerLoad = async ({ params }) => { checklists: checklists.map((c) => ({ ...c, items: itemsByChecklist[c.id] ?? [] - })) + })), + templates }; }; @@ -314,5 +323,47 @@ export const actions: Actions = { const itemId = formData.get('itemId') as string; await db.delete(checklistItems).where(eq(checklistItems.id, itemId)); return { itemDeleted: true }; + }, + + importChecklist: async ({ request, params }) => { + const formData = await request.formData(); + const templateId = formData.get('templateId') as string; + if (!templateId) return fail(400, { error: 'Select a template' }); + + // Get template + const [template] = await db + .select() + .from(checklistTemplates) + .where(eq(checklistTemplates.id, templateId)); + if (!template) return fail(400, { error: 'Template not found' }); + + // Get template items + const items = await db + .select() + .from(templateItems) + .where(eq(templateItems.templateId, templateId)) + .orderBy(templateItems.sortOrder); + + // Create device checklist + const [checklist] = await db + .insert(deviceChecklists) + .values({ + deviceId: params.id, + title: template.title + }) + .returning({ id: deviceChecklists.id }); + + // Copy items + if (items.length > 0 && checklist) { + await db.insert(checklistItems).values( + items.map((item) => ({ + checklistId: checklist.id, + text: item.text, + sortOrder: item.sortOrder + })) + ); + } + + return { checklistImported: true }; } }; diff --git a/src/routes/(app)/devices/[id]/+page.svelte b/src/routes/(app)/devices/[id]/+page.svelte index a201996..5d03bd5 100644 --- a/src/routes/(app)/devices/[id]/+page.svelte +++ b/src/routes/(app)/devices/[id]/+page.svelte @@ -10,6 +10,7 @@ let showDeleteConfirm = $state(false); let showLogForm = $state(false); let showNewChecklist = $state(false); + let showImportChecklist = $state(false); let newItemText: Record = $state({}); @@ -334,12 +335,33 @@

Checklists

- +
+ {#if data.templates.length > 0} + + {/if} + +
+ {#if showImportChecklist} +
+ + +
+ {/if} + {#if showNewChecklist}