feat: integrate barcode scanning functionality and enhance Appwrite setup

- Added a new `barcode_products` collection to the Appwrite setup for managing barcode data.
- Implemented barcode scanning feature with a dedicated modal for scanning and adding products.
- Introduced new components for barcode product preview and management.
- Updated the setup script to seed verified barcode products from a JSON file.
- Enhanced the application state management to handle barcode-related actions and user interactions.
This commit is contained in:
Ned Halksworth
2026-05-27 14:29:22 +01:00
parent a7993af1d2
commit 023ec1096f
19 changed files with 2033 additions and 164 deletions
+90
View File
@@ -0,0 +1,90 @@
import { BUILT_IN_BARCODE_PRODUCTS } from "../data/barcodes";
import { BUILT_IN_FLAVOURS, flavourMeta } from "../data/flavours";
import { caffeinePerCan } from "./metrics";
import type {
BarcodeLookupCatalog,
BarcodeLookupResult,
BarcodeProductDraft,
ResolvedBarcodeProduct,
UserBarcodeMapping,
EntryDraft,
} from "../types";
const knownFlavourNames = new Set(BUILT_IN_FLAVOURS.map((flavour) => flavour.name));
export function normalizeBarcode(value: string) {
return value.replace(/\D/g, "");
}
export function lookupBarcode(
rawBarcode: string,
catalogOrUserMappings: BarcodeLookupCatalog | UserBarcodeMapping[] = [],
): BarcodeLookupResult {
const catalog = Array.isArray(catalogOrUserMappings)
? { userMappings: catalogOrUserMappings }
: catalogOrUserMappings;
const userMappings = catalog.userMappings ?? [];
const verifiedProducts = catalog.verifiedProducts ?? BUILT_IN_BARCODE_PRODUCTS;
const barcode = normalizeBarcode(rawBarcode);
if (!barcode) {
return { status: "unknown", barcode: rawBarcode.trim() };
}
const userMapping = userMappings.find((mapping) => mapping.barcode === barcode);
if (userMapping) {
return { status: "user", barcode, product: resolveProduct(userMapping, "user") };
}
const seedProduct = verifiedProducts[barcode];
if (!seedProduct) {
return { status: "unknown", barcode };
}
if (!knownFlavourNames.has(seedProduct.flavourName)) {
return {
status: "partial",
barcode,
product: seedProduct,
reason: "This barcode has product data, but its flavour is not in the built-in Red Bull list yet.",
};
}
return { status: "known", barcode, product: resolveProduct(seedProduct, "built-in") };
}
export function resolveProduct(
product: BarcodeProductDraft,
source: ResolvedBarcodeProduct["source"],
): ResolvedBarcodeProduct {
const meta = flavourMeta(product.flavourName);
return {
...product,
flavourAccent: meta.accent,
sugarFree: product.sugarFree ?? Boolean(meta.sugarFree),
caffeineMgPerCan: product.caffeineMgPerCan,
source,
};
}
export function barcodeProductToEntryDraft(
product: ResolvedBarcodeProduct,
barcode: string,
): EntryDraft {
return {
cans: 1,
flavour: product.flavourName,
flavourAccent: product.flavourAccent,
sizeMl: product.sizeMl,
pricePerCan: product.pricePerCan,
dateTime: new Date().toISOString(),
notes: `Barcode scan: ${barcode}`,
store: "",
sugarFree: Boolean(product.sugarFree),
caffeineMgPerCan: product.caffeineMgPerCan,
source: "manual",
};
}
export function productCaffeineMg(product: BarcodeProductDraft) {
return caffeinePerCan(product.sizeMl, product.caffeineMgPerCan);
}