@@ -1,4 +1,6 @@
|
||||
import { randomBytes } from "node:crypto";
|
||||
import QRCode from "qrcode";
|
||||
import { env } from "@/lib/env";
|
||||
|
||||
/**
|
||||
* Operation QR tokens are opaque, URL-safe, high-entropy identifiers
|
||||
@@ -12,3 +14,26 @@ export function generateQrToken(): string {
|
||||
.replace(/\//g, "_")
|
||||
.replace(/=+$/, "");
|
||||
}
|
||||
|
||||
/**
|
||||
* The payload baked into the QR image is a full absolute URL so any phone
|
||||
* camera app (which opens scans in a browser) goes straight to the scan page.
|
||||
* APP_URL must match the externally reachable origin — see docs/DEPLOY.md.
|
||||
*/
|
||||
export function scanUrlForToken(token: string): string {
|
||||
const base = env.APP_URL.replace(/\/+$/, "");
|
||||
return `${base}/op/scan/${token}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the scan URL as a data-URL PNG suitable for <img src=...> on the
|
||||
* admin detail page and, later, on the printable traveler card. Error
|
||||
* correction level M balances density against smudge tolerance on paper.
|
||||
*/
|
||||
export async function renderQrPng(token: string): Promise<string> {
|
||||
return QRCode.toDataURL(scanUrlForToken(token), {
|
||||
errorCorrectionLevel: "M",
|
||||
margin: 1,
|
||||
width: 256,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -235,3 +235,26 @@ export const UpdateOperationSchema = z
|
||||
export const ReorderOperationsSchema = z.object({
|
||||
order: z.array(z.string().min(1)).min(1),
|
||||
});
|
||||
|
||||
// ---- operator scan actions ----------------------------------------------
|
||||
|
||||
// A scan-page "Pause" — stops the clock but does not complete the step. The
|
||||
// operator can enter a partial unit count before dropping the claim.
|
||||
export const ReleaseOperationSchema = z.object({
|
||||
unitsProcessed: z.coerce.number().int().min(0).max(1_000_000).nullable().optional(),
|
||||
note: OptionalText,
|
||||
});
|
||||
|
||||
// A scan-page "Done". If the operation was flagged qcRequired the operator
|
||||
// must stamp an inline pass/fail before we allow the close.
|
||||
export const CloseOperationSchema = z.object({
|
||||
unitsProcessed: z.coerce.number().int().min(0).max(1_000_000).nullable().optional(),
|
||||
note: OptionalText,
|
||||
qc: z
|
||||
.object({
|
||||
passed: z.boolean(),
|
||||
notes: OptionalText,
|
||||
measurements: JsonString,
|
||||
})
|
||||
.optional(),
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user