Piattaforme riservate: ruolo dedicato e port della dashboard Stress Index

Il ruolo `campus` diventa `piattaforme` e non apre più solo la sezione
Biohacking: copre tutte le aree riservate, con migrazione guardata sugli
utenti esistenti. Nell'header la vecchia voce Biohacking lascia il posto a
un menu Piattaforme con i due link, visibile solo a chi ha il ruolo, e
/piattaforme diventa la pagina di atterraggio.

Stress Index è portata dentro il sito: le nove viste dell'area
professionisti diventano pagine Astro sotto /piattaforme/stress-index,
con lo stile del sito al posto del design system del prototipo. React
resta solo dove serve davvero — grafici recharts e scheda cliente — il
resto è Astro con un filo di JS. I dati sono ancora quelli dimostrativi,
raccolti in lib/stress-index/data.ts.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-31 15:49:47 +02:00
parent e8e24e086b
commit c4eb598a3a
32 changed files with 2908 additions and 74 deletions
+35 -4
View File
@@ -1,5 +1,6 @@
---
import { site } from '../data/site';
import { piattaforme } from '../data/piattaforme';
import { t, contentImageUrl } from '../lib/content';
import { landingFor } from '../lib/auth';
interface Props {
@@ -11,8 +12,9 @@ const { user, canTags, showTags } = Astro.props;
const path = Astro.url.pathname;
const loginHref = `/login?next=${encodeURIComponent(path)}`;
const areaHref = user ? landingFor(user.role) : '/admin';
// La sezione Biohacking compare nel menu solo a chi può davvero aprirla.
const canBiohacking = user?.role === 'campus' || user?.role === 'admin';
// Le piattaforme riservate compaiono nel menu solo a chi può davvero aprirle.
const canPiattaforme = user?.role === 'piattaforme' || user?.role === 'admin';
const inPiattaforme = piattaforme.some((p) => path === p.href || path.startsWith(`${p.href}/`));
---
<header class="hdr" id="site-header">
<div class="container hdr__in">
@@ -23,8 +25,15 @@ const canBiohacking = user?.role === 'campus' || user?.role === 'admin';
<a href={item.href} class:list={['hdr__link', { 'is-active': path === item.href || (item.href !== '/' && path.startsWith(item.href)) }]}>{t(`global.nav.${i + 1}.label`)}</a>
)
))}
{canBiohacking && (
<a href="/campus" class:list={['hdr__link', 'hdr__link--bio', { 'is-active': path.startsWith('/campus') }]}>Biohacking</a>
{canPiattaforme && (
<details class="plat">
<summary class:list={['hdr__link', 'hdr__link--bio', 'plat__btn', { 'is-active': inPiattaforme }]}>Piattaforme ▾</summary>
<div class="plat__panel">
{piattaforme.map((p) => (
<a href={p.href} class:list={['plat__item', { 'is-active': path === p.href || path.startsWith(`${p.href}/`) }]}>{p.label}</a>
))}
</div>
</details>
)}
</nav>
<div class="hdr__user">
@@ -57,6 +66,17 @@ const canBiohacking = user?.role === 'campus' || user?.role === 'admin';
.hdr__link { font-family: var(--font-heading); font-size: .72rem; font-weight: 600; letter-spacing: .18em; text-transform: uppercase; text-decoration: none; color: var(--c-heading); transition: color .3s ease; }
.hdr__link:hover, .hdr__link.is-active { color: var(--c-accent-dark); }
.hdr__link--bio { color: var(--c-accent-dark); }
/* Menu Piattaforme: su desktop è un pannello che scende dalla voce, in mobile —
dove la nav è già una colonna — le voci si aprono in linea sotto il titolo. */
.plat { position: relative; }
.plat__btn { cursor: pointer; list-style: none; }
.plat__btn::-webkit-details-marker { display: none; }
.plat__panel {
position: absolute; left: 0; top: calc(100% + 14px); background: #fff; min-width: 210px;
box-shadow: 0 8px 22px rgba(0,0,0,.12); display: flex; flex-direction: column; padding: 8px 0; z-index: 200;
}
.plat__item { padding: 10px 16px; font-size: .8rem; text-decoration: none; color: var(--c-heading); }
.plat__item:hover, .plat__item.is-active { background: #f4f2ef; color: var(--c-accent-dark); }
.hdr__burger { display: none; background: none; border: 0; cursor: pointer; padding: 8px; }
.hdr__burger span { display: block; width: 22px; height: 2px; background: var(--c-heading); margin: 5px 0; }
@media (max-width: 991px) {
@@ -64,6 +84,8 @@ const canBiohacking = user?.role === 'campus' || user?.role === 'admin';
.hdr__nav.is-open { display: flex; }
.hdr__link { padding: 12px 0; }
.hdr__burger { display: block; }
.plat__panel { position: static; box-shadow: none; padding: 0 0 8px; min-width: 0; }
.plat__item { padding: 10px 0 10px 14px; }
}
.hdr__user { display: flex; align-items: center; }
.hdr__login { font-family: var(--font-heading); font-size: .72rem; font-weight: 600; letter-spacing: .18em; text-transform: uppercase; text-decoration: none; color: var(--c-heading); }
@@ -86,6 +108,15 @@ const canBiohacking = user?.role === 'campus' || user?.role === 'admin';
toggle.setAttribute('aria-expanded', String(open));
});
// Il pannello Piattaforme è un <details>: da solo resterebbe aperto anche cliccando altrove.
const plat = document.querySelector<HTMLDetailsElement>('.plat');
if (plat) {
document.addEventListener('click', (e) => {
if (plat.open && !plat.contains(e.target as Node)) plat.open = false;
});
document.addEventListener('keydown', (e) => { if (e.key === 'Escape') plat.open = false; });
}
const tagsBtn = document.getElementById('toggle-tags');
if (tagsBtn) {
const on = tagsBtn.dataset.on === '1';
@@ -0,0 +1,12 @@
---
import type { Alert } from '../../lib/stress-index/data';
interface Props { alert: Alert }
const { alert } = Astro.props;
---
<div class:list={['si-alert', alert.severity === 'error' && 'si-alert--error']}>
<span class="si-alert__mark" aria-hidden="true"></span>
<div>
<p class="si-alert__text">{alert.text}</p>
<p class="si-alert__meta">{alert.clientName} · {alert.when}</p>
</div>
</div>
+14
View File
@@ -0,0 +1,14 @@
---
// Riquadro base della dashboard: titolo opzionale e, a destra, un'azione (di solito un link).
interface Props { title?: string; actionLabel?: string; actionHref?: string; class?: string }
const { title, actionLabel, actionHref, class: cls } = Astro.props;
---
<section class:list={['si-card', cls]}>
{(title || actionLabel) && (
<div class="si-card__head">
{title && <h2 class="si-card__title">{title}</h2>}
{actionLabel && actionHref && <a class="si-card__action" href={actionHref}>{actionLabel}</a>}
</div>
)}
<slot />
</section>
@@ -0,0 +1,387 @@
// Scheda cliente: è l'unica vista davvero interattiva della piattaforma (tab, filtri sul
// grafico, note, messaggi, tag), quindi vive come singola isola React. Lo stile arriva dalle
// classi `.si-*` del foglio globale: qui non ci sono classi Tailwind del prototipo originale.
import { useMemo, useState } from 'react';
import DataChart from './charts/DataChart';
import type { Alert, Client, Measurement, Note } from '../../lib/stress-index/data';
interface Props {
client: Client;
measurements: Measurement[];
alerts: Alert[];
notes: Note[];
}
const TABS = [
{ key: 'panoramica', label: 'Panoramica' },
{ key: 'misurazioni', label: 'Misurazioni' },
{ key: 'analytics', label: 'Analytics' },
{ key: 'report', label: 'Report' },
{ key: 'note', label: 'Note' },
{ key: 'messaggi', label: 'Messaggi' },
{ key: 'impostazioni', label: 'Impostazioni' },
];
const METRICHE = [
{ key: 'stress', label: 'Stress', color: '#b05a4e' },
{ key: 'recovery', label: 'Recupero', color: '#6f8f6a' },
{ key: 'balance', label: 'Equilibrio', color: '#c08a3e' },
{ key: 'energy', label: 'Energia', color: '#9c8b70' },
];
const PERIODI = [
{ key: '4', label: 'Ultime 4' },
{ key: '8', label: 'Ultime 8' },
{ key: 'all', label: 'Tutte' },
];
const REPORT = [
{ date: '2026-07-01', label: 'Report mensile — Giugno 2026' },
{ date: '2026-06-01', label: 'Report mensile — Maggio 2026' },
{ date: '2026-05-01', label: 'Report mensile — Aprile 2026' },
];
const MESSAGGI_INIZIALI = [
{ from: 'cliente', text: "Buongiorno dottore, le invio l'esito della misurazione di stamattina.", when: '09:12' },
{ from: 'professionista', text: 'Grazie, la vedo. I valori di recupero sono in miglioramento rispetto alla scorsa settimana.', when: '09:20' },
{ from: 'cliente', text: 'Sì, mi sento meglio, dormo di più.', when: '09:22' },
{ from: 'professionista', text: 'Continui così, ci sentiamo al prossimo controllo.', when: '09:25' },
];
const qualitativa = (v: number) => (v >= 70 ? 'Alto' : v >= 40 ? 'Moderato' : 'Basso');
const livello = (v: number) => (v >= 70 ? 'bad' : v >= 45 ? 'warn' : 'ok');
/** Indicatore ad arco: il valore riempie un semicerchio da sinistra a destra. */
function Gauge({ value, label }: { value: number; label: string }) {
const ratio = Math.max(0, Math.min(1, value / 100));
const R = 56;
const len = Math.PI * R;
return (
<div className="si-gauge">
<svg viewBox="0 0 140 78" className="si-gauge__svg" role="img" aria-label={`${label}: ${value} su 100`}>
<path d="M 14 70 A 56 56 0 0 1 126 70" fill="none" stroke="#e4dfd7" strokeWidth="10" strokeLinecap="round" />
<path
d="M 14 70 A 56 56 0 0 1 126 70" fill="none" strokeWidth="10" strokeLinecap="round"
stroke={value >= 70 ? '#b05a4e' : value >= 45 ? '#c08a3e' : '#6f8f6a'}
strokeDasharray={`${len * ratio} ${len}`}
/>
</svg>
<p className="si-gauge__value">{value}<span className="si-gauge__max">/100</span></p>
<p className="si-gauge__label">{label} · {qualitativa(value)}</p>
</div>
);
}
export default function ClientDetail({ client, measurements, alerts, notes }: Props) {
const [tab, setTab] = useState('panoramica');
const [periodo, setPeriodo] = useState('8');
const [metriche, setMetriche] = useState<string[]>(METRICHE.map((m) => m.key));
const [noteCliente, setNoteCliente] = useState(notes);
const [nuovaNota, setNuovaNota] = useState('');
const [messaggi, setMessaggi] = useState(MESSAGGI_INIZIALI);
const [bozza, setBozza] = useState('');
const [tag, setTag] = useState<string[]>(client.tags);
const [nuovoTag, setNuovoTag] = useState('');
const [promemoria, setPromemoria] = useState(true);
const [reportSemestrale, setReportSemestrale] = useState(false);
const ultima = measurements[measurements.length - 1];
const stress = ultima?.stress ?? client.stress;
const recovery = ultima?.recovery ?? 0;
const balance = ultima?.balance ?? 0;
const energy = ultima?.energy ?? 0;
const datiGrafico = useMemo(
() => (periodo === 'all' ? measurements : measurements.slice(-Number(periodo))),
[measurements, periodo]
);
const serieAttive = METRICHE.filter((m) => metriche.includes(m.key));
const ultimeTre = [...measurements].slice(-3).reverse();
const storico = [...measurements].reverse();
const toggleMetrica = (key: string) =>
setMetriche((prev) => (prev.includes(key) ? prev.filter((k) => k !== key) : [...prev, key]));
const aggiungiNota = () => {
const testo = nuovaNota.trim();
if (!testo) return;
setNoteCliente((prev) => [{ clientId: client.id, text: testo, when: client.lastMeasurement }, ...prev]);
setNuovaNota('');
};
const invia = () => {
const testo = bozza.trim();
if (!testo) return;
setMessaggi((prev) => [...prev, { from: 'professionista', text: testo, when: 'ora' }]);
setBozza('');
};
const aggiungiTag = () => {
const t = nuovoTag.trim();
if (!t || tag.includes(t)) return;
setTag((prev) => [...prev, t]);
setNuovoTag('');
};
return (
<div className="si-stack">
<div className="si-client-head">
<span className="si-avatar si-avatar--lg">{client.initials}</span>
<div>
<h1>{client.name}</h1>
<p className="si-muted si-small">
{client.age} anni · {client.sex} · in carico dal {client.since}
</p>
</div>
</div>
<div className="si-grid si-grid--kpi">
{[
{ label: 'Stress', value: stress, note: qualitativa(stress) },
{ label: 'Recupero', value: recovery, note: qualitativa(recovery) },
{ label: 'Misurazioni', value: measurements.length, note: 'totali' },
{ label: 'In carico da', value: client.since, note: 'data iscrizione' },
].map((k) => (
<div className="si-card" key={k.label}>
<p className="si-kpi__label">{k.label}</p>
<p className="si-kpi__value">{k.value}</p>
<p className="si-muted si-small">{k.note}</p>
</div>
))}
</div>
<div className="si-tabs" role="tablist">
{TABS.map((t) => (
<button
key={t.key} type="button" role="tab" className="si-tabs__btn"
aria-selected={t.key === tab} onClick={() => setTab(t.key)}
>
{t.label}
</button>
))}
</div>
{tab === 'panoramica' && (
<div className="si-stack">
<section className="si-card">
<div className="si-card__head"><h2 className="si-card__title">Stato attuale</h2></div>
<div className="si-gauges">
<Gauge value={stress} label="Stress" />
<Gauge value={recovery} label="Recupero" />
<Gauge value={balance} label="Equilibrio" />
<Gauge value={energy} label="Energia" />
</div>
</section>
<section className="si-card">
<div className="si-card__head">
<h2 className="si-card__title">Andamento clinico</h2>
</div>
<div className="si-chips">
{PERIODI.map((p) => (
<button key={p.key} type="button" className="si-chip" aria-pressed={periodo === p.key} onClick={() => setPeriodo(p.key)}>
{p.label}
</button>
))}
</div>
<div className="si-chips">
{METRICHE.map((m) => (
<button key={m.key} type="button" className="si-chip" aria-pressed={metriche.includes(m.key)} onClick={() => toggleMetrica(m.key)}>
{m.label}
</button>
))}
</div>
<DataChart data={datiGrafico} xKey="date" series={serieAttive} />
</section>
<div className="si-grid si-grid--halves">
<section className="si-card">
<div className="si-card__head"><h2 className="si-card__title">Alert attivi</h2></div>
{alerts.length === 0 ? (
<p className="si-muted si-small">Nessun alert attivo per questo cliente.</p>
) : (
alerts.map((a, i) => (
<div className={`si-alert${a.severity === 'error' ? ' si-alert--error' : ''}`} key={i}>
<span className="si-alert__mark" aria-hidden="true" />
<div>
<p className="si-alert__text">{a.text}</p>
<p className="si-alert__meta">{a.when}</p>
</div>
</div>
))
)}
</section>
<section className="si-card">
<div className="si-card__head"><h2 className="si-card__title">Ultime 3 misurazioni</h2></div>
<ul className="si-list si-list--rows">
{ultimeTre.map((m, i) => (
<li key={i}>
<span>
<strong>{m.date}</strong>
<span className="si-muted"> · {m.type} · stress {m.stress}</span>
</span>
<span className={`si-badge si-badge--${livello(m.stress)}`}>{qualitativa(m.stress)}</span>
</li>
))}
</ul>
</section>
</div>
<section className="si-insight">
<p className="si-insight__title">Sintesi</p>
<p>
{stress >= 70
? `${client.name} mostra un livello di stress elevato nelle ultime misurazioni, con recupero notturno ridotto. Consigliata una revisione del carico settimanale e un lavoro sulla respirazione.`
: `${client.name} mantiene un equilibrio complessivamente stabile. Continuare il monitoraggio periodico e mantenere le abitudini di recupero attuali.`}
</p>
</section>
</div>
)}
{tab === 'misurazioni' && (
<section className="si-card">
<div className="si-card__head"><h2 className="si-card__title">Storico misurazioni</h2></div>
<div className="si-scroll-x">
<table className="si-table">
<thead>
<tr><th>Data</th><th>Tipo</th><th>Stress</th><th>Recupero</th><th>Equilibrio</th><th>Energia</th><th>Durata</th></tr>
</thead>
<tbody>
{storico.map((m, i) => (
<tr key={i}>
<td className="si-table__num">{m.date}</td>
<td>{m.type}</td>
<td className="si-table__num">{m.stress}</td>
<td className="si-table__num">{m.recovery}</td>
<td className="si-table__num">{m.balance}</td>
<td className="si-table__num">{m.energy}</td>
<td className="si-table__num">{m.durationMin} min</td>
</tr>
))}
</tbody>
</table>
</div>
</section>
)}
{tab === 'analytics' && (
<div className="si-stack">
<section className="si-card">
<div className="si-card__head"><h2 className="si-card__title">Trend dei quattro parametri</h2></div>
<DataChart data={measurements} xKey="date" series={METRICHE} />
</section>
<section className="si-card">
<div className="si-card__head"><h2 className="si-card__title">Stress e recupero, ultime misurazioni</h2></div>
<DataChart type="bar" data={ultimeTre} xKey="date" series={[METRICHE[0], METRICHE[1]]} />
</section>
</div>
)}
{tab === 'report' && (
<section className="si-card">
<div className="si-card__head">
<h2 className="si-card__title">Report generati</h2>
<button type="button" className="si-btn">Genera report</button>
</div>
<ul className="si-list si-list--rows">
{REPORT.map((r) => (
<li key={r.date}>
<span><span className="si-muted si-table__num">{r.date}</span> · {r.label}</span>
<button type="button" className="si-btn">Scarica</button>
</li>
))}
</ul>
</section>
)}
{tab === 'note' && (
<section className="si-card">
<div className="si-card__head"><h2 className="si-card__title">Note cliente</h2></div>
<div className="si-stack">
<label className="si-field">
Aggiungi nota
<textarea
rows={3} value={nuovaNota} onChange={(e) => setNuovaNota(e.target.value)}
placeholder="Scrivi una nota clinica su questo cliente…"
/>
</label>
<div><button type="button" className="si-btn si-btn--solid" onClick={aggiungiNota}>Aggiungi nota</button></div>
{noteCliente.length === 0 ? (
<p className="si-muted si-small">Nessuna nota registrata per questo cliente.</p>
) : (
<ul className="si-list">
{noteCliente.map((n, i) => (
<li className="si-note-card" key={i}>
<p className="si-note__meta">{n.when}</p>
<p>{n.text}</p>
</li>
))}
</ul>
)}
</div>
</section>
)}
{tab === 'messaggi' && (
<section className="si-card">
<div className="si-card__head"><h2 className="si-card__title">Messaggi</h2></div>
<ul className="si-chat">
{messaggi.map((m, i) => (
<li key={i} className={`si-chat__row si-chat__row--${m.from}`}>
<div className="si-chat__bubble">
<p>{m.text}</p>
<p className="si-chat__when">{m.when}</p>
</div>
</li>
))}
</ul>
<div className="si-chat__compose">
<label className="si-field si-field--inline">
<span className="si-visually-hidden">Messaggio</span>
<input type="text" value={bozza} onChange={(e) => setBozza(e.target.value)} placeholder="Scrivi un messaggio…" />
</label>
<button type="button" className="si-btn si-btn--solid" onClick={invia}>Invia</button>
</div>
</section>
)}
{tab === 'impostazioni' && (
<div className="si-stack">
<section className="si-card">
<div className="si-card__head"><h2 className="si-card__title">Tag cliente</h2></div>
<div className="si-tag-list">
{tag.map((t) => (
<span className="si-tag si-tag--removable" key={t}>
{t}
<button type="button" aria-label={`Rimuovi tag ${t}`} onClick={() => setTag((prev) => prev.filter((x) => x !== t))}></button>
</span>
))}
</div>
<div className="si-chat__compose">
<label className="si-field si-field--inline">
<span className="si-visually-hidden">Nuovo tag</span>
<input type="text" value={nuovoTag} onChange={(e) => setNuovoTag(e.target.value)} placeholder="Nuovo tag…" />
</label>
<button type="button" className="si-btn" onClick={aggiungiTag}>Aggiungi</button>
</div>
</section>
<section className="si-card">
<div className="si-card__head"><h2 className="si-card__title">Preferenze</h2></div>
<ul className="si-list si-list--rows">
<li>
<span>Promemoria automatici di misurazione</span>
<input type="checkbox" checked={promemoria} onChange={(e) => setPromemoria(e.target.checked)} />
</li>
<li>
<span>Invio automatico del report semestrale</span>
<input type="checkbox" checked={reportSemestrale} onChange={(e) => setReportSemestrale(e.target.checked)} />
</li>
</ul>
</section>
</div>
)}
</div>
);
}
+14
View File
@@ -0,0 +1,14 @@
---
// Indicatore sintetico: valore grande, etichetta e variazione con verso.
interface Props { label: string; value: string | number; delta?: string; trend?: 'up' | 'down' }
const { label, value, delta, trend = 'up' } = Astro.props;
---
<div class="si-card">
<p class="si-kpi__label">{label}</p>
<p class="si-kpi__value">{value}</p>
{delta && (
<p class:list={['si-kpi__delta', trend === 'down' ? 'si-kpi__delta--down' : 'si-kpi__delta--up']}>
{trend === 'down' ? '▼' : '▲'} {delta}
</p>
)}
</div>
@@ -0,0 +1,13 @@
---
// Coppie etichetta/valore incolonnate, usate nei riquadri di riepilogo.
interface Props { items: { label: string; value: string | number }[] }
const { items } = Astro.props;
---
<dl class="si-list si-list--defs">
{items.map((i) => (
<div class="si-list__row">
<dt class="si-muted">{i.label}</dt>
<dd class="si-table__num">{i.value}</dd>
</div>
))}
</dl>
@@ -0,0 +1,56 @@
// Andamento medio dello studio, con selettore di periodo. L'aggregazione media stress e
// recupero di tutti i clienti sugli ultimi N rilievi settimanali.
import { useState } from 'react';
import DataChart from './DataChart';
import type { Measurement } from '../../../lib/stress-index/data';
const PERIODS = [
{ key: '30g', label: '30 giorni', points: 5 },
{ key: '90g', label: '90 giorni', points: 13 },
{ key: '6m', label: '6 mesi', points: 20 },
{ key: 'tutto', label: 'Tutto', points: 20 },
] as const;
interface Props { measurementsByClient: Record<string, Measurement[]> }
function aggregate(byClient: Record<string, Measurement[]>, points: number) {
const out: { date: string; stress: number; recovery: number }[] = [];
for (let fromEnd = points; fromEnd >= 1; fromEnd--) {
let stress = 0, recovery = 0, count = 0, date = '';
for (const series of Object.values(byClient)) {
const point = series[series.length - fromEnd];
if (!point) continue;
stress += point.stress;
recovery += point.recovery;
count += 1;
date = point.date;
}
if (count) out.push({ date, stress: Math.round(stress / count), recovery: Math.round(recovery / count) });
}
return out;
}
export default function AndamentoChart({ measurementsByClient }: Props) {
const [period, setPeriod] = useState<string>('90g');
const active = PERIODS.find((p) => p.key === period) ?? PERIODS[1];
return (
<div>
<div className="si-chips">
{PERIODS.map((p) => (
<button key={p.key} type="button" className="si-chip" aria-pressed={p.key === period} onClick={() => setPeriod(p.key)}>
{p.label}
</button>
))}
</div>
<DataChart
data={aggregate(measurementsByClient, active.points)}
xKey="date"
series={[
{ key: 'stress', label: 'Stress medio' },
{ key: 'recovery', label: 'Recupero medio' },
]}
/>
</div>
);
}
@@ -0,0 +1,50 @@
// Grafico generico della piattaforma (isola React: recharts ha bisogno del DOM).
// I colori sono quelli del sito, non quelli del prototipo originale.
import {
ResponsiveContainer, LineChart, BarChart, Line, Bar, CartesianGrid, XAxis, YAxis, Tooltip, Legend,
} from 'recharts';
export interface ChartSeries { key: string; label: string; color?: string }
interface Props {
type?: 'line' | 'bar';
data: Record<string, string | number>[];
xKey: string;
series: ChartSeries[];
height?: number;
}
const COLORS = ['#9c8b70', '#3a2929', '#6f8f6a', '#c08a3e', '#b05a4e'];
const AXIS = '#8a8a8a';
export default function DataChart({ type = 'line', data, xKey, series, height = 260 }: Props) {
if (!data.length) return <p className="si-chart__empty">Nessun dato nel periodo selezionato.</p>;
const Chart = type === 'bar' ? BarChart : LineChart;
return (
<div className="si-chart" style={{ height }}>
<ResponsiveContainer width="100%" height={height}>
<Chart data={data} margin={{ top: 6, right: 8, bottom: 0, left: -18 }}>
<CartesianGrid stroke="#e4dfd7" strokeDasharray="3 3" vertical={false} />
<XAxis dataKey={xKey} stroke={AXIS} fontSize={11} tickLine={false} />
<YAxis stroke={AXIS} fontSize={11} tickLine={false} axisLine={false} />
<Tooltip
contentStyle={{ border: '1px solid #e4dfd7', borderRadius: 0, fontSize: 12 }}
labelStyle={{ color: '#2b2b2b', fontWeight: 600 }}
/>
{series.length > 1 && <Legend iconType="plainline" wrapperStyle={{ fontSize: 12 }} />}
{series.map((s, i) =>
type === 'bar' ? (
<Bar key={s.key} dataKey={s.key} name={s.label} fill={s.color ?? COLORS[i % COLORS.length]} />
) : (
<Line
key={s.key} type="monotone" dataKey={s.key} name={s.label} dot={false} strokeWidth={2.2}
stroke={s.color ?? COLORS[i % COLORS.length]}
/>
)
)}
</Chart>
</ResponsiveContainer>
</div>
);
}