Aggancia funzioni di libreria e route push alla rosa reale

obiettivi.ts, palloni-core.ts, presenze-mese.ts e user-store.ts non
dipendono più dalla lista statica di crapp-data.ts ma ricevono/leggono la
rosa reale (percentuali obiettivo, rotazione turno palloni, riepilogo
presenze mensile, identità del dispositivo). Le route API per le notifiche
push (push-messaggio, promemoria-palloni, sollecita-presenze) leggono la
squadra lato server con leggiGiocatoriSquadra(), filtrando attivo.
This commit is contained in:
2026-09-03 10:16:52 +02:00
parent 7e93229eee
commit b6c0f40f66
8 changed files with 51 additions and 25 deletions
+8 -8
View File
@@ -28,12 +28,12 @@ export const contestoVuoto: ContestoObiettivi = { eventi: [], presenze: {}, page
const MESE = "2026-08";
function percentualePresenzeMese(ctx: ContestoObiettivi) {
function percentualePresenzeMese(ctx: ContestoObiettivi, rosaSize: number) {
const delMese = ctx.eventi.filter(
(e) => e.data.startsWith(MESE) && (e.tipo === "partita" || e.tipo === "allenamento"),
);
if (delMese.length === 0) return 0;
const posti = delMese.length * giocatori.length;
if (delMese.length === 0 || rosaSize === 0) return 0;
const posti = delMese.length * rosaSize;
const presenti = delMese.reduce((s, e) => {
const risposte = ctx.presenze[e.id] ?? {};
return s + Object.values(risposte).filter((x) => x === "presente" || x === "ritardo").length;
@@ -41,10 +41,10 @@ function percentualePresenzeMese(ctx: ContestoObiettivi) {
return Math.round((presenti / posti) * 100);
}
function percentualeRisposte(ctx: ContestoObiettivi) {
function percentualeRisposte(ctx: ContestoObiettivi, rosaSize: number) {
const daRispondere = ctx.eventi.filter((e) => e.tipo !== "compleanno");
if (daRispondere.length === 0) return 0;
const posti = daRispondere.length * giocatori.length;
if (daRispondere.length === 0 || rosaSize === 0) return 0;
const posti = daRispondere.length * rosaSize;
const risposte = daRispondere.reduce(
(s, e) => s + Object.keys(ctx.presenze[e.id] ?? {}).length,
0,
@@ -65,7 +65,7 @@ export function obiettiviSquadra(
id: "o1",
titolo: "90% di presenze ad agosto",
descrizione: "Media presenze su partite e allenamenti del mese",
valore: percentualePresenzeMese(ctx),
valore: percentualePresenzeMese(ctx, rosa.length),
target: 90,
unita: "%",
scadenza: "2026-08-31",
@@ -76,7 +76,7 @@ export function obiettiviSquadra(
id: "o2",
titolo: "Tutti rispondono alle convocazioni",
descrizione: "Percentuale di risposte date sugli eventi in programma",
valore: percentualeRisposte(ctx),
valore: percentualeRisposte(ctx, rosa.length),
target: 90,
unita: "%",
scadenza: "2026-09-30",
+8 -4
View File
@@ -1,8 +1,11 @@
import { formatData, giocatori } from "./crapp-data";
import { formatData } from "./crapp-data";
import type { Evento } from "./eventi";
export type Turno = { evento_id: string; giocatore_id: string; aggiornato_da: string | null };
/** Candidato al turno palloni: solo id e nome bastano per assegnare e ordinare. */
export type CandidatoTurno = { id: string; nome: string };
/** Eventi che richiedono i palloni (allenamenti, partite, extra), in ordine di data. */
export function eventiPalloni(eventi: Evento[]): Evento[] {
return eventi
@@ -18,10 +21,11 @@ export function eventiPalloni(eventi: Evento[]): Evento[] {
export function completaTurni(
turni: Record<string, string>,
eventi: Evento[],
rosa: CandidatoTurno[],
): Record<string, string> {
const risultato: Record<string, string> = { ...turni };
const conteggio = new Map<string, number>(giocatori.map((g) => [g.id, 0]));
const ultimo = new Map<string, number>(giocatori.map((g) => [g.id, -1]));
const conteggio = new Map<string, number>(rosa.map((g) => [g.id, 0]));
const ultimo = new Map<string, number>(rosa.map((g) => [g.id, -1]));
eventiPalloni(eventi).forEach((evento, indice) => {
const assegnato = risultato[evento.id];
@@ -32,7 +36,7 @@ export function completaTurni(
}
if (assegnato) return;
const scelto = giocatori.slice().sort((a, b) => {
const scelto = rosa.slice().sort((a, b) => {
const ca = conteggio.get(a.id) ?? 0;
const cb = conteggio.get(b.id) ?? 0;
if (ca !== cb) return ca - cb;
+4 -1
View File
@@ -2,6 +2,7 @@ import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { supabase } from "@/integrations/supabase/client";
import { completaTurni } from "./palloni-core";
import { useEventi } from "./eventi";
import { nomeCompleto, useGiocatoriSquadra } from "./giocatori-squadra";
export const TURNI_KEY = ["turni-palloni"] as const;
@@ -26,8 +27,10 @@ export function useTurniPalloni() {
// Cambia raramente: una lettura per sessione è sufficiente.
const query = useQuery({ queryKey: TURNI_KEY, queryFn: fetchTurni, staleTime: 30 * 60_000 });
const { eventi } = useEventi();
const { righe: squadra } = useGiocatoriSquadra();
const salvati = query.data ?? {};
return { ...query, salvati, turni: completaTurni(salvati, eventi) };
const rosa = squadra.filter((g) => g.attivo).map((g) => ({ id: g.id, nome: nomeCompleto(g) }));
return { ...query, salvati, turni: completaTurni(salvati, eventi, rosa) };
}
export function useAssegnaTurno() {
+5 -3
View File
@@ -1,7 +1,7 @@
import { useMemo } from "react";
import { useEventi } from "./eventi";
import { useRispostePresenze } from "./presenze";
import { giocatori } from "./crapp-data";
import { useGiocatoriSquadra } from "./giocatori-squadra";
/**
* Percentuale di presenze dell'ultimo mese (30 giorni), utile per le convocazioni.
@@ -46,6 +46,7 @@ export function usePresenzeUltimoMeseTutti(): Record<
> {
const { eventi } = useEventi();
const { presenze } = useRispostePresenze();
const { righe: squadra } = useGiocatoriSquadra();
return useMemo(() => {
const oggi = new Date();
@@ -53,6 +54,7 @@ export function usePresenzeUltimoMeseTutti(): Record<
inizio.setDate(inizio.getDate() - 30);
const da = inizio.toISOString().slice(0, 10);
const a = oggi.toISOString().slice(0, 10);
const idRosa = squadra.filter((g) => g.attivo).map((g) => g.id);
const rilevanti = eventi.filter(
(e) => (e.tipo === "partita" || e.tipo === "allenamento") && e.data >= da && e.data <= a,
@@ -60,7 +62,7 @@ export function usePresenzeUltimoMeseTutti(): Record<
const out: Record<string, { presenti: number; totali: number; percentuale: number }> = {};
for (const e of rilevanti) {
const ids = e.convocati.length > 0 ? e.convocati : giocatori.map((g) => g.id);
const ids = e.convocati.length > 0 ? e.convocati : idRosa;
for (const id of ids) {
const rec = (out[id] ??= { presenti: 0, totali: 0, percentuale: 0 });
rec.totali += 1;
@@ -72,5 +74,5 @@ export function usePresenzeUltimoMeseTutti(): Record<
rec.percentuale = rec.totali ? Math.round((rec.presenti / rec.totali) * 100) : 0;
}
return out;
}, [eventi, presenze]);
}, [eventi, presenze, squadra]);
}
+5 -3
View File
@@ -1,5 +1,6 @@
import { useSyncExternalStore } from "react";
import { giocatori, type Giocatore } from "./crapp-data";
import type { Giocatore } from "./crapp-data";
import { useGiocatoriSquadra, type GiocatoreSquadra } from "./giocatori-squadra";
import { useIo } from "./rosa";
const KEY = "crapp-user-v1";
@@ -49,9 +50,10 @@ export function useGiocatoreId(): string | null {
}
/** Anagrafica base dal localStorage: le statistiche restano a zero finché non passa da `useIo`. */
export function useGiocatoreBase(): Giocatore | null {
export function useGiocatoreBase(): GiocatoreSquadra | null {
const id = useGiocatoreId();
return id ? (giocatori.find((x) => x.id === id) ?? null) : null;
const { righe } = useGiocatoriSquadra();
return id ? (righe.find((x) => x.id === id) ?? null) : null;
}
/** Giocatore corrente con statistiche calcolate da dati reali (presenze, MVP, badge, …). */
+7 -1
View File
@@ -1,4 +1,6 @@
import { createFileRoute } from "@tanstack/react-router";
import { nomeCompleto } from "@/lib/giocatori-squadra";
import { leggiGiocatoriSquadra } from "@/lib/giocatori-squadra.server";
import { completaTurni, destinatariPromemoriaPalloni, oggiISO } from "@/lib/palloni-core";
import { inviaPush } from "@/lib/webpush.server";
import { leggiEventi } from "@/lib/eventi.server";
@@ -15,7 +17,11 @@ export const Route = createFileRoute("/api/public/promemoria-palloni")({
const salvati: Record<string, string> = {};
for (const riga of righe ?? []) salvati[riga.evento_id] = riga.giocatore_id;
const eventi = await leggiEventi();
const turni = completaTurni(salvati, eventi);
const squadra = await leggiGiocatoriSquadra();
const rosa = squadra
.filter((g) => g.attivo)
.map((g) => ({ id: g.id, nome: nomeCompleto(g) }));
const turni = completaTurni(salvati, eventi, rosa);
const oggi = oggiISO();
const destinatari = destinatariPromemoriaPalloni(turni, eventi, oggi);
+9 -3
View File
@@ -1,6 +1,7 @@
import { createFileRoute } from "@tanstack/react-router";
import { z } from "zod";
import { giocatori } from "@/lib/crapp-data";
import { nomeCompleto } from "@/lib/giocatori-squadra";
import { leggiGiocatoriSquadra } from "@/lib/giocatori-squadra.server";
import { completaTurni, messaggioPalloniOggi, oggiISO } from "@/lib/palloni-core";
import { leggiEventi } from "@/lib/eventi.server";
@@ -43,11 +44,16 @@ export const Route = createFileRoute("/api/public/push-messaggio")({
const salvati: Record<string, string> = {};
for (const riga of righe ?? []) salvati[riga.evento_id] = riga.giocatore_id;
const eventi = await leggiEventi();
const turni = completaTurni(salvati, eventi);
const squadra = await leggiGiocatoriSquadra();
const rosa = squadra
.filter((g) => g.attivo)
.map((g) => ({ id: g.id, nome: nomeCompleto(g) }));
const turni = completaTurni(salvati, eventi, rosa);
const oggi = oggiISO();
const mioId = iscrizione.giocatore_id;
const nome = giocatori.find((g) => g.id === mioId)?.nome ?? "";
const giocatore = squadra.find((g) => g.id === mioId);
const nome = giocatore ? nomeCompleto(giocatore) : "";
return Response.json(messaggioPalloniOggi(turni, eventi, oggi, mioId, nome));
},
+5 -2
View File
@@ -1,7 +1,8 @@
import { createFileRoute } from "@tanstack/react-router";
import { z } from "zod";
import { formatData, giocatori } from "@/lib/crapp-data";
import { formatData } from "@/lib/crapp-data";
import { leggiEventi } from "@/lib/eventi.server";
import { leggiGiocatoriSquadra } from "@/lib/giocatori-squadra.server";
import { inviaPush } from "@/lib/webpush.server";
const schema = z.object({
@@ -27,8 +28,10 @@ export const Route = createFileRoute("/api/public/sollecita-presenze")({
.select("giocatore_id, stato")
.eq("evento_id", evento.id);
const squadra = await leggiGiocatoriSquadra();
const stati = new Map((righe ?? []).map((r) => [r.giocatore_id, r.stato]));
const destinatari = giocatori
const destinatari = squadra
.filter((g) => g.attivo)
.filter((g) => {
const stato = stati.get(g.id);
return stato === undefined || stato === "forse";