/* ShiftBud — main app: calendar (day roster) + sidebar, status colors, coverage, i18n */ /* eslint-disable */ const { useState: uS } = React; const T = (k, p) => window.SBI.t(k, p); const DOW = (d) => window.SBI.dow(d); const MONTH = () => window.SBI.month(); /* ── Error boundary — shows crash details instead of blank screen ── */ class ErrorBoundary extends React.Component { constructor(p) { super(p); this.state = { err: null }; } static getDerivedStateFromError(e) { return { err: e }; } render() { if (!this.state.err) return this.props.children; return (
ShiftBud Error
          {this.state.err.message}{"\n\n"}{this.state.err.stack}
        
); } } const GROUPS = [ { id: "all", label: "f_all", roles: null }, { id: "foh", label: "f_foh", roles: ["Server", "Host", "Barista"] }, { id: "kitchen", label: "f_kitchen", roles: ["Head Chef", "Cook", "Dishwasher"] }, { id: "mgmt", label: "f_mgmt", roles: ["Manager"] }]; /* ---------- Sidebar (menu band) ---------- */ function Sidebar({ view, setView, conflicts, onAccount, restaurantName, ownerName }) { const items = [ { id: "calendar", label: "nav_calendar", icon: "calendar" }, { id: "coverage", label: "nav_coverage", icon: "grid" }, { id: "team", label: "nav_team", icon: "users" }, { id: "inbox", label: "nav_inbox", icon: "chat", badge: conflicts }, { id: "settings", label: "nav_settings", icon: "settings" }]; const me = SB.staffById(1) || { name: ownerName || restaurantName || "Owner", role: "owner", id: 0 }; return ( ); } /* ---------- Topbar ---------- */ function Topbar({ title, sub, covStatus, conflicts, onAdd, weekly, onPrev, onNext, onToday, thisWeek }) { const chipClass = { ok: " ok", gaps: " warn", none: " bad", closed: " muted" }[covStatus.state] || " warn"; return (
{weekly && }
{sub} {title}
{weekly && !thisWeek && } {weekly && }
{covStatus.label}
{conflicts > 0 &&
{conflicts}{T("conflicts")}
}
); } /* ---------- Legend ---------- */ function StatusLegend() { return (
{T("lg_confirmed")} {T("lg_pending")} {T("lg_conflict")}
); } /* ---------- General message alert ---------- */ function GeneralMsgAlert({ msg, onDismiss }) { const staff = SB.staffById(msg.staff_id) || { name: msg.staff_name, color: "#888" }; const time = msg.created_at ? new Date(msg.created_at).toLocaleTimeString("de-DE", { hour: "2-digit", minute: "2-digit" }) : ""; return (
{T("msg_from_staff")}
{staff.name}: "{msg.body}"
{time} via WhatsApp
); } /* ---------- Today rail (conflicts + who's on) ---------- */ function TodayRail({ store, now }) { const onNow = now.dayIndex == null ? [] : store.shifts.filter((s) => s.week === 0 && s.day === now.dayIndex && s.status !== "cancelled" && s.start <= now.hour && s.end > now.hour).sort((a, b) => a.end - b.end); return ( ); } /* ---------- Publish confirmation modal ---------- */ function PublishConfirmModal({ drafts, onConfirm, onClose }) { const staffNames = [...new Set(drafts.map((s) => { const st = SB.staffById(s.staffId); return st ? st.name.split(" ")[0] : "?"; }))]; return ( }>

{T("publish_confirm_body", { n: drafts.length })}

{staffNames.map((name) => ( {name} ))}
{T("publish_confirm_note")}
); } /* ---------- Delete week confirmation modal ---------- */ function DeleteWeekModal({ weekLabel, onConfirm, onClose }) { return ( }>
{T("del_week_body", { label: weekLabel })}
); } /* ---------- Calendar view ---------- */ function CalendarView({ store, openShift, weekDays, now, weekLabel }) { const [group, setGroup] = uS("all"); const [confirmPublish, setConfirmPublish] = uS(false); const [confirmDeleteWeek, setConfirmDeleteWeek] = uS(false); const [aiPlanning, setAiPlanning] = uS(false); const [aiResult, setAiResult] = uS(null); const runAiPlan = async () => { const targetOffset = store.weekOffset; // einmalig speichern — closure-sicher setAiPlanning(true); setAiResult(null); try { // 1. Job starten — antwortet sofort mit job_id const r = await fetch(`/api/planning/generate?week_offset=${targetOffset}`, { method: "POST", credentials: "include", }); if (!r.ok) throw new Error(`HTTP ${r.status}`); const { job_id } = await r.json(); // 2. Pollen bis fertig (alle 3 Sekunden, max. 45 Sekunden = 15 Versuche) for (let i = 0; i < 15; i++) { await new Promise(res => setTimeout(res, 3000)); try { const sr = await fetch(`/api/planning/status/${job_id}`, { credentials: "include" }); if (!sr.ok) continue; // 502 etc. → weiter warten const job = await sr.json(); if (job.status === "done") { setAiResult(job.summary || T("ai_plan_created")); await store.loadShifts(targetOffset); return; } if (job.status === "error") { setAiResult("Fehler: " + (job.summary || "Unbekannter Fehler")); await store.loadShifts(targetOffset); return; } } catch (_) { continue; } // Netzwerkfehler beim Pollen → weiter warten } setAiResult(T("ai_timeout")); await store.loadShifts(targetOffset); } catch (e) { console.error("runAiPlan", e); // Auch bei Fehler Schichten neu laden — könnten bereits erstellt sein await store.loadShifts(targetOffset); setAiResult(T("ai_maybe_created")); } finally { setAiPlanning(false); } }; const cats = store.categories; const validIds = ["all", ...cats.map((c) => c.id)]; const active = validIds.includes(group) ? group : "all"; const weekShifts = store.shifts.filter((s) => s.week === store.weekOffset); const filtered = active === "all" ? weekShifts : weekShifts.filter((s) => s.category === active); const drafts = filtered.filter((s) => s.draft); const onDelete = (s) => {if (window.__SB_reqDelete) window.__SB_reqDelete(s);}; return (
{confirmPublish && ( setConfirmPublish(false)} onConfirm={() => { store.sendAllDrafts(store.weekOffset); setConfirmPublish(false); }} /> )} {confirmDeleteWeek && ( setConfirmDeleteWeek(false)} onConfirm={() => { store.deleteWeek(store.weekOffset); setConfirmDeleteWeek(false); }} /> )}
{cats.map((c) => )}
{store.weekOffset >= 0 && ( )} {weekShifts.length > 0 && }
{aiResult && (
{aiResult}
)} {drafts.length > 0 &&
{drafts.length === 1 ? T("draft_one") : T("draft_many", { n: drafts.length })}
}
); } /* ---------- Coverage view (staffing heatmap) ---------- */ function heat(n) { if (n === 0) return { bg: "#f6d8d3", fg: "#a8281a" }; if (n === 1) return { bg: "#e4efe3", fg: "#3c6a4d" }; if (n === 2) return { bg: "#c2e2cb", fg: "#1f6740" }; if (n === 3) return { bg: "#8fcda4", fg: "#0e5230" }; if (n === 4) return { bg: "#56b27e", fg: "#fff" }; return { bg: "#2f9460", fg: "#fff" }; } function CoverageView({ store, weekDays }) { const open = SB.RESTAURANT.openHour,close = SB.RESTAURANT.closeHour; const hours = [];for (let h = open; h < close; h++) hours.push(h); const weekShifts = store.shifts.filter((s) => s.week === store.weekOffset); const headcount = (day, h) => weekShifts.filter((s) => s.day === day && s.status !== "cancelled" && s.start <= h && s.end > h).length; const days = weekDays || SB.WEEK; let peak = { n: -1 },gaps = []; days.forEach((d) => hours.forEach((h) => { const n = headcount(d.index, h); if (n > peak.n) peak = { n, day: d, h }; if (n === 0) gaps.push({ day: d, h }); })); return (
{peak.n}
{T("peak_head", { d: DOW(peak.day.dow), t: SB.fmtHour(peak.h) })}
{gaps.length}
{T("uncovered", { x: gaps.length ? T("incl", { d: DOW(gaps[0].day.dow), t: SB.fmtHour(gaps[0].h) }) : "" })}
{open}:00–{close % 24 === 0 ? "24" : close}:00
{T("hours_tracked")}

{T("cs_title")}

{T("cs_desc")}
0 {[0, 1, 2, 3, 4, 5].map((n) => )} 5+
{days.map((d) =>
{DOW(d.dow)}{d.num}
)} {hours.map((h) =>
{SB.fmtHour(h)}
{days.map((d) => { const n = headcount(d.index, h); const c = heat(n); return
{n > 0 ? n : ""}
; })}
)}
); } /* ---------- Team view ---------- */ function AddStaffModal({ onClose, onAdd }) { const [name, setName] = uS(""); const [phone, setPhone] = uS(""); const [role, setRole] = uS(""); const valid = name.trim() && phone.trim(); const submit = () => {if (valid) {onAdd({ name: name.trim(), phone: phone.trim(), role: role.trim() || "Staff" });onClose();}}; return ( }>
setName(e.target.value)} placeholder={T("as_name_ph")} autoFocus />
setPhone(e.target.value)} placeholder="+49 151 0000 000" />
setRole(e.target.value)} placeholder={T("as_role_ph")} />
{T("as_note")}
); } function TeamView({ store }) { const [adding, setAdding] = uS(false); const weekHours = (id) => store.shifts.filter((s) => s.staffId === id && s.status !== "cancelled").reduce((a, s) => a + (s.end - s.start), 0); const onToday = (id) => store.shifts.some((s) => s.staffId === id && s.day === SB.TODAY_INDEX && s.status !== "cancelled"); return (

{T("team_members", { n: SB.STAFF.length })}

{T("team_help")}
{SB.STAFF.map((s) => )}
{adding && setAdding(false)} onAdd={store.addStaff} />}
); } /* ---------- Inbox view ---------- */ function InboxView({ store }) { return (

{T("recent_wa")}

{T("auto_claude")}

{T("action_items")}

{store.alerts.length > 0 && {store.alerts.length}}
{store.alerts.length === 0 &&
{T("inbox_zero")}
} {store.alerts.map((a) => )}
); } /* ---------- Settings view ---------- */ function Toggle({ on, onClick }) { return ; } function SettingsRow({ title, desc, children }) { return (
{title}
{desc &&
{desc}
}
{children}
); } function LangSwitch({ lang, onLang }) { return (
); } const LANGS = [ { id: "de", label: "Deutsch", sub: "German" }, { id: "en", label: "English", sub: "Englisch" }]; function LangPicker({ lang, onLang, onClose }) { return ( {T("cancel")}}>
{LANGS.map((l) => )}
); } function LangMenu({ lang, onLang }) { const [open, setOpen] = uS(false); const cur = LANGS.find((l) => l.id === lang); return ( <> {open && setOpen(false)} />} ); } const HOUR_OPTS = (() => {const a = [];for (let h = 0; h <= 24; h += 0.25) a.push(h);return a;})(); function fmtQuarter(h) { const hh = Math.floor(h) % 24 === 0 && h >= 24 ? 24 : Math.floor(h) % 24; const mm = Math.round((h - Math.floor(h)) * 60); return String(hh).padStart(2, "0") + ":" + String(mm).padStart(2, "0"); } function HourSelect({ value, onChange }) { return ( ); } function AddCatInput({ onAdd }) { const [v, setV] = uS(""); const add = () => {if (v.trim()) {onAdd(v.trim());setV("");}}; return (
setV(e.target.value)} onKeyDown={(e) => {if (e.key === "Enter") add();}} />
); } function CategoriesCard({ store }) { return (

{T("s_cats")}

{T("s_cats_d")}
{store.categories.map((c) =>
store.renameCategory(c.id, e.target.value)} />
)}
); } function HoursCard({ store }) { return (

{T("s_hours_tpl")}

{T("s_hours_tpl_d")}
{SB.DOW_KEYS.map((dow) =>
{DOW(dow)}
{store.weekdayHours[dow].map((b, i) =>
store.setDayBlock(dow, i, { start: v })} /> store.setDayBlock(dow, i, { end: v })} />
)} {store.weekdayHours[dow].length === 0 && {T("closed")}}
)}
); } function OwnerNumberRow({ store }) { const ownerStaff = (store.staff || []).find((s) => s.role === "Inhaber"); const realPhone = ownerStaff && !ownerStaff.phone.startsWith("owner:") ? ownerStaff.phone : ""; const [num, setNum] = uS(realPhone); const [editing, setEditing] = uS(false); const [draft, setDraft] = uS(realPhone); React.useEffect(() => { if (realPhone && !num) { setNum(realPhone); setDraft(realPhone); } }, [realPhone]); const save = async () => { const val = draft.trim(); if (val && ownerStaff) await store.updateStaff(ownerStaff.id, { phone: val }); setNum(val || num); setEditing(false); store.pushToast(T("t_owner_changed"), "phone"); }; return (
{T("s_owner")}
{T("s_owner_d")}
{editing &&
{T("owner_warn")}
}
{!editing ?
{num || Noch nicht hinterlegt}
:
setDraft(e.target.value)} autoFocus />
}
); } // ── Bundesland-Presets ──────────────────────────────────────────────────────── const BUNDESLAND_PRESETS = { "Bundesweit": [ { name: "Neujahr", month: 1, day: 1 }, { name: "Tag der Arbeit", month: 5, day: 1 }, { name: "Tag d. Deutschen Einheit", month: 10, day: 3 }, { name: "1. Weihnachtstag", month: 12, day: 25 }, { name: "2. Weihnachtstag", month: 12, day: 26 }, ], "Bayern": [ { name: "Heilige Drei Könige", month: 1, day: 6 }, { name: "Mariä Himmelfahrt", month: 8, day: 15 }, { name: "Allerheiligen", month: 11, day: 1 }, { name: "Heiligabend (½)", month: 12, day: 24 }, { name: "Silvester (½)", month: 12, day: 31 }, ], "NRW": [ { name: "Allerheiligen", month: 11, day: 1 }, ], "Baden-Württemberg": [ { name: "Heilige Drei Könige", month: 1, day: 6 }, { name: "Allerheiligen", month: 11, day: 1 }, ], "Österreich": [ { name: "Heilige Drei Könige", month: 1, day: 6 }, { name: "Staatsfeiertag", month: 5, day: 1 }, { name: "Mariä Himmelfahrt", month: 8, day: 15 }, { name: "Nationalfeiertag", month: 10, day: 26 }, { name: "Allerheiligen", month: 11, day: 1 }, { name: "Mariä Empfängnis", month: 12, day: 8 }, ], "Schweiz": [ { name: "Berchtoldstag", month: 1, day: 2 }, { name: "Bundesfeiertag", month: 8, day: 1 }, ], }; const MONTH_NAMES = ["Jan","Feb","Mär","Apr","Mai","Jun","Jul","Aug","Sep","Okt","Nov","Dez"]; function HolidaysCard({ store }) { const holidays = store.holidays || []; const [newName, setNewName] = uS(""); const [newMonth, setNewMonth] = uS(1); const [newDay, setNewDay] = uS(1); const [showPresets, setShowPresets] = uS(false); const add = () => { const n = newName.trim(); if (!n) return; const already = holidays.some(h => h.month === +newMonth && h.day === +newDay); if (already) { store.pushToast("Dieser Tag existiert bereits", "alert"); return; } store.saveHolidays([...holidays, { name: n, month: +newMonth, day: +newDay }]); setNewName(""); }; const remove = (i) => store.saveHolidays(holidays.filter((_, idx) => idx !== i)); const addPreset = (preset) => { const toAdd = preset.filter(p => !holidays.some(h => h.month === p.month && h.day === p.day)); if (toAdd.length === 0) { store.pushToast("Alle Feiertage bereits vorhanden", "alert"); return; } store.saveHolidays([...holidays, ...toAdd]); store.pushToast(`${toAdd.length} Feiertage hinzugefügt`, "check"); }; return (

Feiertage

Im Kalender angezeigte Feiertage — je nach Bundesland / Land
{/* Preset-Buttons */} {showPresets && (
{Object.entries(BUNDESLAND_PRESETS).map(([label, preset]) => ( ))}
)} {/* Liste */}
{holidays.length === 0 && (
Keine Feiertage konfiguriert — wähle ein Preset oder füge manuell hinzu.
)}
{holidays.sort((a,b) => a.month - b.month || a.day - b.day).map((h, i) => (
{String(h.day).padStart(2,"0")} {MONTH_NAMES[h.month - 1]} {h.name}
))}
{/* Neuen hinzufügen */}
setNewName(e.target.value)} onKeyDown={e => e.key === "Enter" && add()} placeholder="Name (z.B. Betriebsurlaub)" />
); } function SettingsView({ store, lang, onLang }) { const [s, setS] = uS({ absence: true, swap: true, claude: true }); const tog = (k) => setS((p) => ({ ...p, [k]: !p[k] })); const [nameDraft, setNameDraft] = uS(store.restaurantName); const [nameSaving, setNameSaving] = uS(false); const saveName = async () => { const val = nameDraft.trim(); if (!val || val === store.restaurantName) return; setNameSaving(true); await fetch("/auth/me", { method: "PATCH", credentials: "include", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ business_name: val }) }); store.setRestaurantName(val); setNameSaving(false); store.pushToast(T("t_name_saved"), "check"); }; return (

{T("s_restaurant")}

setNameDraft(e.target.value)} onBlur={saveName} onKeyDown={(e) => e.key === "Enter" && saveName()} /> {nameDraft.trim() !== store.restaurantName && ( )}

{T("s_appearance")}

{T("s_wa")}

{T("connected")}
tog("absence")} /> tog("swap")} />

{T("s_assist")}

{T("active")}

{T("danger_zone")}

); } /* ---------- Account menu (user-specific) ---------- */ function DeleteAccountModal({ store, onClose, onDeleteSuccess }) { const [confirm, setConfirm] = uS(""); const [busy, setBusy] = uS(false); const confirmWord = window.SBI.lang === "en" ? "DELETE" : "LÖSCHEN"; const valid = confirm === confirmWord; const doDelete = async () => { if (!valid) return; setBusy(true); try { await fetch("/auth/account", { method: "DELETE", credentials: "include" }); onDeleteSuccess && onDeleteSuccess(); // alle Fenster schließen await store.logout(); // dann ausloggen } catch (e) { setBusy(false); } }; return ( }>
{T("del_account_warn")}
setConfirm(e.target.value)} placeholder={confirmWord} autoFocus />
); } function AccountMenu({ store, lang, onLang, onClose, onProfile, onPassword, onDeleteAccount }) { const me = SB.staffById(1) || { name: store.ownerName || "Owner", role: "owner", id: 0 }; const items = [ { icon: "user", t: "acc_profile", d: "acc_profile_d", act: onProfile }, { icon: "lock", t: "acc_password", d: "acc_password_d", act: onPassword }, { icon: "bell", t: "acc_notif", d: "acc_notif_d", act: () => store.pushToast(T("acc_notif")) }]; return (
e.stopPropagation()}>
{me.name}
{store.ownerEmail || "–"}
{items.map((it) => )}
{T("acc_lang")}
); } /* ---------- Help view ---------- */ const FAQS = [1,2,3,4,5].map(i => ({ q: `faq_q${i}`, a: `faq_a${i}` })); function HelpView({ store }) { const [subject, setSubject] = uS(""); const [message, setMessage] = uS(""); const [sending, setSending] = uS(false); const [sent, setSent] = uS(false); const [openFaq, setOpenFaq] = uS(null); const sendSupport = async () => { if (!subject.trim() || !message.trim()) return; setSending(true); try { await fetch("/auth/support", { method: "POST", credentials: "include", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ subject: subject.trim(), message: message.trim() }), }); setSent(true); setSubject(""); setMessage(""); store.pushToast(T("t_support_sent"), "check"); } catch (e) { store.pushToast("Fehler beim Senden.", "alert"); } finally { setSending(false); } }; return (
{/* FAQ */}

{T("help_faq_title")}

{FAQS.map((f, i) => (
setOpenFaq(openFaq === i ? null : i)}>
{T(f.q)}
{openFaq === i &&
{T(f.a)}
}
))}
{/* Support-Formular */}

{T("help_contact_title")}

{sent ? (
{T("help_sent_ok")}
) : (
setSubject(e.target.value)} placeholder={T("help_subject_ph")} />