Obiettivi cliccabili con card di dettaglio; presenze del mese sui convocati

Ogni obiettivo di squadra apre un drawer che spiega come si calcola, cosa conta,
cosa è una presenza persa, cosa resta fuori, periodo, fonte dei dati ed esempio.
«90% di presenze del mese» usa ora i convocati dell'evento come posti (tutta la
rosa se non ce ne sono), come le presenze collettive: chi non è convocato non
abbassa più la percentuale. Test unit, card e integrazione; docs aggiornate.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
davideandClaude Sonnet 5.5 committed 2026-10-06 11:34:58 +02:00
1 parent 6b8cc33512
commit c2c3b249f2
10 files changed
+779 -73

No files matched your search

+27 -14
View File
@@ -9,6 +9,7 @@ import { CompletaProfilo } from "@/components/crapp/ProfiloAmministrativo";
import { Reveal } from "@/components/motion/Reveal";
import { Barra } from "@/components/motion/Barra";
import { Numero } from "@/components/motion/Numero";
import { ObiettivoDrawer } from "@/components/crapp/ObiettivoDrawer";
import { microcopyObiettivo, progressoObiettivo } from "@/lib/obiettivi";
import { useEventi, type Evento } from "@/lib/eventi";
import { useRispostePresenze } from "@/lib/presenze";
@@ -237,20 +238,32 @@ function Index() {
}
>
{obiettivo ? (
<Card>
<div className="flex items-center gap-2 text-sm font-bold">
<span className="text-base leading-none">{obiettivo.emoji}</span> {obiettivo.titolo}
</div>
<Barra percentuale={progressoObiettivo(obiettivo)} trackClassName="mt-3" />
<p className="mt-2 text-xs text-muted-foreground">
Siamo al <Numero valore={progressoObiettivo(obiettivo)} suffisso="%" /> —{" "}
{obiettivo.valore}/{obiettivo.target} {obiettivo.unita}.
</p>
<p className="mt-1 text-xs font-semibold text-accent">
{microcopyObiettivo(obiettivo)}
</p>
<p className="mt-1 text-xs text-muted-foreground">{obiettivo.impatto}</p>
</Card>
<ObiettivoDrawer obiettivo={obiettivo}>
<button
type="button"
aria-label={`Dettaglio obiettivo: ${obiettivo.titolo}`}
className="block w-full text-left"
>
<Card>
<div className="flex items-center gap-2 text-sm font-bold">
<span className="text-base leading-none">{obiettivo.emoji}</span>{" "}
{obiettivo.titolo}
</div>
<Barra percentuale={progressoObiettivo(obiettivo)} trackClassName="mt-3" />
<p className="mt-2 text-xs text-muted-foreground">
Siamo al <Numero valore={progressoObiettivo(obiettivo)} suffisso="%" /> —{" "}
{obiettivo.valore}/{obiettivo.target} {obiettivo.unita}.
</p>
<p className="mt-1 text-xs font-semibold text-accent">
{microcopyObiettivo(obiettivo)}
</p>
<p className="mt-1 text-xs text-muted-foreground">{obiettivo.impatto}</p>
<p className="mt-2 inline-flex items-center text-xs font-semibold text-accent">
Come funziona <ChevronRight className="h-3.5 w-3.5" />
</p>
</Card>
</button>
</ObiettivoDrawer>
) : null}
</Section>
)}
+40 -30
View File
@@ -1,11 +1,12 @@
import { useMemo, useState } from "react";
import { createFileRoute } from "@tanstack/react-router";
import { Cake, ChevronDown, Crown, Trophy } from "lucide-react";
import { Cake, ChevronDown, ChevronRight, Crown, Trophy } from "lucide-react";
import { cn } from "@/lib/utils";
import { PageHeader, StatTile } from "@/components/crapp/ui-bits";
import { BarraSottosezioni } from "@/components/crapp/BarraSottosezioni";
import { Avatar } from "@/components/crapp/Avatar";
import { formatData, inizialiDa } from "@/lib/crapp-data";
import { ObiettivoDrawer } from "@/components/crapp/ObiettivoDrawer";
import { microcopyObiettivo, progressoObiettivo } from "@/lib/obiettivi";
import {
useRosa,
@@ -432,38 +433,47 @@ function Squadra() {
const pct = progressoObiettivo(o);
const fatto = pct >= 100;
return (
<Reveal
key={o.id}
indice={i}
className={cn(
"premi rounded-3xl bg-card p-4 shadow-card ring-1",
fatto ? "ring-success/40" : pct >= 90 ? "ring-accent/40" : "ring-transparent",
)}
>
<div className="flex items-start gap-2">
<span className="text-lg leading-none">{o.emoji}</span>
<div className="min-w-0 flex-1">
<p className="text-sm font-bold leading-tight">{o.titolo}</p>
<p className="text-xs text-muted-foreground">{o.descrizione}</p>
</div>
<span
<Reveal key={o.id} indice={i}>
<ObiettivoDrawer obiettivo={o}>
<button
type="button"
aria-label={`Dettaglio obiettivo: ${o.titolo}`}
className={cn(
"rounded-full px-2 py-0.5 text-xs font-bold uppercase tracking-wide",
fatto
? "bg-success text-success-foreground"
: "bg-secondary text-muted-foreground",
"premi block w-full rounded-3xl bg-card p-4 text-left shadow-card ring-1",
fatto ? "ring-success/40" : pct >= 90 ? "ring-accent/40" : "ring-transparent",
)}
>
{fatto ? "Completato" : `${pct}%`}
</span>
</div>
<Barra percentuale={pct} trackClassName="mt-3" />
<p className="mt-2 text-xs text-muted-foreground">
{o.valore}/{o.target} {o.unita} · {pct}%
{o.scadenza ? ` · entro il ${formatData(o.scadenza)}` : ""}
</p>
<p className="mt-1 text-xs font-semibold text-accent">{microcopyObiettivo(o)}</p>
<p className="mt-0.5 text-xs text-muted-foreground">{o.impatto}</p>
<div className="flex items-start gap-2">
<span className="text-lg leading-none">{o.emoji}</span>
<div className="min-w-0 flex-1">
<p className="text-sm font-bold leading-tight">{o.titolo}</p>
<p className="text-xs text-muted-foreground">{o.descrizione}</p>
</div>
<span
className={cn(
"rounded-full px-2 py-0.5 text-xs font-bold uppercase tracking-wide",
fatto
? "bg-success text-success-foreground"
: "bg-secondary text-muted-foreground",
)}
>
{fatto ? "Completato" : `${pct}%`}
</span>
</div>
<Barra percentuale={pct} trackClassName="mt-3" />
<p className="mt-2 text-xs text-muted-foreground">
{o.valore}/{o.target} {o.unita} · {pct}%
{o.scadenza ? ` · entro il ${formatData(o.scadenza)}` : ""}
</p>
<p className="mt-1 text-xs font-semibold text-accent">
{microcopyObiettivo(o)}
</p>
<p className="mt-0.5 text-xs text-muted-foreground">{o.impatto}</p>
<p className="mt-2 inline-flex items-center text-xs font-semibold text-accent">
Come funziona <ChevronRight className="h-3.5 w-3.5" />
</p>
</button>
</ObiettivoDrawer>
</Reveal>
);
})}