Add batch print page for printing multiple device labels at once
Deploy to LXC / deploy (push) Successful in 20s
Deploy to LXC / deploy (push) Successful in 20s
- /batch-print page with device table, checkboxes, search, category filter - Select all / individual selection with highlighted rows - "Print N Labels" button opens popup with all selected labels - /print/batch renders one DK-11209 label per device with page breaks - Auto-print on popup open, last label avoids trailing blank page - Sidebar nav item added Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,21 @@
|
||||
import type { PageServerLoad } from './$types';
|
||||
import { db } from '$lib/server/db/index.js';
|
||||
import { devices } from '$lib/server/db/schema.js';
|
||||
import { eq } from 'drizzle-orm';
|
||||
|
||||
export const load: PageServerLoad = async () => {
|
||||
const deviceList = await db
|
||||
.select({
|
||||
id: devices.id,
|
||||
title: devices.title,
|
||||
brand: devices.brand,
|
||||
model: devices.model,
|
||||
serialNumber: devices.serialNumber,
|
||||
category: devices.category
|
||||
})
|
||||
.from(devices)
|
||||
.where(eq(devices.disabled, false))
|
||||
.orderBy(devices.title);
|
||||
|
||||
return { devices: deviceList };
|
||||
};
|
||||
@@ -0,0 +1,111 @@
|
||||
<script lang="ts">
|
||||
import { DEVICE_CATEGORIES } from '$lib/constants.js';
|
||||
|
||||
let { data } = $props();
|
||||
|
||||
let selected = $state<Set<string>>(new Set());
|
||||
let categoryFilter = $state('');
|
||||
let search = $state('');
|
||||
|
||||
const filtered = $derived(
|
||||
data.devices.filter((d) => {
|
||||
if (categoryFilter && d.category !== categoryFilter) return false;
|
||||
if (search) {
|
||||
const q = search.toLowerCase();
|
||||
return (
|
||||
d.title.toLowerCase().includes(q) ||
|
||||
(d.brand ?? '').toLowerCase().includes(q) ||
|
||||
(d.model ?? '').toLowerCase().includes(q) ||
|
||||
(d.serialNumber ?? '').toLowerCase().includes(q)
|
||||
);
|
||||
}
|
||||
return true;
|
||||
})
|
||||
);
|
||||
|
||||
function toggleAll() {
|
||||
if (selected.size === filtered.length) {
|
||||
selected = new Set();
|
||||
} else {
|
||||
selected = new Set(filtered.map((d) => d.id));
|
||||
}
|
||||
}
|
||||
|
||||
function toggle(id: string) {
|
||||
const next = new Set(selected);
|
||||
if (next.has(id)) next.delete(id);
|
||||
else next.add(id);
|
||||
selected = next;
|
||||
}
|
||||
|
||||
function printSelected() {
|
||||
const ids = [...selected].join(',');
|
||||
window.open(`/print/batch?ids=${ids}`, '_blank', 'width=600,height=800');
|
||||
}
|
||||
|
||||
const allSelected = $derived(filtered.length > 0 && selected.size === filtered.length);
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>Batch Print - My Collection</title>
|
||||
</svelte:head>
|
||||
|
||||
<div class="mx-auto max-w-4xl">
|
||||
<div class="mb-6 flex flex-wrap items-center justify-between gap-3">
|
||||
<h1 class="text-2xl font-bold text-gray-900 dark:text-white">Batch Print Labels</h1>
|
||||
<button type="button" onclick={printSelected} disabled={selected.size === 0}
|
||||
class="rounded-md bg-blue-600 px-4 py-2 text-sm font-medium text-white hover:bg-blue-700 disabled:opacity-40 disabled:cursor-not-allowed">
|
||||
Print {selected.size} Label{selected.size !== 1 ? 's' : ''}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Filters -->
|
||||
<div class="mb-4 flex flex-wrap items-center gap-3">
|
||||
<input type="text" bind:value={search} placeholder="Search..."
|
||||
class="flex-1 max-w-sm rounded-md border border-gray-300 px-3 py-2 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 bind:value={categoryFilter}
|
||||
class="rounded-md border border-gray-300 px-3 py-2 text-sm dark:border-gray-600 dark:bg-gray-700 dark:text-white">
|
||||
<option value="">All Categories</option>
|
||||
{#each DEVICE_CATEGORIES as cat}
|
||||
<option value={cat}>{cat}</option>
|
||||
{/each}
|
||||
</select>
|
||||
<span class="text-sm text-gray-500 dark:text-gray-400">{filtered.length} device{filtered.length !== 1 ? 's' : ''}</span>
|
||||
</div>
|
||||
|
||||
<!-- Device list -->
|
||||
<div class="overflow-hidden rounded-lg border border-gray-200 bg-white dark:border-gray-700 dark:bg-gray-800">
|
||||
<table class="w-full text-sm">
|
||||
<thead>
|
||||
<tr class="border-b border-gray-100 bg-gray-50 dark:border-gray-700 dark:bg-gray-800/50">
|
||||
<th class="w-10 px-4 py-2">
|
||||
<input type="checkbox" checked={allSelected} onchange={toggleAll}
|
||||
class="h-4 w-4 rounded border-gray-300 dark:border-gray-600" />
|
||||
</th>
|
||||
<th class="px-4 py-2 text-left font-medium text-gray-500 dark:text-gray-400">Device</th>
|
||||
<th class="px-4 py-2 text-left font-medium text-gray-500 dark:text-gray-400">Category</th>
|
||||
<th class="px-4 py-2 text-left font-medium text-gray-500 dark:text-gray-400">Serial</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{#each filtered as device}
|
||||
<tr class="border-b border-gray-100 last:border-0 hover:bg-gray-50 dark:border-gray-700 dark:hover:bg-gray-700/30
|
||||
{selected.has(device.id) ? 'bg-blue-50 dark:bg-blue-900/10' : ''}">
|
||||
<td class="px-4 py-3">
|
||||
<input type="checkbox" checked={selected.has(device.id)} onchange={() => toggle(device.id)}
|
||||
class="h-4 w-4 rounded border-gray-300 dark:border-gray-600" />
|
||||
</td>
|
||||
<td class="px-4 py-3">
|
||||
<span class="font-medium text-gray-900 dark:text-white">{device.title}</span>
|
||||
{#if device.brand || device.model}
|
||||
<span class="ml-1 text-gray-400 dark:text-gray-500">{[device.brand, device.model].filter(Boolean).join(' ')}</span>
|
||||
{/if}
|
||||
</td>
|
||||
<td class="px-4 py-3 text-gray-600 dark:text-gray-400">{device.category}</td>
|
||||
<td class="px-4 py-3 font-mono text-xs text-gray-500 dark:text-gray-400">{device.serialNumber ?? '—'}</td>
|
||||
</tr>
|
||||
{/each}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
Reference in New Issue
Block a user