// MissionChef — Fake API layer + demo data
// Simulates a REST API with artificial latency. Swap implementation → real backend.
//
// Usage:
//   await mcApi.get('/reservations')
//   await mcApi.post('/reservations', payload)
//   mcApi.subscribe(store => ...)  // react to local mutations

// ─── DEMO BUSINESS ─────────────────────────────────────────────────────────────
const DEMO_BIZ = {
  id: 'osteria-botanica',
  name: 'Osteria Botanica',
  legalName: 'Botanica Gastro GmbH',
  tagline: 'Kempten · Italienisch · 60 Plätze',
  address: 'Illerstraße 14',
  zip: '87435',
  city: 'Kempten',
  country: 'Deutschland',
  vat: 'DE 287 552 610',
  taxNumber: '127/189/45123',
  registry: 'HRB 12845 · AG Kempten',
  ceo: 'Paul Wagner',
  phone: '+49 831 512 06 00',
  email: 'hallo@osteria-botanica.de',
  website: 'www.osteria-botanica.de',
  opens: '17:00',
  closes: '23:30',
  weeklyClosed: ['Montag'],
  seats: 60,
  currency: 'EUR',
  locale: 'de-DE',
  timezone: 'Europe/Berlin',
  fiscalYearStart: '01-01',
  logoColor: '#AEC854',
  plan: 'Suite',
};

// ─── BILLING / SUBSCRIPTION ───────────────────────────────────────────────────
const BILLING = {
  plan: 'suite',
  planLabel: 'MissionChef Suite',
  planPrice: 249,
  seats: 9,
  pricePerSeat: 12,
  cycle: 'monthly',
  trial: false,
  nextInvoice: (() => { const d = new Date(); d.setDate(1); d.setMonth(d.getMonth()+1); return d.toISOString().slice(0,10); })(),
  paymentMethod: {
    type: 'card', brand: 'Visa', last4: '4242', exp: '08/2028', holder: 'Botanica Gastro GmbH',
  },
  billingEmail: 'buchhaltung@osteria-botanica.de',
  invoiceAddress: { name: 'Botanica Gastro GmbH', street: 'Illerstraße 14', zip: '87435', city: 'Kempten', country: 'Deutschland', vat: 'DE 287 552 610' },
  stripe: { customerId: 'cus_NBRf3WZqgk9EpK', connected: true, mode: 'live', webhooksOk: true, lastEvent: '4 Min.' },
  invoices: [
    { id: 'inv_01JKH2', no: 'MC-2026-0004', date: '2026-04-01', period: 'Apr 2026', amount: 357.00, status: 'paid',    method: 'Visa ····4242' },
    { id: 'inv_01JKG1', no: 'MC-2026-0003', date: '2026-03-01', period: 'Mär 2026', amount: 357.00, status: 'paid',    method: 'Visa ····4242' },
    { id: 'inv_01JKF0', no: 'MC-2026-0002', date: '2026-02-01', period: 'Feb 2026', amount: 345.00, status: 'paid',    method: 'Visa ····4242' },
    { id: 'inv_01JKE9', no: 'MC-2026-0001', date: '2026-01-01', period: 'Jan 2026', amount: 345.00, status: 'paid',    method: 'Visa ····4242' },
    { id: 'inv_01JKD8', no: 'MC-2025-0012', date: '2025-12-01', period: 'Dez 2025', amount: 333.00, status: 'paid',    method: 'Visa ····4242' },
    { id: 'inv_01JKC7', no: 'MC-2025-0011', date: '2025-11-01', period: 'Nov 2025', amount: 333.00, status: 'paid',    method: 'Visa ····4242' },
  ],
  usage: {
    transactions30d: 2847, transactionFees: 52.18,
    orders30d: 1923, reservations30d: 412, activeUsers: 9,
  },
};

// Plans available for upgrade/downgrade
const PLANS = [
  { id: 'starter', name: 'Starter',  price:  79, seats: 3,  features: ['Kasse · POS', 'Reservierungen', '3 Nutzer', 'E-Mail Support'] },
  { id: 'pro',     name: 'Pro',      price: 149, seats: 6,  features: ['+ KDS & Dienstplan', '+ Warenwirtschaft', '6 Nutzer', 'Chat Support'] },
  { id: 'suite',   name: 'Suite',    price: 249, seats: 9,  features: ['+ Catering & Events', '+ Berichte & Lohn-Export', '+ DATEV', '9 Nutzer inkl.', 'Prio-Support'] },
  { id: 'enterprise', name: 'Enterprise', price: 'Custom', seats: '∞', features: ['+ Multi-Location', '+ API-Zugang', '+ SSO / SAML', 'Dedicated CSM'] },
];

// ─── APP USERS / AUTH ─────────────────────────────────────────────────────────
// App-login users (separate role from payroll "area").
// Built inline from STAFF after declaration — see bottom of file.

const rand = (seed) => {
  let s = seed;
  return () => {
    s = (s * 9301 + 49297) % 233280;
    return s / 233280;
  };
};
const R = rand(42);
const pick = (arr) => arr[Math.floor(R() * arr.length)];

// Staff — full personnel records
// Onboarding steps (canonical, order preserved)
const ONBOARDING_STEPS = [
  { id: 'contract',  label: 'Arbeitsvertrag unterschrieben' },
  { id: 'id',        label: 'Ausweis / Aufenthaltstitel' },
  { id: 'tax',       label: 'Steuer-ID & Sozialversicherung' },
  { id: 'bank',      label: 'Bankverbindung erfasst' },
  { id: 'health',    label: 'Gesundheitszeugnis (§ 43 IfSG)' },
  { id: 'haccp',     label: 'HACCP / Hygiene-Belehrung' },
  { id: 'uniform',   label: 'Uniform & Schließfach ausgegeben' },
  { id: 'access',    label: 'POS/KDS Zugang eingerichtet' },
  { id: 'buddy',     label: 'Einarbeitungs-Buddy zugewiesen' },
  { id: 'probation', label: 'Probezeit-Gespräch geplant' },
];
const OFFBOARDING_STEPS = [
  { id: 'notice',    label: 'Kündigung / Aufhebung erhalten' },
  { id: 'handover',  label: 'Übergabe-Plan erstellt' },
  { id: 'keys',      label: 'Schlüssel & Spind zurück' },
  { id: 'uniform_r', label: 'Uniform zurück / abgeschrieben' },
  { id: 'access_r',  label: 'POS/KDS Zugang deaktiviert' },
  { id: 'final_pay', label: 'Endabrechnung + Urlaubsabgeltung' },
  { id: 'cert',      label: 'Arbeitszeugnis ausgestellt' },
  { id: 'exit',      label: 'Exit-Gespräch geführt' },
];

function onboardingFor(doneIds) {
  return ONBOARDING_STEPS.map(s => ({ ...s, done: doneIds.includes(s.id), doneAt: doneIds.includes(s.id) ? isoDaysAgo(R() * 30 | 0) : null }));
}
function offboardingFor(doneIds) {
  return OFFBOARDING_STEPS.map(s => ({ ...s, done: doneIds.includes(s.id), doneAt: doneIds.includes(s.id) ? isoDaysAgo(R() * 14 | 0) : null }));
}
function isoDaysAgo(d) { const x = new Date(); x.setDate(x.getDate() - d); return x.toISOString(); }
function isoFuture(d)  { const x = new Date(); x.setDate(x.getDate() + d); return x.toISOString(); }

const STAFF = [
  { id: 'u1', name: 'Luca Fontana', role: 'Küchenchef', area: 'Küche',
    tags: ['Pass','Head'], color: '#AEC854', wage: 22, email: 'luca@osteria-botanica.de', phone: '0170 245 2211',
    status: 'active', employmentType: 'Vollzeit', contractHours: 40, hiredAt: '2022-03-15', birthday: '1988-07-12',
    address: 'Bahnhofstr. 14, 87435 Kempten', iban: 'DE89 3704 0044 0532 0130 00', taxId: '81 123 456 789', ssn: '15 120788 F 089',
    emergency: { name: 'Giulia Fontana', relation: 'Ehefrau', phone: '0170 245 2212' },
    vacation: { entitled: 28, taken: 12, pending: 3 },
    sickDays: 2, pto: [],
    certifications: [
      { id: 'c1', name: 'Gesundheitszeugnis § 43 IfSG', issued: '2022-03-10', expires: '2027-03-10', status: 'valid' },
      { id: 'c2', name: 'HACCP-Schulung',  issued: '2024-04-02', expires: '2026-04-02', status: 'valid' },
      { id: 'c3', name: 'Brandschutz-Helfer', issued: '2023-09-15', expires: '2026-09-15', status: 'valid' },
    ],
    documents: [
      { id: 'd1', name: 'Arbeitsvertrag_Fontana.pdf', kind: 'Vertrag', size: '248 KB', uploaded: '2022-03-14' },
      { id: 'd2', name: 'Gesundheitszeugnis.pdf',      kind: 'Zertifikat', size: '1.2 MB', uploaded: '2022-03-10' },
      { id: 'd3', name: 'HACCP_2024.pdf',              kind: 'Zertifikat', size: '480 KB', uploaded: '2024-04-03' },
    ],
    onboarding: { status: 'complete', startedAt: '2022-03-01', completedAt: '2022-04-15', steps: onboardingFor(ONBOARDING_STEPS.map(s=>s.id)) },
    offboarding: null,
    notes: [
      { id: 'n1', author: 'Paul Wagner', at: isoDaysAgo(45), text: 'Jährliches Gespräch — Gehaltserhöhung auf 22€/h bestätigt.' },
      { id: 'n2', author: 'Paul Wagner', at: isoDaysAgo(120), text: 'Übernimmt Mentoring für Tomás Ribeiro.' },
    ],
  },
  { id: 'u2', name: 'Sara Perrone', role: 'Sous-Chef',  area: 'Küche',
    tags: ['Sauté','Pasta'], color: '#E97A3E', wage: 18, email: 'sara@osteria-botanica.de', phone: '0170 611 8820',
    status: 'active', employmentType: 'Vollzeit', contractHours: 40, hiredAt: '2022-09-01', birthday: '1992-11-28',
    address: 'Fürstenstr. 8, 87435 Kempten', iban: 'DE12 7015 0000 0012 3456 78', taxId: '72 456 789 123', ssn: '22 281192 P 044',
    emergency: { name: 'Marco Perrone', relation: 'Bruder', phone: '0170 611 8822' },
    vacation: { entitled: 26, taken: 8, pending: 0 }, sickDays: 4, pto: [],
    certifications: [
      { id: 'c1', name: 'Gesundheitszeugnis § 43 IfSG', issued: '2022-08-25', expires: '2027-08-25', status: 'valid' },
      { id: 'c2', name: 'HACCP-Schulung', issued: '2023-01-15', expires: '2025-01-15', status: 'expiring' },
    ],
    documents: [
      { id: 'd1', name: 'Arbeitsvertrag_Perrone.pdf', kind: 'Vertrag', size: '240 KB', uploaded: '2022-08-30' },
      { id: 'd2', name: 'Zeugnis_Vorarbeitgeber.pdf', kind: 'Zeugnis', size: '380 KB', uploaded: '2022-08-28' },
    ],
    onboarding: { status: 'complete', startedAt: '2022-09-01', completedAt: '2022-10-10', steps: onboardingFor(ONBOARDING_STEPS.map(s=>s.id)) },
    offboarding: null,
    notes: [],
  },
  { id: 'u3', name: 'Mateo Ricci', role: 'Pastamacher', area: 'Küche',
    tags: ['Pasta'], color: '#C4DE6A', wage: 15, email: 'mateo@osteria-botanica.de', phone: '0176 223 3098',
    status: 'active', employmentType: 'Vollzeit', contractHours: 40, hiredAt: '2023-06-01', birthday: '1998-03-04',
    address: 'Ludwigstr. 22, 87435 Kempten', iban: 'DE45 1001 0010 0987 6543 21', taxId: '55 789 123 456', ssn: '05 040398 R 012',
    emergency: { name: 'Elena Ricci', relation: 'Mutter', phone: '0176 223 0001' },
    vacation: { entitled: 26, taken: 15, pending: 5 }, sickDays: 1, pto: [],
    certifications: [
      { id: 'c1', name: 'Gesundheitszeugnis § 43 IfSG', issued: '2023-05-28', expires: '2028-05-28', status: 'valid' },
      { id: 'c2', name: 'HACCP-Schulung', issued: '2023-06-15', expires: '2025-06-15', status: 'expiring' },
    ],
    documents: [
      { id: 'd1', name: 'Arbeitsvertrag_Ricci.pdf', kind: 'Vertrag', size: '240 KB', uploaded: '2023-05-20' },
    ],
    onboarding: { status: 'complete', startedAt: '2023-06-01', completedAt: '2023-07-12', steps: onboardingFor(ONBOARDING_STEPS.map(s=>s.id)) },
    offboarding: null,
    notes: [],
  },
  { id: 'u4', name: 'Amelie Berger', role: 'Chef de rang', area: 'Service',
    tags: ['Lead','Wein'], color: '#7BB0BD', wage: 16, email: 'amelie@osteria-botanica.de', phone: '0152 870 4421',
    status: 'active', employmentType: 'Vollzeit', contractHours: 40, hiredAt: '2021-11-01', birthday: '1995-05-19',
    address: 'Salzstr. 3, 87437 Kempten', iban: 'DE78 7004 0041 0532 0130 11', taxId: '11 234 567 890', ssn: '19 190595 B 033',
    emergency: { name: 'Thomas Berger', relation: 'Vater', phone: '0831 900 1122' },
    vacation: { entitled: 28, taken: 10, pending: 0 }, sickDays: 0, pto: [],
    certifications: [
      { id: 'c1', name: 'Gesundheitszeugnis § 43 IfSG', issued: '2021-10-20', expires: '2026-10-20', status: 'valid' },
      { id: 'c2', name: 'Sommelier IHK Stufe 1', issued: '2023-03-10', expires: null, status: 'valid' },
    ],
    documents: [
      { id: 'd1', name: 'Arbeitsvertrag_Berger.pdf', kind: 'Vertrag', size: '238 KB', uploaded: '2021-10-28' },
      { id: 'd2', name: 'Sommelier_IHK.pdf', kind: 'Zertifikat', size: '720 KB', uploaded: '2023-03-12' },
    ],
    onboarding: { status: 'complete', startedAt: '2021-11-01', completedAt: '2021-12-10', steps: onboardingFor(ONBOARDING_STEPS.map(s=>s.id)) },
    offboarding: null,
    notes: [],
  },
  { id: 'u5', name: 'Nils Hofer', role: 'Kellner', area: 'Service',
    tags: ['Bar'], color: '#D9642C', wage: 14, email: 'nils@osteria-botanica.de', phone: '0157 444 1139',
    status: 'active', employmentType: 'Teilzeit', contractHours: 24, hiredAt: '2024-02-15', birthday: '2000-08-22',
    address: 'Rothenstein 19, 87435 Kempten', iban: 'DE33 4005 0000 0001 2345 67', taxId: '33 567 890 234', ssn: '22 220800 H 078',
    emergency: { name: 'Anja Hofer', relation: 'Mutter', phone: '0170 800 6611' },
    vacation: { entitled: 20, taken: 4, pending: 2 }, sickDays: 3, pto: [],
    certifications: [
      { id: 'c1', name: 'Gesundheitszeugnis § 43 IfSG', issued: '2024-02-10', expires: '2029-02-10', status: 'valid' },
    ],
    documents: [
      { id: 'd1', name: 'Arbeitsvertrag_Hofer.pdf', kind: 'Vertrag', size: '236 KB', uploaded: '2024-02-12' },
    ],
    onboarding: { status: 'complete', startedAt: '2024-02-15', completedAt: '2024-03-20', steps: onboardingFor(ONBOARDING_STEPS.map(s=>s.id)) },
    offboarding: null,
    notes: [],
  },
  { id: 'u6', name: 'Jana Weiß', role: 'Kellnerin', area: 'Service',
    tags: [], color: '#8A8178', wage: 14, email: 'jana@osteria-botanica.de', phone: '0151 330 6755',
    status: 'active', employmentType: 'Minijob', contractHours: 10, hiredAt: '2024-09-01', birthday: '2002-02-14',
    address: 'Memmingerstr. 55, 87437 Kempten', iban: 'DE55 7201 0080 0000 9988 77', taxId: '44 678 901 345', ssn: '14 140202 W 056',
    emergency: { name: 'Markus Weiß', relation: 'Partner', phone: '0151 330 6700' },
    vacation: { entitled: 8, taken: 2, pending: 0 }, sickDays: 0, pto: [],
    certifications: [
      { id: 'c1', name: 'Gesundheitszeugnis § 43 IfSG', issued: '2024-08-28', expires: '2029-08-28', status: 'valid' },
    ],
    documents: [
      { id: 'd1', name: 'Arbeitsvertrag_Weiss.pdf', kind: 'Vertrag', size: '232 KB', uploaded: '2024-08-30' },
    ],
    onboarding: { status: 'in-progress', startedAt: '2024-09-01', completedAt: null, steps: onboardingFor(['contract','id','tax','bank','health','haccp','uniform','access']) },
    offboarding: null,
    notes: [{ id: 'n1', author: 'Amelie Berger', at: isoDaysAgo(6), text: 'Buddy-Phase läuft gut, eigenständig im Service.' }],
  },
  { id: 'u7', name: 'Tomás Ribeiro', role: 'Runner', area: 'Service',
    tags: ['Neu'], color: '#6E8A28', wage: 13, email: 'tomas@osteria-botanica.de', phone: '0160 881 0002',
    status: 'onboarding', employmentType: 'Teilzeit', contractHours: 20, hiredAt: isoDaysAgo(10).slice(0,10), birthday: '2004-09-10',
    address: 'Poststr. 11, 87435 Kempten', iban: 'DE66 1203 0000 0011 2233 44', taxId: '66 890 123 567', ssn: '10 100904 R 023',
    emergency: { name: 'Ana Ribeiro', relation: 'Schwester', phone: '0160 881 0003' },
    vacation: { entitled: 20, taken: 0, pending: 0 }, sickDays: 0, pto: [],
    certifications: [
      { id: 'c1', name: 'Gesundheitszeugnis § 43 IfSG', issued: isoDaysAgo(8).slice(0,10), expires: null, status: 'valid' },
    ],
    documents: [
      { id: 'd1', name: 'Arbeitsvertrag_Ribeiro.pdf', kind: 'Vertrag', size: '234 KB', uploaded: isoDaysAgo(9).slice(0,10) },
    ],
    onboarding: { status: 'in-progress', startedAt: isoDaysAgo(10), completedAt: null, steps: onboardingFor(['contract','id','tax','bank','health']) },
    offboarding: null,
    notes: [{ id: 'n1', author: 'Paul Wagner', at: isoDaysAgo(2), text: 'Erste Woche super absolviert. HACCP-Schulung für Mittwoch geplant.' }],
  },
  { id: 'u8', name: 'Elena Kraus', role: 'Spüle', area: 'Küche',
    tags: [], color: '#564F47', wage: 13, email: 'elena@osteria-botanica.de', phone: '0174 502 1187',
    status: 'offboarding', employmentType: 'Minijob', contractHours: 12, hiredAt: '2023-11-10', birthday: '1985-12-03',
    address: 'Reichsstr. 44, 87435 Kempten', iban: 'DE99 5001 0517 0123 4567 89', taxId: '99 012 345 678', ssn: '03 031285 K 067',
    emergency: { name: 'Peter Kraus', relation: 'Ehemann', phone: '0174 502 1100' },
    vacation: { entitled: 12, taken: 10, pending: 2 }, sickDays: 6, pto: [],
    certifications: [
      { id: 'c1', name: 'Gesundheitszeugnis § 43 IfSG', issued: '2023-11-05', expires: '2028-11-05', status: 'valid' },
    ],
    documents: [
      { id: 'd1', name: 'Arbeitsvertrag_Kraus.pdf', kind: 'Vertrag', size: '228 KB', uploaded: '2023-11-08' },
      { id: 'd2', name: 'Kuendigung_Kraus.pdf', kind: 'Kündigung', size: '120 KB', uploaded: isoDaysAgo(18).slice(0,10) },
    ],
    onboarding: { status: 'complete', startedAt: '2023-11-10', completedAt: '2023-12-15', steps: onboardingFor(ONBOARDING_STEPS.map(s=>s.id)) },
    offboarding: { status: 'in-progress', startedAt: isoDaysAgo(18), lastDay: isoFuture(12).slice(0,10), reason: 'Eigene Kündigung · Umzug', steps: offboardingFor(['notice','handover','keys']) },
    notes: [{ id: 'n1', author: 'Paul Wagner', at: isoDaysAgo(18), text: 'Kündigung eingegangen, letzter Arbeitstag in 30 Tagen.' }],
  },
  { id: 'u9', name: 'Paul Wagner', role: 'Restaurant-Leitung', area: 'Management',
    tags: ['Admin'], color: '#1B1B17', wage: 26, email: 'paul@osteria-botanica.de', phone: '0831 512 0601',
    status: 'active', employmentType: 'Vollzeit', contractHours: 45, hiredAt: '2020-06-01', birthday: '1984-01-22',
    address: 'Lindauerstr. 2, 87437 Kempten', iban: 'DE22 3704 0044 0111 2233 44', taxId: '22 345 678 901', ssn: '22 220184 W 011',
    emergency: { name: 'Sophie Wagner', relation: 'Ehefrau', phone: '0831 512 0602' },
    vacation: { entitled: 30, taken: 15, pending: 0 }, sickDays: 0, pto: [],
    certifications: [
      { id: 'c1', name: 'Gesundheitszeugnis § 43 IfSG', issued: '2020-05-28', expires: '2025-05-28', status: 'expired' },
      { id: 'c2', name: 'IHK Gastronomiemeister', issued: '2019-11-10', expires: null, status: 'valid' },
      { id: 'c3', name: 'Ersthelfer-Ausbildung', issued: '2023-03-15', expires: '2025-03-15', status: 'expiring' },
    ],
    documents: [
      { id: 'd1', name: 'Arbeitsvertrag_Wagner.pdf', kind: 'Vertrag', size: '260 KB', uploaded: '2020-05-28' },
      { id: 'd2', name: 'IHK_Gastronomiemeister.pdf', kind: 'Zertifikat', size: '1.8 MB', uploaded: '2019-11-12' },
    ],
    onboarding: { status: 'complete', startedAt: '2020-06-01', completedAt: '2020-07-15', steps: onboardingFor(ONBOARDING_STEPS.map(s=>s.id)) },
    offboarding: null,
    notes: [],
  },
];

// Menu / Products
const MENU = [
  { id: 'm01', cat: 'Antipasti',   name: 'Burrata e Pomodoro',      price: 12.50, cost: 3.10, allergens:['Milch'], desc: 'Burrata Pugliese, Ochsenherztomate, Basilikum-Öl' },
  { id: 'm02', cat: 'Antipasti',   name: 'Vitello tonnato',         price: 14.00, cost: 4.20, allergens:['Fisch','Ei'], desc: 'Hauchdünn geschnittenes Kalb, Thunfisch-Kapern-Creme' },
  { id: 'm03', cat: 'Antipasti',   name: 'Carpaccio di manzo',      price: 15.50, cost: 4.80, allergens:['Gluten'], desc: 'Rinderfilet, Parmesan, Rucola, Trüffel-Öl' },
  { id: 'm04', cat: 'Antipasti',   name: 'Polpo alla griglia',      price: 16.50, cost: 5.90, allergens:['Schalentiere'], desc: 'Gegrillter Pulpo, Kichererbsen, Chorizo' },
  { id: 'm05', cat: 'Pasta',       name: 'Tagliatelle al tartufo',  price: 22.00, cost: 5.20, allergens:['Gluten','Ei'], desc: 'Frische Tagliatelle, schwarzer Trüffel, Butter' },
  { id: 'm06', cat: 'Pasta',       name: 'Risotto ai funghi',       price: 18.50, cost: 4.40, allergens:['Milch'], desc: 'Carnaroli, Steinpilze, Parmigiano' },
  { id: 'm07', cat: 'Pasta',       name: 'Spaghetti alle vongole',  price: 19.00, cost: 5.80, allergens:['Gluten','Weichtiere'], desc: 'Venusmuscheln, Weißwein, Peperoncino' },
  { id: 'm08', cat: 'Pasta',       name: 'Lasagne della casa',      price: 16.50, cost: 3.90, allergens:['Gluten','Milch','Ei'], desc: 'Ragù bolognese, Besciamella, 6 Lagen' },
  { id: 'm09', cat: 'Secondi',     name: 'Bistecca Fiorentina 400g',price: 38.00, cost:14.20, allergens:[], desc: 'Chianina-Rind, Rosmarin, Flor de Sal' },
  { id: 'm10', cat: 'Secondi',     name: 'Branzino in crosta di sale', price: 32.00, cost: 9.40, allergens:['Fisch'], desc: 'Ganzer Wolfsbarsch, Salzkruste, Fenchel' },
  { id: 'm11', cat: 'Secondi',     name: 'Ossobuco alla milanese',  price: 28.00, cost: 7.60, allergens:['Gluten'], desc: 'Kalbshaxe, Gremolata, Safran-Risotto' },
  { id: 'm12', cat: 'Secondi',     name: 'Melanzane alla parmigiana', price: 19.00, cost: 3.10, allergens:['Milch'], desc: 'Aubergine, Mozzarella, Tomate — vegetarisch' },
  { id: 'm13', cat: 'Dolci',       name: 'Tiramisù della casa',     price:  8.50, cost: 1.80, allergens:['Ei','Gluten','Milch'], desc: 'Hausgemacht, Mascarpone, Espresso' },
  { id: 'm14', cat: 'Dolci',       name: 'Panna cotta ai frutti',   price:  7.50, cost: 1.50, allergens:['Milch'], desc: 'Vanille-Panna cotta, rote Beeren' },
  { id: 'm15', cat: 'Dolci',       name: 'Cannoli siciliani',       price:  8.00, cost: 2.10, allergens:['Gluten','Milch'], desc: 'Ricotta, Pistazie, Schokolade' },
  { id: 'm16', cat: 'Bar',         name: 'Aperol Spritz',           price:  8.50, cost: 1.70, allergens:[], desc: 'Aperol, Prosecco, Soda' },
  { id: 'm17', cat: 'Bar',         name: 'Negroni',                 price:  9.50, cost: 2.20, allergens:[], desc: 'Gin, Campari, roter Wermut' },
  { id: 'm18', cat: 'Bar',         name: 'Espresso',                price:  2.80, cost: 0.35, allergens:[], desc: 'Bio-Arabica, Röstung Kempten' },
  { id: 'm19', cat: 'Wein',        name: 'Primitivo 0,2l',          price:  7.50, cost: 2.00, allergens:['Sulfite'], desc: 'Puglia, 13,5% Vol.' },
  { id: 'm20', cat: 'Wein',        name: 'Barolo Flasche',          price: 64.00, cost:22.00, allergens:['Sulfite'], desc: 'Piemont 2019, Nebbiolo' },
];

// Rooms
const ROOMS = [
  { id: 'rm-main',   name: 'Hauptsaal',   capacity: 48 },
  { id: 'rm-winter', name: 'Wintergarten', capacity: 24 },
  { id: 'rm-terrace',name: 'Terrasse',     capacity: 32 },
  { id: 'rm-bar',    name: 'Bar',          capacity: 12 },
];

// Tables / Floorplan (x,y in a 600x400 grid) — each scoped to a room
const TABLES = [
  // Hauptsaal
  { id: 't01', room: 'rm-main', label: '01', seats: 2, x:  80, y:  80, shape: 'round',  status: 'open' },
  { id: 't02', room: 'rm-main', label: '02', seats: 2, x: 170, y:  80, shape: 'round',  status: 'reserved' },
  { id: 't03', room: 'rm-main', label: '03', seats: 4, x: 280, y:  80, shape: 'square', status: 'open' },
  { id: 't04', room: 'rm-main', label: '04', seats: 4, x: 380, y:  80, shape: 'square', status: 'seated' },
  { id: 't05', room: 'rm-main', label: '05', seats: 6, x: 495, y:  80, shape: 'long',   status: 'seated' },
  { id: 't06', room: 'rm-main', label: '06', seats: 2, x:  80, y: 180, shape: 'round',  status: 'open' },
  { id: 't07', room: 'rm-main', label: '07', seats: 4, x: 170, y: 180, shape: 'square', status: 'seated' },
  { id: 't08', room: 'rm-main', label: '08', seats: 4, x: 280, y: 180, shape: 'square', status: 'reserved' },
  { id: 't09', room: 'rm-main', label: '09', seats: 6, x: 400, y: 180, shape: 'long',   status: 'open' },
  { id: 't10', room: 'rm-main', label: '10', seats: 8, x: 525, y: 180, shape: 'long',   status: 'reserved' },
  { id: 't11', room: 'rm-main', label: '11', seats: 4, x: 100, y: 320, shape: 'square', status: 'open' },
  { id: 't12', room: 'rm-main', label: '12', seats: 4, x: 210, y: 320, shape: 'square', status: 'seated' },
  { id: 't13', room: 'rm-main', label: '13', seats: 2, x: 310, y: 320, shape: 'round',  status: 'open' },
  // Wintergarten
  { id: 't20', room: 'rm-winter', label: 'W1', seats: 4, x:  90, y: 110, shape: 'square', status: 'open' },
  { id: 't21', room: 'rm-winter', label: 'W2', seats: 4, x: 220, y: 110, shape: 'square', status: 'reserved' },
  { id: 't22', room: 'rm-winter', label: 'W3', seats: 6, x: 360, y: 110, shape: 'long',   status: 'open' },
  { id: 't23', room: 'rm-winter', label: 'W4', seats: 2, x:  90, y: 240, shape: 'round',  status: 'seated' },
  { id: 't24', room: 'rm-winter', label: 'W5', seats: 2, x: 180, y: 240, shape: 'round',  status: 'open' },
  { id: 't25', room: 'rm-winter', label: 'W6', seats: 6, x: 360, y: 240, shape: 'long',   status: 'reserved' },
  // Terrasse
  { id: 't30', room: 'rm-terrace', label: 'T1', seats: 4, x: 100, y: 100, shape: 'round', status: 'open' },
  { id: 't31', room: 'rm-terrace', label: 'T2', seats: 4, x: 220, y: 100, shape: 'round', status: 'open' },
  { id: 't32', room: 'rm-terrace', label: 'T3', seats: 4, x: 340, y: 100, shape: 'round', status: 'seated' },
  { id: 't33', room: 'rm-terrace', label: 'T4', seats: 4, x: 460, y: 100, shape: 'round', status: 'open' },
  { id: 't34', room: 'rm-terrace', label: 'T5', seats: 2, x: 100, y: 230, shape: 'round', status: 'reserved' },
  { id: 't35', room: 'rm-terrace', label: 'T6', seats: 2, x: 220, y: 230, shape: 'round', status: 'open' },
  { id: 't36', room: 'rm-terrace', label: 'T7', seats: 8, x: 410, y: 230, shape: 'long',  status: 'open' },
  { id: 't37', room: 'rm-terrace', label: 'T8', seats: 4, x: 100, y: 340, shape: 'square',status: 'open' },
  // Bar
  { id: 't14', room: 'rm-bar', label: 'Bar 1', seats: 1, x: 140, y: 200, shape: 'round', status: 'open' },
  { id: 't15', room: 'rm-bar', label: 'Bar 2', seats: 1, x: 200, y: 200, shape: 'round', status: 'seated' },
  { id: 't16', room: 'rm-bar', label: 'Bar 3', seats: 1, x: 260, y: 200, shape: 'round', status: 'open' },
  { id: 't17', room: 'rm-bar', label: 'Bar 4', seats: 1, x: 320, y: 200, shape: 'round', status: 'open' },
  { id: 't18', room: 'rm-bar', label: 'Lounge 1', seats: 4, x: 450, y: 130, shape: 'square', status: 'reserved' },
  { id: 't19', room: 'rm-bar', label: 'Lounge 2', seats: 4, x: 450, y: 240, shape: 'square', status: 'open' },
];

// Decor / Floorplan elements (walls, planters, doors, bar, windows, etc) — scoped per room
const DECOR = [
  // Hauptsaal
  { id: 'd01', room: 'rm-main', kind: 'wall',     x: 0,   y: 250, w: 360, h: 6 },
  { id: 'd02', room: 'rm-main', kind: 'wall',     x: 360, y: 250, w: 6,   h: 90 },
  { id: 'd03', room: 'rm-main', kind: 'window',   x: 60,  y: 4,   w: 220, h: 6 },
  { id: 'd04', room: 'rm-main', kind: 'door',     x: 300, y: 4,   w: 60,  h: 6 },
  { id: 'd06', room: 'rm-main', kind: 'plant',    x: 30,  y: 250 },
  { id: 'd07', room: 'rm-main', kind: 'plant',    x: 350, y: 260 },
  { id: 'd09', room: 'rm-main', kind: 'planter',  x: 0,   y: 130, w: 6, h: 100 },
  { id: 'd10', room: 'rm-main', kind: 'kitchen',  x: 0,   y: 380, w: 100, h: 30, label: 'Pass' },
  { id: 'd11', room: 'rm-main', kind: 'restroom', x: 540, y: 380, w: 60,  h: 30, label: 'WC' },
  // Wintergarten — glass-roofed, lots of windows + plants
  { id: 'd20', room: 'rm-winter', kind: 'window', x: 10,  y: 4,   w: 580, h: 6 },
  { id: 'd21', room: 'rm-winter', kind: 'window', x: 4,   y: 60,  w: 6,   h: 280 },
  { id: 'd22', room: 'rm-winter', kind: 'window', x: 590, y: 60,  w: 6,   h: 280 },
  { id: 'd23', room: 'rm-winter', kind: 'door',   x: 280, y: 380, w: 60,  h: 6 },
  { id: 'd24', room: 'rm-winter', kind: 'plant',  x: 30,  y: 30 },
  { id: 'd25', room: 'rm-winter', kind: 'plant',  x: 540, y: 30 },
  { id: 'd26', room: 'rm-winter', kind: 'plant',  x: 30,  y: 350 },
  { id: 'd27', room: 'rm-winter', kind: 'plant',  x: 540, y: 350 },
  { id: 'd28', room: 'rm-winter', kind: 'planter',x: 295, y: 170, w: 6, h: 60 },
  // Terrasse — outdoor, planters at edges
  { id: 'd30', room: 'rm-terrace', kind: 'planter', x: 10, y: 30,  w: 6, h: 320 },
  { id: 'd31', room: 'rm-terrace', kind: 'planter', x: 584,y: 30,  w: 6, h: 320 },
  { id: 'd32', room: 'rm-terrace', kind: 'planter', x: 30, y: 30,  w: 540, h: 6 },
  { id: 'd33', room: 'rm-terrace', kind: 'door',    x: 280,y: 380, w: 60,  h: 6 },
  { id: 'd34', room: 'rm-terrace', kind: 'plant',   x: 40, y: 50 },
  { id: 'd35', room: 'rm-terrace', kind: 'plant',   x: 540,y: 50 },
  { id: 'd36', room: 'rm-terrace', kind: 'plant',   x: 40, y: 340 },
  { id: 'd37', room: 'rm-terrace', kind: 'plant',   x: 540,y: 340 },
  // Bar
  { id: 'd40', room: 'rm-bar', kind: 'bar',      x: 80,  y: 100, w: 320, h: 30, label: 'Theke' },
  { id: 'd41', room: 'rm-bar', kind: 'wall',     x: 410, y: 80,  w: 6,   h: 280 },
  { id: 'd42', room: 'rm-bar', kind: 'door',     x: 30,  y: 380, w: 60,  h: 6 },
  { id: 'd43', room: 'rm-bar', kind: 'plant',    x: 30,  y: 30 },
  { id: 'd44', room: 'rm-bar', kind: 'plant',    x: 30,  y: 320 },
  { id: 'd45', room: 'rm-bar', kind: 'restroom', x: 500, y: 380, w: 60, h: 30, label: 'WC' },
];

// Reservations — today
function makeReservations() {
  const today = new Date(); today.setHours(17,0,0,0);
  const names = [
    ['Familie Hofer', 4], ['Dr. Obermaier', 2], ['Weber + 3', 4], ['Fischer', 6],
    ['Nowak', 2], ['Bauer', 4], ['Group Tech Allgäu', 8], ['Frau Richter', 2],
    ['Herr König', 4], ['Schmidt', 2], ['Martinez', 3], ['Petrova', 2],
  ];
  const res = [];
  for (let i = 0; i < names.length; i++) {
    const [name, party] = names[i];
    const mins = [0,15,30,45,60,90,120,150,180,210,240,270][i];
    const at = new Date(today.getTime() + mins*60*1000);
    const table = TABLES.find(t => t.seats >= party && t.seats <= party + 2) || TABLES[i % TABLES.length];
    res.push({
      id: 'r' + (100 + i),
      name, party, at: at.toISOString(), tableId: table.id,
      note: i % 5 === 0 ? 'Geburtstag, Kerze bitte' : (i % 4 === 0 ? 'Fensterplatz bevorzugt' : ''),
      status: i < 3 ? 'checked-in' : (i < 8 ? 'confirmed' : 'waitlist'),
      phone: '+49 ' + (1510 + i * 13) + ' ' + (100000 + i * 7777),
      source: i % 4 === 0 ? 'Telefon' : (i % 4 === 1 ? 'Website' : (i % 4 === 2 ? 'Google' : 'Google Reservations')),
    });
  }
  return res;
}

// Open tickets (KDS + POS)
function makeTickets() {
  const now = Date.now();
  return [
    { id: 'k2840', table: '07', seats: 4, opened: now - 14*60*1000, course: 'main', server: 'u4', items: [
      { id: 'i1', mid: 'm01', qty: 2, state: 'done' },
      { id: 'i2', mid: 'm06', qty: 1, state: 'cooking', mods: ['ohne Parmigiano'] },
      { id: 'i3', mid: 'm09', qty: 1, state: 'cooking', mods: ['medium-rare'] },
      { id: 'i4', mid: 'm19', qty: 2, state: 'done' },
    ]},
    { id: 'k2841', table: '04', seats: 2, opened: now - 8*60*1000, course: 'starter', server: 'u5', items: [
      { id: 'i5', mid: 'm03', qty: 1, state: 'new' },
      { id: 'i6', mid: 'm07', qty: 1, state: 'new' },
      { id: 'i7', mid: 'm16', qty: 2, state: 'done' },
    ]},
    { id: 'k2842', table: '12', seats: 4, opened: now - 22*60*1000, course: 'dessert', server: 'u4', items: [
      { id: 'i8', mid: 'm13', qty: 2, state: 'cooking' },
      { id: 'i9', mid: 'm15', qty: 1, state: 'new' },
      { id: 'i10', mid: 'm18', qty: 3, state: 'new' },
    ]},
    { id: 'k2843', table: '05', seats: 6, opened: now - 3*60*1000, course: 'starter', server: 'u6', items: [
      { id: 'i11', mid: 'm01', qty: 3, state: 'new' },
      { id: 'i12', mid: 'm04', qty: 2, state: 'new' },
      { id: 'i13', mid: 'm20', qty: 1, state: 'done' },
    ]},
    { id: 'k2844', table: '15', seats: 1, opened: now - 1*60*1000, course: 'bar', server: 'u5', items: [
      { id: 'i14', mid: 'm17', qty: 1, state: 'new' },
    ]},
  ];
}

// Shifts — current week (Mo…So)
function makeShifts() {
  const monday = new Date();
  monday.setDate(monday.getDate() - ((monday.getDay() + 6) % 7));
  monday.setHours(0,0,0,0);
  const shifts = [];
  const template = [
    { uid: 'u1', start: 14, end: 23 },
    { uid: 'u2', start: 15, end: 24 },
    { uid: 'u3', start: 16, end: 23 },
    { uid: 'u4', start: 16, end: 24 },
    { uid: 'u5', start: 17, end: 24 },
    { uid: 'u6', start: 17, end: 24 },
    { uid: 'u7', start: 18, end: 23 },
    { uid: 'u8', start: 18, end: 24 },
  ];
  for (let d = 0; d < 7; d++) {
    if (d === 0) continue; // Montag Ruhetag
    template.forEach((s, i) => {
      if ((d + i) % 4 === 0) return; // frei-tag
      const date = new Date(monday.getTime() + d*86400000);
      const start = new Date(date); start.setHours(s.start);
      const end = new Date(date); end.setHours(s.end);
      shifts.push({
        id: `s${d}${i}`,
        uid: s.uid,
        start: start.toISOString(),
        end: end.toISOString(),
        area: STAFF.find(x => x.id === s.uid).area,
        status: (d === 2 && i === 4) ? 'trade-open' : 'confirmed',
      });
    });
  }
  return shifts;
}

// Inventory / Warenwirtschaft
const INVENTORY = [
  { id: 'n01', sku: 'VEAL-001',    name: 'Kalbshaxe (kg)',        stock: 14.2, par: 18, price: 22.50, supplier: 'Metzgerei Huber', category: 'Fleisch', lead: 1 },
  { id: 'n02', sku: 'BEEF-FIL-02', name: 'Rinderfilet (kg)',       stock:  6.8, par: 12, price: 42.00, supplier: 'Metzgerei Huber', category: 'Fleisch', lead: 1 },
  { id: 'n03', sku: 'FISH-BRAN',   name: 'Wolfsbarsch (Stk)',      stock: 9,    par: 16, price: 14.50, supplier: 'Bodensee Fisch',  category: 'Fisch', lead: 2 },
  { id: 'n04', sku: 'FISH-VONG',   name: 'Venusmuscheln (kg)',     stock:  2.4, par:  6, price: 18.00, supplier: 'Bodensee Fisch',  category: 'Fisch', lead: 2 },
  { id: 'n05', sku: 'DAIRY-BURR',  name: 'Burrata 125g (Stk)',     stock: 18,   par: 24, price:  3.20, supplier: 'Caseificio Pugliese', category: 'Milch', lead: 3 },
  { id: 'n06', sku: 'DAIRY-MASC',  name: 'Mascarpone (kg)',        stock:  3.2, par:  4, price:  9.80, supplier: 'Caseificio Pugliese', category: 'Milch', lead: 3 },
  { id: 'n07', sku: 'FLOUR-00',    name: 'Mehl Tipo 00 (kg)',      stock: 22,   par: 25, price:  1.80, supplier: 'Molino Veronese', category: 'Trocken', lead: 5 },
  { id: 'n08', sku: 'RICE-CARN',   name: 'Carnaroli Reis (kg)',    stock: 12,   par: 15, price:  4.20, supplier: 'Molino Veronese', category: 'Trocken', lead: 5 },
  { id: 'n09', sku: 'WINE-PRIM',   name: 'Primitivo Flasche',      stock: 42,   par: 60, price:  6.50, supplier: 'Cantina Salento', category: 'Wein',    lead: 7 },
  { id: 'n10', sku: 'WINE-BARO',   name: 'Barolo Flasche',         stock: 11,   par: 18, price: 22.00, supplier: 'Cantina Salento', category: 'Wein',    lead: 7 },
  { id: 'n11', sku: 'VEG-TOM',     name: 'Ochsenherztomate (kg)',  stock:  4.1, par:  8, price:  5.80, supplier: 'Bio-Hof Seibold',  category: 'Gemüse', lead: 1 },
  { id: 'n12', sku: 'VEG-FUNGHI',  name: 'Steinpilze frisch (kg)', stock:  1.8, par:  3, price: 34.00, supplier: 'Bio-Hof Seibold',  category: 'Gemüse', lead: 1 },
  { id: 'n13', sku: 'HERB-TRUFF',  name: 'Trüffel schwarz (g)',     stock: 85,   par:150, price:  2.80, supplier: 'Tartufi Alba',    category: 'Gewürze',lead:10 },
  { id: 'n14', sku: 'OIL-EVOO',    name: 'Olivenöl Extra Vergine (l)', stock: 8, par: 12, price: 11.50, supplier: 'Frantoio Allgäu', category: 'Öle',   lead: 3 },
  { id: 'n15', sku: 'CHOC-PIST',   name: 'Pistazien gemahlen (kg)', stock:  1.5, par:  2, price: 42.00, supplier: 'Bronte Siciliano',category: 'Trocken',lead: 7 },
];

// Purchase orders
const ORDERS = [
  { id: 'po201', supplier: 'Metzgerei Huber',      total: 486.40, status: 'delivered', eta: '2026-04-20', items: 8 },
  { id: 'po202', supplier: 'Bodensee Fisch',       total: 312.80, status: 'in-transit', eta: '2026-04-23', items: 5 },
  { id: 'po203', supplier: 'Caseificio Pugliese',  total: 198.20, status: 'open',       eta: '2026-04-24', items: 6 },
  { id: 'po204', supplier: 'Cantina Salento',      total: 1240.00, status: 'approved',  eta: '2026-04-28', items: 12 },
];

// Catering events
// Catering — detailed events with menu, team, equipment, timeline, costs
const EVENT_TIMELINE_TEMPLATE = [
  { id: 't1', label: 'Anfrage eingegangen',  group: 'Verkauf' },
  { id: 't2', label: 'Angebot versendet',    group: 'Verkauf' },
  { id: 't3', label: 'Auftrag bestätigt',    group: 'Verkauf' },
  { id: 't4', label: 'Anzahlung erhalten',   group: 'Finanzen' },
  { id: 't5', label: 'Menü finalisiert',     group: 'Küche' },
  { id: 't6', label: 'Team eingeplant',      group: 'Personal' },
  { id: 't7', label: 'Equipment bestellt',   group: 'Logistik' },
  { id: 't8', label: 'Anlieferung · Aufbau', group: 'Logistik' },
  { id: 't9', label: 'Event durchgeführt',   group: 'Service' },
  { id: 't10',label: 'Rechnung versendet',   group: 'Finanzen' },
  { id: 't11',label: 'Restzahlung · Abschluss', group: 'Finanzen' },
];

const EVENTS = [
  {
    id: 'ev01', name: 'Hochzeit Fischer',
    customer: { name: 'Familie Fischer',  contact: 'Stefanie Fischer', email: 'stefanie.fischer@gmail.com', phone: '+49 175 4420 118' },
    date: '2026-04-26', startTime: '15:00', endTime: '01:00',
    guests: 120, venue: 'Schlossgarten Linz', address: 'Schlossweg 1 · 4020 Linz',
    type: 'Flying Buffet', state: 'production',
    lead: 'u1',
    deposit: 4455.00, depositPaid: true, paidTotal: 4455.00,
    menu: [
      { id: 'm1', course: 'Empfang',  items: [
        { name: 'Prosecco Frizzante',          qtyPerPerson: 1.5, unitCost: 2.40, unitPrice: 5.80 },
        { name: 'Antipasti-Stationen (gemischt)', qtyPerPerson: 3,   unitCost: 2.10, unitPrice: 5.20 },
      ]},
      { id: 'm2', course: 'Hauptgang', items: [
        { name: 'Risotto al tartufo',          qtyPerPerson: 1, unitCost: 6.40, unitPrice: 16.50 },
        { name: 'Kalbsschulter · 12h Brasato', qtyPerPerson: 1, unitCost: 8.80, unitPrice: 22.00 },
        { name: 'Gegrilltes Gemüse · vegan',    qtyPerPerson: 1, unitCost: 3.20, unitPrice:  9.50 },
      ]},
      { id: 'm3', course: 'Dessert',  items: [
        { name: 'Hochzeitstorte 5-stöckig',    qtyPerPerson: 1, unitCost: 4.10, unitPrice: 11.00 },
        { name: 'Dolce-Table (6 Sorten)',      qtyPerPerson: 2, unitCost: 1.40, unitPrice:  3.60 },
      ]},
      { id: 'm4', course: 'Getränke',  items: [
        { name: 'Weinbegleitung 3-Gang',       qtyPerPerson: 1, unitCost: 7.20, unitPrice: 18.00 },
        { name: 'Wasser · Kaffee · Digestif',   qtyPerPerson: 1, unitCost: 2.10, unitPrice:  5.90 },
      ]},
    ],
    team: [
      { id: 'tm1', uid: 'u3', role: 'Küchenchef',    hours: 14, ratePerHour: 42 },
      { id: 'tm2', uid: 'u4', role: 'Sous-Chef',     hours: 12, ratePerHour: 28 },
      { id: 'tm3', uid: 'u2', role: 'Service-Lead',  hours: 10, ratePerHour: 24 },
      { id: 'tm4', uid: 'u5', role: 'Service',       hours: 10, ratePerHour: 18 },
      { id: 'tm5', uid: 'u7', role: 'Service',       hours: 10, ratePerHour: 18 },
      { id: 'tm6', uid: 'u8', role: 'Aushilfe',      hours: 10, ratePerHour: 14 },
    ],
    equipment: [
      { id: 'eq1', item: 'Stehtische (weiß)',       qty: 12, supplier: 'Festhütte Allgäu', unitCost: 18 },
      { id: 'eq2', item: 'Banketttische (rund,10p)', qty: 12, supplier: 'Festhütte Allgäu', unitCost: 22 },
      { id: 'eq3', item: 'Stühle Chiavari Gold',     qty:120, supplier: 'Festhütte Allgäu', unitCost:  3.50 },
      { id: 'eq4', item: 'Porzellan-Paket 3-Gang',   qty:120, supplier: 'Porzellanhaus Müller', unitCost: 4.80 },
      { id: 'eq5', item: 'Chafing-Dishes (Buffet)',  qty:  8, supplier: 'Eigenbestand',      unitCost:  0 },
      { id: 'eq6', item: 'Food-Truck-Grill mobil',   qty:  1, supplier: 'Allgäu Event GmbH', unitCost:240 },
    ],
    timeline: { t1: true, t2: true, t3: true, t4: true, t5: true, t6: true, t7: true, t8: false, t9: false, t10: false, t11: false },
    notes: 'Brautpaar ist glutenfrei (Option pro Gang notiert). Feuerwerk um 23:00 — Küchenteam bleibt bis 00:30.',
  },
  {
    id: 'ev02', name: 'Firmenabend Allgäuer IT',
    customer: { name: 'Allgäuer IT GmbH', contact: 'Peter Reichert', email: 'p.reichert@allgaeuer-it.de', phone: '+49 831 52 040 12' },
    date: '2026-05-02', startTime: '19:00', endTime: '23:30',
    guests: 45, venue: 'Factory Kempten', address: 'Industriestraße 24 · 87437 Kempten',
    type: 'Set-Menu 4-Gang', state: 'confirmed', lead: 'u2',
    deposit: 1687.50, depositPaid: true, paidTotal: 1687.50,
    menu: [
      { id: 'm1', course: 'Amuse-bouche', items: [{ name: 'Bruschetta-Variation', qtyPerPerson: 2, unitCost: 1.10, unitPrice: 3.40 }]},
      { id: 'm2', course: 'Vorspeise',    items: [{ name: 'Vitello tonnato',      qtyPerPerson: 1, unitCost: 4.80, unitPrice: 14.50 }]},
      { id: 'm3', course: 'Hauptgang',    items: [
        { name: 'Branzino · gegrillt',    qtyPerPerson: 0.5, unitCost: 8.20, unitPrice: 21.00 },
        { name: 'Tagliatelle · Ragù',      qtyPerPerson: 0.5, unitCost: 4.90, unitPrice: 16.50 },
      ]},
      { id: 'm4', course: 'Dessert',      items: [{ name: 'Tiramisù · Kaffee', qtyPerPerson: 1, unitCost: 2.10, unitPrice: 8.50 }]},
      { id: 'm5', course: 'Getränkepauschale', items: [{ name: 'Flat 4h · Weine/Bier/Wasser', qtyPerPerson: 1, unitCost: 14.80, unitPrice: 38.00 }]},
    ],
    team: [
      { id: 'tm1', uid: 'u3', role: 'Küchenchef',   hours: 8, ratePerHour: 42 },
      { id: 'tm2', uid: 'u2', role: 'Service-Lead', hours: 7, ratePerHour: 24 },
      { id: 'tm3', uid: 'u5', role: 'Service',      hours: 6, ratePerHour: 18 },
      { id: 'tm4', uid: 'u7', role: 'Service',      hours: 6, ratePerHour: 18 },
    ],
    equipment: [
      { id: 'eq1', item: 'Eigentransport · Vans 2x', qty: 2, supplier: 'Eigenbestand', unitCost: 60 },
      { id: 'eq2', item: 'Warmhaltewagen 6-GN',      qty: 2, supplier: 'Eigenbestand', unitCost: 0 },
      { id: 'eq3', item: 'Mietgeschirr 4-Gang',      qty:45, supplier: 'Porzellanhaus Müller', unitCost: 4.80 },
    ],
    timeline: { t1: true, t2: true, t3: true, t4: true, t5: true, t6: true, t7: false, t8: false, t9: false, t10: false, t11: false },
    notes: 'CEO-Ansprache 20:15 — Hauptgang um 20:30 servieren.',
  },
  {
    id: 'ev03', name: 'Geburtstag Dr. Obermaier',
    customer: { name: 'Dr. Martin Obermaier', contact: 'Dr. Martin Obermaier', email: 'm.obermaier@kanzlei-obermaier.de', phone: '+49 831 890 12 44' },
    date: '2026-05-08', startTime: '18:30', endTime: '23:00',
    guests: 22, venue: 'Privat · Hildegardplatz', address: 'Hildegardplatz 3 · 87435 Kempten',
    type: 'Family Style · 5-Gang', state: 'offer', lead: 'u1',
    deposit: 0, depositPaid: false, paidTotal: 0,
    menu: [
      { id: 'm1', course: 'Aperitivo',  items: [{ name: 'Spritz-Bar · 3 Varianten', qtyPerPerson: 2, unitCost: 2.20, unitPrice: 6.50 }]},
      { id: 'm2', course: 'Antipasti',  items: [{ name: 'Antipasti-Platte family style', qtyPerPerson: 1, unitCost: 5.40, unitPrice: 16.00 }]},
      { id: 'm3', course: 'Pasta',      items: [{ name: 'Cacio e Pepe · frisch',    qtyPerPerson: 1, unitCost: 3.80, unitPrice: 12.00 }]},
      { id: 'm4', course: 'Hauptgang',  items: [{ name: 'Porchetta · Bratkartoffeln', qtyPerPerson: 1, unitCost: 7.80, unitPrice: 22.00 }]},
      { id: 'm5', course: 'Dessert',    items: [{ name: 'Panna Cotta · Beeren',     qtyPerPerson: 1, unitCost: 2.20, unitPrice: 8.50 }]},
    ],
    team: [
      { id: 'tm1', uid: 'u3', role: 'Küchenchef',   hours: 7, ratePerHour: 42 },
      { id: 'tm2', uid: 'u2', role: 'Service-Lead', hours: 6, ratePerHour: 24 },
      { id: 'tm3', uid: 'u7', role: 'Service',      hours: 6, ratePerHour: 18 },
    ],
    equipment: [
      { id: 'eq1', item: 'Eigentransport · Van',   qty: 1, supplier: 'Eigenbestand', unitCost: 30 },
      { id: 'eq2', item: 'Mietgeschirr 5-Gang',    qty:22, supplier: 'Porzellanhaus Müller', unitCost: 5.60 },
    ],
    timeline: { t1: true, t2: true, t3: false, t4: false, t5: false, t6: false, t7: false, t8: false, t9: false, t10: false, t11: false },
    notes: 'Angebot per Mail am 20.04. · Wartet auf Rückmeldung. Lactose-Intoleranz (2x).',
  },
  {
    id: 'ev04', name: 'Sommerfest Stadtwerke Kempten',
    customer: { name: 'Stadtwerke Kempten AG', contact: 'Julia Brenner', email: 'j.brenner@sw-kempten.de', phone: '+49 831 581 80 00' },
    date: '2026-06-14', startTime: '16:00', endTime: '23:00',
    guests: 280, venue: 'Rathausplatz Kempten', address: 'Rathausplatz 29 · 87435 Kempten',
    type: 'Food-Station · Flying', state: 'planning', lead: 'u2',
    deposit: 6720.00, depositPaid: false, paidTotal: 0,
    menu: [
      { id: 'm1', course: 'Station 1 · Pizza',   items: [{ name: 'Pizza-Stand (4 Sorten)', qtyPerPerson: 2.5, unitCost: 1.80, unitPrice: 5.00 }]},
      { id: 'm2', course: 'Station 2 · Pasta',   items: [{ name: 'Pasta-Bar frisch (3 Saucen)', qtyPerPerson: 1, unitCost: 3.10, unitPrice: 9.00 }]},
      { id: 'm3', course: 'Station 3 · Grill',    items: [{ name: 'Porchetta-Sandwich', qtyPerPerson: 1, unitCost: 4.20, unitPrice: 11.00 }]},
      { id: 'm4', course: 'Station 4 · Süß',      items: [{ name: 'Gelato-Wagen (6 Sorten)', qtyPerPerson: 2, unitCost: 1.10, unitPrice: 3.50 }]},
      { id: 'm5', course: 'Getränke',             items: [{ name: 'Bier · Wein · AFG · Flat 5h', qtyPerPerson: 1, unitCost: 12.40, unitPrice: 32.00 }]},
    ],
    team: [
      { id: 'tm1', uid: 'u3', role: 'Küchenchef',    hours: 12, ratePerHour: 42 },
      { id: 'tm2', uid: 'u4', role: 'Sous-Chef',     hours: 11, ratePerHour: 28 },
      { id: 'tm3', uid: 'u2', role: 'Event-Lead',    hours: 11, ratePerHour: 26 },
      { id: 'tm4', uid: 'u5', role: 'Service',       hours: 10, ratePerHour: 18 },
      { id: 'tm5', uid: 'u6', role: 'Service',       hours: 10, ratePerHour: 18 },
      { id: 'tm6', uid: 'u7', role: 'Service',       hours: 10, ratePerHour: 18 },
      { id: 'tm7', uid: 'u8', role: 'Aushilfe',      hours: 10, ratePerHour: 14 },
    ],
    equipment: [
      { id: 'eq1', item: 'Pizzaofen mobil',           qty:  1, supplier: 'Allgäu Event GmbH', unitCost: 480 },
      { id: 'eq2', item: 'Gelato-Wagen gekühlt',      qty:  1, supplier: 'Allgäu Event GmbH', unitCost: 280 },
      { id: 'eq3', item: 'Pavillons 4x4m',            qty:  6, supplier: 'Festhütte Allgäu',   unitCost: 110 },
      { id: 'eq4', item: 'Biertische & Bänke Set',    qty: 30, supplier: 'Festhütte Allgäu',   unitCost:  18 },
      { id: 'eq5', item: 'Einweg-Ökogeschirr Paket',   qty:280, supplier: 'BioDepot GmbH',      unitCost:   1.80 },
      { id: 'eq6', item: 'Strom · Genset 20kVA',      qty:  1, supplier: 'Allgäu Event GmbH', unitCost: 340 },
    ],
    timeline: { t1: true, t2: true, t3: true, t4: false, t5: false, t6: false, t7: false, t8: false, t9: false, t10: false, t11: false },
    notes: 'Vertrag liegt vor · Anzahlung fällig 15.05. · Stromanschluss muss bei Stadt angemeldet werden.',
  },
];

// Compute totals (revenue, food-cost, labor, rental, profit) from event components
function eventTotals(e) {
  const revenue = e.menu.reduce((s, course) =>
    s + course.items.reduce((a, i) => a + i.qtyPerPerson * i.unitPrice * e.guests, 0), 0);
  const foodCost = e.menu.reduce((s, course) =>
    s + course.items.reduce((a, i) => a + i.qtyPerPerson * i.unitCost * e.guests, 0), 0);
  const laborCost = e.team.reduce((s, t) => {
    const h = (t.hoursActual != null ? t.hoursActual : t.hours);
    return s + h * t.ratePerHour;
  }, 0);
  const laborPlanned = e.team.reduce((s, t) => s + t.hours * t.ratePerHour, 0);
  const rentalCost = e.equipment.reduce((s, q) => s + q.qty * q.unitCost, 0);
  const totalCost = foodCost + laborCost + rentalCost;
  const profit = revenue - totalCost;
  const margin = revenue > 0 ? (profit / revenue) * 100 : 0;
  return { revenue, foodCost, laborCost, laborPlanned, rentalCost, totalCost, profit, margin };
}
// Attach derived .total so old UI keeps working
EVENTS.forEach(e => { e.total = eventTotals(e).revenue; });

// Seed realistic Ist-Stunden for past/production events (demo only)
EVENTS.forEach(e => {
  if (e.state === 'production' || e.state === 'completed') {
    e.team.forEach((tm, idx) => {
      // Some realistic overruns: most stay on plan, 1-2 go +0.5 to +2h, occasional underrun
      const jitter = [0, 0, 0.5, 1, 1.5, 2, -0.5, 0][(idx + e.id.length) % 8];
      tm.hoursActual = Math.max(0, tm.hours + jitter);
      tm.billable = true;
    });
  }
});

// Transactions — last 14 days
function makeTxn() {
  const out = [];
  const now = new Date();
  for (let d = 13; d >= 0; d--) {
    const day = new Date(now); day.setDate(day.getDate() - d); day.setHours(12,0,0,0);
    const isoDay = day.toISOString().slice(0,10);
    const covers = 60 + Math.round(R() * 80);
    const avg = 48 + R() * 22;
    out.push({ date: isoDay, covers, revenue: Math.round(covers * avg * 100)/100, labor: Math.round(covers * 12 * 100)/100, food: Math.round(covers * 13.5 * 100)/100 });
  }
  return out;
}

// Time entries — worked shifts with clock-in/out (last 60 days for all staff)
function makeTimeEntries() {
  const out = [];
  const today = new Date(); today.setHours(0,0,0,0);
  let counter = 1;
  for (const u of STAFF) {
    if (u.status === 'onboarding') continue; // no history yet for new hires
    const daysBack = u.id === 'u8' ? 25 : 60;
    for (let d = daysBack; d >= 0; d--) {
      // ~5 days/week — skip ~2 random days
      if (R() < 0.28) continue;
      if (u.employmentType === 'Minijob' && R() < 0.45) continue;
      const day = new Date(today); day.setDate(day.getDate() - d);
      // shift length varies by role
      const base = u.area === 'Küche' ? 8.5 : u.area === 'Service' ? 7.5 : 8;
      const hrs = Math.max(3, base + (R() - 0.5) * 2); // 3–10.5h
      const startH = u.area === 'Küche' ? 14 + R()*2 : 16 + R()*2;
      const start = new Date(day); start.setHours(Math.floor(startH), Math.round((startH%1)*60), 0, 0);
      const end = new Date(start.getTime() + hrs * 3600 * 1000);
      const breakMin = hrs > 6 ? 30 : 15;
      const worked = hrs - breakMin/60;
      out.push({
        id: 'te' + (counter++),
        uid: u.id,
        date: day.toISOString().slice(0,10),
        clockIn: start.toISOString(),
        clockOut: end.toISOString(),
        breakMin,
        hoursWorked: Math.round(worked * 100) / 100,
        overtime: Math.max(0, Math.round((worked - 8) * 100) / 100),
        nightHours: Math.max(0, Math.round((end.getHours() >= 22 ? Math.min(worked, end.getHours() - 22 + (end.getMinutes()/60)) : 0) * 100) / 100),
        approved: d > 1,
        notes: '',
      });
    }
  }
  return out;
}

// Export config
const EXPORT_CONFIG = {
  autoEnabled: true,
  schedule: 'monthly', // 'monthly' | 'weekly' | 'biweekly'
  dayOfMonth: 1,        // 1st of each month
  time: '06:00',
  format: 'csv',        // 'csv' | 'xlsx' | 'datev' | 'pdf'
  deliver: 'email',     // 'email' | 'datev' | 'download'
  email: 'buchhaltung@osteria-botanica.de',
  includeOvertime: true,
  includeSickDays: true,
  lastRun: isoDaysAgo(21),
  nextRun: isoFuture(9),
  history: [
    { id: 'ex1', runAt: isoDaysAgo(21), period: '2025-02', employees: 9, hours: 1428.5, format: 'CSV', status: 'ok' },
    { id: 'ex2', runAt: isoDaysAgo(52), period: '2025-01', employees: 9, hours: 1511.0, format: 'CSV', status: 'ok' },
    { id: 'ex3', runAt: isoDaysAgo(83), period: '2024-12', employees: 8, hours: 1392.5, format: 'DATEV', status: 'ok' },
  ],
};

// Notifications
const NOTIFS = [
  { id: 'nt1', t: 'new', msg: 'Neue Reservierung · Familie Hofer · 19:30', tone: 'leaf', at: Date.now() - 3*60*1000 },
  { id: 'nt2', t: 'warn', msg: 'Lager: Venusmuscheln unter Par-Level', tone: 'ember', at: Date.now() - 18*60*1000 },
  { id: 'nt3', t: 'info', msg: 'Schicht-Tausch-Anfrage: Nils ↔ Jana (Do)', tone: 'info', at: Date.now() - 42*60*1000 },
  { id: 'nt4', t: 'ok', msg: 'Bestellung #po201 geliefert (Huber)', tone: 'leaf', at: Date.now() - 2*60*60*1000 },
];

// ─── STATE STORE ───────────────────────────────────────────────────────────────
const mcStore = {
  biz: DEMO_BIZ,
  staff: STAFF,
  menu: MENU,
  rooms: ROOMS,
  tables: TABLES,
  decor: DECOR,
  reservations: makeReservations(),
  tickets: makeTickets(),
  shifts: makeShifts(),
  inventory: INVENTORY,
  orders: ORDERS,
  events: EVENTS,
  eventTimelineTemplate: EVENT_TIMELINE_TEMPLATE,
  txn: makeTxn(),
  timeEntries: makeTimeEntries(),
  exportConfig: EXPORT_CONFIG,
  onboardingSteps: ONBOARDING_STEPS,
  offboardingSteps: OFFBOARDING_STEPS,
  billing: BILLING,
  plans: PLANS,
  users: STAFF.map((s, i) => {
    const roles = ['Admin','Manager','Küchenchef','Personal','Personal','Personal','Personal','Personal','Owner'];
    const mfa = [true, true, false, true, false, false, false, false, true];
    const strength = ['strong','strong','medium','strong','medium','weak','medium','weak','strong'];
    return {
      uid: s.id, email: s.email, name: s.name, avatar: s.color,
      appRole: roles[i] || 'Personal',
      mfa: mfa[i] || false,
      mfaMethod: mfa[i] ? (i === 8 ? 'authenticator' : 'sms') : null,
      password: { lastChanged: new Date(Date.now() - (30+i*8)*86400000).toISOString(), strength: strength[i] || 'medium', forceReset: s.id === 'u7' },
      lastLogin: new Date(Date.now() - i*3600000 - 600000).toISOString(),
      sessions: i === 8 ? 2 : 1,
      status: s.status === 'offboarded' ? 'disabled' : 'active',
    };
  }),
  security: {
    orgMfaRequired: false, orgMfaForRoles: ['Admin', 'Owner'],
    sessionTimeoutMin: 480, ipAllowlist: [], passwordMinLen: 10, passwordRotationDays: 180,
    auditLog: [
      { id: 'a1', at: new Date(Date.now()-5*60000).toISOString(),  actor: 'Paul Wagner',  action: 'Login erfolgreich',            ip: '91.64.22.14',  tone: 'leaf'  },
      { id: 'a2', at: new Date(Date.now()-42*60000).toISOString(), actor: 'Amelie Berger',action: 'Passwort geändert',            ip: '91.64.22.14',  tone: 'info'  },
      { id: 'a3', at: new Date(Date.now()-3*3600000).toISOString(),actor: 'System',        action: 'Monatlicher Export versandt',   ip: '—',            tone: 'leaf'  },
      { id: 'a4', at: new Date(Date.now()-8*3600000).toISOString(),actor: 'Nils Hofer',    action: 'Fehlgeschlagener Login (2x)',   ip: '84.131.9.201', tone: 'ember' },
      { id: 'a5', at: new Date(Date.now()-26*3600000).toISOString(),actor:'Paul Wagner',   action: 'Neuer Mitarbeiter angelegt',    ip: '91.64.22.14',  tone: 'info'  },
      { id: 'a6', at: new Date(Date.now()-2*86400000).toISOString(),actor:'Paul Wagner',   action: 'MFA aktiviert (Authenticator)', ip: '91.64.22.14',  tone: 'leaf'  },
    ],
  },
  notifs: NOTIFS,
  // live POS carts keyed by tableId
  carts: {},
  // session
  session: { uid: 'u9', name: 'Paul Wagner', role: 'Restaurant-Leitung' },
};

// Subscribers
const _subs = new Set();
function _emit() { _subs.forEach(fn => { try { fn(mcStore); } catch(e){} }); }

// ─── FAKE API ─────────────────────────────────────────────────────────────────
// Simulates fetch() with ~80-200ms latency. Mutations update mcStore + emit.
// Later, replace `handle` with a real fetch(url, opts).json().
function _delay(ms) { return new Promise(r => setTimeout(r, ms)); }

const routes = {
  // GET
  'GET /biz':          () => mcStore.biz,
  'GET /staff':        () => mcStore.staff,
  'GET /menu':         () => mcStore.menu,
  'GET /rooms':        () => mcStore.rooms,
  'GET /tables':       () => mcStore.tables,
  'GET /decor':        () => mcStore.decor,
  'GET /reservations': () => mcStore.reservations,
  'GET /tickets':      () => mcStore.tickets,
  'GET /shifts':       () => mcStore.shifts,
  'GET /inventory':    () => mcStore.inventory,
  'GET /orders':       () => mcStore.orders,
  'GET /events':       () => mcStore.events,
  'GET /events/timeline-template': () => mcStore.eventTimelineTemplate,
  'GET /txn':          () => mcStore.txn,
  'GET /notifs':       () => mcStore.notifs,
  'GET /time-entries': () => mcStore.timeEntries,
  'GET /export-config':() => mcStore.exportConfig,
  'GET /billing':      () => mcStore.billing,
  'GET /plans':        () => mcStore.plans,
  'GET /users':        () => mcStore.users,
  'GET /security':     () => mcStore.security,
  'GET /summary':      () => {
    const today = mcStore.txn[mcStore.txn.length - 1];
    const yest  = mcStore.txn[mcStore.txn.length - 2];
    return {
      revenue: today.revenue,
      revenueDelta: ((today.revenue - yest.revenue) / yest.revenue) * 100,
      covers: today.covers,
      coversDelta: ((today.covers - yest.covers) / yest.covers) * 100,
      foodCostPct: (today.food / today.revenue) * 100,
      laborCostPct: (today.labor / today.revenue) * 100,
      openTickets: mcStore.tickets.length,
      reservationsToday: mcStore.reservations.length,
      tablesSeated: mcStore.tables.filter(t => t.status === 'seated').length,
      tablesReserved: mcStore.tables.filter(t => t.status === 'reserved').length,
      tablesOpen: mcStore.tables.filter(t => t.status === 'open').length,
    };
  },
};

const mcApi = {
  async get(path) {
    await _delay(80 + Math.random() * 120);
    const fn = routes['GET ' + path];
    if (!fn) throw new Error('Route not found: GET ' + path);
    // deep-copy to prevent mutation
    return JSON.parse(JSON.stringify(fn()));
  },

  async post(path, body) {
    await _delay(100);
    if (path === '/decor') {
      const d = { id: 'd' + Date.now(), ...body };
      mcStore.decor.push(d);
      _emit();
      return d;
    }
    if (path === '/tables') {
      const d = { id: 't' + Date.now(), status: 'open', shape: 'square', seats: 4, ...body };
      mcStore.tables.push(d);
      _emit();
      return d;
    }
    if (path === '/reservations') {
      const r = { id: 'r' + (Date.now()), status: 'confirmed', ...body };
      mcStore.reservations.push(r);
      _emit();
      return r;
    }
    if (path === '/tickets') {
      const t = { id: 'k' + (2845 + mcStore.tickets.length), opened: Date.now(), items: [], ...body };
      mcStore.tickets.push(t);
      _emit();
      return t;
    }
    if (path === '/events') {
      const e = {
        id: 'ev' + Date.now(), state: 'offer', lead: 'u1',
        menu: [], team: [], equipment: [],
        timeline: { t1: true }, notes: '',
        deposit: 0, depositPaid: false, paidTotal: 0,
        ...body,
      };
      e.total = eventTotals(e).revenue;
      mcStore.events.push(e);
      _emit();
      return e;
    }
    if (path === '/staff') {
      const u = {
        id: 'u' + (Date.now()), status: 'onboarding', tags: [], color: '#8A8178',
        vacation: { entitled: 20, taken: 0, pending: 0 }, sickDays: 0, pto: [],
        certifications: [], documents: [], notes: [],
        onboarding: { status: 'in-progress', startedAt: new Date().toISOString(), completedAt: null, steps: mcStore.onboardingSteps.map(s => ({ ...s, done: false, doneAt: null })) },
        offboarding: null,
        ...body,
      };
      mcStore.staff.push(u);
      _emit();
      return u;
    }
    if (path.startsWith('/staff/') && path.endsWith('/notes')) {
      const uid = path.split('/')[2];
      const u = mcStore.staff.find(x => x.id === uid);
      const n = { id: 'n' + Date.now(), at: new Date().toISOString(), ...body };
      u.notes.unshift(n); _emit(); return n;
    }
    if (path.startsWith('/staff/') && path.endsWith('/offboard')) {
      const uid = path.split('/')[2];
      const u = mcStore.staff.find(x => x.id === uid);
      u.status = 'offboarding';
      u.offboarding = {
        status: 'in-progress', startedAt: new Date().toISOString(),
        lastDay: body.lastDay, reason: body.reason || '',
        steps: mcStore.offboardingSteps.map(s => ({ ...s, done: false, doneAt: null })),
      };
      _emit(); return u;
    }
    if (path === '/exports/run') {
      const rec = { id: 'ex' + Date.now(), runAt: new Date().toISOString(), period: body.period, employees: mcStore.staff.filter(s=>s.status!=='offboarded').length, hours: body.hours || 0, format: (body.format||'csv').toUpperCase(), status: 'ok' };
      mcStore.exportConfig.history.unshift(rec);
      mcStore.exportConfig.lastRun = rec.runAt;
      _emit(); return rec;
    }
    throw new Error('Route not found: POST ' + path);
  },

  async patch(path, body) {
    await _delay(60);
    if (path.startsWith('/tables/')) {
      const id = path.split('/')[2];
      const t = mcStore.tables.find(x => x.id === id);
      if (!t) throw new Error('not found');
      Object.assign(t, body);
      _emit();
      return t;
    }
    if (path.startsWith('/decor/')) {
      const id = path.split('/')[2];
      const d = mcStore.decor.find(x => x.id === id);
      if (!d) throw new Error('not found');
      Object.assign(d, body);
      _emit();
      return d;
    }
    if (path.startsWith('/reservations/')) {
      const id = path.split('/')[2];
      const r = mcStore.reservations.find(x => x.id === id);
      if (!r) throw new Error('not found');
      Object.assign(r, body);
      _emit();
      return r;
    }
    if (path.startsWith('/tickets/')) {
      const [, , tid, , iid] = path.split('/');  // /tickets/:id/items/:iid
      const t = mcStore.tickets.find(x => x.id === tid);
      if (!t) throw new Error('not found');
      if (iid) {
        const it = t.items.find(x => x.id === iid);
        Object.assign(it, body);
      } else {
        Object.assign(t, body);
      }
      _emit();
      return t;
    }
    if (path.startsWith('/shifts/')) {
      const id = path.split('/')[2];
      const s = mcStore.shifts.find(x => x.id === id);
      Object.assign(s, body);
      _emit();
      return s;
    }
    // /staff/:uid — update personnel record fields
    if (path.startsWith('/staff/') && path.split('/').length === 3) {
      const uid = path.split('/')[2];
      const u = mcStore.staff.find(x => x.id === uid);
      if (!u) throw new Error('not found');
      Object.assign(u, body);
      _emit(); return u;
    }
    // /staff/:uid/onboarding/:stepId — toggle onboarding step
    if (path.match(/^\/staff\/[^/]+\/onboarding\/[^/]+$/)) {
      const [, , uid, , sid] = path.split('/');
      const u = mcStore.staff.find(x => x.id === uid);
      const step = u.onboarding.steps.find(s => s.id === sid);
      step.done = !!body.done;
      step.doneAt = body.done ? new Date().toISOString() : null;
      const allDone = u.onboarding.steps.every(s => s.done);
      if (allDone) { u.onboarding.status = 'complete'; u.onboarding.completedAt = new Date().toISOString(); u.status = 'active'; }
      else { u.onboarding.status = 'in-progress'; u.onboarding.completedAt = null; }
      _emit(); return u;
    }
    // /staff/:uid/offboarding/:stepId — toggle offboarding step
    if (path.match(/^\/staff\/[^/]+\/offboarding\/[^/]+$/)) {
      const [, , uid, , sid] = path.split('/');
      const u = mcStore.staff.find(x => x.id === uid);
      const step = u.offboarding.steps.find(s => s.id === sid);
      step.done = !!body.done;
      step.doneAt = body.done ? new Date().toISOString() : null;
      const allDone = u.offboarding.steps.every(s => s.done);
      if (allDone) { u.offboarding.status = 'complete'; u.status = 'offboarded'; }
      _emit(); return u;
    }
    // /time-entries/:id — approve / edit
    if (path.startsWith('/time-entries/')) {
      const id = path.split('/')[2];
      const t = mcStore.timeEntries.find(x => x.id === id);
      if (!t) throw new Error('not found');
      Object.assign(t, body);
      _emit(); return t;
    }
    if (path.startsWith('/events/')) {
      const parts = path.split('/'); // /events/:id[/menu/:mid[/items/:iid]][/team/:tid][/equipment/:eid][/timeline/:tkey]
      const id = parts[2];
      const e = mcStore.events.find(x => x.id === id);
      if (!e) throw new Error('not found');
      if (parts.length === 3) {
        Object.assign(e, body);
      } else if (parts[3] === 'menu' && parts[5] === 'items') {
        const course = e.menu.find(m => m.id === parts[4]);
        const item = course.items.find(i => i.name === parts[6]); // use name as key for simplicity
        Object.assign(item, body);
      } else if (parts[3] === 'menu') {
        const course = e.menu.find(m => m.id === parts[4]);
        Object.assign(course, body);
      } else if (parts[3] === 'team') {
        const tm = e.team.find(t => t.id === parts[4]);
        Object.assign(tm, body);
      } else if (parts[3] === 'equipment') {
        const eq = e.equipment.find(q => q.id === parts[4]);
        Object.assign(eq, body);
      } else if (parts[3] === 'timeline') {
        e.timeline = { ...e.timeline, [parts[4]]: !!body.done };
      }
      e.total = eventTotals(e).revenue;
      _emit(); return e;
    }
    if (path === '/export-config') {
      Object.assign(mcStore.exportConfig, body);
      _emit(); return mcStore.exportConfig;
    }
    if (path === '/billing') { Object.assign(mcStore.billing, body); _emit(); return mcStore.billing; }
    if (path === '/billing/payment') { Object.assign(mcStore.billing.paymentMethod, body); _emit(); return mcStore.billing; }
    if (path === '/security') { Object.assign(mcStore.security, body); _emit(); return mcStore.security; }
    if (path.startsWith('/users/')) {
      const uid = path.split('/')[2];
      const u = mcStore.users.find(x => x.uid === uid);
      if (!u) throw new Error('not found');
      Object.assign(u, body);
      if (body.password) u.password = { ...u.password, ...body.password, lastChanged: new Date().toISOString(), forceReset: false };
      _emit(); return u;
    }
    throw new Error('Route not found: PATCH ' + path);
  },

  async delete(path) {
    await _delay(60);
    if (path.startsWith('/decor/')) {
      const id = path.split('/')[2];
      mcStore.decor = mcStore.decor.filter(x => x.id !== id);
      _emit();
      return { ok: true };
    }
    if (path.startsWith('/tables/')) {
      const id = path.split('/')[2];
      mcStore.tables = mcStore.tables.filter(x => x.id !== id);
      _emit();
      return { ok: true };
    }
    if (path.startsWith('/reservations/')) {
      const id = path.split('/')[2];
      mcStore.reservations = mcStore.reservations.filter(x => x.id !== id);
      _emit();
      return { ok: true };
    }
    if (path.startsWith('/tickets/')) {
      const parts = path.split('/');
      const tid = parts[2];
      const iid = parts[4];
      const t = mcStore.tickets.find(x => x.id === tid);
      if (iid) t.items = t.items.filter(x => x.id !== iid);
      else mcStore.tickets = mcStore.tickets.filter(x => x.id !== tid);
      _emit();
      return { ok: true };
    }
    throw new Error('Route not found: DELETE ' + path);
  },

  // Direct store (for reactive components). In a real backend, use SWR/React-Query.
  store: mcStore,
  subscribe(fn) { _subs.add(fn); return () => _subs.delete(fn); },
  mutate(fn) { fn(mcStore); _emit(); },
};

Object.assign(window, { mcApi, mcStore, eventTotals });
