inventory: barcode scanner in Add-item dialog

Add a camera barcode/QR scanner (components/scan/barcode-scanner.tsx,
@zxing/browser lazy-loaded) and a Barcode/NDC field to the pharmacy
Add-item dialog. Scanning a GS1 DataMatrix auto-fills expiry (AI 17) and
lot (AI 10 -> notes), with the GTIN (AI 01) as the barcode; plain 1D
barcodes drop straight into the field. Persists a nullable inventory
barcode column (migration 0036) through the type/validation/service, and
surfaces it on the item detail. i18n added to all locales.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Khalid Abdi
2026-07-18 23:32:45 +03:00
parent bac3e7a1d0
commit 0754ce96a4
19 changed files with 5067 additions and 2 deletions
@@ -0,0 +1 @@
ALTER TABLE "inventory" ADD COLUMN "barcode" text;
File diff suppressed because it is too large Load Diff
+7
View File
@@ -253,6 +253,13 @@
"when": 1783530491321,
"tag": "0035_slippery_retro_girl",
"breakpoints": true
},
{
"idx": 36,
"version": "7",
"when": 1784406410448,
"tag": "0036_aspiring_expediter",
"breakpoints": true
}
]
}
+2
View File
@@ -29,6 +29,8 @@ export const inventory = pgTable(
stockQuantity: integer("stock_quantity").notNull().default(0),
reorderThreshold: integer("reorder_threshold").notNull().default(0),
location: text("location").notNull().default(""),
// Scanned medication barcode / NDC (GTIN when read from a GS1 DataMatrix).
barcode: text("barcode"),
expiresAt: date("expires_at"),
notes: text("notes"),
createdBy: text("created_by").references(() => user.id, {
+1
View File
@@ -13,6 +13,7 @@ export const inventoryInputSchema = z.object({
stockQuantity: z.number().int().min(0).max(1_000_000).default(0),
reorderThreshold: z.number().int().min(0).max(1_000_000).default(0),
location: z.string().trim().max(200).default(""),
barcode: z.string().trim().max(120).nullish(),
expiresAt: z
.string()
.regex(/^\d{4}-\d{2}-\d{2}$/, "Date must be YYYY-MM-DD.")
+2
View File
@@ -21,6 +21,7 @@ function toInventoryItem(row: InventoryRow): InventoryItem {
stockQuantity: row.stockQuantity,
reorderThreshold: row.reorderThreshold,
location: row.location,
barcode: row.barcode,
expiresAt: row.expiresAt,
notes: row.notes,
createdAt: row.createdAt.toISOString(),
@@ -38,6 +39,7 @@ function columns(orgId: string, input: InventoryInput, createdBy?: string) {
stockQuantity: input.stockQuantity,
reorderThreshold: input.reorderThreshold,
location: input.location,
barcode: input.barcode ?? null,
expiresAt: input.expiresAt ?? null,
notes: input.notes ?? null,
...(createdBy ? { createdBy } : {}),
+1
View File
@@ -11,6 +11,7 @@ export type InventoryItem = {
stockQuantity: number;
reorderThreshold: number;
location: string;
barcode: string | null;
expiresAt: string | null; // YYYY-MM-DD
notes: string | null;
createdAt: string;