import React, { useState, useEffect, useCallback } from "react";
import {
Sun, Moon, Check, ClipboardList, MessageSquarePlus,
MapPin, ChevronRight, AlertTriangle, RotateCcw, History, X, User,
Lock, Megaphone, ClipboardCheck, Pin, LogOut, ShieldCheck,
BookOpen, Beer, FileText, Search, AlertCircle, Plus, Trash2, Eye
} from "lucide-react";
// ---------------------------------------------------------------------------
// Taproom Shift Manager — phone-first checklist + shift-handoff tool
// Built for ~15 part-time bartenders across two taproom locations.
// Includes a password-gated Staff Access area for managers:
// • post pre-shift notes bartenders see on the Open screen
// • review submitted closing-checklist reports
// All data lives in the app's persistent storage.
// ---------------------------------------------------------------------------
const LOCATIONS = ["Main Taproom", "Second Location"];
// Manager passcode. Change this to whatever you want — single shared code.
const MANAGER_CODE = "brew2025";
// Checklist templates. Edit these lists to match your real SOPs — each item
// is just a line of text. Grouped into sections for scannability on a phone.
const OPEN_LIST = [
{ section: "Opening Duties", items: [
"Familiarize yourself with the current menu (and what we're out of)",
"Walk through taproom, brewery, patio — tables clean, everything looks good",
"Lights and music on, check volume level inside and out",
"Make sure enough garnishes are prepared and bar is stocked as much as possible",
]},
];
// Closing list is the summer version for now. A Winter variant can be added
// later (swap the patio/umbrella/cushion steps). See CLOSE_LIST_LABEL below.
const CLOSE_LIST_LABEL = "Closing checklist";
const CLOSE_LIST = [
{ section: "Floor & Tables", items: [
"Wipe all tables, inside and out",
"Dry mop / sweep taproom — check under tables for crumbs, check patio for trash",
"Push in chairs, reset moved tables and chairs on patio, close umbrellas",
"Put away cushions, make sure heat lamps are off",
]},
{ section: "Bar & Draft", items: [
"Spray and wipe all bar surfaces and crowler machine (if used)",
"Use squirt bottle to rinse taps and put in stoppers",
"Remove drip tray cover, wipe both sides, rinse and wipe tray, put cover over drain",
"Wash bar mats and leave flat to dry",
"Dishwasher: remove plug, drain, turn off",
]},
{ section: "Glassware & Restock", items: [
"Do all dishes and put clean glasses away",
"Restock to-go fridge & bar fridge as much as possible",
"Refill / cut garnishes for tomorrow",
]},
{ section: "Trash & Equipment", items: [
"Dump recycling bins in blue bin on loading dock, return to taproom",
"Put trash on loading dock and put in new bag",
"Plug in handhelds",
]},
{ section: "Cash", items: [
"Count cash and \u201Cend drawer\u201D in system",
]},
{ section: "Lock Up", items: [
"Close and lock loading dock side door, garage door, and warehouse door",
"Set both thermostats to \u201CAway\u201D or turn off",
"Turn off music (pause Spotify) — pause bathroom music by light switches",
"Flip black power switch in warehouse to turn off speakers and patio lights",
"Turn off taproom lights (leave \u201Cback bar\u201D light on low)",
"Close taproom doors to brewery and warehouse",
"Clock out",
"Lock front door, back door — make sure exiting door is locked behind you",
]},
];
// Seed content — example beer sheets and SOPs so the binder isn't empty.
// You'll replace these via Staff Access. Beers/SOPs are shared across both
// locations; SOP acknowledgements are tracked per person.
const SEED_BEERS = [
{ id: "b1", name: "Mainstay IPA", style: "West Coast IPA", abv: "6.8%", ibu: "65",
desc: "Flagship IPA. Citrus and pine, Centennial + Simcoe hops, dry finish. Our best seller — lead with this for hop-forward guests. Pairs with spicy food and burgers." },
{ id: "b2", name: "Golden Hour", style: "Kölsch", abv: "4.9%", ibu: "22",
desc: "Crisp, light, easy. The gateway beer for guests who 'don't like craft beer.' Clean, slightly fruity, crackery malt. Great on the patio." },
{ id: "b3", name: "Night Shift", style: "Oatmeal Stout", abv: "5.6%", ibu: "30",
desc: "Smooth, roasty, notes of coffee and chocolate. Not heavy despite the dark color — tell guests that. Pairs with dessert." },
];
const SEED_SOPS = [
{ id: "s1", title: "Pouring a proper pour", body: "Open the tap fully and quickly — never pour with a partly-open faucet (causes foam). Hold the glass at 45°, straighten as it fills, aim for one to one-and-a-half fingers of head. If a line pours foamy, note it in the Shift Log so we can check the line.", posted: "Example SOP" },
];
async function safeGet(key) {
try {
const r = await window.storage.get(key);
return r ? r.value : null;
} catch { return null; }
}
async function safeSet(key, value) {
try { await window.storage.set(key, value); } catch { /* no-op */ }
}
const todayKey = () => new Date().toISOString().slice(0, 10);
export default function App() {
const [location, setLocation] = useState(LOCATIONS[0]);
const [tab, setTab] = useState("open"); // open | close | log
const [name, setName] = useState("");
const [openState, setOpenState] = useState({});
const [closeState, setCloseState] = useState({});
const [logEntries, setLogEntries] = useState([]);
const [history, setHistory] = useState([]); // closing reports
const [shiftNote, setShiftNote] = useState(""); // manager pre-shift note
const [loaded, setLoaded] = useState(false);
const [draft, setDraft] = useState("");
const [flag, setFlag] = useState(false);
// manager area
const [showStaff, setShowStaff] = useState(false);
const [unlocked, setUnlocked] = useState(false);
const [codeInput, setCodeInput] = useState("");
const [codeErr, setCodeErr] = useState(false);
const [noteDraft, setNoteDraft] = useState("");
const [mgrTab, setMgrTab] = useState("notes"); // notes | reports
const [savedFlash, setSavedFlash] = useState(false);
// quality attestation gate (closing only)
const [attestOpen, setAttestOpen] = useState(false);
const [attestName, setAttestName] = useState("");
const [attestErr, setAttestErr] = useState(false);
// binder: beer sheets + SOPs + per-person acknowledgements
const [beers, setBeers] = useState([]);
const [sops, setSops] = useState([]);
const [acks, setAcks] = useState({}); // { sopId: [names] }
const [binderView, setBinderView] = useState("beers"); // beers | sops
const [search, setSearch] = useState("");
const [openSheet, setOpenSheet] = useState(null); // beer or sop being read
const [readSopId, setReadSopId] = useState(null); // sop open in read-modal
// manager binder editors
const [beerForm, setBeerForm] = useState(null); // {id?,name,style,abv,ibu,desc}
const [sopForm, setSopForm] = useState(null); // {id?,title,body}
const dayPrefix = `${location}|${todayKey()}`;
// load persisted state when location changes
useEffect(() => {
let active = true;
(async () => {
setLoaded(false);
const o = await safeGet(`open|${dayPrefix}`);
const c = await safeGet(`close|${dayPrefix}`);
const l = await safeGet(`log|${location}`);
const h = await safeGet(`reports|${location}`);
const sn = await safeGet(`shiftnote|${location}`);
const nm = await safeGet("bartender-name");
const bz = await safeGet("binder-beers");
const sp = await safeGet("binder-sops");
const ak = await safeGet("binder-acks");
if (!active) return;
setOpenState(o ? JSON.parse(o) : {});
setCloseState(c ? JSON.parse(c) : {});
setLogEntries(l ? JSON.parse(l) : []);
setHistory(h ? JSON.parse(h) : []);
setShiftNote(sn || "");
setNoteDraft(sn || "");
setBeers(bz ? JSON.parse(bz) : SEED_BEERS);
setSops(sp ? JSON.parse(sp) : SEED_SOPS);
setAcks(ak ? JSON.parse(ak) : {});
if (nm) setName(nm);
setLoaded(true);
})();
return () => { active = false; };
}, [location]); // eslint-disable-line
const toggle = useCallback((which, id) => {
if (which === "open") {
setOpenState(prev => {
const next = { ...prev, [id]: !prev[id] };
safeSet(`open|${dayPrefix}`, JSON.stringify(next));
return next;
});
} else {
setCloseState(prev => {
const next = { ...prev, [id]: !prev[id] };
safeSet(`close|${dayPrefix}`, JSON.stringify(next));
return next;
});
}
}, [dayPrefix]);
const saveName = (v) => { setName(v); safeSet("bartender-name", v); };
const addLog = () => {
if (!draft.trim()) return;
const entry = {
id: Date.now(),
text: draft.trim(),
flag,
by: name || "Unnamed",
when: new Date().toLocaleString([], { month: "short", day: "numeric", hour: "numeric", minute: "2-digit" }),
};
const next = [entry, ...logEntries].slice(0, 60);
setLogEntries(next);
safeSet(`log|${location}`, JSON.stringify(next));
setDraft(""); setFlag(false);
};
const removeLog = (id) => {
const next = logEntries.filter(e => e.id !== id);
setLogEntries(next);
safeSet(`log|${location}`, JSON.stringify(next));
};
const currentList = tab === "open" ? OPEN_LIST : CLOSE_LIST;
const currentState = tab === "open" ? openState : closeState;
const totalItems = currentList.reduce((n, s) => n + s.items.length, 0);
const doneItems = currentList.reduce(
(n, s) => n + s.items.filter((_, i) => currentState[`${s.section}-${i}`]).length, 0
);
const pct = totalItems ? Math.round((doneItems / totalItems) * 100) : 0;
const complete = doneItems === totalItems && totalItems > 0;
// capture full detail when a shift is finished
const finishShift = (attestedName) => {
const flagged = logEntries.filter(e => e.flag).map(e => e.text);
const record = {
id: Date.now(),
type: tab,
location,
by: name || "Unnamed",
date: new Date().toLocaleString([], { weekday: "short", month: "short", day: "numeric", hour: "numeric", minute: "2-digit" }),
items: `${doneItems}/${totalItems}`,
sections: currentList.map(s => ({
section: s.section,
items: s.items.map((label, i) => ({ label, done: !!currentState[`${s.section}-${i}`] })),
})),
flaggedNotes: flagged,
attestedBy: attestedName || null,
};
const next = [record, ...history].slice(0, 60);
setHistory(next);
safeSet(`reports|${location}`, JSON.stringify(next));
if (tab === "open") { setOpenState({}); safeSet(`open|${dayPrefix}`, JSON.stringify({})); }
else { setCloseState({}); safeSet(`close|${dayPrefix}`, JSON.stringify({})); }
};
// Open submits directly; Close goes through the quality attestation gate.
const onSubmit = () => {
if (tab === "close") { setAttestName(name || ""); setAttestErr(false); setAttestOpen(true); }
else finishShift(null);
};
const confirmAttest = () => {
if (!attestName.trim()) { setAttestErr(true); return; }
finishShift(attestName.trim());
setAttestOpen(false); setAttestName(""); setAttestErr(false);
};
// ---- binder actions ----
const persistBeers = (next) => { setBeers(next); safeSet("binder-beers", JSON.stringify(next)); };
const persistSops = (next) => { setSops(next); safeSet("binder-sops", JSON.stringify(next)); };
const persistAcks = (next) => { setAcks(next); safeSet("binder-acks", JSON.stringify(next)); };
// bartender acknowledges an SOP (records their name once)
const acknowledgeSop = (sopId) => {
const who = (name || "").trim();
if (!who) return; // require a name in the header field
const list = acks[sopId] || [];
if (!list.includes(who)) persistAcks({ ...acks, [sopId]: [...list, who] });
setReadSopId(null);
};
const hasRead = (sopId) => {
const who = (name || "").trim();
return who ? (acks[sopId] || []).includes(who) : false;
};
const unreadSops = sops.filter(s => !hasRead(s.id));
// manager: save/delete beer sheets
const saveBeer = () => {
if (!beerForm.name.trim()) return;
const next = beerForm.id
? beers.map(b => b.id === beerForm.id ? beerForm : b)
: [...beers, { ...beerForm, id: "b" + Date.now() }];
persistBeers(next); setBeerForm(null);
};
const deleteBeer = (id) => persistBeers(beers.filter(b => b.id !== id));
// manager: save/delete SOPs. New SOP starts unacknowledged by everyone.
const saveSop = () => {
if (!sopForm.title.trim()) return;
let next, newId = sopForm.id;
if (sopForm.id) {
next = sops.map(s => s.id === sopForm.id ? { ...s, title: sopForm.title, body: sopForm.body } : s);
} else {
newId = "s" + Date.now();
next = [{ id: newId, title: sopForm.title, body: sopForm.body, posted: new Date().toLocaleDateString() }, ...sops];
}
persistSops(next); setSopForm(null);
};
const deleteSop = (id) => {
persistSops(sops.filter(s => s.id !== id));
const { [id]: _drop, ...rest } = acks; persistAcks(rest);
};
const resetList = () => {
if (tab === "open") { setOpenState({}); safeSet(`open|${dayPrefix}`, JSON.stringify({})); }
else { setCloseState({}); safeSet(`close|${dayPrefix}`, JSON.stringify({})); }
};
// ---- manager actions ----
const tryUnlock = () => {
if (codeInput.trim() === MANAGER_CODE) {
setUnlocked(true); setCodeErr(false); setCodeInput("");
} else { setCodeErr(true); }
};
const saveNote = () => {
setShiftNote(noteDraft);
safeSet(`shiftnote|${location}`, noteDraft);
setSavedFlash(true);
setTimeout(() => setSavedFlash(false), 1800);
};
const clearNote = () => {
setShiftNote(""); setNoteDraft("");
safeSet(`shiftnote|${location}`, "");
};
const closeStaff = () => { setShowStaff(false); setUnlocked(false); setCodeInput(""); setCodeErr(false); };
const closingReports = history.filter(h => h.type === "close");
return (
{/* ---- Header ---- */}
saveName(e.target.value)} />
{/* ---- Tabs ---- */}
{!loaded &&
Note from management — read before your shift
)}
{currentList.map(sec => (
))}
>
)}
{loaded && tab === "log" && (
<>
{logEntries.length === 0 &&
))}
>
)}
{loaded && tab === "binder" && (
<>
{/* unread SOP alert banner */}
{unreadSops.length > 0 && (
{unreadSops.length === 1 ? "New procedure to read" : `${unreadSops.length} new procedures to read`}
)}
{/* beers / sops switch */}
{binderView === "beers" && (
<>
setSearch(e.target.value)} />
{beers.filter(b =>
(b.name + b.style).toLowerCase().includes(search.toLowerCase())
).map(b => (
))}
{beers.length === 0 &&
{/* ---- Beer sheet reader ---- */}
{openSheet && openSheet.kind === "beer" && (
)}
);
}
function TabBtn({ active, onClick, icon, label, badge }) {
return (
);
}
// ---------------------------------------------------------------------------
const COPPER = "#c98a3c";
const AMBER = "#e6b450";
const PAPER = "#f3ece0";
const S = {
shell: { maxWidth: 480, margin: "0 auto", minHeight: "100vh", background: "#1c1815",
color: PAPER, fontFamily: "'Inter', system-ui, sans-serif", paddingBottom: 0 },
header: { padding: "20px 18px 14px", background: "linear-gradient(165deg,#241f1a,#191512)",
borderBottom: "1px solid #322a22" },
brandRow: { display: "flex", alignItems: "center", gap: 11, marginBottom: 16 },
brandMark: { width: 38, height: 38, borderRadius: 9, background: `linear-gradient(135deg,${COPPER},${AMBER})`,
color: "#1c1815", fontSize: 22, display: "flex", alignItems: "center", justifyContent: "center", fontWeight: 800 },
brandName: { fontSize: 16, fontWeight: 800, letterSpacing: 1.5, fontFamily: "'Oswald','Inter',sans-serif" },
brandSub: { fontSize: 11, color: "#9a8a73", letterSpacing: 0.5, marginTop: 1 },
locWrap: { display: "flex", gap: 6, background: "#171310", padding: 4, borderRadius: 11, marginBottom: 11 },
locBtn: { flex: 1, padding: "9px 6px", borderRadius: 8, border: "none", background: "transparent",
color: "#9a8a73", fontSize: 12.5, fontWeight: 600, cursor: "pointer", display: "flex",
alignItems: "center", justifyContent: "center" },
locBtnActive: { background: `linear-gradient(135deg,${COPPER},#a9712f)`, color: "#1c1815", boxShadow: "0 2px 8px rgba(201,138,60,.3)" },
nameWrap: { display: "flex", alignItems: "center", gap: 8, background: "#171310",
border: "1px solid #2c251e", borderRadius: 9, padding: "9px 12px" },
nameInput: { flex: 1, background: "transparent", border: "none", outline: "none",
color: PAPER, fontSize: 14, fontFamily: "inherit" },
tabs: { display: "flex", background: "#191512", borderBottom: "1px solid #2c251e", position: "sticky", top: 0, zIndex: 5 },
tabBtn: { flex: 1, padding: "11px 0 9px", background: "transparent", border: "none",
borderBottom: "2px solid transparent", color: "#7d6f5c", cursor: "pointer",
display: "flex", flexDirection: "column", alignItems: "center" },
tabBtnActive: { color: AMBER, borderBottom: `2px solid ${COPPER}` },
tabLabel: { fontSize: 11.5, fontWeight: 600, letterSpacing: 0.3 },
main: { padding: "16px 16px 0", minHeight: "50vh" },
loading: { textAlign: "center", color: "#7d6f5c", padding: 40, fontSize: 14 },
noteBanner: { background: "linear-gradient(135deg,#2c2415,#241d12)", border: "1px solid #5a4827",
borderRadius: 13, padding: 14, marginBottom: 16 },
noteBannerHead: { fontSize: 11.5, fontWeight: 700, letterSpacing: 0.4, color: AMBER,
display: "flex", alignItems: "center", marginBottom: 7, textTransform: "uppercase" },
noteBannerBody: { fontSize: 14.5, lineHeight: 1.45, color: "#f0e6d2", whiteSpace: "pre-wrap" },
progCard: { background: "linear-gradient(135deg,#262019,#1f1a15)", border: "1px solid #34291f",
borderRadius: 14, padding: 16, marginBottom: 18 },
progTop: { display: "flex", justifyContent: "space-between", alignItems: "baseline" },
progLabel: { fontSize: 13, fontWeight: 600, color: "#cdbca3" },
progPct: { fontSize: 26, fontWeight: 800, color: AMBER, fontFamily: "'Oswald','Inter',sans-serif" },
progTrack: { height: 7, background: "#171310", borderRadius: 4, overflow: "hidden", margin: "10px 0 7px" },
progFill: { height: "100%", background: `linear-gradient(90deg,${COPPER},${AMBER})`, borderRadius: 4, transition: "width .3s ease" },
progMeta: { fontSize: 11.5, color: "#9a8a73" },
section: { marginBottom: 18 },
secTitle: { fontSize: 11, fontWeight: 700, letterSpacing: 1.4, textTransform: "uppercase",
color: COPPER, margin: "0 0 9px 2px" },
row: { width: "100%", display: "flex", alignItems: "center", gap: 12, padding: "13px 13px",
background: "#221d18", border: "1px solid #2c251e", borderRadius: 11, marginBottom: 7,
cursor: "pointer", textAlign: "left", transition: "background .15s" },
rowDone: { background: "#1d1a16", borderColor: "#28231d" },
box: { width: 22, height: 22, borderRadius: 6, border: "2px solid #4a3f31", flexShrink: 0,
display: "flex", alignItems: "center", justifyContent: "center", transition: "all .15s" },
boxOn: { background: AMBER, borderColor: AMBER },
rowText: { fontSize: 14, lineHeight: 1.35, color: PAPER },
rowTextDone: { color: "#7d6f5c", textDecoration: "line-through" },
actionRow: { display: "flex", gap: 10, margin: "8px 0 28px" },
ghostBtn: { display: "flex", alignItems: "center", justifyContent: "center", padding: "13px 18px",
background: "transparent", border: "1px solid #3a3128", borderRadius: 11, color: "#b7a589",
fontSize: 14, fontWeight: 600, cursor: "pointer" },
primaryBtn: { flex: 1, display: "flex", alignItems: "center", justifyContent: "center",
padding: "13px", background: `linear-gradient(135deg,${COPPER},#a9712f)`, border: "none",
borderRadius: 11, color: "#1c1815", fontSize: 14.5, fontWeight: 700, cursor: "pointer",
boxShadow: "0 4px 14px rgba(201,138,60,.28)" },
primaryBtnDisabled: { background: "#2c251e", color: "#6b5e4c", boxShadow: "none", cursor: "not-allowed" },
logIntro: { fontSize: 13, lineHeight: 1.45, color: "#a89a82", margin: "2px 2px 14px" },
composer: { background: "#221d18", border: "1px solid #2c251e", borderRadius: 13, padding: 13, marginBottom: 18 },
composerInput: { width: "100%", background: "#171310", border: "1px solid #2c251e", borderRadius: 9,
color: PAPER, fontSize: 14, padding: 11, fontFamily: "inherit", resize: "vertical", outline: "none", boxSizing: "border-box" },
composerRow: { display: "flex", gap: 8, marginTop: 10 },
flagBtn: { display: "flex", alignItems: "center", padding: "9px 13px", background: "transparent",
border: "1px solid #3a3128", borderRadius: 9, color: "#9a8a73", fontSize: 12.5, fontWeight: 600, cursor: "pointer" },
flagBtnOn: { borderColor: "#e0922f", color: "#e0922f", background: "rgba(224,146,47,.1)" },
postBtn: { marginLeft: "auto", display: "flex", alignItems: "center", padding: "9px 16px",
background: `linear-gradient(135deg,${COPPER},#a9712f)`, border: "none", borderRadius: 9,
color: "#1c1815", fontSize: 13.5, fontWeight: 700, cursor: "pointer" },
empty: { textAlign: "center", color: "#7d6f5c", fontSize: 13.5, padding: "22px 10px" },
logCard: { background: "#221d18", border: "1px solid #2c251e", borderRadius: 12, padding: 13, marginBottom: 9 },
logCardFlag: { borderColor: "#5c4527", background: "#241c14" },
logHead: { display: "flex", alignItems: "center", gap: 8, marginBottom: 6 },
logBy: { fontSize: 12.5, fontWeight: 700, color: AMBER, display: "flex", alignItems: "center" },
logWhen: { fontSize: 11, color: "#7d6f5c" },
logDel: { marginLeft: "auto", background: "transparent", border: "none", color: "#6b5e4c", cursor: "pointer", padding: 2 },
logText: { fontSize: 14, lineHeight: 1.4, color: "#e8ddcb", whiteSpace: "pre-wrap" },
footer: { padding: "26px 16px 30px", textAlign: "center", borderTop: "1px solid #241f1a", marginTop: 10 },
staffLink: { display: "inline-flex", alignItems: "center", background: "transparent", border: "none",
color: "#6b5e4c", fontSize: 12, fontWeight: 600, letterSpacing: 0.5, cursor: "pointer", padding: 8 },
overlay: { position: "fixed", inset: 0, background: "rgba(0,0,0,.6)", display: "flex",
alignItems: "flex-end", justifyContent: "center", zIndex: 50 },
drawer: { width: "100%", maxWidth: 480, maxHeight: "82vh", overflowY: "auto", background: "#1f1a15",
borderTop: `3px solid ${COPPER}`, borderRadius: "18px 18px 0 0", padding: "18px 16px 26px" },
drawerHead: { display: "flex", alignItems: "center", marginBottom: 16 },
drawerTitle: { fontSize: 14.5, fontWeight: 700, color: PAPER, display: "flex", alignItems: "center" },
lockPane: { padding: "10px 4px 6px" },
lockHint: { fontSize: 13.5, color: "#a89a82", margin: "0 0 12px" },
codeInput: { width: "100%", background: "#171310", border: "1px solid #3a3128", borderRadius: 10,
color: PAPER, fontSize: 16, padding: "13px 14px", outline: "none", boxSizing: "border-box", marginBottom: 4 },
codeInputErr: { borderColor: "#c0572f" },
codeErrText: { fontSize: 12.5, color: "#e0915f", margin: "6px 2px 0" },
unlockBtn: { width: "100%", marginTop: 14, padding: 13, background: `linear-gradient(135deg,${COPPER},#a9712f)`,
border: "none", borderRadius: 10, color: "#1c1815", fontSize: 14.5, fontWeight: 700, cursor: "pointer" },
mgrTabs: { display: "flex", gap: 6, background: "#171310", padding: 4, borderRadius: 11, marginBottom: 16 },
mgrTab: { flex: 1, display: "flex", alignItems: "center", justifyContent: "center", padding: "10px 6px",
borderRadius: 8, border: "none", background: "transparent", color: "#9a8a73", fontSize: 12.5,
fontWeight: 600, cursor: "pointer" },
mgrTabOn: { background: "#2c2417", color: AMBER },
mgrPane: { paddingBottom: 6 },
mgrHint: { fontSize: 12.5, lineHeight: 1.45, color: "#a89a82", margin: "0 0 11px" },
noteInput: { width: "100%", background: "#171310", border: "1px solid #2c251e", borderRadius: 10,
color: PAPER, fontSize: 14.5, padding: 12, fontFamily: "inherit", resize: "vertical", outline: "none", boxSizing: "border-box" },
mgrBtnRow: { display: "flex", gap: 9, marginTop: 11 },
ghostBtnSm: { padding: "11px 16px", background: "transparent", border: "1px solid #3a3128",
borderRadius: 10, color: "#b7a589", fontSize: 13.5, fontWeight: 600, cursor: "pointer" },
savePrimary: { flex: 1, padding: 11, background: `linear-gradient(135deg,${COPPER},#a9712f)`,
border: "none", borderRadius: 10, color: "#1c1815", fontSize: 14, fontWeight: 700, cursor: "pointer" },
currentNote: { marginTop: 14, padding: 12, background: "#241d12", border: "1px solid #4a3a1f",
borderRadius: 10, fontSize: 13.5, lineHeight: 1.4, color: "#e8ddcb" },
currentNoteLabel: { color: AMBER, fontWeight: 700, fontSize: 12 },
report: { background: "#221d18", border: "1px solid #2c251e", borderRadius: 12, padding: 13, marginBottom: 10 },
reportHead: { display: "flex", alignItems: "center", justifyContent: "space-between" },
reportBy: { fontSize: 14, fontWeight: 700, color: PAPER },
reportDate: { fontSize: 11.5, color: "#9a8a73", marginTop: 1 },
reportChip: { fontSize: 12.5, fontWeight: 700, padding: "5px 11px", borderRadius: 7 },
attestCard: { width: "100%", maxWidth: 440, background: "#1f1a15", borderTop: `3px solid ${AMBER}`,
borderRadius: "18px 18px 0 0", padding: "22px 20px 26px", textAlign: "left" },
attestIcon: { width: 48, height: 48, borderRadius: 12, background: `linear-gradient(135deg,${COPPER},${AMBER})`,
display: "flex", alignItems: "center", justifyContent: "center", marginBottom: 14 },
attestTitle: { fontSize: 18, fontWeight: 700, color: PAPER, marginBottom: 8 },
attestBody: { fontSize: 13.5, lineHeight: 1.5, color: "#c9bca5", margin: "0 0 10px" },
attestList: { margin: "0 0 14px", padding: "0 0 0 18px", color: "#e0d4bf", fontSize: 13.5, lineHeight: 1.7 },
attestSign: { fontSize: 13, fontWeight: 600, color: AMBER, margin: "0 0 9px" },
attestBtnRow: { display: "flex", gap: 9, marginTop: 14 },
attestLine: { marginTop: 9, padding: "8px 11px", background: "#1b2419", border: "1px solid #2f4a2f",
borderRadius: 8, fontSize: 12.5, color: "#bfe0bf", display: "flex", alignItems: "center" },
missBlock: { marginTop: 10, padding: "9px 11px", background: "#2a1f15", borderRadius: 8,
fontSize: 12.5, lineHeight: 1.4, color: "#e6b07a" },
flagBlock: { marginTop: 9, padding: "9px 11px", background: "#241c14", border: "1px solid #4a3722",
borderRadius: 8, fontSize: 12.5, lineHeight: 1.5, color: "#e8c79a" },
flagLine: { marginTop: 3 },
details: { marginTop: 10 },
summary: { fontSize: 12.5, color: COPPER, fontWeight: 600, cursor: "pointer" },
repSec: { fontSize: 10.5, fontWeight: 700, letterSpacing: 1, textTransform: "uppercase", color: "#8a7a63", marginBottom: 4 },
repItem: { display: "flex", alignItems: "center", fontSize: 13, padding: "2px 0" },
lockOutBtn: { width: "100%", marginTop: 16, padding: 11, background: "transparent",
border: "1px solid #3a3128", borderRadius: 10, color: "#9a8a73", fontSize: 13, fontWeight: 600,
cursor: "pointer", display: "flex", alignItems: "center", justifyContent: "center" },
tabBadge: { position: "absolute", top: -6, right: -10, minWidth: 16, height: 16, padding: "0 4px",
borderRadius: 8, background: "#d9602f", color: "#fff", fontSize: 10, fontWeight: 700,
display: "flex", alignItems: "center", justifyContent: "center" },
sopAlert: { background: "linear-gradient(135deg,#2c2415,#241d12)", border: "1px solid #5a4827",
borderRadius: 13, padding: 14, marginBottom: 16 },
sopAlertHead: { display: "flex", alignItems: "center", fontSize: 14, fontWeight: 700, color: AMBER, marginBottom: 4 },
sopAlertSub: { fontSize: 12, color: "#b7a589", marginBottom: 10, lineHeight: 1.4 },
sopAlertItem: { width: "100%", display: "flex", alignItems: "center", padding: "11px 12px",
background: "#1c1711", border: "1px solid #4a3a1f", borderRadius: 9, marginBottom: 7,
color: "#f0e6d2", fontSize: 13.5, fontWeight: 500, cursor: "pointer" },
sopAlertTag: { fontSize: 12, color: AMBER, fontWeight: 600, marginLeft: 8, whiteSpace: "nowrap" },
binderSwitch: { display: "flex", gap: 6, background: "#171310", padding: 4, borderRadius: 11, marginBottom: 14 },
binderSwBtn: { flex: 1, display: "flex", alignItems: "center", justifyContent: "center", padding: "10px 6px",
borderRadius: 8, border: "none", background: "transparent", color: "#9a8a73", fontSize: 13, fontWeight: 600, cursor: "pointer" },
binderSwOn: { background: "#2c2417", color: AMBER },
searchWrap: { display: "flex", alignItems: "center", gap: 8, background: "#171310",
border: "1px solid #2c251e", borderRadius: 10, padding: "10px 13px", marginBottom: 12 },
searchInput: { flex: 1, background: "transparent", border: "none", outline: "none", color: PAPER, fontSize: 14, fontFamily: "inherit" },
beerRow: { width: "100%", display: "flex", alignItems: "center", gap: 12, padding: "13px 14px",
background: "#221d18", border: "1px solid #2c251e", borderRadius: 11, marginBottom: 8, cursor: "pointer" },
beerName: { fontSize: 14.5, fontWeight: 600, color: PAPER },
beerStyle: { fontSize: 12, color: "#9a8a73", marginTop: 1 },
beerStats: { display: "flex", flexDirection: "column", alignItems: "flex-end", gap: 2, fontSize: 11.5, color: COPPER, fontWeight: 600 },
sopRow: { width: "100%", display: "flex", alignItems: "center", padding: "13px 14px",
background: "#221d18", border: "1px solid #2c251e", borderRadius: 11, marginBottom: 8, cursor: "pointer" },
sopRowTitle: { fontSize: 14, fontWeight: 600, color: PAPER },
sopRowMeta: { fontSize: 11.5, marginTop: 2, fontWeight: 600 },
sheetStyle: { fontSize: 13, color: COPPER, fontWeight: 600, marginBottom: 14 },
sheetStatRow: { display: "flex", gap: 10, marginBottom: 16 },
sheetStat: { flex: 1, background: "#171310", border: "1px solid #2c251e", borderRadius: 10, padding: "12px", textAlign: "center" },
sheetStatNum: { fontSize: 20, fontWeight: 800, color: AMBER, fontFamily: "'Oswald','Inter',sans-serif" },
sheetStatLbl: { fontSize: 10.5, color: "#9a8a73", letterSpacing: 1, marginTop: 2 },
sheetDesc: { fontSize: 14.5, lineHeight: 1.55, color: "#e8ddcb", whiteSpace: "pre-wrap" },
sopBody: { fontSize: 14.5, lineHeight: 1.6, color: "#e8ddcb", whiteSpace: "pre-wrap", marginBottom: 18 },
sopAckBtn: { width: "100%", padding: 13, background: `linear-gradient(135deg,${COPPER},#a9712f)`,
border: "none", borderRadius: 11, color: "#1c1815", fontSize: 14.5, fontWeight: 700, cursor: "pointer",
display: "flex", alignItems: "center", justifyContent: "center" },
sopAcked: { padding: 12, background: "#1b2419", border: "1px solid #2f4a2f", borderRadius: 10,
color: "#bfe0bf", fontSize: 13.5, display: "flex", alignItems: "center", justifyContent: "center" },
sopNoName: { padding: 12, background: "#241c14", border: "1px solid #4a3a1f", borderRadius: 10,
color: "#e6b07a", fontSize: 13, textAlign: "center", lineHeight: 1.4 },
mgrSubHead: { display: "flex", alignItems: "center", fontSize: 14, fontWeight: 700, color: PAPER, marginBottom: 6 },
addMini: { marginLeft: "auto", display: "flex", alignItems: "center", gap: 3, padding: "5px 10px",
background: "#2c2417", border: "1px solid #4a3a1f", borderRadius: 8, color: AMBER, fontSize: 12, fontWeight: 600, cursor: "pointer" },
editBox: { background: "#171310", border: "1px solid #3a3128", borderRadius: 11, padding: 13, marginBottom: 12 },
editInput: { width: "100%", background: "#221d18", border: "1px solid #2c251e", borderRadius: 8,
color: PAPER, fontSize: 14, padding: "10px 12px", fontFamily: "inherit", outline: "none", boxSizing: "border-box" },
mgrSopCard: { background: "#221d18", border: "1px solid #2c251e", borderRadius: 11, padding: 12, marginBottom: 8 },
mgrSopTop: { display: "flex", alignItems: "center", gap: 8 },
mgrSopTitle: { flex: 1, fontSize: 13.5, fontWeight: 600, color: PAPER },
iconBtn: { width: 28, height: 28, borderRadius: 7, border: "1px solid #3a3128", background: "#1c1711",
color: "#9a8a73", display: "flex", alignItems: "center", justifyContent: "center", cursor: "pointer" },
readTrack: { marginTop: 8, fontSize: 12, color: "#b7a589", display: "flex", alignItems: "flex-start", lineHeight: 1.4 },
};
const CSS = `
* { -webkit-tap-highlight-color: transparent; }
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;600;700;800&family=Oswald:wght@500;600;700&display=swap');
::placeholder { color: #6b5e4c; }
button:focus-visible, input:focus-visible, textarea:focus-visible { outline: 2px solid ${AMBER}; outline-offset: 2px; }
@media (prefers-reduced-motion: reduce) { * { transition: none !important; } }
`;
◗
SHIFT MANAGER
Taproom Operations
{LOCATIONS.map(loc => (
))}
Loading shift…
}
{loaded && tab !== "log" && (
<>
{/* manager pre-shift note shows on the OPEN screen */}
{tab === "open" && shiftNote && (
{shiftNote}
{tab === "open" ? "Opening checklist" : CLOSE_LIST_LABEL}
{pct}%
{doneItems} of {totalItems} done
{sec.section}
{sec.items.map((item, i) => { const id = `${sec.section}-${i}`; const on = !!currentState[id]; return ( ); })}
Pass anything the next shift needs to know — a keg that blew,
a broken stool, a regular's tab, a delivery coming in.
No notes yet. Start the handoff above.
}
{logEntries.map(e => (
{e.flag && }
{e.by}
{e.when}
{e.text}
{name.trim()
? "Tap each one, read it, and confirm — your name is recorded."
: "Add your name at the top of the app first, then read & confirm."}
{unreadSops.map(s => (
))}
No beer sheets yet.
}
>
)}
{binderView === "sops" && (
<>
{sops.map(s => (
))}
{sops.length === 0 && No SOPs posted yet.
}
>
)}
>
)}
setOpenSheet(null)}>
)}
{/* ---- SOP reader + acknowledge ---- */}
{readSopId && (() => {
const s = sops.find(x => x.id === readSopId);
if (!s) return null;
const already = hasRead(s.id);
return (
e.stopPropagation()}>
{openSheet.name}
{openSheet.ibu ? : null}
{openSheet.style}
{openSheet.abv}
ABV
{openSheet.ibu}
IBU
{openSheet.desc}
setReadSopId(null)}>
);
})()}
{/* ---- Quality attestation gate (closing) ---- */}
{attestOpen && (
e.stopPropagation()}>
{s.title}
You've confirmed you read this.
) : !name.trim() ? (
{s.body}
{already ? (
Add your name at the top of the app, then you can confirm.
) : (
)}
setAttestOpen(false)}>
)}
{/* ---- Staff Access footer link ---- */}
{/* ---- Manager drawer ---- */}
{showStaff && (
e.stopPropagation()}>
One last thing before you go
You've checked everything off — thank you. Before you submit, put your name to it. By signing, you're confirming you actually looked, not just ticked boxes:
- Every table is wiped and clean — inside and on the patio
- The floor is swept, including under the tables
- The bar and taps are genuinely clean, not just rinsed
Type your name to confirm the room is guest-ready:
{ setAttestName(e.target.value); setAttestErr(false); }} onKeyDown={e => e.key === "Enter" && confirmAttest()} autoFocus /> {attestErr &&Please type your name to submit.
}
e.stopPropagation()}>
Staff Access — {location}
{!unlocked && (
)}
{unlocked && (
<>
{mgrTab === "notes" && (
)}
{mgrTab === "reports" && (
{r.items}
{incomplete.length > 0 && (
Skipped: {incomplete.map(i => i.label).join(" · ")}
)}
{r.flaggedNotes?.length > 0 && (
Signed guest-ready by {r.attestedBy}
)}
))}
);
})}
)}
{mgrTab === "binder" && (
Enter the manager passcode.
{ setCodeInput(e.target.value); setCodeErr(false); }} onKeyDown={e => e.key === "Enter" && tryUnlock()} autoFocus /> {codeErr &&That code didn't match. Try again.
}
This shows at the top of the Open screen for every bartender at {location} until you clear it.
Every submitted closing checklist for {location}, newest first.
{closingReports.length === 0 && (No closing reports submitted yet.
)}
{closingReports.map(r => {
const incomplete = r.sections?.flatMap(s => s.items).filter(i => !i.done) || [];
return (
{r.by}
{r.date}
Flagged in shift log:
{r.flaggedNotes.map((t, i) =>
)}
{r.attestedBy && (
• {t}
)}
Full checklist
{r.sections?.map(s => ({s.section}
{s.items.map((it, i) => (
{it.done
?
: }
{it.label}
))}
{/* SOPs management with read tracking */}
SOPs
{readers.length === 0
? No one has confirmed yet
: {readers.length} read: {readers.join(", ")}}
);
})}
{/* Beer sheets management */}
Beer sheets
{beerForm && (
))}
)}
>
)}
A new SOP alerts every bartender until they read & confirm. Below, see who's acknowledged each one.
{sopForm && (
setSopForm({ ...sopForm, title: e.target.value })} />
)}
{sops.map(s => {
const readers = acks[s.id] || [];
return (
{s.title}
setBeerForm({ ...beerForm, name: e.target.value })} />
setBeerForm({ ...beerForm, style: e.target.value })} />
)}
{beers.map(b => (
setBeerForm({ ...beerForm, abv: e.target.value })} />
setBeerForm({ ...beerForm, ibu: e.target.value })} />
{b.name} · {b.style}