From 510eb719ebebbf1d305390c032725679272e12fd Mon Sep 17 00:00:00 2001 From: grabowski Date: Thu, 9 Apr 2026 18:02:13 +0700 Subject: [PATCH] Add batch print page for printing multiple device labels at once - /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) --- src/lib/components/layout/Sidebar.svelte | 5 + src/routes/(app)/batch-print/+page.server.ts | 21 ++++ src/routes/(app)/batch-print/+page.svelte | 111 ++++++++++++++++++ .../(print)/print/batch/+page.server.ts | 37 ++++++ src/routes/(print)/print/batch/+page.svelte | 80 +++++++++++++ 5 files changed, 254 insertions(+) create mode 100644 src/routes/(app)/batch-print/+page.server.ts create mode 100644 src/routes/(app)/batch-print/+page.svelte create mode 100644 src/routes/(print)/print/batch/+page.server.ts create mode 100644 src/routes/(print)/print/batch/+page.svelte diff --git a/src/lib/components/layout/Sidebar.svelte b/src/lib/components/layout/Sidebar.svelte index 0120c7b..11e850d 100644 --- a/src/lib/components/layout/Sidebar.svelte +++ b/src/lib/components/layout/Sidebar.svelte @@ -48,6 +48,11 @@ 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: '/batch-print', + label: 'Batch Print', + icon: 'M17 17h2a2 2 0 002-2v-4a2 2 0 00-2-2H5a2 2 0 00-2 2v4a2 2 0 002 2h2m2 4h6a2 2 0 002-2v-4a2 2 0 00-2-2H9a2 2 0 00-2 2v4a2 2 0 002 2zm8-12V5a2 2 0 00-2-2H9a2 2 0 00-2 2v4h10z' + }, { href: '/gallery', label: 'Gallery', diff --git a/src/routes/(app)/batch-print/+page.server.ts b/src/routes/(app)/batch-print/+page.server.ts new file mode 100644 index 0000000..4710a36 --- /dev/null +++ b/src/routes/(app)/batch-print/+page.server.ts @@ -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 }; +}; diff --git a/src/routes/(app)/batch-print/+page.svelte b/src/routes/(app)/batch-print/+page.svelte new file mode 100644 index 0000000..cd4d2d0 --- /dev/null +++ b/src/routes/(app)/batch-print/+page.svelte @@ -0,0 +1,111 @@ + + + + Batch Print - My Collection + + +
+
+

Batch Print Labels

+ +
+ + +
+ + + {filtered.length} device{filtered.length !== 1 ? 's' : ''} +
+ + +
+ + + + + + + + + + + {#each filtered as device} + + + + + + + {/each} + +
+ + DeviceCategorySerial
+ toggle(device.id)} + class="h-4 w-4 rounded border-gray-300 dark:border-gray-600" /> + + {device.title} + {#if device.brand || device.model} + {[device.brand, device.model].filter(Boolean).join(' ')} + {/if} + {device.category}{device.serialNumber ?? '—'}
+
+
diff --git a/src/routes/(print)/print/batch/+page.server.ts b/src/routes/(print)/print/batch/+page.server.ts new file mode 100644 index 0000000..e953869 --- /dev/null +++ b/src/routes/(print)/print/batch/+page.server.ts @@ -0,0 +1,37 @@ +import type { PageServerLoad } from './$types'; +import { db } from '$lib/server/db/index.js'; +import { devices } from '$lib/server/db/schema.js'; +import { sql } from 'drizzle-orm'; +import { error } from '@sveltejs/kit'; +import { generateQrSvg } from '$lib/server/qr.js'; + +export const load: PageServerLoad = async ({ url }) => { + const idsParam = url.searchParams.get('ids'); + if (!idsParam) error(400, 'No devices selected'); + + const ids = idsParam.split(',').filter(Boolean); + if (ids.length === 0) error(400, 'No devices selected'); + + 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(sql`${devices.id} IN ${ids}`); + + // Generate QR codes for each + const labels = await Promise.all( + deviceList.map(async (device) => { + const shortId = device.id.slice(0, 8).toUpperCase(); + const qrSvg = await generateQrSvg(shortId); + return { ...device, qrSvg, shortId }; + }) + ); + + return { labels }; +}; diff --git a/src/routes/(print)/print/batch/+page.svelte b/src/routes/(print)/print/batch/+page.svelte new file mode 100644 index 0000000..52a91b8 --- /dev/null +++ b/src/routes/(print)/print/batch/+page.svelte @@ -0,0 +1,80 @@ + + + + Batch Print - {data.labels.length} Labels + + +
+
+ ← Back + +
+
+ Setup: Brother QL-820NWB, paper "29mm x 62mm" (DK-11209), margins minimum. +
+
+ +{#each data.labels as label} +
+
+
+ {@html label.qrSvg} +
+
+
+ {label.title} +
+ {#if label.brand || label.model} +
+ {[label.brand, label.model].filter(Boolean).join(' ')} +
+ {/if} + {#if label.serialNumber} +
+ S/N: {label.serialNumber} +
+ {/if} +
+ {label.shortId} +
+
+
+
+{/each} + +