Initial commit: Apothecary v0.4.0
This commit is contained in:
@@ -0,0 +1,155 @@
|
||||
export type ProductStatus = "active" | "consumed" | "gone";
|
||||
export type ProductKind = "bulk" | "discrete";
|
||||
export type AuditMode = "weigh" | "estimate" | "presence";
|
||||
|
||||
export interface Audit {
|
||||
date: string;
|
||||
mode: AuditMode;
|
||||
value: number;
|
||||
prev: number | null;
|
||||
confirmedBy: string | null;
|
||||
}
|
||||
|
||||
export interface Product {
|
||||
id: string;
|
||||
sku: string;
|
||||
assetTag: string | null;
|
||||
name: string;
|
||||
brandId: string | null;
|
||||
shopId: string | null;
|
||||
binId: string | null;
|
||||
type: string;
|
||||
kind: ProductKind;
|
||||
weight: number;
|
||||
lastAuditWeight: number | null;
|
||||
countOriginal: number;
|
||||
countLastAudit: number | null;
|
||||
unitWeight: number;
|
||||
price: number;
|
||||
thc: number;
|
||||
cbd: number;
|
||||
totalCannabinoids: number;
|
||||
purchaseDate: string;
|
||||
status: ProductStatus;
|
||||
consumedDate: string | null;
|
||||
goneDate: string | null;
|
||||
rating: number | null;
|
||||
notes: string | null;
|
||||
strainId: string | null;
|
||||
audits: Audit[];
|
||||
}
|
||||
|
||||
export interface Strain {
|
||||
id: string;
|
||||
name: string;
|
||||
brandId: string | null;
|
||||
type: string;
|
||||
defaultThc: number | null;
|
||||
defaultCbd: number | null;
|
||||
defaultTotalCannabinoids: number | null;
|
||||
notes: string | null;
|
||||
}
|
||||
|
||||
export interface Brand {
|
||||
id: string;
|
||||
name: string;
|
||||
}
|
||||
|
||||
export interface Shop {
|
||||
id: string;
|
||||
name: string;
|
||||
location: string | null;
|
||||
}
|
||||
|
||||
export interface Bin {
|
||||
id: string;
|
||||
name: string;
|
||||
location: string | null;
|
||||
capacity: number;
|
||||
}
|
||||
|
||||
export interface TypeConfig {
|
||||
id: string;
|
||||
kind: ProductKind;
|
||||
auditMode: AuditMode;
|
||||
cadenceDays: number;
|
||||
unit: string;
|
||||
weighable: boolean;
|
||||
}
|
||||
|
||||
export interface Bootstrap {
|
||||
products: Product[];
|
||||
brands: Brand[];
|
||||
shops: Shop[];
|
||||
bins: Bin[];
|
||||
strains: Strain[];
|
||||
today: string;
|
||||
}
|
||||
|
||||
// Type config lives client-side — static, not user data.
|
||||
// Mirrors data.js TYPES array.
|
||||
export const TYPES: TypeConfig[] = [
|
||||
{ id: "Flower", kind: "bulk", auditMode: "weigh", cadenceDays: 14, unit: "g", weighable: true },
|
||||
{ id: "Concentrate", kind: "bulk", auditMode: "estimate", cadenceDays: 21, unit: "g", weighable: true },
|
||||
{ id: "Tincture", kind: "bulk", auditMode: "estimate", cadenceDays: 30, unit: "ml", weighable: false },
|
||||
{ id: "Pre-roll", kind: "discrete", auditMode: "presence", cadenceDays: 30, unit: "ct", weighable: false },
|
||||
{ id: "Edible", kind: "discrete", auditMode: "presence", cadenceDays: 60, unit: "ct", weighable: false },
|
||||
{ id: "Vaporizer", kind: "discrete", auditMode: "presence", cadenceDays: 30, unit: "ct", weighable: false },
|
||||
];
|
||||
|
||||
export const TODAY_STR = "2026-04-25";
|
||||
|
||||
// Helpers — match data.js DATA_HELPERS API
|
||||
export const helpers = {
|
||||
shopName(data: { shops: Shop[] }, id: string | null): string {
|
||||
return data.shops.find((s) => s.id === id)?.name ?? "—";
|
||||
},
|
||||
brandName(data: { brands: Brand[] }, id: string | null): string {
|
||||
return data.brands.find((b) => b.id === id)?.name ?? "—";
|
||||
},
|
||||
typeConfig(id: string): TypeConfig {
|
||||
return TYPES.find((t) => t.id === id) ?? TYPES[0]!;
|
||||
},
|
||||
daysSince(iso: string | null, today = TODAY_STR): number {
|
||||
if (!iso) return Infinity;
|
||||
return Math.floor((+new Date(today) - +new Date(iso)) / 86_400_000);
|
||||
},
|
||||
lastAudit(p: Product): Audit | null {
|
||||
return p.audits.length > 0 ? p.audits[p.audits.length - 1]! : null;
|
||||
},
|
||||
daysSinceCheck(p: Product, today = TODAY_STR): number {
|
||||
const last = p.audits.length > 0 ? p.audits[p.audits.length - 1]!.date : p.purchaseDate;
|
||||
return Math.floor((+new Date(today) - +new Date(last)) / 86_400_000);
|
||||
},
|
||||
auditOverdue(p: Product, today = TODAY_STR): boolean {
|
||||
if (p.status !== "active") return false;
|
||||
const cfg = TYPES.find((t) => t.id === p.type);
|
||||
if (!cfg) return false;
|
||||
return this.daysSinceCheck(p, today) >= cfg.cadenceDays;
|
||||
},
|
||||
estimatedRemaining(p: Product, today = TODAY_STR): number {
|
||||
if (p.status !== "active") return 0;
|
||||
if (p.kind === "discrete") {
|
||||
return p.countLastAudit ?? p.countOriginal;
|
||||
}
|
||||
const last = this.lastAudit(p);
|
||||
const baseDate = last ? last.date : p.purchaseDate;
|
||||
const baseValue = last ? last.value : p.weight;
|
||||
const daysSinceBase = Math.max(
|
||||
0,
|
||||
Math.floor((+new Date(today) - +new Date(baseDate)) / 86_400_000),
|
||||
);
|
||||
const expectedLifespan =
|
||||
p.type === "Flower" ? 35 : p.type === "Concentrate" ? 40 : 90;
|
||||
const dailyBurn = p.weight / expectedLifespan;
|
||||
return Math.max(0, baseValue - dailyBurn * daysSinceBase);
|
||||
},
|
||||
pctRemaining(p: Product, today = TODAY_STR): number {
|
||||
if (p.kind === "discrete") {
|
||||
const cur = p.countLastAudit ?? p.countOriginal;
|
||||
return p.countOriginal > 0 ? cur / p.countOriginal : 0;
|
||||
}
|
||||
const est = this.estimatedRemaining(p, today);
|
||||
return p.weight > 0 ? est / p.weight : 0;
|
||||
},
|
||||
};
|
||||
Reference in New Issue
Block a user