Files
CRAPP/src/lib/cacche.ts
T
davideandClaude Sonnet 5 689468141d Sposta roster, eventi, presenze, voti, scout e palloni su PocketBase
Riscrive i moduli dati che parlavano con Supabase per usare l'SDK
PocketBase, mantenendo invariate le firme degli hook React Query così i
componenti a valle non cambiano: giocatori-squadra, eventi, presenze,
cacche, pagelle, mvp-voti, badge-social, scout (store/live/stato),
palloni.

Aggiunge due helper condivisi in src/integrations/pocketbase/:
- upsert.ts: PocketBase non ha upsert nativo, questi replicano il
  pattern onConflict di Supabase (per id significativo o per filtro su
  combinazione di campi), verificati contro un'istanza reale così da non
  duplicare record sulla stessa scrittura ripetuta;
- formato.ts: normalizza le date PocketBase ("AAAA-MM-GG HH:MM:SS.sssZ",
  spazio non "T") a ISO stretto, altrimenti Date.parse e i confronti
  testuali con altre date si comportano in modo incoerente.

Corregge anche due problemi trovati testando contro un'istanza reale:
- alcune API rule confrontavano un campo relazione con @request.auth.id
  senza richiedere l'autenticazione, lasciando passare richieste anonime
  quando entrambi i lati erano vuoti;
- gli hook non riconoscevano il superuser PocketBase (solo il ruolo
  admin applicativo), bloccando le operazioni fatte con pbAdmin.

Aggiunta la colonna "casa" mancante su eventi_app e i campi di sistema
created/updated (non generati automaticamente da PocketBase per le
collection create via migration, a differenza di quanto assunto
inizialmente).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-11 14:58:27 +02:00

153 lines
4.8 KiB
TypeScript

import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { pb } from "@/integrations/pocketbase/client";
import { upsertByFilter } from "@/integrations/pocketbase/upsert";
import { oggiISO } from "./palloni-core";
/** Ora di apertura del sondaggio, il giorno stesso della partita. */
export const ORA_APERTURA_SONDAGGIO = 8;
/** Istante del fischio d'inizio: `oraEvento` mancante equivale a fine giornata (non chiude mai prima). */
function inizioPartita(dataEvento: string, oraEvento: string): number {
return new Date(`${dataEvento}T${oraEvento || "23:59"}:00`).getTime();
}
/** Il sondaggio apre alle 8:00 del giorno della partita e chiude al fischio d'inizio. */
export function sondaggioAperto(
dataEvento: string,
oraEvento: string,
adesso = new Date(),
): boolean {
if (dataEvento !== oggiISO(adesso)) return false;
const apertura = new Date(`${dataEvento}T00:00:00`);
apertura.setHours(ORA_APERTURA_SONDAGGIO, 0, 0, 0);
return (
adesso.getTime() >= apertura.getTime() &&
adesso.getTime() < inizioPartita(dataEvento, oraEvento)
);
}
/**
* True se il fischio d'inizio è già passato (oggi o in un giorno precedente): serve a
* distinguere, nel messaggio mostrato quando il sondaggio è chiuso, "non ancora aperto"
* da "già chiuso" — altrimenti dopo la partita si continuerebbe a dire "apre alle 8:00".
*/
export function sondaggioTerminato(
dataEvento: string,
oraEvento: string,
adesso = new Date(),
): boolean {
const oggi = oggiISO(adesso);
if (dataEvento < oggi) return true;
if (dataEvento > oggi) return false;
return adesso.getTime() >= inizioPartita(dataEvento, oraEvento);
}
/** Sondaggio goliardico pre-partita: quante cacche prima del match di campionato. */
export type RigaCacche = {
evento_id: string;
giocatore_id: string;
quantita: number;
};
type RigaCacchePocketBase = {
evento: string;
giocatore: string;
quantita: number;
};
export const CACCHE_KEY = ["cacche"] as const;
export function useCacche() {
const query = useQuery({
queryKey: CACCHE_KEY,
staleTime: 10 * 60_000,
queryFn: async (): Promise<RigaCacche[]> => {
const righe = await pb.collection("cacche_partita").getFullList<RigaCacchePocketBase>();
return righe.map((r) => ({
evento_id: r.evento,
giocatore_id: r.giocatore,
quantita: r.quantita,
}));
},
});
return { ...query, righe: query.data ?? [] };
}
export function useSalvaCacche() {
const qc = useQueryClient();
return useMutation({
mutationFn: async (riga: RigaCacche) => {
await upsertByFilter(
"cacche_partita",
"evento = {:evento} && giocatore = {:giocatore}",
{ evento: riga.evento_id, giocatore: riga.giocatore_id },
{ evento: riga.evento_id, giocatore: riga.giocatore_id, quantita: riga.quantita },
);
return riga;
},
onSuccess: (riga) => {
qc.setQueryData<RigaCacche[]>(CACCHE_KEY, (prec) => {
const altre = (prec ?? []).filter(
(r) => !(r.evento_id === riga.evento_id && r.giocatore_id === riga.giocatore_id),
);
return [...altre, riga];
});
},
});
}
export type StatCacche = {
totale: number;
giornate: number;
media: number;
record: number;
/** Giornate con almeno 3 cacche: soglia del badge segreto. */
giornateTop: number;
};
function arrotonda(n: number) {
return Math.round(n * 10) / 10;
}
/** Statistiche per giocatore: giocatoreId -> conteggi. */
export function statisticheCacche(righe: RigaCacche[]): Record<string, StatCacche> {
const out: Record<string, StatCacche> = {};
for (const r of righe) {
const cur = out[r.giocatore_id] ?? {
totale: 0,
giornate: 0,
media: 0,
record: 0,
giornateTop: 0,
};
cur.totale += r.quantita;
cur.giornate += 1;
cur.record = Math.max(cur.record, r.quantita);
if (r.quantita >= 3) cur.giornateTop += 1;
out[r.giocatore_id] = cur;
}
for (const s of Object.values(out)) s.media = arrotonda(s.totale / s.giornate);
return out;
}
/** Media di squadra di una singola partita. */
export function mediaPartita(righe: RigaCacche[], eventoId: string) {
const dellaPartita = righe.filter((r) => r.evento_id === eventoId);
if (dellaPartita.length === 0) return 0;
return arrotonda(dellaPartita.reduce((s, r) => s + r.quantita, 0) / dellaPartita.length);
}
/** Media di squadra su tutte le partite censite. */
export function mediaStagione(righe: RigaCacche[]) {
if (righe.length === 0) return 0;
return arrotonda(righe.reduce((s, r) => s + r.quantita, 0) / righe.length);
}
/** Record assoluto della stagione: chi e quante. */
export function recordStagione(righe: RigaCacche[]): RigaCacche | null {
return righe.reduce<RigaCacche | null>(
(best, r) => (!best || r.quantita > best.quantita ? r : best),
null,
);
}