Initial commit: Apothecary v0.4.0
This commit is contained in:
+185
@@ -0,0 +1,185 @@
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { api } from "./api.js";
|
||||
import type { Bin, Bootstrap, Product } from "./types.js";
|
||||
import { computeStats } from "./stats.js";
|
||||
import { Sidebar } from "./components/Sidebar.js";
|
||||
import type { ViewKey } from "./components/Sidebar.js";
|
||||
import { Dashboard } from "./views/Dashboard.js";
|
||||
import { Inventory } from "./views/Inventory.js";
|
||||
import { BinsView } from "./views/BinsView.js";
|
||||
import { BrandsView } from "./views/BrandsView.js";
|
||||
import { ShopsView } from "./views/ShopsView.js";
|
||||
import { ChartsView } from "./views/ChartsView.js";
|
||||
import { SettingsView } from "./views/SettingsView.js";
|
||||
import type { ThemeKey } from "./views/SettingsView.js";
|
||||
import { ProductDetail } from "./components/ProductDetail.js";
|
||||
import { AddProductFlow } from "./components/modals/AddProductFlow.js";
|
||||
import { ConsumeFlow } from "./components/modals/ConsumeFlow.js";
|
||||
import { MarkGoneFlow } from "./components/modals/MarkGoneFlow.js";
|
||||
import { AuditFlow } from "./components/modals/AuditFlow.js";
|
||||
import {
|
||||
AddBinModal,
|
||||
AddBrandModal,
|
||||
AddShopModal,
|
||||
EditBinModal,
|
||||
} from "./components/modals/CatalogModals.js";
|
||||
|
||||
type ModalKey =
|
||||
| "add"
|
||||
| "consume"
|
||||
| "gone"
|
||||
| "audit"
|
||||
| "addBrand"
|
||||
| "addShop"
|
||||
| "addBin"
|
||||
| "editBin"
|
||||
| null;
|
||||
|
||||
export function App() {
|
||||
const [view, setView] = useState<ViewKey>("dashboard");
|
||||
const [selected, setSelected] = useState<Product | null>(null);
|
||||
const [modal, setModal] = useState<ModalKey>(null);
|
||||
const [modalProduct, setModalProduct] = useState<Product | null>(null);
|
||||
const [modalBin, setModalBin] = useState<Bin | null>(null);
|
||||
|
||||
const [theme, setTheme] = useState<ThemeKey>(
|
||||
() => (localStorage.getItem("apothecary.theme") as ThemeKey | null) ?? "light",
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
document.documentElement.dataset.theme = theme;
|
||||
localStorage.setItem("apothecary.theme", theme);
|
||||
}, [theme]);
|
||||
|
||||
const { data, isLoading, error } = useQuery<Bootstrap>({
|
||||
queryKey: ["bootstrap"],
|
||||
queryFn: api.bootstrap,
|
||||
});
|
||||
|
||||
const stats = useMemo(() => (data ? computeStats(data) : null), [data]);
|
||||
|
||||
// Re-sync the selected product reference whenever bootstrap refetches
|
||||
// — otherwise the drawer keeps showing stale audit history after a
|
||||
// mutation invalidates the cache.
|
||||
useEffect(() => {
|
||||
if (selected && data) {
|
||||
const fresh = data.products.find((p) => p.id === selected.id);
|
||||
if (fresh && fresh !== selected) setSelected(fresh);
|
||||
}
|
||||
}, [data]); // eslint-disable-line react-hooks/exhaustive-deps
|
||||
|
||||
const openAdd = () => {
|
||||
setModalProduct(null);
|
||||
setModal("add");
|
||||
};
|
||||
const openConsume = (p?: Product) => {
|
||||
setModalProduct(p ?? null);
|
||||
setSelected(null);
|
||||
setModal("consume");
|
||||
};
|
||||
const openMarkGone = (p?: Product) => {
|
||||
setModalProduct(p ?? null);
|
||||
setSelected(null);
|
||||
setModal("gone");
|
||||
};
|
||||
const openAudit = (p?: Product) => {
|
||||
setModalProduct(p ?? null);
|
||||
setModal("audit");
|
||||
};
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="parchment" style={{ padding: 60, color: "var(--ink-3)" }}>
|
||||
Loading…
|
||||
</div>
|
||||
);
|
||||
}
|
||||
if (error || !data || !stats) {
|
||||
return (
|
||||
<div className="parchment" style={{ padding: 60, color: "var(--terracotta)" }}>
|
||||
Failed to load: {String(error ?? "no data")}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="app-shell" data-screen-label="App">
|
||||
<Sidebar
|
||||
view={view}
|
||||
onNav={setView}
|
||||
onAddProduct={openAdd}
|
||||
onMarkFinished={() => openConsume()}
|
||||
onAudit={() => openAudit()}
|
||||
/>
|
||||
|
||||
<main className="main parchment" style={{ minWidth: 0 }}>
|
||||
{view === "dashboard" && (
|
||||
<Dashboard
|
||||
data={data}
|
||||
stats={stats}
|
||||
onAuditProduct={openAudit}
|
||||
onSelectProduct={setSelected}
|
||||
/>
|
||||
)}
|
||||
{view === "inventory" && (
|
||||
<Inventory
|
||||
data={data}
|
||||
onSelectProduct={setSelected}
|
||||
onAddProduct={openAdd}
|
||||
onAuditNew={() => openAudit()}
|
||||
/>
|
||||
)}
|
||||
{view === "bins" && (
|
||||
<BinsView
|
||||
data={data}
|
||||
onSelectProduct={setSelected}
|
||||
onAddBin={() => setModal("addBin")}
|
||||
onEditBin={(bin) => {
|
||||
setModalBin(bin);
|
||||
setModal("editBin");
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{view === "shops" && (
|
||||
<ShopsView data={data} onAddShop={() => setModal("addShop")} />
|
||||
)}
|
||||
{view === "brands" && (
|
||||
<BrandsView data={data} onAddBrand={() => setModal("addBrand")} />
|
||||
)}
|
||||
{view === "charts" && <ChartsView data={data} stats={stats} />}
|
||||
{view === "settings" && (
|
||||
<SettingsView data={data} theme={theme} onThemeChange={setTheme} />
|
||||
)}
|
||||
</main>
|
||||
|
||||
{selected && (
|
||||
<ProductDetail
|
||||
product={selected}
|
||||
data={data}
|
||||
onClose={() => setSelected(null)}
|
||||
onConsume={openConsume}
|
||||
onMarkGone={openMarkGone}
|
||||
onAudit={openAudit}
|
||||
/>
|
||||
)}
|
||||
|
||||
{modal === "add" && <AddProductFlow data={data} onClose={() => setModal(null)} />}
|
||||
{modal === "consume" && (
|
||||
<ConsumeFlow data={data} onClose={() => setModal(null)} product={modalProduct} />
|
||||
)}
|
||||
{modal === "gone" && (
|
||||
<MarkGoneFlow data={data} onClose={() => setModal(null)} product={modalProduct} />
|
||||
)}
|
||||
{modal === "audit" && (
|
||||
<AuditFlow data={data} onClose={() => setModal(null)} product={modalProduct} />
|
||||
)}
|
||||
{modal === "addBrand" && <AddBrandModal onClose={() => setModal(null)} />}
|
||||
{modal === "addShop" && <AddShopModal onClose={() => setModal(null)} />}
|
||||
{modal === "addBin" && <AddBinModal onClose={() => setModal(null)} />}
|
||||
{modal === "editBin" && modalBin && (
|
||||
<EditBinModal bin={modalBin} onClose={() => setModal(null)} />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user