379 lines
15 KiB
JavaScript
379 lines
15 KiB
JavaScript
// 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;
|
|
}
|
|
};
|