Pages Medium
Admin — Inventory Tracker
Restaurant inventory: searchable stock table with par-level bars, low-stock alerts, supplier column, last-delivery dates, inline reorder action and bulk export.
Open in Lab
MCP
html css vanilla-js
Targets: JS HTML
Code
:root {
--cream: #f5f0e8;
--cream-2: #ece4d4;
--bone: #faf7f1;
--terracotta: #c1714a;
--terracotta-d: #a05a38;
--forest: #2d4a3e;
--forest-d: #1e3329;
--gold: #c9a84c;
--gold-light: #e6c97a;
--ink: #2c1a0e;
--ink-2: #4a3828;
--warm-gray: #7a6a58;
--success: #4f7a3a;
--danger: #b3432a;
--warning: #d99020;
--font-display: "Playfair Display", Georgia, serif;
--font-body: "Inter", system-ui, sans-serif;
--font-mono: "JetBrains Mono", ui-monospace, monospace;
--r-sm: 6px;
--r-md: 10px;
--r-lg: 14px;
}
*,
*::before,
*::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
html,
body {
height: 100%;
}
body {
font-family: var(--font-body);
background: var(--cream);
color: var(--ink);
-webkit-font-smoothing: antialiased;
}
.app {
height: 100vh;
display: grid;
grid-template-columns: 248px 1fr;
}
/* Rail (same as dashboard) */
.rail {
background: var(--forest);
color: var(--bone);
display: flex;
flex-direction: column;
border-right: 1px solid var(--forest-d);
}
.rail-brand {
padding: 22px 22px 18px;
display: flex;
align-items: center;
gap: 12px;
border-bottom: 1px solid rgba(250, 247, 241, 0.1);
}
.brand-mark {
width: 36px;
height: 36px;
background: var(--gold);
color: var(--ink);
font-family: var(--font-display);
font-weight: 800;
font-size: 0.92rem;
border-radius: 8px;
display: grid;
place-items: center;
}
.brand-name {
font-family: var(--font-display);
font-weight: 700;
font-size: 1.05rem;
}
.rail-nav {
flex: 1;
display: flex;
flex-direction: column;
padding: 12px 12px 18px;
gap: 2px;
}
.r-link {
display: flex;
align-items: center;
gap: 12px;
padding: 10px 14px;
border-radius: 8px;
text-decoration: none;
color: rgba(250, 247, 241, 0.78);
font-size: 0.92rem;
font-weight: 500;
}
.r-link:hover {
background: rgba(250, 247, 241, 0.06);
color: var(--bone);
}
.r-link.is-active {
background: var(--bone);
color: var(--forest-d);
font-weight: 700;
}
.r-icon {
font-size: 1rem;
width: 22px;
text-align: center;
}
.r-badge {
margin-left: auto;
background: var(--gold);
color: var(--ink);
font-family: var(--font-mono);
font-size: 0.7rem;
font-weight: 700;
padding: 2px 7px;
border-radius: 999px;
min-width: 24px;
text-align: center;
}
.r-badge-warn {
background: var(--warning);
}
.rail-foot {
padding: 14px 16px;
border-top: 1px solid rgba(250, 247, 241, 0.1);
}
.rail-user {
display: flex;
align-items: center;
gap: 10px;
}
.user-avatar {
width: 38px;
height: 38px;
background: var(--terracotta);
color: var(--bone);
border-radius: 999px;
display: grid;
place-items: center;
font-family: var(--font-display);
font-weight: 800;
}
.user-name {
font-size: 0.92rem;
font-weight: 700;
color: var(--bone);
}
.user-role {
font-size: 0.74rem;
color: rgba(250, 247, 241, 0.5);
}
/* Main */
.main {
overflow-y: auto;
padding: 28px 36px 56px;
}
.top {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 22px;
gap: 14px;
flex-wrap: wrap;
}
.kicker {
font-size: 0.72rem;
letter-spacing: 0.18em;
text-transform: uppercase;
color: var(--terracotta);
font-weight: 700;
}
.top h1 {
font-family: var(--font-display);
font-weight: 800;
font-size: 2rem;
letter-spacing: -0.015em;
}
.top-tools {
display: flex;
gap: 8px;
}
.ghost,
.primary {
border-radius: 999px;
padding: 9px 16px;
font-family: inherit;
font-size: 0.84rem;
font-weight: 700;
cursor: pointer;
border: 1px solid transparent;
}
.ghost {
background: var(--bone);
border-color: rgba(44, 26, 14, 0.12);
color: var(--ink-2);
}
.ghost:hover {
background: var(--cream-2);
color: var(--ink);
}
.primary {
background: var(--forest);
color: var(--bone);
}
.primary:hover {
background: var(--forest-d);
}
/* Status strip */
.status {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 12px;
margin-bottom: 22px;
}
@media (max-width: 980px) {
.status {
grid-template-columns: 1fr 1fr;
}
}
.stat {
background: var(--bone);
border: 1px solid rgba(44, 26, 14, 0.08);
border-radius: var(--r-md);
padding: 16px 18px 14px;
display: flex;
flex-direction: column;
gap: 2px;
}
.stat-warn {
border-left: 3px solid var(--warning);
}
.stat-danger {
border-left: 3px solid var(--danger);
}
.stat-label {
font-size: 0.72rem;
letter-spacing: 0.06em;
color: var(--warm-gray);
font-weight: 600;
}
.stat-value {
font-family: var(--font-display);
font-weight: 800;
font-size: 1.7rem;
letter-spacing: -0.005em;
}
.stat-value small {
font-family: var(--font-body);
font-weight: 600;
font-size: 0.84rem;
color: var(--warm-gray);
}
.stat-meta {
font-size: 0.72rem;
color: var(--warm-gray);
font-weight: 600;
}
.is-up {
color: var(--success);
}
/* Toolbar */
.toolbar {
display: flex;
justify-content: space-between;
align-items: center;
gap: 14px;
margin-bottom: 14px;
flex-wrap: wrap;
}
.chips {
display: flex;
flex-wrap: wrap;
gap: 6px;
}
.chip {
background: var(--bone);
border: 1px solid rgba(44, 26, 14, 0.1);
color: var(--ink-2);
padding: 7px 14px;
border-radius: 999px;
font-family: inherit;
font-size: 0.82rem;
font-weight: 600;
cursor: pointer;
display: inline-flex;
align-items: center;
gap: 6px;
transition: background 0.15s, color 0.15s, border-color 0.15s;
}
.chip span {
font-family: var(--font-mono);
font-size: 0.66rem;
background: var(--cream-2);
color: var(--warm-gray);
padding: 2px 7px;
border-radius: 999px;
font-weight: 700;
}
.chip:hover {
border-color: var(--terracotta);
color: var(--terracotta-d);
}
.chip.is-active {
background: var(--forest);
color: var(--bone);
border-color: var(--forest-d);
}
.chip.is-active span {
background: rgba(250, 247, 241, 0.18);
color: var(--gold-light);
}
.search {
display: inline-flex;
align-items: center;
background: var(--bone);
border: 1px solid rgba(44, 26, 14, 0.1);
padding: 7px 14px;
border-radius: 999px;
gap: 8px;
color: var(--warm-gray);
}
.search input {
background: transparent;
border: none;
outline: none;
font-family: inherit;
font-size: 0.86rem;
color: var(--ink);
width: 240px;
}
/* Table */
.table-wrap {
background: var(--bone);
border: 1px solid rgba(44, 26, 14, 0.08);
border-radius: var(--r-lg);
overflow: hidden;
}
.t {
width: 100%;
border-collapse: collapse;
}
.t thead {
background: var(--cream-2);
}
.t th {
text-align: left;
font-size: 0.7rem;
letter-spacing: 0.14em;
text-transform: uppercase;
color: var(--warm-gray);
font-weight: 700;
padding: 12px 16px;
}
.t td {
padding: 14px 16px;
border-top: 1px solid rgba(44, 26, 14, 0.06);
font-size: 0.92rem;
vertical-align: middle;
}
.col-stock {
width: 220px;
}
.col-action {
width: 140px;
text-align: right;
}
.row-hidden {
display: none !important;
}
.ing {
display: flex;
align-items: center;
gap: 12px;
}
.ing-glyph {
width: 34px;
height: 34px;
background: var(--cream-2);
border-radius: 8px;
display: grid;
place-items: center;
font-size: 1.15rem;
flex-shrink: 0;
}
.ing-info {
min-width: 0;
}
.ing-name {
font-weight: 700;
}
.ing-code {
font-family: var(--font-mono);
font-size: 0.72rem;
color: var(--warm-gray);
}
.supplier {
font-weight: 600;
}
.supplier small {
display: block;
font-weight: 500;
font-size: 0.74rem;
color: var(--warm-gray);
}
.delivery {
font-family: var(--font-mono);
font-weight: 600;
}
.delivery small {
display: block;
font-family: var(--font-body);
font-weight: 500;
font-size: 0.74rem;
color: var(--warm-gray);
}
/* Par bar */
.par {
display: flex;
flex-direction: column;
gap: 5px;
}
.par-text {
display: flex;
justify-content: space-between;
font-family: var(--font-mono);
font-size: 0.78rem;
font-weight: 600;
color: var(--ink-2);
}
.par-text strong {
color: var(--ink);
}
.par-bar {
height: 7px;
background: var(--cream-2);
border-radius: 999px;
overflow: hidden;
}
.par-fill {
height: 100%;
background: var(--success);
border-radius: 999px;
transition: width 0.4s ease;
}
.par.is-warn .par-fill {
background: var(--warning);
}
.par.is-danger .par-fill {
background: var(--danger);
}
.par.is-warn .par-text strong {
color: var(--warning);
}
.par.is-danger .par-text strong {
color: var(--danger);
}
/* Actions */
.tag-86 {
display: inline-block;
background: var(--danger);
color: var(--bone);
font-family: var(--font-mono);
font-weight: 700;
font-size: 0.7rem;
padding: 4px 8px;
border-radius: 999px;
letter-spacing: 0.06em;
}
.btn-reorder {
background: var(--terracotta);
color: var(--bone);
border: none;
font-family: inherit;
font-size: 0.78rem;
font-weight: 700;
border-radius: 999px;
padding: 7px 14px;
cursor: pointer;
}
.btn-reorder:hover {
background: var(--terracotta-d);
}
.btn-quiet {
background: transparent;
border: 1px solid rgba(44, 26, 14, 0.16);
color: var(--warm-gray);
font-family: inherit;
font-size: 0.78rem;
font-weight: 600;
border-radius: 999px;
padding: 7px 12px;
cursor: pointer;
}
.btn-quiet:hover {
border-color: var(--terracotta);
color: var(--terracotta-d);
}
.empty-msg {
text-align: center;
padding: 36px 24px;
color: var(--warm-gray);
font-style: italic;
}
/* Toast */
.toast {
position: fixed;
bottom: 28px;
left: 50%;
transform: translateX(-50%);
background: var(--forest-d);
color: var(--bone);
padding: 10px 18px;
border-radius: 999px;
font-size: 0.86rem;
font-weight: 600;
box-shadow: 0 12px 30px rgba(0, 0, 0, 0.18);
z-index: 10;
}
/* Drawer */
.drawer-overlay {
position: fixed;
inset: 0;
background: rgba(0, 0, 0, 0.4);
z-index: 40;
}
.drawer {
position: fixed;
top: 0;
right: 0;
height: 100%;
width: 320px;
background: var(--bone);
z-index: 41;
padding: 1.5rem;
box-shadow: -4px 0 24px rgba(0, 0, 0, 0.12);
display: flex;
flex-direction: column;
}
.drawer-head {
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
margin-bottom: 1.5rem;
}
.drawer-title {
font-family: var(--font-display);
font-weight: 800;
font-size: 1.25rem;
color: var(--ink);
}
.drawer-close {
background: transparent;
border: 1px solid rgba(44, 26, 14, 0.16);
color: var(--warm-gray);
width: 32px;
height: 32px;
border-radius: var(--r-sm);
cursor: pointer;
font-size: 0.9rem;
display: grid;
place-items: center;
}
.drawer-close:hover {
border-color: var(--terracotta);
color: var(--terracotta-d);
}
.drawer-body {
display: flex;
flex-direction: column;
flex: 1;
}
.drawer-meta {
font-size: 0.82rem;
color: var(--warm-gray);
font-weight: 600;
margin-bottom: 1.25rem;
}
.field-label {
font-size: 0.78rem;
font-weight: 700;
letter-spacing: 0.06em;
text-transform: uppercase;
color: var(--warm-gray);
display: block;
margin-bottom: 0;
}
.field-label small {
font-family: var(--font-mono);
font-size: 0.7rem;
font-weight: 600;
color: var(--warm-gray);
text-transform: none;
letter-spacing: 0;
}
.qty-row {
display: flex;
flex-direction: row;
align-items: center;
gap: 0.5rem;
margin: 1rem 0;
}
.qty-input {
text-align: center;
width: 80px;
font-size: 1.5rem;
font-family: var(--font-mono);
font-weight: 700;
color: var(--ink);
border: 1px solid rgba(44, 26, 14, 0.16);
border-radius: var(--r-sm);
padding: 0.5rem;
background: var(--cream);
outline: none;
}
.qty-input:focus {
border-color: var(--forest);
}
.qty-btn {
width: 40px;
height: 40px;
border-radius: var(--r-sm);
border: 1px solid rgba(44, 26, 14, 0.16);
background: var(--bone);
color: var(--ink-2);
font-size: 1.2rem;
font-weight: 700;
cursor: pointer;
display: grid;
place-items: center;
line-height: 1;
}
.qty-btn:hover {
border-color: var(--forest);
background: var(--cream-2);
color: var(--forest);
}
.drawer-par {
font-family: var(--font-mono);
font-size: 0.78rem;
font-weight: 600;
color: var(--warm-gray);
}
.drawer-actions {
display: flex;
flex-direction: row;
gap: 0.75rem;
margin-top: 1.5rem;
justify-content: flex-end;
}
.btn-ghost {
background: var(--bone);
border: 1px solid rgba(44, 26, 14, 0.12);
color: var(--ink-2);
border-radius: 999px;
padding: 9px 16px;
font-family: inherit;
font-size: 0.84rem;
font-weight: 700;
cursor: pointer;
}
.btn-ghost:hover {
background: var(--cream-2);
color: var(--ink);
}
.btn-primary {
background: var(--forest);
color: var(--bone);
border: 1px solid transparent;
border-radius: 999px;
padding: 9px 16px;
font-family: inherit;
font-size: 0.84rem;
font-weight: 700;
cursor: pointer;
}
.btn-primary:hover {
background: var(--forest-d);
}
@media (max-width: 880px) {
.app {
grid-template-columns: 1fr;
grid-template-rows: auto 1fr;
}
.rail {
flex-direction: row;
border-right: none;
border-bottom: 1px solid var(--forest-d);
overflow-x: auto;
padding: 8px 12px;
}
.rail-brand,
.rail-foot {
display: none;
}
.rail-nav {
flex-direction: row;
padding: 0;
}
.t thead {
display: none;
}
.t tr {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 8px;
padding: 12px 16px;
border-top: 1px solid rgba(44, 26, 14, 0.06);
}
.t td {
padding: 0;
border: none;
}
.t td.col-stock,
.t td.col-action {
grid-column: 1 / -1;
}
}const ITEMS = [
// produce
{
id: "tom",
name: "Heirloom tomato",
glyph: "🍅",
code: "PR-014",
cat: "produce",
supplier: "Aravaca Farm",
region: "Own garden · Madrid",
stock: 14,
par: 18,
lastIso: -1,
},
{
id: "bas",
name: "Garden basil",
glyph: "🌿",
code: "PR-022",
cat: "produce",
supplier: "Aravaca Farm",
region: "Own garden",
stock: 4,
par: 6,
lastIso: -1,
},
{
id: "fen",
name: "Fennel bulb",
glyph: "🥬",
code: "PR-031",
cat: "produce",
supplier: "Mercado Central",
region: "Madrid",
stock: 12,
par: 16,
lastIso: -2,
},
{
id: "pot",
name: "Yukon potato",
glyph: "🥔",
code: "PR-007",
cat: "produce",
supplier: "Mercado Central",
region: "Castilla-La Mancha",
stock: 38,
par: 40,
lastIso: -2,
},
{
id: "lem",
name: "Preserved lemon",
glyph: "🍋",
code: "PR-052",
cat: "produce",
supplier: "Casa Olivar (in-house)",
region: "Pantry",
stock: 22,
par: 24,
lastIso: -7,
},
// meat / fish
{
id: "rib",
name: "Ribeye · dry-aged 28d",
glyph: "🥩",
code: "MF-101",
cat: "meat",
supplier: "Heritage Foods",
region: "USA",
stock: 9,
par: 12,
lastIso: -3,
lastWeight: "16.4 kg",
},
{
id: "bra",
name: "Branzino · whole",
glyph: "🐟",
code: "MF-203",
cat: "meat",
supplier: "Pescados Marín",
region: "Galicia",
stock: 6,
par: 10,
lastIso: -1,
},
{
id: "sal",
name: "Salmón · plancha",
glyph: "🐟",
code: "MF-204",
cat: "meat",
supplier: "Pescados Marín",
region: "Galicia",
stock: 0,
par: 8,
lastIso: -1,
oos: true,
},
{
id: "lam",
name: "Lamb shoulder",
glyph: "🐑",
code: "MF-307",
cat: "meat",
supplier: "Heritage Foods",
region: "USA",
stock: 11,
par: 14,
lastIso: -3,
},
{
id: "oct",
name: "Pulpo · charred",
glyph: "🐙",
code: "MF-220",
cat: "meat",
supplier: "Pescados Marín",
region: "Galicia",
stock: 5,
par: 7,
lastIso: -1,
},
// dairy
{
id: "bur",
name: "Burrata · 250g",
glyph: "🧀",
code: "DA-031",
cat: "dairy",
supplier: "Salumi de Madrid",
region: "Spain · import",
stock: 14,
par: 18,
lastIso: -2,
},
{
id: "par",
name: "Parmigiano · 24m",
glyph: "🧀",
code: "DA-042",
cat: "dairy",
supplier: "Salumi de Madrid",
region: "Emilia-Romagna",
stock: 7,
par: 8,
lastIso: -7,
},
{
id: "but",
name: "Cultured butter",
glyph: "🧈",
code: "DA-018",
cat: "dairy",
supplier: "Local dairy",
region: "León",
stock: 22,
par: 24,
lastIso: -3,
},
{
id: "mil",
name: "Whole milk · 1L",
glyph: "🥛",
code: "DA-001",
cat: "dairy",
supplier: "Local dairy",
region: "León",
stock: 32,
par: 30,
lastIso: -1,
},
// dry
{
id: "rou",
name: "Sourdough starter",
glyph: "🌾",
code: "DR-001",
cat: "dry",
supplier: "Casa Olivar (in-house)",
region: "Pantry",
stock: 1,
par: 1,
lastIso: -120,
},
{
id: "flo",
name: "00 flour · 25 kg",
glyph: "🌾",
code: "DR-014",
cat: "dry",
supplier: "Molino Carbonell",
region: "Valencia",
stock: 4,
par: 6,
lastIso: -4,
},
{
id: "pas",
name: "Pappardelle dough",
glyph: "🍝",
code: "DR-066",
cat: "dry",
supplier: "Casa Olivar (in-house)",
region: "Pantry",
stock: 9,
par: 12,
lastIso: -1,
},
{
id: "ric",
name: "Carnaroli rice",
glyph: "🍚",
code: "DR-022",
cat: "dry",
supplier: "Riso Acquerello",
region: "Piedmont",
stock: 7,
par: 8,
lastIso: -10,
},
{
id: "oil",
name: "Olive oil · extra virgin",
glyph: "🫒",
code: "DR-104",
cat: "dry",
supplier: "Cooperativa Andújar",
region: "Andalucía",
stock: 18,
par: 18,
lastIso: -14,
},
{
id: "sal2",
name: "Sea salt · Maldon",
glyph: "🧂",
code: "DR-205",
cat: "dry",
supplier: "Maldon",
region: "UK · import",
stock: 12,
par: 12,
lastIso: -30,
},
{
id: "veg",
name: "Carbon · oak",
glyph: "🪵",
code: "DR-301",
cat: "dry",
supplier: "Carbón Sevilla",
region: "Andalucía",
stock: 28,
par: 40,
lastIso: -7,
},
// bev
{
id: "tin",
name: "Tinto · Mencía '22",
glyph: "🍷",
code: "BV-401",
cat: "bev",
supplier: "Vinos Bierzo",
region: "León",
stock: 18,
par: 24,
lastIso: -7,
},
{
id: "alb",
name: "Albariño '23",
glyph: "🥂",
code: "BV-402",
cat: "bev",
supplier: "Pazo de Señorans",
region: "Galicia",
stock: 22,
par: 24,
lastIso: -7,
},
{
id: "ver",
name: "Vermut casa · tap",
glyph: "🥃",
code: "BV-501",
cat: "bev",
supplier: "Casa Olivar (in-house)",
region: "On-tap",
stock: 3,
par: 4,
lastIso: -2,
},
{
id: "neg",
name: "Campari",
glyph: "🍸",
code: "BV-606",
cat: "bev",
supplier: "Distribuciones Sur",
region: "Italy · import",
stock: 4,
par: 4,
lastIso: -14,
},
{
id: "agu",
name: "Agua mineral · 1L",
glyph: "💧",
code: "BV-701",
cat: "bev",
supplier: "Solán de Cabras",
region: "Cuenca",
stock: 96,
par: 96,
lastIso: -2,
},
// misc
{
id: "cof",
name: "Espresso beans · 1kg",
glyph: "☕",
code: "MS-110",
cat: "misc",
supplier: "Café Cordobés",
region: "Andalucía",
stock: 5,
par: 6,
lastIso: -4,
},
{
id: "can",
name: "Beeswax candles",
glyph: "🕯",
code: "MS-220",
cat: "misc",
supplier: "Mercado Central",
region: "Madrid",
stock: 30,
par: 40,
lastIso: -21,
},
];
const CAT_LABEL = {
produce: "Produce",
meat: "Meat & fish",
dairy: "Dairy",
dry: "Dry goods",
bev: "Beverages",
misc: "Misc",
};
const rowsEl = document.getElementById("rows");
const emptyEl = document.getElementById("empty");
const chips = document.getElementById("chips");
const searchEl = document.getElementById("search");
const lowCountEl = document.getElementById("lowCount");
const outCountEl = document.getElementById("outCount");
const toast = document.getElementById("toast");
const drawerOverlay = document.getElementById("drawerOverlay");
const adjustDrawer = document.getElementById("adjustDrawer");
const drawerTitle = document.getElementById("drawerTitle");
const drawerMeta = document.getElementById("drawerMeta");
const drawerUnit = document.getElementById("drawerUnit");
const drawerQty = document.getElementById("drawerQty");
const drawerPar = document.getElementById("drawerPar");
const drawerClose = document.getElementById("drawerClose");
const drawerCancel = document.getElementById("drawerCancel");
const drawerSave = document.getElementById("drawerSave");
const drawerDec = document.getElementById("drawerDec");
const drawerInc = document.getElementById("drawerInc");
let cat = "all";
let q = "";
let adjustingId = null;
function relDate(dDays) {
const d = new Date();
d.setDate(d.getDate() + dDays);
const iso = d.toISOString().slice(0, 10);
let txt = "—";
if (dDays === 0) txt = "today";
else if (dDays === -1) txt = "yesterday";
else if (dDays > -30) txt = `${Math.abs(dDays)} days ago`;
else txt = `${Math.round(Math.abs(dDays) / 7)} weeks ago`;
return { iso, txt };
}
function tone(item) {
if (item.oos || item.stock === 0) return "is-danger";
const ratio = item.stock / item.par;
if (ratio <= 0.4) return "is-warn";
return "";
}
function visible(item) {
if (cat !== "all" && item.cat !== cat) return false;
if (!q) return true;
const haystack = `${item.name} ${item.supplier} ${item.code} ${item.region}`.toLowerCase();
return haystack.includes(q);
}
function render() {
let visibleCount = 0;
let low = 0;
let out = 0;
rowsEl.innerHTML = ITEMS.map((item) => {
const hidden = !visible(item);
if (!hidden) visibleCount += 1;
const t = tone(item);
if (t === "is-warn") low += 1;
if (t === "is-danger") out += 1;
const pct = Math.min(100, Math.round((item.stock / Math.max(1, item.par)) * 100));
const d = relDate(item.lastIso);
return `<tr class="${hidden ? "row-hidden" : ""}" data-id="${item.id}">
<td class="col-name">
<div class="ing">
<div class="ing-glyph">${item.glyph}</div>
<div class="ing-info">
<p class="ing-name">${item.name}</p>
<p class="ing-code">${item.code} · ${CAT_LABEL[item.cat]}</p>
</div>
</div>
</td>
<td>
<p class="supplier">${item.supplier}<small>${item.region}</small></p>
</td>
<td class="col-stock">
<div class="par ${t}">
<p class="par-text">
<strong>${item.stock}</strong>
<span>par ${item.par} · ${pct}%</span>
</p>
<div class="par-bar"><div class="par-fill" style="width:${pct}%"></div></div>
</div>
</td>
<td>
<p class="delivery">${d.txt}<small>${d.iso}${item.lastWeight ? ` · ${item.lastWeight}` : ""}</small></p>
</td>
<td class="col-action">
${
item.oos
? `<span class="tag-86">86'd</span>`
: t === ""
? `<button class="btn-quiet" data-action="adjust" data-id="${item.id}">Adjust</button>`
: `<button class="btn-reorder" data-action="reorder" data-id="${item.id}">Reorder</button>`
}
</td>
</tr>`;
}).join("");
lowCountEl.firstChild.textContent = String(low) + " ";
outCountEl.firstChild.textContent = String(out) + " ";
emptyEl.hidden = visibleCount > 0;
}
function openAdjust(id) {
const item = ITEMS.find((i) => i.id === id);
if (!item) return;
adjustingId = id;
drawerTitle.textContent = item.name;
drawerMeta.textContent = `${CAT_LABEL[item.cat]} · ${item.stock === 0 ? "Out of stock" : item.stock <= item.par * 0.4 ? "Low stock" : "In stock"}`;
drawerUnit.textContent = item.unit || "units";
drawerQty.value = item.stock;
drawerPar.textContent = `Par level: ${item.par}`;
drawerOverlay.hidden = false;
adjustDrawer.hidden = false;
drawerQty.focus();
}
function closeDrawer() {
drawerOverlay.hidden = true;
adjustDrawer.hidden = true;
adjustingId = null;
}
drawerClose.addEventListener("click", closeDrawer);
drawerCancel.addEventListener("click", closeDrawer);
drawerOverlay.addEventListener("click", closeDrawer);
drawerDec.addEventListener("click", () => {
const val = Number(drawerQty.value);
if (val > 0) drawerQty.value = val - 1;
});
drawerInc.addEventListener("click", () => {
drawerQty.value = Number(drawerQty.value) + 1;
});
drawerSave.addEventListener("click", () => {
const item = ITEMS.find((i) => i.id === adjustingId);
if (!item) return;
item.stock = Number(drawerQty.value);
item.oos = item.stock === 0;
item.lastIso = 0;
closeDrawer();
render();
showToast(`${item.name} adjusted to ${item.stock}`);
});
chips.addEventListener("click", (e) => {
const btn = e.target.closest("[data-cat]");
if (!btn) return;
cat = btn.dataset.cat;
chips.querySelectorAll(".chip").forEach((c) => c.classList.toggle("is-active", c === btn));
render();
});
searchEl.addEventListener("input", (e) => {
q = e.target.value.trim().toLowerCase();
render();
});
rowsEl.addEventListener("click", (e) => {
const btn = e.target.closest("[data-action]");
if (!btn) return;
const action = btn.dataset.action;
const id = btn.dataset.id;
if (action === "reorder") {
const item = ITEMS.find((i) => i.id === id);
if (item) {
item.stock = item.par;
item.lastIso = 0;
item.oos = false;
render();
showToast(`Reordered · ${item.name} to par`);
}
} else if (action === "adjust") {
openAdjust(id);
}
});
function showToast(msg) {
toast.textContent = msg;
toast.hidden = false;
clearTimeout(showToast._t);
showToast._t = setTimeout(() => (toast.hidden = true), 2400);
}
render();<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link
rel="stylesheet"
href="https://fonts.googleapis.com/css2?family=Playfair+Display:wght@700;800&family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@500;600;700&display=swap"
/>
<link rel="stylesheet" href="style.css" />
<title>Inventory · Casa Olivar Admin</title>
</head>
<body>
<div class="app">
<aside class="rail">
<header class="rail-brand">
<span class="brand-mark">CO</span>
<span class="brand-name">Casa Olivar</span>
</header>
<nav class="rail-nav">
<a class="r-link" href="#">
<span class="r-icon">📊</span><span>Dashboard</span>
</a>
<a class="r-link" href="#">
<span class="r-icon">📅</span><span>Reservations</span>
<span class="r-badge">14</span>
</a>
<a class="r-link" href="#"><span class="r-icon">🍽</span><span>Menu</span></a>
<a class="r-link is-active" href="#">
<span class="r-icon">📦</span><span>Inventory</span>
<span class="r-badge r-badge-warn">3</span>
</a>
<a class="r-link" href="#"><span class="r-icon">👥</span><span>Staff</span></a>
</nav>
<footer class="rail-foot">
<div class="rail-user">
<span class="user-avatar">A</span>
<div>
<p class="user-name">Aitor Mendizabal</p>
<p class="user-role">Head chef</p>
</div>
</div>
</footer>
</aside>
<main class="main">
<header class="top">
<div>
<p class="kicker">Stockroom · last sync 06:18</p>
<h1>Inventory</h1>
</div>
<div class="top-tools">
<button class="ghost" type="button">⤓ Export CSV</button>
<button class="primary" type="button">+ Add item</button>
</div>
</header>
<!-- Status strip -->
<section class="status">
<article class="stat">
<p class="stat-label">In stock</p>
<p class="stat-value">142 <small>SKUs</small></p>
<p class="stat-meta is-up">▲ 6 since Monday</p>
</article>
<article class="stat stat-warn">
<p class="stat-label">Low stock</p>
<p class="stat-value" id="lowCount">3 <small>need attention</small></p>
<p class="stat-meta">par level threshold · 20%</p>
</article>
<article class="stat stat-danger">
<p class="stat-label">Out of stock</p>
<p class="stat-value" id="outCount">1 <small>86'd</small></p>
<p class="stat-meta">Salmón plancha is unavailable tonight</p>
</article>
<article class="stat">
<p class="stat-label">This week's deliveries</p>
<p class="stat-value">4 <small>scheduled</small></p>
<p class="stat-meta">Next: Wed 07:00 — Heritage Foods</p>
</article>
</section>
<!-- Filters + search -->
<section class="toolbar">
<nav class="chips" id="chips">
<button class="chip is-active" data-cat="all">All <span>34</span></button>
<button class="chip" data-cat="produce">Produce <span>9</span></button>
<button class="chip" data-cat="meat">Meat & fish <span>7</span></button>
<button class="chip" data-cat="dairy">Dairy <span>4</span></button>
<button class="chip" data-cat="dry">Dry goods <span>7</span></button>
<button class="chip" data-cat="bev">Beverages <span>5</span></button>
<button class="chip" data-cat="misc">Misc <span>2</span></button>
</nav>
<div class="search">
<svg viewBox="0 0 24 24" width="14" height="14"><circle cx="11" cy="11" r="7" fill="none" stroke="currentColor" stroke-width="2"/><path d="m20 20-3.5-3.5" stroke="currentColor" stroke-width="2" stroke-linecap="round"/></svg>
<input id="search" type="search" placeholder="Search item, supplier, code…" />
</div>
</section>
<!-- Table -->
<section class="table-wrap">
<table class="t" aria-label="Inventory">
<thead>
<tr>
<th class="col-name">Ingredient</th>
<th>Supplier</th>
<th class="col-stock">Stock vs par</th>
<th>Last delivery</th>
<th class="col-action">Action</th>
</tr>
</thead>
<tbody id="rows"></tbody>
</table>
<p class="empty-msg" id="empty" hidden>No items match.</p>
</section>
<!-- Toast -->
<div class="toast" id="toast" hidden role="status" aria-live="polite"></div>
</main>
</div>
<div class="drawer-overlay" id="drawerOverlay" hidden></div>
<aside class="drawer" id="adjustDrawer" hidden aria-modal="true" role="dialog" aria-label="Adjust stock">
<header class="drawer-head">
<h2 class="drawer-title" id="drawerTitle">Adjust stock</h2>
<button class="drawer-close" id="drawerClose" aria-label="Close">✕</button>
</header>
<div class="drawer-body">
<p class="drawer-meta" id="drawerMeta"></p>
<label class="field-label">Current stock <small id="drawerUnit"></small></label>
<div class="qty-row">
<button class="qty-btn" id="drawerDec">−</button>
<input class="qty-input" id="drawerQty" type="number" min="0" step="1" />
<button class="qty-btn" id="drawerInc">+</button>
</div>
<p class="drawer-par" id="drawerPar"></p>
<div class="drawer-actions">
<button class="btn-ghost" id="drawerCancel">Cancel</button>
<button class="btn-primary" id="drawerSave">Save adjustment</button>
</div>
</div>
</aside>
<script src="script.js"></script>
</body>
</html>Admin · Inventory Tracker
Stock dashboard the chef checks every morning. Top strip shows three counts (in-stock · low · out). Filter chips switch between sections (Produce · Meat & fish · Dry goods · Beverages · Misc). Table rows: ingredient + supplier + current stock with par-level bar (green / amber / red), last delivery date, and a quick Reorder button on low-stock rows. Search filters by name, supplier or section.