Initial commit: Apothecary v0.4.0

This commit is contained in:
2026-05-03 20:19:26 -04:00
commit 027cf032be
55 changed files with 14678 additions and 0 deletions
+378
View File
@@ -0,0 +1,378 @@
// Sample inventory data
window.SAMPLE_DATA = (function() {
const TODAY = "2026-04-25";
const daysAgo = (n) => {
const d = new Date(TODAY); d.setDate(d.getDate() - n);
return d.toISOString().slice(0, 10);
};
// Shops are objects now
const SHOPS = [
{ id: "shp-01", name: "Greenleaf Co-op", location: "Capitol Hill" },
{ id: "shp-02", name: "Verdant Apothecary", location: "Ballard" },
{ id: "shp-03", name: "The Field House", location: "Fremont" },
{ id: "shp-04", name: "Northstar Dispensary",location: "Roosevelt" },
{ id: "shp-05", name: "Wildwood Provisions", location: "West Seattle" }
];
// Brands are objects too
const BRANDS = [
{ id: "brd-01", name: "Foxglove Farms" },
{ id: "brd-02", name: "Slow Burn" },
{ id: "brd-03", name: "Heirloom Botanicals" },
{ id: "brd-04", name: "Terra Vera" },
{ id: "brd-05", name: "North Field" },
{ id: "brd-06", name: "Cinder & Sage" },
{ id: "brd-07", name: "Old Forest" },
{ id: "brd-08", name: "Marigold Ext." }
];
// Type config — kind + audit cadence
// bulk: track weight (g) — audit re-weighs (or estimates for concentrate)
// discrete: track count (units) — audit confirms presence by SKU/asset
const TYPES = [
{ 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 }
];
const BINS = [
{ id: "bin-01", name: "Top Drawer", location: "Bedroom", capacity: 14 },
{ id: "bin-02", name: "Apothecary Box", location: "Office shelf", capacity: 10 },
{ id: "bin-03", name: "The Safe", location: "Closet", capacity: 8 },
{ id: "bin-04", name: "Travel Tin", location: "Backpack", capacity: 4 },
{ id: "bin-05", name: "Cold Storage", location: "Fridge — back", capacity: 6 }
];
let n = 1;
const mk = (o) => {
const id = "prd-" + String(n).padStart(4, "0");
n++;
const sku = o.sku || ("SKU-" + Math.random().toString(36).slice(2, 8).toUpperCase());
return {
id,
sku,
assetTag: null,
name: "",
brandId: null,
shopId: null,
type: "Flower",
kind: "bulk", // "bulk" or "discrete"
// Bulk fields
weight: 0, // total at purchase (g/ml)
lastAuditWeight: null, // last measured weight
// Discrete fields
countOriginal: 0, // units at purchase
countLastAudit: null, // units confirmed at last audit
unitWeight: 0, // bulk-equivalent grams per unit (for grams stats)
// Pricing
price: 0,
thc: 0,
cbd: 0,
totalCannabinoids: 0,
purchaseDate: TODAY,
binId: "bin-01",
// Lifecycle
status: "active", // "active" | "consumed" | "gone"
consumedDate: null,
goneDate: null,
rating: null,
notes: null,
// Audits — newest last
audits: [],
...o
};
};
const products = [];
// ─── BULK: FLOWER ────────────────────────────────────────────────
products.push(mk({
name: "Garden Ghost", brandId: "brd-01", shopId: "shp-01",
type: "Flower", kind: "bulk",
weight: 3.5, lastAuditWeight: 2.4,
price: 48, thc: 24.6, cbd: 0.3, totalCannabinoids: 28.4,
purchaseDate: daysAgo(17), binId: "bin-01",
audits: [
{ date: daysAgo(10), mode: "weigh", value: 3.0, prev: 3.5 },
{ date: daysAgo(3), mode: "weigh", value: 2.4, prev: 3.0 }
]
}));
products.push(mk({
name: "Honeydew Pine", brandId: "brd-02", shopId: "shp-02",
type: "Flower", kind: "bulk",
weight: 7, lastAuditWeight: 5.6,
price: 85, thc: 21.0, cbd: 0.5, totalCannabinoids: 25.1,
purchaseDate: daysAgo(11), binId: "bin-01",
audits: [
{ date: daysAgo(2), mode: "weigh", value: 5.6, prev: 7.0 }
]
}));
products.push(mk({
name: "Late Pear", brandId: "brd-01", shopId: "shp-01",
type: "Flower", kind: "bulk",
weight: 3.5, lastAuditWeight: 3.5,
price: 50, thc: 25.2, cbd: 0.2, totalCannabinoids: 28.9,
purchaseDate: daysAgo(6), binId: "bin-05"
// recently bought, no audit yet
}));
products.push(mk({
name: "Copper Fennel", brandId: "brd-02", shopId: "shp-02",
type: "Flower", kind: "bulk",
weight: 3.5, lastAuditWeight: 0.5,
price: 42, thc: 20.4, cbd: 0.5, totalCannabinoids: 24.0,
purchaseDate: daysAgo(26), binId: "bin-01",
audits: [
{ date: daysAgo(12), mode: "weigh", value: 1.6, prev: 3.5 },
{ date: daysAgo(2), mode: "weigh", value: 0.5, prev: 1.6 }
]
}));
// Overdue audit — Flower past 14d cadence
products.push(mk({
name: "Slate Cherry", brandId: "brd-06", shopId: "shp-04",
type: "Flower", kind: "bulk",
weight: 3.5, lastAuditWeight: 3.5,
price: 46, thc: 22.8, cbd: 0.4, totalCannabinoids: 26.5,
purchaseDate: daysAgo(22), binId: "bin-01"
// No audit since purchase, 22d > 14d cadence → overdue
}));
// ─── BULK: CONCENTRATE ──────────────────────────────────────────
products.push(mk({
name: "Indigo Cellar Live Rosin", brandId: "brd-03", shopId: "shp-03",
type: "Concentrate", kind: "bulk",
weight: 1, lastAuditWeight: 0.6,
price: 65, thc: 78.4, cbd: 0.2, totalCannabinoids: 84.0,
purchaseDate: daysAgo(28), binId: "bin-03",
assetTag: "AT-0042",
audits: [
{ date: daysAgo(15), mode: "estimate", value: 0.8, prev: 1.0 },
{ date: daysAgo(2), mode: "estimate", value: 0.6, prev: 0.8 }
]
}));
products.push(mk({
name: "Slate Apricot Hash", brandId: "brd-04", shopId: "shp-04",
type: "Concentrate", kind: "bulk",
weight: 2, lastAuditWeight: 1.4,
price: 80, thc: 62.0, cbd: 1.1, totalCannabinoids: 70.5,
purchaseDate: daysAgo(23), binId: "bin-03",
audits: [
{ date: daysAgo(8), mode: "estimate", value: 1.4, prev: 2.0 }
]
}));
products.push(mk({
name: "Birchwater Live Resin", brandId: "brd-03", shopId: "shp-04",
type: "Concentrate", kind: "bulk",
weight: 1, lastAuditWeight: 1.0,
price: 70, thc: 76.0, cbd: 0.3, totalCannabinoids: 82.1,
purchaseDate: daysAgo(4), binId: "bin-03"
}));
// ─── BULK: TINCTURE ─────────────────────────────────────────────
products.push(mk({
name: "Nightjar Tincture 30ml", brandId: "brd-08", shopId: "shp-02",
type: "Tincture", kind: "bulk",
weight: 30, lastAuditWeight: 22,
price: 60, thc: 0.5, cbd: 18.0, totalCannabinoids: 19.2,
purchaseDate: daysAgo(62), binId: "bin-02",
audits: [
{ date: daysAgo(31), mode: "estimate", value: 26, prev: 30 },
{ date: daysAgo(5), mode: "estimate", value: 22, prev: 26 }
]
}));
// ─── DISCRETE: PRE-ROLLS ───────────────────────────────────────
products.push(mk({
name: "Mossback Pre-rolls", brandId: "brd-07", shopId: "shp-03",
type: "Pre-roll", kind: "discrete",
countOriginal: 5, countLastAudit: 3, unitWeight: 0.7,
price: 38, thc: 19.8, cbd: 0.4, totalCannabinoids: 23.0,
purchaseDate: daysAgo(9), binId: "bin-04",
audits: [
{ date: daysAgo(2), mode: "presence", value: 3, prev: 5, confirmedBy: "SKU" }
]
}));
products.push(mk({
name: "Mossback Pre-rolls", brandId: "brd-07", shopId: "shp-03",
type: "Pre-roll", kind: "discrete",
countOriginal: 5, countLastAudit: 5, unitWeight: 0.7,
price: 38, thc: 19.8, cbd: 0.4, totalCannabinoids: 23.0,
purchaseDate: daysAgo(3), binId: "bin-04"
}));
products.push(mk({
name: "Quiet Meadow Singles", brandId: "brd-05", shopId: "shp-05",
type: "Pre-roll", kind: "discrete",
countOriginal: 3, countLastAudit: 1, unitWeight: 1.0,
price: 24, thc: 22.0, cbd: 0.3, totalCannabinoids: 25.0,
purchaseDate: daysAgo(34), binId: "bin-04",
audits: [
{ date: daysAgo(20), mode: "presence", value: 2, prev: 3, confirmedBy: "SKU" },
{ date: daysAgo(2), mode: "presence", value: 1, prev: 2, confirmedBy: "SKU" }
]
}));
// ─── DISCRETE: EDIBLES ─────────────────────────────────────────
products.push(mk({
name: "Ember Lily Gummies (10 ct)", brandId: "brd-06", shopId: "shp-01",
type: "Edible", kind: "discrete",
countOriginal: 10, countLastAudit: 6, unitWeight: 0,
price: 22, thc: 5.0, cbd: 1.0, totalCannabinoids: 6.4,
purchaseDate: daysAgo(20), binId: "bin-02",
audits: [
{ date: daysAgo(5), mode: "presence", value: 6, prev: 10, confirmedBy: "SKU" }
]
}));
products.push(mk({
name: "Marigold Mints (20 ct)", brandId: "brd-08", shopId: "shp-02",
type: "Edible", kind: "discrete",
countOriginal: 20, countLastAudit: 14, unitWeight: 0,
price: 28, thc: 2.5, cbd: 0, totalCannabinoids: 3.0,
purchaseDate: daysAgo(48), binId: "bin-02",
audits: [
{ date: daysAgo(7), mode: "presence", value: 14, prev: 20, confirmedBy: "SKU" }
]
}));
// ─── DISCRETE: VAPORIZER ───────────────────────────────────────
products.push(mk({
name: "Quiet Meadow Disposable", brandId: "brd-05", shopId: "shp-05",
type: "Vaporizer", kind: "discrete",
countOriginal: 1, countLastAudit: 1, unitWeight: 1.0,
price: 55, thc: 84.0, cbd: 0.1, totalCannabinoids: 88.2,
purchaseDate: daysAgo(14), binId: "bin-04",
audits: [
{ date: daysAgo(1), mode: "presence", value: 1, prev: 1, confirmedBy: "SKU" }
]
}));
// ─── CONSUMED (used up — counts as consumption) ─────────────────
products.push(mk({
name: "Oolong 19", brandId: "brd-04", shopId: "shp-01",
type: "Flower", kind: "bulk",
weight: 3.5, lastAuditWeight: 0,
price: 46, thc: 23.0, cbd: 0.4, totalCannabinoids: 27.0,
purchaseDate: daysAgo(66), binId: null,
status: "consumed", consumedDate: daysAgo(31),
rating: 4, notes: "Smooth, citrusy. Daytime favorite."
}));
products.push(mk({
name: "Stonefruit OG", brandId: "brd-07", shopId: "shp-03",
type: "Flower", kind: "bulk",
weight: 7, lastAuditWeight: 0,
price: 78, thc: 22.5, cbd: 0.6, totalCannabinoids: 26.4,
purchaseDate: daysAgo(103), binId: null,
status: "consumed", consumedDate: daysAgo(56),
rating: 5, notes: "Best of the season. Rebuy."
}));
products.push(mk({
name: "Lavender Coast", brandId: "brd-01", shopId: "shp-05",
type: "Flower", kind: "bulk",
weight: 3.5, lastAuditWeight: 0,
price: 44, thc: 19.0, cbd: 0.8, totalCannabinoids: 22.2,
purchaseDate: daysAgo(80), binId: null,
status: "consumed", consumedDate: daysAgo(47),
rating: 3, notes: "Mellow but underwhelming."
}));
products.push(mk({
name: "Violet Tea", brandId: "brd-06", shopId: "shp-04",
type: "Flower", kind: "bulk",
weight: 3.5, lastAuditWeight: 0,
price: 50, thc: 24.1, cbd: 0.3, totalCannabinoids: 28.0,
purchaseDate: daysAgo(55), binId: null,
status: "consumed", consumedDate: daysAgo(21),
rating: 4, notes: "Floral nose, nice evenings."
}));
// ─── GONE (lost / damaged — counts as $ spent, NOT consumption) ─
products.push(mk({
name: "Quiet Meadow Singles", brandId: "brd-05", shopId: "shp-05",
type: "Pre-roll", kind: "discrete",
countOriginal: 3, countLastAudit: 0, unitWeight: 1.0,
price: 24, thc: 22.0, cbd: 0.3, totalCannabinoids: 25.0,
purchaseDate: daysAgo(72), binId: null,
status: "gone", goneDate: daysAgo(40),
notes: "Pack went through the wash. Lesson learned.",
audits: [
{ date: daysAgo(40), mode: "presence", value: 0, prev: 3, confirmedBy: "lost" }
]
}));
products.push(mk({
name: "Ember Lily Gummies (10 ct)", brandId: "brd-06", shopId: "shp-01",
type: "Edible", kind: "discrete",
countOriginal: 10, countLastAudit: 4, unitWeight: 0,
price: 22, thc: 5.0, cbd: 1.0, totalCannabinoids: 6.4,
purchaseDate: daysAgo(95), binId: null,
status: "gone", goneDate: daysAgo(15),
notes: "Expired. Tossed the rest."
}));
return {
products,
bins: BINS,
shops: SHOPS,
brands: BRANDS,
types: TYPES,
today: TODAY
};
})();
// Helpers — exported so screens can use consistent logic
window.DATA_HELPERS = {
shopName: (data, id) => data.shops.find(s => s.id === id)?.name || "—",
brandName: (data, id) => data.brands.find(b => b.id === id)?.name || "—",
typeConfig: (data, id) => data.types.find(t => t.id === id) || data.types[0],
// Days since a given ISO date string
daysSince: (iso, today = "2026-04-25") => {
if (!iso) return Infinity;
return Math.floor((new Date(today) - new Date(iso)) / 86400000);
},
// Last audit record (newest)
lastAudit: (p) => p.audits && p.audits.length ? p.audits[p.audits.length - 1] : null,
// Days since last audit (or since purchase if none)
daysSinceCheck: (p, today = "2026-04-25") => {
const last = p.audits && p.audits.length ? p.audits[p.audits.length - 1].date : p.purchaseDate;
return Math.floor((new Date(today) - new Date(last)) / 86400000);
},
// Is audit overdue based on per-type cadence
auditOverdue: (data, p, today = "2026-04-25") => {
if (p.status !== "active") return false;
const cfg = data.types.find(t => t.id === p.type);
if (!cfg) return false;
return window.DATA_HELPERS.daysSinceCheck(p, today) >= cfg.cadenceDays;
},
// Estimated remaining (bulk: decay from last audit; discrete: countLastAudit)
estimatedRemaining: (p, today = "2026-04-25") => {
if (p.status !== "active") return 0;
if (p.kind === "discrete") {
return p.countLastAudit != null ? p.countLastAudit : p.countOriginal;
}
// Bulk: linear decay between last audit and average lifespan estimate
const last = window.DATA_HELPERS.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)) / 86400000));
// Decay rate: assume original weight is consumed over (purchase → expected lifespan ~ 35d for flower, 40 concentrate, 90 tincture)
const expectedLifespan = p.type === "Flower" ? 35 : p.type === "Concentrate" ? 40 : 90;
const dailyBurn = p.weight / expectedLifespan;
const est = Math.max(0, baseValue - dailyBurn * daysSinceBase);
return est;
},
// Original-percent remaining (for low-stock on bulk)
pctRemaining: (p, today = "2026-04-25") => {
if (p.kind === "discrete") {
const cur = p.countLastAudit != null ? p.countLastAudit : p.countOriginal;
return p.countOriginal > 0 ? cur / p.countOriginal : 0;
}
const est = window.DATA_HELPERS.estimatedRemaining(p, today);
return p.weight > 0 ? est / p.weight : 0;
}
};