Add batch print page for printing multiple device labels at once
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:
2026-04-09 18:02:13 +07:00
parent c7f7be64c4
commit 510eb719eb
5 changed files with 254 additions and 0 deletions
@@ -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 };
};
+111
View File
@@ -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>