Add checklist templates with import into devices

- checklist_templates and template_items tables for reusable checklists
- /checklists page: create/edit/delete templates with ordered items
- "Import" button on device detail imports a template as a new checklist
  with all items copied (unchecked)
- Sidebar nav item for Checklists between Locations and Gallery

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-07 10:36:02 +07:00
parent dbe82c1019
commit 1351b77034
6 changed files with 309 additions and 5 deletions
+52 -1
View File
@@ -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 };
}
};
+26 -4
View File
@@ -10,6 +10,7 @@
let showDeleteConfirm = $state(false);
let showLogForm = $state(false);
let showNewChecklist = $state(false);
let showImportChecklist = $state(false);
let newItemText: Record<string, string> = $state({});
</script>
@@ -334,12 +335,33 @@
<div class="rounded-lg border border-gray-200 bg-white p-5 dark:border-gray-700 dark:bg-gray-800">
<div class="mb-3 flex items-center justify-between">
<h2 class="text-sm font-semibold uppercase tracking-wider text-gray-400 dark:text-gray-500">Checklists</h2>
<button onclick={() => (showNewChecklist = !showNewChecklist)}
class="text-sm text-blue-600 hover:text-blue-700 dark:text-blue-400">
{showNewChecklist ? 'Cancel' : 'New Checklist'}
</button>
<div class="flex gap-2">
{#if data.templates.length > 0}
<button onclick={() => { showImportChecklist = !showImportChecklist; showNewChecklist = false; }}
class="text-sm text-blue-600 hover:text-blue-700 dark:text-blue-400">
{showImportChecklist ? 'Cancel' : 'Import'}
</button>
{/if}
<button onclick={() => { showNewChecklist = !showNewChecklist; showImportChecklist = false; }}
class="text-sm text-blue-600 hover:text-blue-700 dark:text-blue-400">
{showNewChecklist ? 'Cancel' : 'New'}
</button>
</div>
</div>
{#if showImportChecklist}
<form method="POST" action="?/importChecklist" use:enhance class="mb-4 flex gap-2">
<select name="templateId" required
class="flex-1 rounded-md border border-gray-300 px-3 py-1.5 text-sm dark:border-gray-600 dark:bg-gray-700 dark:text-white">
<option value="">Select template...</option>
{#each data.templates as t}
<option value={t.id}>{t.title}</option>
{/each}
</select>
<button type="submit" class="rounded-md bg-blue-600 px-3 py-1.5 text-sm font-medium text-white hover:bg-blue-700">Import</button>
</form>
{/if}
{#if showNewChecklist}
<form method="POST" action="?/createChecklist" use:enhance class="mb-4 flex gap-2">
<input type="text" name="title" required placeholder="Checklist name..."