Calcola davvero le serie di presenze e sblocca badge e obiettivo.
I tre contatori (serieAllenamenti, seriePartite, serieConferme) e streak erano zero fisso in useRosa(): la sezione Serie di presenze del profilo mostrava sempre progresso nullo, i badge legati alla costanza erano impossibili da sbloccare e l'obiettivo Continuita di squadra restava a 0/12. serieConsecutiva() e serieConferme() (src/lib/presenze.ts) derivano le serie dagli eventi passati e da risposte_presenze gia in cache, senza query aggiuntive. Migration m9: nuova colonna risposto_il con l'istante della prima risposta, resa immutabile da un trigger, confrontata con eventi_app.creato_il per le conferme entro 24 ore. Serviva perche aggiornato_il registra l'ultima modifica, quindi chi rispondeva subito e cambiava idea dopo risultava lento. Corregge anche la barra di progresso, che misurava valore/prossimo e tornava indietro a ogni traguardo raggiunto (2/3 = 67%, poi 3/6 = 50%). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
+6
-1
@@ -20,6 +20,9 @@ export type Evento = {
|
||||
casa: boolean;
|
||||
/** Le pagelle di questa partita non accettano più voti. */
|
||||
pagelleChiuse: boolean;
|
||||
/** Quando l'evento è stato creato: è l'istante della convocazione.
|
||||
* Assente sugli eventi generati dal client (compleanni, bozze non salvate). */
|
||||
creatoIl?: string | undefined;
|
||||
};
|
||||
|
||||
export type RigaEvento = {
|
||||
@@ -34,6 +37,7 @@ export type RigaEvento = {
|
||||
campionato: boolean;
|
||||
casa: boolean | null;
|
||||
pagelle_chiuse: boolean;
|
||||
creato_il?: string;
|
||||
};
|
||||
|
||||
/** Conversione riga database -> modello applicativo (riusabile anche lato server). */
|
||||
@@ -50,11 +54,12 @@ export function daRiga(r: RigaEvento): Evento {
|
||||
campionato: !!r.campionato,
|
||||
casa: r.casa ?? true,
|
||||
pagelleChiuse: !!r.pagelle_chiuse,
|
||||
creatoIl: r.creato_il,
|
||||
};
|
||||
}
|
||||
|
||||
const COLONNE =
|
||||
"id, tipo, titolo, luogo, data, ora, note, convocati, campionato, casa, pagelle_chiuse";
|
||||
"id, tipo, titolo, luogo, data, ora, note, convocati, campionato, casa, pagelle_chiuse, creato_il";
|
||||
|
||||
/** Categoria mostrata in interfaccia: le amichevoli sono partite fuori campionato. */
|
||||
export type CategoriaEvento = "allenamento" | "partita" | "amichevole" | "evento";
|
||||
|
||||
+91
-13
@@ -2,12 +2,16 @@ import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import { supabase } from "@/integrations/supabase/client";
|
||||
import type { Stato } from "./crapp-data";
|
||||
import type { Evento } from "./eventi";
|
||||
import { aggiornaSerie } from "./serie";
|
||||
|
||||
export const PRESENZE_KEY = ["risposte-presenze"] as const;
|
||||
|
||||
/** eventoId -> giocatoreId -> stato */
|
||||
export type MappaPresenze = Record<string, Record<string, Stato>>;
|
||||
|
||||
/** eventoId -> giocatoreId -> istante della prima risposta (ISO). */
|
||||
export type MappaTempiRisposta = Record<string, Record<string, string>>;
|
||||
|
||||
/** Allenamenti e partite CrAPP che contano per le statistiche di presenza. */
|
||||
function eventiContanoPresenze(eventi: Evento[], giocatoreId?: string) {
|
||||
return eventi.filter(
|
||||
@@ -36,22 +40,87 @@ export function totaliEventiGiocatore(giocatoreId: string, eventi: Evento[]): nu
|
||||
return eventiContanoPresenze(eventi, giocatoreId).length;
|
||||
}
|
||||
|
||||
async function fetchPresenze(): Promise<MappaPresenze> {
|
||||
/**
|
||||
* Serie di presenze consecutive su eventi già passati, in ordine di data:
|
||||
* ogni presenza (o ritardo) vale +1, qualsiasi altra risposta — o nessuna
|
||||
* risposta — azzera la serie. Senza `tipo` conta partite e allenamenti insieme.
|
||||
*/
|
||||
export function serieConsecutiva(
|
||||
giocatoreId: string,
|
||||
eventi: Evento[],
|
||||
presenze: MappaPresenze,
|
||||
tipo?: "partita" | "allenamento",
|
||||
oggi: string = oggiIso(),
|
||||
): number {
|
||||
return serieSu(giocatoreId, eventi, oggi, tipo, (e) => {
|
||||
const stato = presenze[e.id]?.[giocatoreId];
|
||||
return stato === "presente" || stato === "ritardo";
|
||||
});
|
||||
}
|
||||
|
||||
const ORE_24 = 24 * 60 * 60 * 1000;
|
||||
|
||||
/**
|
||||
* Serie di conferme rapide: risposte arrivate entro 24 ore dalla convocazione
|
||||
* (`creatoIl` dell'evento). Gli eventi senza istante di creazione — quelli generati
|
||||
* dal client, non salvati a database — non spezzano la serie: vengono saltati.
|
||||
*/
|
||||
export function serieConferme(
|
||||
giocatoreId: string,
|
||||
eventi: Evento[],
|
||||
tempi: MappaTempiRisposta,
|
||||
oggi: string = oggiIso(),
|
||||
): number {
|
||||
return serieSu(
|
||||
giocatoreId,
|
||||
eventi.filter((e) => e.creatoIl),
|
||||
oggi,
|
||||
undefined,
|
||||
(e) => {
|
||||
const risposto = tempi[e.id]?.[giocatoreId];
|
||||
return risposto !== undefined && Date.parse(risposto) - Date.parse(e.creatoIl!) <= ORE_24;
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
function oggiIso() {
|
||||
return new Date().toISOString().slice(0, 10);
|
||||
}
|
||||
|
||||
/** Scorre gli eventi già passati in ordine di data applicando la regola delle serie. */
|
||||
function serieSu(
|
||||
giocatoreId: string,
|
||||
eventi: Evento[],
|
||||
oggi: string,
|
||||
tipo: "partita" | "allenamento" | undefined,
|
||||
onorato: (e: Evento) => boolean,
|
||||
): number {
|
||||
return eventiContanoPresenze(eventi, giocatoreId)
|
||||
.filter((e) => (tipo === undefined || e.tipo === tipo) && e.data <= oggi)
|
||||
.sort((a, b) => a.data.localeCompare(b.data))
|
||||
.reduce((serie, e) => aggiornaSerie(serie, onorato(e)), 0);
|
||||
}
|
||||
|
||||
type LetturaPresenze = { presenze: MappaPresenze; tempi: MappaTempiRisposta };
|
||||
|
||||
async function fetchPresenze(): Promise<LetturaPresenze> {
|
||||
const { data, error } = await supabase
|
||||
.from("risposte_presenze")
|
||||
.select("evento_id, giocatore_id, stato");
|
||||
.select("evento_id, giocatore_id, stato, risposto_il");
|
||||
if (error) throw error;
|
||||
const mappa: MappaPresenze = {};
|
||||
const presenze: MappaPresenze = {};
|
||||
const tempi: MappaTempiRisposta = {};
|
||||
for (const riga of data ?? []) {
|
||||
(mappa[riga.evento_id] ??= {})[riga.giocatore_id] = riga.stato as Stato;
|
||||
(presenze[riga.evento_id] ??= {})[riga.giocatore_id] = riga.stato as Stato;
|
||||
(tempi[riga.evento_id] ??= {})[riga.giocatore_id] = riga.risposto_il;
|
||||
}
|
||||
return mappa;
|
||||
return { presenze, tempi };
|
||||
}
|
||||
|
||||
/** Una lettura per sessione: le risposte cambiano poco durante la navigazione. */
|
||||
export function useRispostePresenze() {
|
||||
const query = useQuery({ queryKey: PRESENZE_KEY, queryFn: fetchPresenze, staleTime: 5 * 60_000 });
|
||||
return { ...query, presenze: query.data ?? {} };
|
||||
return { ...query, presenze: query.data?.presenze ?? {}, tempi: query.data?.tempi ?? {} };
|
||||
}
|
||||
|
||||
export function usePresenzeEvento(eventoId: string) {
|
||||
@@ -86,13 +155,22 @@ export function useSalvaPresenza() {
|
||||
},
|
||||
// Scrittura unica + aggiornamento cache locale, nessuna rilettura.
|
||||
onSuccess: (input) => {
|
||||
queryClient.setQueryData<MappaPresenze>(PRESENZE_KEY, (prec) => {
|
||||
const mappa: MappaPresenze = { ...(prec ?? {}) };
|
||||
const evento = { ...(mappa[input.eventoId] ?? {}) };
|
||||
if (input.stato === null) delete evento[input.giocatoreId];
|
||||
else evento[input.giocatoreId] = input.stato;
|
||||
mappa[input.eventoId] = evento;
|
||||
return mappa;
|
||||
queryClient.setQueryData<LetturaPresenze>(PRESENZE_KEY, (prec) => {
|
||||
const presenze: MappaPresenze = { ...(prec?.presenze ?? {}) };
|
||||
const tempi: MappaTempiRisposta = { ...(prec?.tempi ?? {}) };
|
||||
const stati = { ...(presenze[input.eventoId] ?? {}) };
|
||||
const istanti = { ...(tempi[input.eventoId] ?? {}) };
|
||||
if (input.stato === null) {
|
||||
delete stati[input.giocatoreId];
|
||||
delete istanti[input.giocatoreId];
|
||||
} else {
|
||||
stati[input.giocatoreId] = input.stato;
|
||||
// Come a database: l'istante è quello della prima risposta, non dei ripensamenti.
|
||||
istanti[input.giocatoreId] ??= new Date().toISOString();
|
||||
}
|
||||
presenze[input.eventoId] = stati;
|
||||
tempi[input.eventoId] = istanti;
|
||||
return { presenze, tempi };
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
+13
-7
@@ -9,7 +9,13 @@ import { useTurniPalloni } from "./palloni";
|
||||
import { useInfortuniERitardi } from "./infortuni";
|
||||
import { useGiocatoreId } from "./user-store";
|
||||
import { useEventi } from "./eventi";
|
||||
import { contaPresenzeGiocatore, totaliEventiGiocatore, useRispostePresenze } from "./presenze";
|
||||
import {
|
||||
contaPresenzeGiocatore,
|
||||
serieConferme,
|
||||
serieConsecutiva,
|
||||
totaliEventiGiocatore,
|
||||
useRispostePresenze,
|
||||
} from "./presenze";
|
||||
import { obiettiviOrdinati } from "./obiettivi";
|
||||
import { useCsi } from "./csi";
|
||||
import { partiteGiocate } from "./csi-core";
|
||||
@@ -33,7 +39,7 @@ export function useRosa(): Giocatore[] {
|
||||
const { turni } = useTurniPalloni();
|
||||
const { infortuni, ritardi } = useInfortuniERitardi();
|
||||
const { eventi } = useEventi();
|
||||
const { presenze: mappaPresenze } = useRispostePresenze();
|
||||
const { presenze: mappaPresenze, tempi } = useRispostePresenze();
|
||||
|
||||
const votiMvp = voti.data ?? [];
|
||||
|
||||
@@ -54,10 +60,10 @@ export function useRosa(): Giocatore[] {
|
||||
iniziali: iniziali(g.nome, g.cognome),
|
||||
presenze: contaPresenzeGiocatore(g.id, eventi, mappaPresenze),
|
||||
totaliEventi: totaliEventiGiocatore(g.id, eventi),
|
||||
streak: 0,
|
||||
serieAllenamenti: 0,
|
||||
seriePartite: 0,
|
||||
serieConferme: 0,
|
||||
streak: serieConsecutiva(g.id, eventi, mappaPresenze),
|
||||
serieAllenamenti: serieConsecutiva(g.id, eventi, mappaPresenze, "allenamento"),
|
||||
seriePartite: serieConsecutiva(g.id, eventi, mappaPresenze, "partita"),
|
||||
serieConferme: serieConferme(g.id, eventi, tempi),
|
||||
mvp: mvpVinti[g.id] ?? 0,
|
||||
mediaVoto: medie[g.id]?.media ?? 0,
|
||||
palloni: palloni[g.id] ?? 0,
|
||||
@@ -66,7 +72,7 @@ export function useRosa(): Giocatore[] {
|
||||
infortuni: infortuni[g.id] ?? 0,
|
||||
ritardi: ritardi[g.id] ?? 0,
|
||||
}));
|
||||
}, [squadra, votiMvp, pagelle, cacche, turni, infortuni, ritardi, eventi, mappaPresenze]);
|
||||
}, [squadra, votiMvp, pagelle, cacche, turni, infortuni, ritardi, eventi, mappaPresenze, tempi]);
|
||||
}
|
||||
|
||||
/** Il giocatore selezionato sul dispositivo, con le statistiche complete. */
|
||||
|
||||
+6
-2
@@ -67,9 +67,13 @@ function messaggioSerie(valore: number, prossimo: number | null, label: string)
|
||||
|
||||
export function statoSerie(def: SerieDef, g: Giocatore): SerieStato {
|
||||
const valore = def.valore(g);
|
||||
const prossimo = def.traguardi.find((t) => valore < t) ?? null;
|
||||
const i = def.traguardi.findIndex((t) => valore < t);
|
||||
const prossimo = i < 0 ? null : def.traguardi[i]!;
|
||||
const manca = prossimo ? prossimo - valore : 0;
|
||||
const progresso = prossimo ? Math.min(100, Math.round((valore / prossimo) * 100)) : 100;
|
||||
// Progresso dentro il livello corrente: fra il traguardo già preso e il prossimo,
|
||||
// altrimenti la barra tornerebbe indietro ogni volta che se ne raggiunge uno.
|
||||
const base = i <= 0 ? 0 : def.traguardi[i - 1]!;
|
||||
const progresso = prossimo ? Math.round(((valore - base) / (prossimo - base)) * 100) : 100;
|
||||
return {
|
||||
def,
|
||||
valore,
|
||||
|
||||
Reference in New Issue
Block a user