// Bellia — Paramètres multi-restaurants // 3 variations divergentes : A cartes visuelles · B tableau dense pro · C split workspace const GROUP = { name: 'Cantine Corner', legal: 'SARL Cantine Corner', siren: '892 451 770', owner: 'Yannis Bellia', since: '2021', }; const RESTAURANTS_FULL = [ { id: 'rep', short: 'République', city: 'Paris 11e', addr: '12 rue Oberkampf', initials: 'CR', color: '#E89B6C', cover: 'linear-gradient(135deg,#F7D7BD 0%,#E89B6C 100%)', status: 'actif', covers: 78, openSince: 'mars 2021', director: { name: 'Marc Delaunay', initials: 'MD', email: 'marc.d@cantinecorner.fr' }, ca: 105674, target: 110000, foodcost: 32, foodcostTarget: 28, mass: 35, massTarget: 32, margin: 68, trend: 8.1, tickets: 2980, siret: '892 451 770 00021', tva: 'FR 32 892 451 770', regime: 'Normal — TVA mensuelle', integrations: { caisse: 'Lightspeed', banque: 'Qonto', livraison: 'Uber Eats + Deliveroo', paie: 'PayFit' }, intgStatus: { caisse: 'ok', banque: 'ok', livraison: 'ok', paie: 'warn' }, spark: [82, 88, 95, 91, 102, 98, 110, 115, 108, 122, 130, 125, 138, 142], }, { id: 'bat', short: 'Bastille', city: 'Paris 12e', addr: '38 rue de la Roquette', initials: 'CB', color: '#C97849', cover: 'linear-gradient(135deg,#FBE9D5 0%,#C97849 100%)', status: 'actif', covers: 62, openSince: 'sept. 2022', director: { name: 'Aline Lacombe', initials: 'AL', email: 'aline.l@cantinecorner.fr' }, ca: 84320, target: 88000, foodcost: 29, foodcostTarget: 28, mass: 33, massTarget: 32, margin: 71, trend: 4.2, tickets: 2410, siret: '892 451 770 00039', tva: 'FR 32 892 451 770', regime: 'Normal — TVA mensuelle', integrations: { caisse: 'Lightspeed', banque: 'Qonto', livraison: 'Uber Eats', paie: 'PayFit' }, intgStatus: { caisse: 'ok', banque: 'ok', livraison: 'ok', paie: 'ok' }, spark: [70, 72, 78, 75, 82, 86, 88, 84, 90, 92, 96, 98, 102, 105], }, { id: 'mtg', short: 'Montrouge', city: 'Hauts-de-Seine', addr: '4 av. de la République', initials: 'CM', color: '#D9923B', cover: 'linear-gradient(135deg,#FCEDDC 0%,#D9923B 100%)', status: 'attention', covers: 54, openSince: 'janv. 2024', director: { name: 'Théo Roux', initials: 'TR', email: 'theo.r@cantinecorner.fr' }, ca: 62180, target: 72000, foodcost: 35, foodcostTarget: 28, mass: 38, massTarget: 32, margin: 64, trend: -2.4, tickets: 1860, siret: '892 451 770 00054', tva: 'FR 32 892 451 770', regime: 'Normal — TVA mensuelle', integrations: { caisse: 'Tiller', banque: 'Qonto', livraison: '—', paie: 'Silae' }, intgStatus: { caisse: 'ok', banque: 'ok', livraison: 'off', paie: 'warn' }, spark: [62, 58, 65, 60, 68, 62, 70, 65, 72, 68, 65, 70, 66, 63], }, ]; // Group consolidated KPIs const GROUP_KPIS = (() => { const ca = RESTAURANTS_FULL.reduce((a, r) => a + r.ca, 0); const target = RESTAURANTS_FULL.reduce((a, r) => a + r.target, 0); const tickets = RESTAURANTS_FULL.reduce((a, r) => a + r.tickets, 0); const margin = RESTAURANTS_FULL.reduce((a, r) => a + r.margin * r.ca, 0) / ca; const foodcost = RESTAURANTS_FULL.reduce((a, r) => a + r.foodcost * r.ca, 0) / ca; return { ca, target, tickets, margin, foodcost, count: RESTAURANTS_FULL.length }; })(); const TEAM = [ { id: 'yb', name: 'Yannis Bellia', role: 'Propriétaire', initials: 'YB', c: '#E89B6C', email: 'yannis@cantinecorner.fr', scope: 'Tous les restaurants', perms: 'Admin global', last: 'à l\'instant' }, { id: 'md', name: 'Marc Delaunay', role: 'Directeur de site', initials: 'MD', c: '#C97849', email: 'marc.d@cantinecorner.fr', scope: 'République', perms: 'Lecture + édition', last: 'il y a 12 min' }, { id: 'al', name: 'Aline Lacombe', role: 'Directrice de site', initials: 'AL', c: '#D9923B', email: 'aline.l@cantinecorner.fr', scope: 'Bastille', perms: 'Lecture + édition', last: 'il y a 1 h' }, { id: 'tr', name: 'Théo Roux', role: 'Directeur de site', initials: 'TR', c: '#7FB069', email: 'theo.r@cantinecorner.fr', scope: 'Montrouge', perms: 'Lecture + édition', last: 'hier' }, { id: 'cb', name: 'Cabinet Berthier', role: 'Comptable externe', initials: 'CB', c: '#8A7B69', email: 'compta@berthier.fr', scope: 'Tous (lecture seule)', perms: 'Lecture', last: 'lun. 09:00' }, ]; const STATUS_MAP = { actif: { c: C.ok, bg: C.okSft, label: 'Actif', dot: C.ok }, attention: { c: C.warn, bg: C.warnSft, label: 'Attention', dot: C.warn }, pause: { c: C.mute, bg: C.cardSoft, label: 'En pause', dot: C.mute2 }, }; const INTG_STATUS = { ok: { c: C.ok, bg: C.okSft, label: 'Connecté', icon: 'checkC' }, warn: { c: C.warn, bg: C.warnSft, label: 'À vérifier', icon: 'alertT' }, off: { c: C.mute, bg: C.cardSoft, label: 'Non connecté', icon: 'x' }, }; // ============================================================ // MAIN SCREEN // ============================================================ const RestaurantsScreenWrapper = ({ onNav }) => { const [variant, setVariant] = React.useState(() => { try { return localStorage.getItem('bellia-restos-variant') || 'A'; } catch { return 'A'; } }); const setTweak = (k, v) => { if (k === 'variant') { setVariant(v); try { localStorage.setItem('bellia-restos-variant', v); } catch {} } }; return ; }; const RestaurantsScreen = ({ onNav, tweaks, setTweak }) => { const variant = tweaks?.variant || 'A'; return (
}>Exporter }>Ajouter un restaurant } /> {/* Sub-tabs (paramètres groupe) */}
{[ { id: 'restos', l: 'Restaurants', n: 3, active: true }, { id: 'team', l: 'Équipe & droits', n: 5 }, { id: 'group', l: 'Groupe' }, { id: 'integ', l: 'Intégrations' }, { id: 'bill', l: 'Facturation' }, ].map(t => ( ))}
{/* Variation switcher */}
VARIATION
{[ { id: 'A', l: 'Cartes' }, { id: 'B', l: 'Tableau' }, { id: 'C', l: 'Atelier' }, ].map(v => ( ))}
{/* Hero KPI consolidé groupe — partagé par les 3 variations */} {/* Variation body */}
{variant === 'A' && } {variant === 'B' && } {variant === 'C' && }
{/* Section Équipe & droits — toujours visible en bas */} ); }; // ============================================================ // HERO consolidé // ============================================================ const GroupHero = () => { const k = GROUP_KPIS; return (
Groupe consolidé · mai 2026
CA TTC CUMULÉ
{eur(k.ca)}
}>+5,6 % vs N-1 Objectif {eur(k.target)} · {pct((k.ca / k.target) * 100, 0)} atteint
= 70 ? 'ok' : 'warn'} />
); }; const HeroStat = ({ label, value, sub, subKind }) => { const subColor = subKind === 'ok' ? C.ok : subKind === 'warn' ? C.warn : C.mute; return (
{label.toUpperCase()}
{value}
{sub}
); }; // ============================================================ // VARIANT A — CARTES VISUELLES + BENCHMARK BAR // ============================================================ const VariantA = () => ( <>
Vos restaurants
Cliquez sur une carte pour gérer le détail · les indicateurs sont mis à jour en temps réel
}>Filtres }>Comparer
{RESTAURANTS_FULL.map(r => )}
); const RestoCard = ({ resto }) => { const s = STATUS_MAP[resto.status]; const targetPct = (resto.ca / resto.target) * 100; return (
{ e.currentTarget.style.boxShadow = '0 16px 36px -24px rgba(36,27,18,0.3)'; e.currentTarget.style.borderColor = C.orangeSft; }} onMouseLeave={e => { e.currentTarget.style.boxShadow = 'none'; e.currentTarget.style.borderColor = C.line; }} > {/* cover */}
}>{s.label}
{resto.initials}
{resto.short}
{resto.addr} · {resto.city}
{/* KPI */}
CA MAI
{eur(resto.ca)}
= 0 ? 'ok' : 'err'} size="sm" icon={= 0 ? 'trendUp' : 'trendDn'} size={10} />}> {signedPct(resto.trend)}
Objectif {eur(resto.target)} {pct(targetPct, 0)}
{/* 3 mini KPI */}
resto.foodcostTarget ? 'err' : 'ok'} /> resto.massTarget ? 'warn' : 'ok'} />
{/* Directeur */}
{resto.director.name}
Directeur
); }; const MiniInline = ({ label, value, kind }) => { const color = kind === 'ok' ? C.ok : kind === 'warn' ? C.warn : kind === 'err' ? C.err : C.ink; return (
{label.toUpperCase()}
{value}
); }; // Benchmark const BenchmarkBar = () => { const metrics = [ { id: 'ca', label: 'CA TTC', fmt: v => eur(v), sort: (a, b) => b - a, key: 'ca', higherBetter: true }, { id: 'mar', label: 'Marge', fmt: v => pct(v, 0), sort: (a, b) => b - a, key: 'margin', higherBetter: true }, { id: 'fc', label: 'Food cost', fmt: v => pct(v, 0), sort: (a, b) => a - b, key: 'foodcost', higherBetter: false }, { id: 'tr', label: 'Évolution', fmt: v => signedPct(v), sort: (a, b) => b - a, key: 'trend', higherBetter: true }, ]; return (
Benchmark inter-restaurants
Sur mai 2026 · meilleur = orange foncé
Exporter le benchmark
{metrics.map(m => { const sorted = [...RESTAURANTS_FULL].sort((a, b) => m.sort(a[m.key], b[m.key])); const max = Math.max(...RESTAURANTS_FULL.map(r => Math.abs(r[m.key]))); return (
{m.label.toUpperCase()}
{sorted.map((r, i) => { const val = r[m.key]; const w = (Math.abs(val) / max) * 100; const isBest = i === 0; return (
{r.short}
{m.fmt(val)}
); })}
); })}
); }; // ============================================================ // VARIANT B — TABLEAU DENSE PRO + LIGNES EXPANDABLES // ============================================================ const VariantB = () => { const [expanded, setExpanded] = React.useState('rep'); const [selected, setSelected] = React.useState(['rep', 'bat', 'mtg']); const toggle = id => setSelected(s => s.includes(id) ? s.filter(x => x !== id) : [...s, id]); return (
Vue tableau · {GROUP_KPIS.count} restos
{selected.length > 0 && ( {selected.length} sélectionné{selected.length > 1 ? 's' : ''} )}
}>Consolider
{['Restaurant', 'Statut', 'CA TTC', 'Objectif', 'Marge', 'Food cost', 'Masse sal.', 'Tickets', 'Évolution', 'Directeur', ''].map((h, i) => ( ))} {RESTAURANTS_FULL.map((r, i) => { const s = STATUS_MAP[r.status]; const isExp = expanded === r.id; const targetPct = (r.ca / r.target) * 100; return ( {isExp && ( )} ); })} {/* footer totals */}
{h}
toggle(r.id)} style={{ accentColor: C.orange, cursor: 'pointer' }} /> }>{s.label} {eur(r.ca)}
{eur(r.target)}
= 90 ? C.ok : C.orange }} />
{pct(r.margin, 0)} r.foodcostTarget ? C.err : C.ok)}>{pct(r.foodcost, 0)} r.massTarget ? C.warn : C.ok)}>{pct(r.mass, 0)} {num(r.tickets)} = 0 ? C.ok : C.err, fontSize: 12, fontWeight: 700 }}> = 0 ? 'trendUp' : 'trendDn'} size={11} /> {signedPct(r.trend)}
{r.director.name.split(' ')[0]}
TOTAL GROUPE {eur(GROUP_KPIS.ca)} {eur(GROUP_KPIS.target)} {pct(GROUP_KPIS.margin, 1)} {pct(GROUP_KPIS.foodcost, 1)} {num(GROUP_KPIS.tickets)} +5,6 %
); }; const thStyle = (w, align) => ({ padding: '14px 16px', textAlign: align || 'left', width: w || 'auto', fontSize: 10, color: C.mute, fontWeight: 700, letterSpacing: 0.6, textTransform: 'uppercase', borderBottom: `1px solid ${C.line}`, whiteSpace: 'nowrap', }); const tdStyle = (align, num, color) => ({ padding: '14px 16px', textAlign: align || 'left', fontSize: 12.5, color: color || C.ink, fontWeight: num ? 700 : 400, fontVariantNumeric: num ? 'tabular-nums' : 'normal', verticalAlign: 'middle', }); const ExpandedDetail = ({ resto }) => (
resto.foodcostTarget ? 'err' : 'ok'} actual={`actuel ${pct(resto.foodcost, 0)}`} /> resto.massTarget ? 'warn' : 'ok'} actual={`actuel ${pct(resto.mass, 0)}`} /> = resto.target ? 'ok' : 'warn'} actual={`atteint ${pct((resto.ca / resto.target) * 100, 0)}`} /> Tout gérer}> {Object.entries(resto.integrations).map(([k, v]) => { const st = INTG_STATUS[resto.intgStatus[k]]; return (
{k}
{v}
}>{st.label}
); })}
); const DetailBlock = ({ title, icon, children, right }) => (
{title}
{right}
{children}
); const DetailRow = ({ k, v, mono, accent, actual }) => (
{k}
{v} {actual &&
{actual}
}
); // ============================================================ // VARIANT C — SPLIT WORKSPACE (liste + fiche détail) // ============================================================ const VariantC = () => { const [sel, setSel] = React.useState('rep'); const r = RESTAURANTS_FULL.find(x => x.id === sel); return (
{/* Liste */}
Restaurants · 3
{RESTAURANTS_FULL.map(rr => { const active = sel === rr.id; const s = STATUS_MAP[rr.status]; return ( ); })}
VUE GROUPE
{/* Fiche détail */}
); }; const RestoDetailPanel = ({ resto }) => { const s = STATUS_MAP[resto.status]; return ( <> {/* Cover + ident */}
}>{s.label}
{resto.initials}
Cantine Corner — {resto.short}
{resto.addr} · {resto.city} · {resto.covers} couverts · ouvert depuis {resto.openSince}
Dirigé par {resto.director.name}
}>Archiver }>Modifier la fiche
{/* KPI ribbon */}
= 0 ? 'ok' : 'err'} /> = 70 ? 'ok' : 'warn'} /> resto.foodcostTarget ? 'err' : 'ok'} /> resto.massTarget ? 'warn' : 'ok'} />
{/* Sections settings */}
{resto.initials}
Logo & monogramme
SVG · 240 × 240 · mis à jour il y a 2 mois
Modifier
{resto.color} } />
resto.foodcostTarget} /> resto.massTarget} /> }>Ajouter}> {Object.entries(resto.integrations).map(([k, v]) => { const st = INTG_STATUS[resto.intgStatus[k]]; return (
{k}
{v}
}>{st.label}
); })}
); }; const SettingsCard = ({ title, icon, desc, children, right }) => (
{title}
{desc}
{right}
{children}
); const FieldShow = ({ k, v, mono }) => (
{k} {v}
); const SliderField = ({ label, value, actual, unit, max, warn }) => { const m = max || 50; const pos = (value / m) * 100; const actualPos = (actual / m) * 100; return (
{label}
actuel {actual.toFixed(0)}{unit} {value}{unit}
); }; // ============================================================ // TEAM SECTION (commun aux 3 variantes) // ============================================================ const TeamSection = () => (
Équipe & droits
5 utilisateurs · 2 dirigeants, 3 directeurs de site · accès comptable externe en lecture seule
}>Rôles & permissions }>Inviter
{['Membre', 'Rôle', 'Restaurants', 'Permissions', 'Dernière activité', ''].map(h => ( ))} {TEAM.map((t, i) => ( ))}
{h}
{t.name}
{t.email}
{t.role} {t.scope} {t.perms} {t.last}
); window.RestaurantsScreen = RestaurantsScreen; window.RestaurantsScreenWrapper = RestaurantsScreenWrapper; window.RESTAURANTS_FULL = RESTAURANTS_FULL; window.GROUP_KPIS = GROUP_KPIS; window.STATUS_MAP_R = STATUS_MAP;