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
+2 -2
View File
@@ -16,7 +16,7 @@ const me = Astro.locals.user!;
<select class="afield" name="role">
<option value="user">user</option>
<option value="superuser">superuser</option>
<option value="campus">campus</option>
<option value="piattaforme">piattaforme</option>
<option value="admin">admin</option>
</select>
<button class="abtn" type="submit">Crea</button>
@@ -32,7 +32,7 @@ const me = Astro.locals.user!;
<td>{u.username}{u.id === me.id && ' (tu)'}</td>
<td>
<select class="role-sel" data-id={u.id}>
{['user', 'superuser', 'campus', 'admin'].map((r) => <option value={r} selected={u.role === r}>{r}</option>)}
{['user', 'superuser', 'piattaforme', 'admin'].map((r) => <option value={r} selected={u.role === r}>{r}</option>)}
</select>
</td>
<td>
+46
View File
@@ -0,0 +1,46 @@
---
// Indice delle piattaforme riservate: è la pagina di atterraggio del ruolo `piattaforme`
// (vedi landingFor in lib/auth). L'accesso è già filtrato dal middleware.
import Base from '../../layouts/Base.astro';
import { piattaforme } from '../../data/piattaforme';
export const prerender = false;
---
<Base title="Piattaforme" description="Le piattaforme riservate InsanityLab.">
<section class="section plat-index">
<div class="container">
<p class="plat-index__eyebrow">Area riservata</p>
<h1>Piattaforme</h1>
<p class="plat-index__lead">Gli strumenti riservati agli utenti abilitati.</p>
<div class="plat-index__grid">
{piattaforme.map((p) => (
<a class="plat-card" href={p.href}>
<h2 class="plat-card__title">{p.label}</h2>
<p class="plat-card__desc">{p.description}</p>
<span class="plat-card__cta">Entra</span>
</a>
))}
</div>
</div>
</section>
</Base>
<style>
.plat-index__eyebrow {
font-family: var(--font-heading); font-size: .72rem; font-weight: 600; letter-spacing: .18em;
text-transform: uppercase; color: var(--c-accent-dark); margin: 0 0 .6em;
}
.plat-index__lead { max-width: 46ch; color: var(--c-text-light); margin-bottom: 46px; }
.plat-index__grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); gap: 24px; }
.plat-card {
display: block; text-decoration: none; background: var(--c-bg-alt); padding: 34px 30px 30px;
border-top: 3px solid var(--c-accent); transition: transform .3s ease, box-shadow .3s ease;
}
.plat-card:hover { transform: translateY(-4px); box-shadow: 0 14px 30px rgba(0,0,0,.08); }
.plat-card__title { font-size: 1.25rem; margin-bottom: .4em; }
.plat-card__desc { color: var(--c-text-light); font-size: .92rem; margin-bottom: 1.4em; }
.plat-card__cta {
font-family: var(--font-heading); font-size: .72rem; font-weight: 600; letter-spacing: .18em;
text-transform: uppercase; color: var(--c-accent-dark);
}
.plat-card__cta::after { content: ' →'; }
</style>
@@ -0,0 +1,62 @@
---
// Analytics di studio: numeri aggregati sul periodo. Il selettore di periodo è ancora
// dimostrativo (i dati sorgente sono un'istantanea sola), come nel prototipo.
import StressIndex from '../../../layouts/StressIndex.astro';
import Card from '../../../components/stress-index/Card.astro';
import Kpi from '../../../components/stress-index/Kpi.astro';
import StatList from '../../../components/stress-index/StatList.astro';
import DataChart from '../../../components/stress-index/charts/DataChart';
import { studioKpis, scoreDistribution, segments, topPerformers, criticalClients } from '../../../lib/stress-index/data';
export const prerender = false;
---
<StressIndex title="Analytics" crumbs={['Analytics']}>
<div class="si-head si-head--row">
<div>
<h1>Analytics</h1>
<p>Andamento dello studio nel periodo selezionato</p>
</div>
<label class="si-field si-field--inline">
<span class="si-visually-hidden">Intervallo</span>
<select>
<option value="7">Ultimi 7 giorni</option>
<option value="30" selected>Ultimi 30 giorni</option>
<option value="90">Ultimi 90 giorni</option>
<option value="365">Ultimo anno</option>
</select>
</label>
</div>
<div class="si-grid si-grid--kpi">
<Kpi label="Clienti attivi" value={studioKpis.totalClients} />
<Kpi label="Misurazioni nel periodo" value={studioKpis.measurementsThisMonth} />
<Kpi label="Alert attivi" value={studioKpis.activeAlerts} />
<Kpi label="Aderenza" value={`${studioKpis.retentionRate}%`} />
</div>
<div class="si-stack si-mt">
<Card title="Distribuzione dello score dei clienti">
<DataChart client:visible type="bar" data={scoreDistribution} xKey="range" series={[{ key: 'count', label: 'Clienti' }]} />
</Card>
<Card title="Confronto fra segmenti">
<div class="si-grid si-grid--halves">
{segments.map((s) => (
<div class="si-segment">
<p class="si-segment__label">{s.label}</p>
<p class="si-kpi__value">{s.avgStress}</p>
<p class="si-muted si-small">stress medio · {s.count} {s.count === 1 ? 'cliente' : 'clienti'}</p>
</div>
))}
</div>
</Card>
<div class="si-grid si-grid--halves">
<Card title="Andamento migliore">
<StatList items={topPerformers.map((c) => ({ label: c.name, value: c.stress }))} />
</Card>
<Card title="Clienti critici">
<StatList items={criticalClients.map((c) => ({ label: c.name, value: c.stress }))} />
</Card>
</div>
</div>
</StressIndex>
@@ -0,0 +1,21 @@
---
// Scheda del singolo cliente. La pagina raccoglie i dati e li passa all'isola React che
// gestisce tab e interazioni.
import StressIndex from '../../../../layouts/StressIndex.astro';
import ClientDetail from '../../../../components/stress-index/ClientDetail';
import { findClient, measurementsByClient, alerts, notes } from '../../../../lib/stress-index/data';
export const prerender = false;
const { id } = Astro.params;
const client = findClient(id ?? '');
if (!client) return Astro.redirect('/piattaforme/stress-index/clienti');
---
<StressIndex title={client.name} crumbs={['Clienti', client.name]}>
<ClientDetail
client:load
client={client}
measurements={measurementsByClient[client.id] ?? []}
alerts={alerts.filter((a) => a.clientId === client.id)}
notes={notes.filter((n) => n.clientId === client.id)}
/>
</StressIndex>
@@ -0,0 +1,89 @@
---
// Elenco clienti (CRM). Il filtro per nome/email è puro JS sulle righe già renderizzate:
// i clienti sono una decina, non vale un'isola React per cercarli.
import StressIndex from '../../../../layouts/StressIndex.astro';
import Card from '../../../../components/stress-index/Card.astro';
import { clients } from '../../../../lib/stress-index/data';
export const prerender = false;
const livello = (stress: number) => (stress >= 70 ? 'bad' : stress >= 45 ? 'warn' : 'ok');
---
<StressIndex title="Clienti" crumbs={['Clienti']}>
<div class="si-head">
<h1>Clienti</h1>
<p>{clients.length} in carico</p>
</div>
<Card>
<div class="si-toolbar">
<label class="si-field si-field--inline">
<span class="si-visually-hidden">Cerca cliente</span>
<input type="search" id="si-cerca" placeholder="Cerca per nome o email" autocomplete="off" />
</label>
<p class="si-muted si-small" id="si-conteggio">{clients.length} risultati</p>
</div>
<table class="si-table" id="si-tabella-clienti">
<thead>
<tr>
<th>Cliente</th><th>Età</th><th>Tag</th><th>Ultima misurazione</th><th>Stress</th><th>Alert</th>
</tr>
</thead>
<tbody>
{clients.map((c) => (
<tr data-cerca={`${c.name} ${c.email}`.toLowerCase()}>
<td>
<a class="si-client" href={`/piattaforme/stress-index/clienti/${c.id}`}>
<span class="si-avatar">{c.initials}</span>
<span>
<span class="si-client__name">{c.name}</span>
<span class="si-client__mail">{c.email}</span>
</span>
</a>
</td>
<td>{c.age} anni</td>
<td>{c.tags[0] && <span class="si-tag">{c.tags[0]}</span>}</td>
<td>{c.lastMeasurement}</td>
<td>
<div class="si-meter">
<span class="si-bar">
<span class:list={['si-bar__fill', `si-bar__fill--${livello(c.stress)}`]} style={`width:${c.stress}%`}></span>
</span>
<span class="si-table__num">{c.stress}</span>
</div>
</td>
<td>
{c.alerts > 0
? <span class:list={['si-badge', c.alerts >= 2 ? 'si-badge--bad' : 'si-badge--warn']}>{c.alerts}</span>
: <span class="si-muted">—</span>}
</td>
</tr>
))}
</tbody>
</table>
<p class="si-muted si-small si-empty" id="si-vuoto" hidden>Nessun cliente corrisponde alla ricerca.</p>
</Card>
</StressIndex>
<script>
// Filtro dell'elenco: nasconde le righe che non contengono il testo cercato e tiene
// aggiornato il conteggio dei risultati.
const input = document.getElementById('si-cerca') as HTMLInputElement | null;
const table = document.getElementById('si-tabella-clienti');
const counter = document.getElementById('si-conteggio');
const empty = document.getElementById('si-vuoto');
if (input && table && counter && empty) {
const rows = Array.from(table.querySelectorAll<HTMLTableRowElement>('tbody tr'));
input.addEventListener('input', () => {
const q = input.value.trim().toLowerCase();
let visible = 0;
for (const row of rows) {
const match = !q || (row.dataset.cerca ?? '').includes(q);
row.hidden = !match;
if (match) visible += 1;
}
counter.textContent = `${visible} ${visible === 1 ? 'risultato' : 'risultati'}`;
empty.hidden = visible > 0;
});
}
</script>
@@ -0,0 +1,121 @@
---
// Impostazioni: profilo, notifiche, account. I campi sono ancora un modulo dimostrativo —
// non c'è un endpoint di salvataggio — ma la struttura è quella definitiva.
import StressIndex from '../../../layouts/StressIndex.astro';
import Card from '../../../components/stress-index/Card.astro';
import { professional } from '../../../lib/stress-index/data';
export const prerender = false;
const [nome = '', cognome = ''] = professional.name.replace(/^Dr\.?\s*/i, '').split(' ');
const GIORNI = ['Lunedì', 'Martedì', 'Mercoledì', 'Giovedì', 'Venerdì', 'Sabato', 'Domenica'];
const ORE = ['07:00', '08:00', '09:00', '18:00', '19:00', '20:00'];
const TABS = [
{ key: 'profilo', label: 'Profilo' },
{ key: 'notifiche', label: 'Notifiche' },
{ key: 'account', label: 'Account' },
];
---
<StressIndex title="Impostazioni" crumbs={['Impostazioni']}>
<div class="si-head">
<h1>Impostazioni</h1>
<p>Profilo, notifiche e account</p>
</div>
<div class="si-tabs" role="tablist" id="si-set-tabs">
{TABS.map((t, i) => (
<button type="button" role="tab" class="si-tabs__btn" data-tab={t.key} aria-selected={i === 0 ? 'true' : 'false'}>{t.label}</button>
))}
</div>
<section data-panel="profilo" class="si-stack">
<Card title="Dati professionista">
<div class="si-form-grid">
<label class="si-field">Titolo<input type="text" value="Dr." /></label>
<label class="si-field">Professione<input type="text" value={professional.role} /></label>
<label class="si-field">Nome<input type="text" value={nome} /></label>
<label class="si-field">Cognome<input type="text" value={cognome} /></label>
<label class="si-field">Specializzazione<input type="text" value={professional.role} /></label>
</div>
</Card>
<Card title="Studio">
<div class="si-form-grid">
<label class="si-field">Nome studio<input type="text" value={professional.studio} /></label>
<label class="si-field">Indirizzo<input type="text" placeholder="Via Roma 1, Bologna" /></label>
<label class="si-field">Telefono<input type="tel" placeholder="+39 051 123456" /></label>
<label class="si-field">Sito web<input type="url" placeholder="https://studio.it" /></label>
</div>
<p class="si-mt"><button type="button" class="si-btn si-btn--solid">Salva</button></p>
</Card>
</section>
<section data-panel="notifiche" hidden>
<Card title="Preferenze di notifica">
<ul class="si-list si-list--rows si-prefs">
<li>
<span>
<strong>Riassunto settimanale via email</strong>
<span class="si-muted si-small">Un riepilogo periodico dell'andamento dei clienti</span>
</span>
<input type="checkbox" checked />
</li>
<li>
<span>
<strong>Alert immediati</strong>
<span class="si-muted si-small">Email non appena viene generato un alert cliente</span>
</span>
<input type="checkbox" checked />
</li>
<li>
<span>
<strong>Novità di prodotto</strong>
<span class="si-muted si-small">Aggiornamenti sulle nuove funzionalità</span>
</span>
<input type="checkbox" />
</li>
</ul>
<div class="si-form-grid si-mt">
<label class="si-field">Giorno dell'invio
<select>{GIORNI.map((g) => <option>{g}</option>)}</select>
</label>
<label class="si-field">Ora dell'invio
<select>{ORE.map((o) => <option>{o}</option>)}</select>
</label>
</div>
<p class="si-mt"><button type="button" class="si-btn si-btn--solid">Salva preferenze</button></p>
</Card>
</section>
<section data-panel="account" hidden class="si-stack">
<Card title="Accesso">
<label class="si-field">Email<input type="email" value={professional.email} readonly /></label>
<p class="si-muted si-small si-mt">Per cambiare l'indirizzo email serve una richiesta al supporto.</p>
<div class="si-form-grid si-mt">
<label class="si-field">Password attuale<input type="password" autocomplete="current-password" /></label>
<label class="si-field">Nuova password<input type="password" autocomplete="new-password" /></label>
<label class="si-field">Conferma nuova password<input type="password" autocomplete="new-password" /></label>
</div>
<p class="si-mt"><button type="button" class="si-btn si-btn--solid">Aggiorna password</button></p>
</Card>
<Card title="Abbonamento">
<p class="si-muted si-small">Per il piano e la fatturazione fa riferimento al supporto Stress Index.</p>
</Card>
<p><a class="si-btn" href="/admin/logout">Esci dall'account</a></p>
</section>
</StressIndex>
<script>
const tabs = document.getElementById('si-set-tabs');
if (tabs) {
const buttons = Array.from(tabs.querySelectorAll<HTMLButtonElement>('.si-tabs__btn'));
const panels = Array.from(document.querySelectorAll<HTMLElement>('[data-panel]'));
for (const btn of buttons) {
btn.addEventListener('click', () => {
for (const b of buttons) b.setAttribute('aria-selected', String(b === btn));
for (const p of panels) p.hidden = p.dataset.panel !== btn.dataset.tab;
});
}
}
</script>
@@ -0,0 +1,88 @@
---
// Dashboard "Oggi": ciò che il professionista deve guardare per primo — alert, clienti da
// ricontattare, note recenti. L'accesso è già filtrato dal middleware (ruolo piattaforme/admin).
import StressIndex from '../../../layouts/StressIndex.astro';
import Card from '../../../components/stress-index/Card.astro';
import Kpi from '../../../components/stress-index/Kpi.astro';
import AlertRow from '../../../components/stress-index/AlertRow.astro';
import AndamentoChart from '../../../components/stress-index/charts/AndamentoChart';
import {
alerts, clients, measurementsByClient, notes, studioKpis, professional, OGGI, daysSince,
} from '../../../lib/stress-index/data';
export const prerender = false;
const prioritari = alerts.slice(0, 4);
const misurazioniOggi = Object.values(measurementsByClient).flat().filter((m) => m.date === OGGI).length;
const daContattare = [...clients]
.sort((a, b) => daysSince(b.lastMeasurement) - daysSince(a.lastMeasurement))
.slice(0, 5);
const ultimeNote = [...notes]
.sort((a, b) => (a.when < b.when ? 1 : -1))
.slice(0, 4)
.map((n) => ({ ...n, clientName: clients.find((c) => c.id === n.clientId)?.name }));
---
<StressIndex title="Oggi">
<div class="si-head">
<h1>Buongiorno, {professional.name.replace(/^Dr\.\s*/, '')}</h1>
<p>Oggi, {OGGI}</p>
</div>
<div class="si-grid si-grid--kpi">
<Kpi label="Clienti attivi" value={studioKpis.totalClients} delta="+2 questo mese" trend="up" />
<Kpi
label="Misurazioni oggi"
value={misurazioniOggi}
delta={misurazioniOggi === 0 ? 'Nessuna ancora' : 'In aggiornamento'}
trend={misurazioniOggi === 0 ? 'down' : 'up'}
/>
<Kpi label="Alert attivi" value={studioKpis.activeAlerts} delta="+3 questa settimana" trend="down" />
<Kpi label="Stress medio" value={studioKpis.avgStress} delta="-4% vs mese scorso" trend="up" />
</div>
<div class="si-grid si-grid--2 si-mt">
<div class="si-stack">
<Card title="Andamento medio aggregato">
<AndamentoChart client:load measurementsByClient={measurementsByClient} />
</Card>
<Card title="Alert prioritari" actionLabel="Vedi tutti" actionHref="/piattaforme/stress-index/clienti">
{prioritari.map((a) => <AlertRow alert={a} />)}
</Card>
<Card title="Da contattare">
<ul class="si-list si-list--rows">
{daContattare.map((c) => (
<li>
<span>
<strong>{c.name}</strong>
<span class="si-muted"> · ultima misurazione {daysSince(c.lastMeasurement)} giorni fa</span>
</span>
<a class="si-btn" href={`/piattaforme/stress-index/clienti/${c.id}`}>Scheda</a>
</li>
))}
</ul>
</Card>
</div>
<div class="si-stack">
<Card title="Misurazioni di oggi">
{misurazioniOggi === 0 ? (
<p class="si-muted si-small">Nessuna misurazione registrata oggi. Le rilevazioni dei clienti compaiono qui appena arrivano.</p>
) : (
<p class="si-kpi__value">{misurazioniOggi}</p>
)}
</Card>
<Card title="Ultime note">
<ul class="si-list">
{ultimeNote.map((n) => (
<li class="si-note">
<p>{n.text}</p>
<p class="si-note__meta">{n.clientName ?? 'Nota generale'} · {n.when}</p>
</li>
))}
</ul>
</Card>
</div>
</div>
</StressIndex>
@@ -0,0 +1,33 @@
---
// Organizzazione: raggruppa più professionisti sotto un'unica struttura. Finché non c'è
// il backend, il modulo raccoglie solo il nome e il pulsante resta disabilitato a vuoto.
import StressIndex from '../../../layouts/StressIndex.astro';
export const prerender = false;
---
<StressIndex title="Organizzazione" crumbs={['Organizzazione']}>
<div class="si-center">
<div class="si-card si-org">
<h1>Crea la tua organizzazione</h1>
<p class="si-muted si-small">
Raggruppa più professionisti sotto un'unica organizzazione per condividere clienti,
dati e impostazioni dello studio.
</p>
<label class="si-field si-mt">
Nome organizzazione
<input type="text" id="si-org-nome" placeholder="Studio Bianchi Associati" />
</label>
<p class="si-mt">
<button type="button" class="si-btn si-btn--solid si-org__cta" id="si-org-crea" disabled>Crea organizzazione</button>
</p>
</div>
</div>
</StressIndex>
<script>
// Il pulsante si sblocca solo con un nome valorizzato.
const input = document.getElementById('si-org-nome') as HTMLInputElement | null;
const btn = document.getElementById('si-org-crea') as HTMLButtonElement | null;
if (input && btn) {
input.addEventListener('input', () => { btn.disabled = input.value.trim().length === 0; });
}
</script>
@@ -0,0 +1,121 @@
---
// Modulo Sport: carico di lavoro e sessioni degli atleti seguiti. Le tre viste sono già
// tutte renderizzate: il cambio scheda è solo un mostra/nascondi, non serve un'isola.
import StressIndex from '../../../../layouts/StressIndex.astro';
import Card from '../../../../components/stress-index/Card.astro';
import Kpi from '../../../../components/stress-index/Kpi.astro';
import DataChart from '../../../../components/stress-index/charts/DataChart';
import { sessions, athletes, sessionsPerWeek } from '../../../../lib/stress-index/data';
export const prerender = false;
const trimpMedio = Math.round(sessions.reduce((s, x) => s + x.trimp, 0) / sessions.length);
const sessioniSettimana = sessionsPerWeek[sessionsPerWeek.length - 1]?.count ?? 0;
const nomeAtleta = (id: string) => athletes.find((a) => a.clientId === id)?.name ?? id;
const TABS = [
{ key: 'dashboard', label: 'Dashboard' },
{ key: 'sessioni', label: 'Sessioni' },
{ key: 'atleti', label: 'Atleti' },
];
---
<StressIndex title="Sport" crumbs={['Sport']}>
<div class="si-head">
<h1>Sport</h1>
<p>Sessioni, carico e monitoraggio degli atleti</p>
</div>
<div class="si-tabs" role="tablist" id="si-sport-tabs">
{TABS.map((t, i) => (
<button type="button" role="tab" class="si-tabs__btn" data-tab={t.key} aria-selected={i === 0 ? 'true' : 'false'}>{t.label}</button>
))}
</div>
<section data-panel="dashboard" class="si-stack">
<div class="si-grid si-grid--kpi">
<Kpi label="Sessioni totali" value={sessions.length} />
<Kpi label="Sessioni questa settimana" value={sessioniSettimana} />
<Kpi label="TRIMP medio" value={trimpMedio} />
<Kpi label="Atleti attivi" value={athletes.length} />
</div>
<Card title="Sessioni per settimana">
<DataChart client:visible type="bar" data={sessionsPerWeek} xKey="week" series={[{ key: 'count', label: 'Sessioni' }]} />
</Card>
<Card title="Ultime sessioni">
<div class="si-scroll-x">
<table class="si-table">
<thead><tr><th>Atleta</th><th>Data</th><th>Sport</th><th>Durata</th><th>TRIMP</th><th>HR medio</th><th>DFA A1</th></tr></thead>
<tbody>
{sessions.map((s) => (
<tr>
<td><strong>{nomeAtleta(s.athleteId)}</strong></td>
<td class="si-table__num">{s.date}</td>
<td>{s.type}</td>
<td class="si-table__num">{s.durationMin} min</td>
<td class="si-table__num">{s.trimp}</td>
<td class="si-table__num">{s.hrAvg}</td>
<td class="si-table__num">{s.dfaA1}</td>
</tr>
))}
</tbody>
</table>
</div>
</Card>
</section>
<section data-panel="sessioni" hidden>
<Card title="Tutte le sessioni">
<div class="si-scroll-x">
<table class="si-table">
<thead><tr><th>Atleta</th><th>Data</th><th>Sport</th><th>Durata</th><th>Carico</th><th>TRIMP</th><th>HR medio</th><th>DFA A1</th></tr></thead>
<tbody>
{sessions.map((s) => (
<tr>
<td><strong>{nomeAtleta(s.athleteId)}</strong></td>
<td class="si-table__num">{s.date}</td>
<td>{s.type}</td>
<td class="si-table__num">{s.durationMin} min</td>
<td class="si-table__num">{s.load}</td>
<td class="si-table__num">{s.trimp}</td>
<td class="si-table__num">{s.hrAvg}</td>
<td class="si-table__num">{s.dfaA1}</td>
</tr>
))}
</tbody>
</table>
</div>
</Card>
</section>
<section data-panel="atleti" hidden>
<Card title="Atleti monitorati">
<table class="si-table">
<thead><tr><th>Atleta</th><th>Sport</th><th>Carico settimanale</th></tr></thead>
<tbody>
{athletes.map((a) => (
<tr>
<td><a href={`/piattaforme/stress-index/clienti/${a.clientId}`}>{a.name}</a></td>
<td>{a.sport}</td>
<td class="si-table__num">{a.weeklyLoad}</td>
</tr>
))}
</tbody>
</table>
</Card>
</section>
</StressIndex>
<script>
// Schede: un solo pannello visibile alla volta, selezione riflessa su aria-selected.
const tabs = document.getElementById('si-sport-tabs');
if (tabs) {
const buttons = Array.from(tabs.querySelectorAll<HTMLButtonElement>('.si-tabs__btn'));
const panels = Array.from(document.querySelectorAll<HTMLElement>('[data-panel]'));
for (const btn of buttons) {
btn.addEventListener('click', () => {
for (const b of buttons) b.setAttribute('aria-selected', String(b === btn));
for (const p of panels) p.hidden = p.dataset.panel !== btn.dataset.tab;
});
}
}
</script>
@@ -0,0 +1,43 @@
---
// Monitoraggio live: finché non arriva la sorgente dati in tempo reale la vista resta
// vuota per costruzione, e spiega cosa succederà quando un atleta avvia una sessione.
import StressIndex from '../../../../layouts/StressIndex.astro';
export const prerender = false;
const PASSI = [
"L'atleta avvia una sessione dall'app Stress Index collegata alla fascia cardio.",
'I dati di frequenza cardiaca e DFA A1 arrivano in tempo reale a questa vista.',
"La scheda dell'atleta compare qui da sola, con le metriche aggiornate in continuo.",
'A fine sessione la scheda si chiude e il riepilogo confluisce nello storico Sport.',
];
---
<StressIndex title="Team live" crumbs={['Sport', 'Team live']}>
<div class="si-head si-head--row">
<div>
<h1>Team live</h1>
<p>Nessun atleta in sessione</p>
</div>
<label class="si-field si-field--inline">
<span class="si-visually-hidden">Ordina per</span>
<select>
<option value="nome">Ordina per nome</option>
<option value="hr">Ordina per HR corrente</option>
<option value="carico">Ordina per carico sessione</option>
</select>
</label>
</div>
<div class="si-card si-live">
<p class="si-live__title">Nessun atleta in sessione</p>
<p class="si-muted si-small">Quando un atleta avvia una sessione dall'app, la sua scheda compare qui in tempo reale.</p>
<div class="si-live__how">
<p class="si-card__title">Come funziona</p>
<ol class="si-steps">
{PASSI.map((p, i) => (
<li><span class="si-steps__num">{i + 1}</span><span>{p}</span></li>
))}
</ol>
</div>
</div>
</StressIndex>