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:
@@ -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",
|
||||
|
||||
@@ -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
@@ -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() {
|
||||
|
||||
@@ -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]);
|
||||
}
|
||||
|
||||
@@ -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, …). */
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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));
|
||||
},
|
||||
|
||||
@@ -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";
|
||||
|
||||
Reference in New Issue
Block a user