Add text input type to checklist items for measurements
Checklist items can now be either 'checkbox' (tick/untick) or 'input' (text value with optional unit). Useful for recording measurements like RPM, wow & flutter, torque, voltage, dB levels, etc. - itemType and unit fields on template_items and checklist_items - Template page shows type selector and unit field when adding items - Device checklist renders input items as labeled text fields with unit - Save button persists measured values - Import copies item types and units from templates Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -213,6 +213,8 @@ export const templateItems = pgTable(
|
|||||||
.notNull()
|
.notNull()
|
||||||
.references(() => checklistTemplates.id, { onDelete: 'cascade' }),
|
.references(() => checklistTemplates.id, { onDelete: 'cascade' }),
|
||||||
text: text('text').notNull(),
|
text: text('text').notNull(),
|
||||||
|
itemType: text('item_type').notNull().default('checkbox'), // 'checkbox' or 'input'
|
||||||
|
unit: text('unit'), // e.g. 'RPM', 'dB', '%'
|
||||||
sortOrder: integer('sort_order').default(0).notNull()
|
sortOrder: integer('sort_order').default(0).notNull()
|
||||||
},
|
},
|
||||||
(table) => [index('template_items_template_idx').on(table.templateId)]
|
(table) => [index('template_items_template_idx').on(table.templateId)]
|
||||||
@@ -241,7 +243,10 @@ export const checklistItems = pgTable(
|
|||||||
.notNull()
|
.notNull()
|
||||||
.references(() => deviceChecklists.id, { onDelete: 'cascade' }),
|
.references(() => deviceChecklists.id, { onDelete: 'cascade' }),
|
||||||
text: text('text').notNull(),
|
text: text('text').notNull(),
|
||||||
|
itemType: text('item_type').notNull().default('checkbox'), // 'checkbox' or 'input'
|
||||||
|
unit: text('unit'), // e.g. 'RPM', 'dB', '%'
|
||||||
checked: boolean('checked').default(false).notNull(),
|
checked: boolean('checked').default(false).notNull(),
|
||||||
|
value: text('value'), // for input type items
|
||||||
sortOrder: integer('sort_order').default(0).notNull(),
|
sortOrder: integer('sort_order').default(0).notNull(),
|
||||||
createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull()
|
createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull()
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ export const load: PageServerLoad = async () => {
|
|||||||
.orderBy(checklistTemplates.title);
|
.orderBy(checklistTemplates.title);
|
||||||
|
|
||||||
const templateIds = templates.map((t) => t.id);
|
const templateIds = templates.map((t) => t.id);
|
||||||
let itemsByTemplate: Record<string, Array<{ id: string; text: string; sortOrder: number }>> = {};
|
let itemsByTemplate: Record<string, Array<{ id: string; text: string; itemType: string; unit: string | null; sortOrder: number }>> = {};
|
||||||
|
|
||||||
if (templateIds.length > 0) {
|
if (templateIds.length > 0) {
|
||||||
const allItems = await db
|
const allItems = await db
|
||||||
@@ -19,6 +19,8 @@ export const load: PageServerLoad = async () => {
|
|||||||
id: templateItems.id,
|
id: templateItems.id,
|
||||||
templateId: templateItems.templateId,
|
templateId: templateItems.templateId,
|
||||||
text: templateItems.text,
|
text: templateItems.text,
|
||||||
|
itemType: templateItems.itemType,
|
||||||
|
unit: templateItems.unit,
|
||||||
sortOrder: templateItems.sortOrder
|
sortOrder: templateItems.sortOrder
|
||||||
})
|
})
|
||||||
.from(templateItems)
|
.from(templateItems)
|
||||||
@@ -65,6 +67,8 @@ export const actions: Actions = {
|
|||||||
const formData = await request.formData();
|
const formData = await request.formData();
|
||||||
const templateId = formData.get('templateId') as string;
|
const templateId = formData.get('templateId') as string;
|
||||||
const text = (formData.get('text') as string)?.trim();
|
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' });
|
if (!text) return fail(400, { error: 'Item text is required' });
|
||||||
|
|
||||||
const existing = await db
|
const existing = await db
|
||||||
@@ -77,6 +81,8 @@ export const actions: Actions = {
|
|||||||
await db.insert(templateItems).values({
|
await db.insert(templateItems).values({
|
||||||
templateId,
|
templateId,
|
||||||
text,
|
text,
|
||||||
|
itemType,
|
||||||
|
unit: unit || null,
|
||||||
sortOrder: (existing[0]?.sortOrder ?? -1) + 1
|
sortOrder: (existing[0]?.sortOrder ?? -1) + 1
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,9 @@
|
|||||||
let showNewTemplate = $state(false);
|
let showNewTemplate = $state(false);
|
||||||
function resetForm(form: HTMLFormElement) {
|
function resetForm(form: HTMLFormElement) {
|
||||||
const input = form.querySelector('input[name="text"]') as HTMLInputElement;
|
const input = form.querySelector('input[name="text"]') as HTMLInputElement;
|
||||||
|
const unit = form.querySelector('input[name="unit"]') as HTMLInputElement;
|
||||||
if (input) input.value = '';
|
if (input) input.value = '';
|
||||||
|
if (unit) unit.value = '';
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -85,6 +87,11 @@
|
|||||||
<div class="group flex items-center gap-2 rounded-md py-1">
|
<div class="group flex items-center gap-2 rounded-md py-1">
|
||||||
<span class="text-sm text-gray-400 dark:text-gray-500">{item.sortOrder + 1}.</span>
|
<span class="text-sm text-gray-400 dark:text-gray-500">{item.sortOrder + 1}.</span>
|
||||||
<span class="flex-1 text-sm text-gray-700 dark:text-gray-300">{item.text}</span>
|
<span class="flex-1 text-sm text-gray-700 dark:text-gray-300">{item.text}</span>
|
||||||
|
{#if item.itemType === 'input'}
|
||||||
|
<span class="rounded bg-purple-100 px-1.5 py-0.5 text-xs text-purple-700 dark:bg-purple-900/40 dark:text-purple-300">
|
||||||
|
input{#if item.unit}: {item.unit}{/if}
|
||||||
|
</span>
|
||||||
|
{/if}
|
||||||
<form method="POST" action="?/deleteItem" use:enhance>
|
<form method="POST" action="?/deleteItem" use:enhance>
|
||||||
<input type="hidden" name="itemId" value={item.id} />
|
<input type="hidden" name="itemId" value={item.id} />
|
||||||
<button type="submit" class="hidden text-gray-400 hover:text-red-500 group-hover:block dark:text-gray-500 dark:hover:text-red-400" title="Remove">
|
<button type="submit" class="hidden text-gray-400 hover:text-red-500 group-hover:block dark:text-gray-500 dark:hover:text-red-400" title="Remove">
|
||||||
@@ -108,12 +115,22 @@
|
|||||||
};
|
};
|
||||||
}}
|
}}
|
||||||
data-template={template.id}
|
data-template={template.id}
|
||||||
class="mt-2 flex gap-2">
|
class="mt-2 space-y-2">
|
||||||
<input type="hidden" name="templateId" value={template.id} />
|
<input type="hidden" name="templateId" value={template.id} />
|
||||||
<input type="text" name="text" required
|
<div class="flex gap-2">
|
||||||
placeholder="Add item..."
|
<input type="text" name="text" required
|
||||||
class="flex-1 rounded-md border border-gray-200 px-3 py-1.5 text-sm focus:border-blue-500 focus:ring-1 focus:ring-blue-500 focus:outline-none dark:border-gray-600 dark:bg-gray-700 dark:text-white dark:placeholder-gray-400" />
|
placeholder="Add item..."
|
||||||
<button type="submit" class="rounded-md border border-gray-300 px-3 py-1.5 text-sm text-gray-600 hover:bg-gray-100 dark:border-gray-600 dark:text-gray-400 dark:hover:bg-gray-700">Add</button>
|
class="flex-1 rounded-md border border-gray-200 px-3 py-1.5 text-sm focus:border-blue-500 focus:ring-1 focus:ring-blue-500 focus:outline-none dark:border-gray-600 dark:bg-gray-700 dark:text-white dark:placeholder-gray-400" />
|
||||||
|
<select name="itemType"
|
||||||
|
class="rounded-md border border-gray-200 px-2 py-1.5 text-sm dark:border-gray-600 dark:bg-gray-700 dark:text-white">
|
||||||
|
<option value="checkbox">Checkbox</option>
|
||||||
|
<option value="input">Text input</option>
|
||||||
|
</select>
|
||||||
|
<input type="text" name="unit"
|
||||||
|
placeholder="Unit"
|
||||||
|
class="w-20 rounded-md border border-gray-200 px-2 py-1.5 text-sm focus:border-blue-500 focus:ring-1 focus:ring-blue-500 focus:outline-none dark:border-gray-600 dark:bg-gray-700 dark:text-white dark:placeholder-gray-400" />
|
||||||
|
<button type="submit" class="rounded-md border border-gray-300 px-3 py-1.5 text-sm text-gray-600 hover:bg-gray-100 dark:border-gray-600 dark:text-gray-400 dark:hover:bg-gray-700">Add</button>
|
||||||
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
{/each}
|
{/each}
|
||||||
|
|||||||
@@ -115,7 +115,7 @@ export const load: PageServerLoad = async ({ params }) => {
|
|||||||
|
|
||||||
const checklistIds = checklists.map((c) => c.id);
|
const checklistIds = checklists.map((c) => c.id);
|
||||||
let itemsByChecklist: Record<string, typeof allItems> = {};
|
let itemsByChecklist: Record<string, typeof allItems> = {};
|
||||||
let allItems: Array<{ id: string; checklistId: string; text: string; checked: boolean; sortOrder: number; createdAt: Date }> = [];
|
let allItems: Array<{ id: string; checklistId: string; text: string; itemType: string; unit: string | null; checked: boolean; value: string | null; sortOrder: number; createdAt: Date }> = [];
|
||||||
|
|
||||||
if (checklistIds.length > 0) {
|
if (checklistIds.length > 0) {
|
||||||
allItems = await db
|
allItems = await db
|
||||||
@@ -284,6 +284,8 @@ export const actions: Actions = {
|
|||||||
const formData = await request.formData();
|
const formData = await request.formData();
|
||||||
const checklistId = formData.get('checklistId') as string;
|
const checklistId = formData.get('checklistId') as string;
|
||||||
const text = (formData.get('text') as string)?.trim();
|
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' });
|
if (!text) return fail(400, { error: 'Item text is required' });
|
||||||
|
|
||||||
// Get next sort order
|
// Get next sort order
|
||||||
@@ -299,6 +301,8 @@ export const actions: Actions = {
|
|||||||
await db.insert(checklistItems).values({
|
await db.insert(checklistItems).values({
|
||||||
checklistId,
|
checklistId,
|
||||||
text,
|
text,
|
||||||
|
itemType,
|
||||||
|
unit: unit || null,
|
||||||
sortOrder: nextOrder
|
sortOrder: nextOrder
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -318,6 +322,19 @@ export const actions: Actions = {
|
|||||||
return { itemToggled: true };
|
return { itemToggled: true };
|
||||||
},
|
},
|
||||||
|
|
||||||
|
saveChecklistValue: async ({ request }) => {
|
||||||
|
const formData = await request.formData();
|
||||||
|
const itemId = formData.get('itemId') as string;
|
||||||
|
const value = formData.get('value') as string;
|
||||||
|
|
||||||
|
await db
|
||||||
|
.update(checklistItems)
|
||||||
|
.set({ value: value || null })
|
||||||
|
.where(eq(checklistItems.id, itemId));
|
||||||
|
|
||||||
|
return { valueSaved: true };
|
||||||
|
},
|
||||||
|
|
||||||
deleteChecklistItem: async ({ request }) => {
|
deleteChecklistItem: async ({ request }) => {
|
||||||
const formData = await request.formData();
|
const formData = await request.formData();
|
||||||
const itemId = formData.get('itemId') as string;
|
const itemId = formData.get('itemId') as string;
|
||||||
@@ -359,6 +376,8 @@ export const actions: Actions = {
|
|||||||
items.map((item) => ({
|
items.map((item) => ({
|
||||||
checklistId: checklist.id,
|
checklistId: checklist.id,
|
||||||
text: item.text,
|
text: item.text,
|
||||||
|
itemType: item.itemType,
|
||||||
|
unit: item.unit,
|
||||||
sortOrder: item.sortOrder
|
sortOrder: item.sortOrder
|
||||||
}))
|
}))
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -13,7 +13,9 @@
|
|||||||
let showImportChecklist = $state(false);
|
let showImportChecklist = $state(false);
|
||||||
function resetInput(formEl: HTMLFormElement) {
|
function resetInput(formEl: HTMLFormElement) {
|
||||||
const input = formEl.querySelector('input[name="text"]') as HTMLInputElement;
|
const input = formEl.querySelector('input[name="text"]') as HTMLInputElement;
|
||||||
|
const unit = formEl.querySelector('input[name="unit"]') as HTMLInputElement;
|
||||||
if (input) input.value = '';
|
if (input) input.value = '';
|
||||||
|
if (unit) unit.value = '';
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -405,36 +407,62 @@
|
|||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<!-- Items -->
|
<!-- Items -->
|
||||||
<div class="space-y-1">
|
<div class="space-y-1.5">
|
||||||
{#each checklist.items as item}
|
{#each checklist.items as item}
|
||||||
<div class="group flex items-center gap-2">
|
{#if item.itemType === 'input'}
|
||||||
<form method="POST" action="?/toggleChecklistItem" use:enhance class="flex items-center">
|
<!-- Input type item -->
|
||||||
<input type="hidden" name="itemId" value={item.id} />
|
<div class="group flex items-center gap-2">
|
||||||
<input type="hidden" name="checked" value={String(item.checked)} />
|
<span class="flex-shrink-0 text-sm text-gray-700 dark:text-gray-300">{item.text}</span>
|
||||||
<button type="submit" class="flex h-5 w-5 flex-shrink-0 items-center justify-center rounded border
|
<form method="POST" action="?/saveChecklistValue" use:enhance class="flex flex-1 items-center gap-1">
|
||||||
{item.checked
|
<input type="hidden" name="itemId" value={item.id} />
|
||||||
? 'border-green-500 bg-green-500 text-white dark:border-green-400 dark:bg-green-500'
|
<input type="text" name="value" value={item.value ?? ''}
|
||||||
: 'border-gray-300 hover:border-blue-400 dark:border-gray-600 dark:hover:border-blue-500'}
|
placeholder="—"
|
||||||
">
|
class="w-24 rounded border border-gray-200 px-2 py-0.5 text-sm text-right font-mono focus:border-blue-500 focus:ring-1 focus:ring-blue-500 focus:outline-none dark:border-gray-600 dark:bg-gray-700 dark:text-white" />
|
||||||
{#if item.checked}
|
{#if item.unit}
|
||||||
<svg class="h-3 w-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
<span class="text-xs text-gray-400 dark:text-gray-500">{item.unit}</span>
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="3" d="M5 13l4 4L19 7" />
|
|
||||||
</svg>
|
|
||||||
{/if}
|
{/if}
|
||||||
</button>
|
<button type="submit" class="rounded px-1.5 py-0.5 text-xs text-blue-600 hover:bg-blue-50 dark:text-blue-400 dark:hover:bg-blue-900/20">Save</button>
|
||||||
</form>
|
</form>
|
||||||
<span class="flex-1 text-sm {item.checked ? 'text-gray-400 line-through dark:text-gray-500' : 'text-gray-700 dark:text-gray-300'}">
|
<form method="POST" action="?/deleteChecklistItem" use:enhance>
|
||||||
{item.text}
|
<input type="hidden" name="itemId" value={item.id} />
|
||||||
</span>
|
<button type="submit" class="hidden text-gray-400 hover:text-red-500 group-hover:block dark:text-gray-500 dark:hover:text-red-400" title="Remove">
|
||||||
<form method="POST" action="?/deleteChecklistItem" use:enhance>
|
<svg class="h-3.5 w-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
<input type="hidden" name="itemId" value={item.id} />
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
|
||||||
<button type="submit" class="hidden text-gray-400 hover:text-red-500 group-hover:block dark:text-gray-500 dark:hover:text-red-400" title="Remove">
|
</svg>
|
||||||
<svg class="h-3.5 w-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
</button>
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
|
</form>
|
||||||
</svg>
|
</div>
|
||||||
</button>
|
{:else}
|
||||||
</form>
|
<!-- Checkbox type item -->
|
||||||
</div>
|
<div class="group flex items-center gap-2">
|
||||||
|
<form method="POST" action="?/toggleChecklistItem" use:enhance class="flex items-center">
|
||||||
|
<input type="hidden" name="itemId" value={item.id} />
|
||||||
|
<input type="hidden" name="checked" value={String(item.checked)} />
|
||||||
|
<button type="submit" class="flex h-5 w-5 flex-shrink-0 items-center justify-center rounded border
|
||||||
|
{item.checked
|
||||||
|
? 'border-green-500 bg-green-500 text-white dark:border-green-400 dark:bg-green-500'
|
||||||
|
: 'border-gray-300 hover:border-blue-400 dark:border-gray-600 dark:hover:border-blue-500'}
|
||||||
|
">
|
||||||
|
{#if item.checked}
|
||||||
|
<svg class="h-3 w-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="3" d="M5 13l4 4L19 7" />
|
||||||
|
</svg>
|
||||||
|
{/if}
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
<span class="flex-1 text-sm {item.checked ? 'text-gray-400 line-through dark:text-gray-500' : 'text-gray-700 dark:text-gray-300'}">
|
||||||
|
{item.text}
|
||||||
|
</span>
|
||||||
|
<form method="POST" action="?/deleteChecklistItem" use:enhance>
|
||||||
|
<input type="hidden" name="itemId" value={item.id} />
|
||||||
|
<button type="submit" class="hidden text-gray-400 hover:text-red-500 group-hover:block dark:text-gray-500 dark:hover:text-red-400" title="Remove">
|
||||||
|
<svg class="h-3.5 w-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -454,6 +482,13 @@
|
|||||||
<input type="text" name="text" required
|
<input type="text" name="text" required
|
||||||
placeholder="Add item..."
|
placeholder="Add item..."
|
||||||
class="flex-1 rounded-md border border-gray-200 px-2 py-1 text-sm focus:border-blue-500 focus:ring-1 focus:ring-blue-500 focus:outline-none dark:border-gray-600 dark:bg-gray-700 dark:text-white dark:placeholder-gray-400" />
|
class="flex-1 rounded-md border border-gray-200 px-2 py-1 text-sm focus:border-blue-500 focus:ring-1 focus:ring-blue-500 focus:outline-none dark:border-gray-600 dark:bg-gray-700 dark:text-white dark:placeholder-gray-400" />
|
||||||
|
<select name="itemType"
|
||||||
|
class="rounded-md border border-gray-200 px-1 py-1 text-xs dark:border-gray-600 dark:bg-gray-700 dark:text-white">
|
||||||
|
<option value="checkbox">Check</option>
|
||||||
|
<option value="input">Input</option>
|
||||||
|
</select>
|
||||||
|
<input type="text" name="unit" placeholder="Unit"
|
||||||
|
class="w-16 rounded-md border border-gray-200 px-2 py-1 text-xs focus:border-blue-500 focus:ring-1 focus:ring-blue-500 focus:outline-none dark:border-gray-600 dark:bg-gray-700 dark:text-white dark:placeholder-gray-400" />
|
||||||
<button type="submit" class="rounded-md px-2 py-1 text-sm text-blue-600 hover:bg-blue-50 dark:text-blue-400 dark:hover:bg-blue-900/20">
|
<button type="submit" class="rounded-md px-2 py-1 text-sm text-blue-600 hover:bg-blue-50 dark:text-blue-400 dark:hover:bg-blue-900/20">
|
||||||
<svg class="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
<svg class="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4" />
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4" />
|
||||||
|
|||||||
Reference in New Issue
Block a user