"use client"; import { FormEvent, useCallback, useEffect, useMemo, useState } from "react"; import Link from "next/link"; import { useRouter } from "next/navigation"; import { ProjectSwitcher } from "@/features/predictions/components/project-switcher"; import { FieldRendererRegistry } from "@/features/predictions/components/fields/field-renderer-registry"; import { WeightDoubleSlider } from "@/features/predictions/components/fields/weight-double-slider"; import predictionStyles from "@/features/predictions/styles/predictions.module.css"; import { closePredictionGame, finalizePredictionGame, getPredictionBoard, getPredictionCards, getPredictionScoreboard, openPredictionGame, setPredictionOutcomes, suggestPredictionScores, validatePredictionScores, } from "@/lib/predictions-client"; import { authenticatedFetch, clearSession, getCurrentProjectId, getApiUrl, loadSession, logoutSession, type AuthUser, type UserRole, } from "@/lib/auth"; import { getMyProjects } from "@/lib/projects-client"; import { ProjectPhotoModal } from "@/features/predictions/components/project-photo-modal"; import type { PredictionBoardCard, PredictionBoardResponse, PredictionCard, PredictionDraftValue, PredictionScoringEntry, PredictionScoreboardItem, } from "@/types/predictions"; import type { ProjectSummary } from "@/types/projects"; import styles from "./page.module.css"; type UserListItem = { id: string; username: string; displayName: string | null; profileImageUrl: string | null; workspaceId?: string | null; currentProjectId?: string | null; role: UserRole; createdAt: string; }; type OutcomeDraftValue = { valueText: string; valueNumber: string; valueDate: string }; type OutcomeDraft = Record>; type ScoreDraft = Record>; type ScoreDraftByCard = Record; type ScoresByCard = Record; type ScoresByBabyCard = ScoresByCard; type ScoreDraftByBabyCard = ScoreDraftByCard; function babyCardKey(babyIndex: number, cardId: string) { return `${babyIndex}::${cardId}`; } type AdminTab = "projects" | "users" | "final"; function formatDate(value: string) { return new Date(value).toLocaleDateString("fr-FR"); } function formatRawValue( value: { valueText: string | null; valueNumber: number | null; valueDate: string | null }, unit?: string | null, ) { if (value.valueNumber != null) { return `${value.valueNumber}${unit ? ` ${unit}` : ""}`; } if (value.valueText && value.valueText.trim().length > 0) { return value.valueText; } if (value.valueDate) { return value.valueDate.slice(0, 10); } return "-"; } function formatLabeledValue( label: string, value: { valueText: string | null; valueNumber: number | null; valueDate: string | null }, unit?: string | null, ) { return `${label}: ${formatRawValue(value, unit)}`; } function buildOutcomeDraftKey(cardId: string, babyIndex: number) { return `${cardId}::${babyIndex}`; } function initializeOutcomeDraft( cards: PredictionCard[], board: PredictionBoardResponse | null, babyIndex: number, ) { const boardByCardId = new Map((board?.cards ?? []).map((card) => [card.id, card])); const draft: OutcomeDraft = {}; for (const card of cards) { const key = buildOutcomeDraftKey(card.id, babyIndex); const boardCard = boardByCardId.get(card.id); const outcomesByField = new Map((boardCard?.outcomes ?? []).map((outcome) => [outcome.fieldId, outcome])); draft[key] = {}; for (const field of card.fields) { const outcome = outcomesByField.get(field.id); draft[key][field.id] = { valueText: outcome?.valueText ?? "", valueNumber: outcome?.valueNumber != null ? `${outcome.valueNumber}` : "", valueDate: outcome?.valueDate ? outcome.valueDate.slice(0, 10) : "", }; } } return draft; } function getOutcomeDraftValues( card: PredictionCard, outcomeDraft: OutcomeDraft, babyIndex: number, ): PredictionDraftValue[] { const cardDraft = outcomeDraft[buildOutcomeDraftKey(card.id, babyIndex)] ?? {}; return card.fields.map((field) => { const draftValue = cardDraft[field.id] ?? { valueText: "", valueNumber: "", valueDate: "" }; const parsedNumber = Number(draftValue.valueNumber); return { fieldId: field.id, valueText: draftValue.valueText, valueNumber: draftValue.valueNumber.trim().length === 0 || Number.isNaN(parsedNumber) ? null : parsedNumber, valueDate: draftValue.valueDate, }; }); } function createScoreDraft(entries: PredictionScoringEntry[]) { return entries.reduce((accumulator, entry) => { accumulator[entry.id] = entry.scores.reduce>((scoreAccumulator, score) => { scoreAccumulator[score.fieldId] = `${score.awardedPoints ?? score.suggestedPoints}`; return scoreAccumulator; }, {}); return accumulator; }, {}); } function parsePointsInput(rawValue: string | undefined, fallback: number) { const parsed = Number(rawValue); if (Number.isNaN(parsed) || parsed < 0) { return fallback; } return Math.round(parsed); } export default function AdminPage() { const router = useRouter(); const [currentUser, setCurrentUser] = useState(null); const [projects, setProjects] = useState([]); const [activeProjectId, setActiveProjectId] = useState(getCurrentProjectId()); const [activeAdminTab, setActiveAdminTab] = useState("projects"); const [photoModalProjectId, setPhotoModalProjectId] = useState(null); const [users, setUsers] = useState([]); const [message, setMessage] = useState(""); const [loading, setLoading] = useState(false); const [newUsername, setNewUsername] = useState(""); const [newDisplayName, setNewDisplayName] = useState(""); const [newPassword, setNewPassword] = useState(""); const [newRole, setNewRole] = useState("FAMILY"); const [newProjectId, setNewProjectId] = useState(""); const [editingUserId, setEditingUserId] = useState(null); const [editUsername, setEditUsername] = useState(""); const [editDisplayName, setEditDisplayName] = useState(""); const [editPassword, setEditPassword] = useState(""); const [editRole, setEditRole] = useState("FAMILY"); const [predictionCards, setPredictionCards] = useState([]); const [boardsByBaby, setBoardsByBaby] = useState>({}); const [scoreboard, setScoreboard] = useState([]); const [outcomeDraft, setOutcomeDraft] = useState({}); const [scoresByBabyCard, setScoresByBabyCard] = useState({}); const [scoreDraftByBabyCard, setScoreDraftByBabyCard] = useState({}); const [wizardStepIndex, setWizardStepIndex] = useState(0); const [predictionMessage, setPredictionMessage] = useState(""); const apiUrl = getApiUrl(); const isAdmin = currentUser?.role === "ADMIN"; const primaryBoard = boardsByBaby[1] ?? null; const isGameOpen = primaryBoard?.game?.status === "OPEN"; const isGameClosed = primaryBoard?.game?.status === "CLOSED"; const isFinalized = Boolean(primaryBoard?.game?.finalized); const totalWorkflowSteps = predictionCards.length + 1; const isRecapStep = isGameClosed && !isFinalized && wizardStepIndex >= predictionCards.length; const currentStepCard = useMemo( () => (!isRecapStep ? predictionCards[wizardStepIndex] ?? null : null), [isRecapStep, predictionCards, wizardStepIndex], ); const currentBoardCard = useMemo(() => { if (!currentStepCard) { return null; } return primaryBoard?.cards.find((card) => card.id === currentStepCard.id) ?? null; }, [currentStepCard, primaryBoard?.cards]); const activeProjectBabyCount = useMemo(() => { const project = projects.find((item) => item.id === activeProjectId); return Math.max(1, project?.babyCount ?? 1); }, [activeProjectId, projects]); const currentScoringEntriesByBaby = useMemo(() => { if (!currentStepCard) { return {}; } const result: Record = {}; for (let babyIdx = 1; babyIdx <= activeProjectBabyCount; babyIdx++) { result[babyIdx] = scoresByBabyCard[babyCardKey(babyIdx, currentStepCard.id)] ?? []; } return result; }, [currentStepCard, scoresByBabyCard, activeProjectBabyCount]); const currentScoreDraftByBaby = useMemo(() => { if (!currentStepCard) { return {}; } const result: Record = {}; for (let babyIdx = 1; babyIdx <= activeProjectBabyCount; babyIdx++) { result[babyIdx] = scoreDraftByBabyCard[babyCardKey(babyIdx, currentStepCard.id)] ?? {}; } return result; }, [currentStepCard, scoreDraftByBabyCard, activeProjectBabyCount]); const totalsByUserId = useMemo(() => { return new Map(scoreboard.map((item) => [item.userId, item.totalPoints])); }, [scoreboard]); const winnerSummary = useMemo(() => { if (scoreboard.length === 0) { return null; } const maxPoints = scoreboard[0].totalPoints; const winners = scoreboard.filter((item) => item.totalPoints === maxPoints); return { points: maxPoints, winners, }; }, [scoreboard]); const remainingEntriesToScore = useMemo(() => { let total = 0; for (let babyIdx = 1; babyIdx <= activeProjectBabyCount; babyIdx++) { const board = boardsByBaby[babyIdx]; if (!board) continue; for (const card of board.cards) { total += card.entries.filter((entry) => !entry.isScored).length; } } return total; }, [boardsByBaby, activeProjectBabyCount]); const canFinalizeContest = isGameClosed && !isFinalized && remainingEntriesToScore === 0 && predictionCards.length > 0; const activeProjectName = useMemo(() => { return projects.find((project) => project.id === activeProjectId)?.name ?? null; }, [activeProjectId, projects]); const getBabyLabel = useCallback((idx: number): string => { const project = projects.find((p) => p.id === activeProjectId); return project?.babies?.find((b) => b.babyIndex === idx)?.label ?? `Bébé ${idx}`; }, [activeProjectId, projects]); const handleProjectChanged = useCallback((projectId: string) => { setActiveProjectId((current) => (current === projectId ? current : projectId)); setPredictionMessage(""); }, []); useEffect(() => { const session = loadSession(); if (!session) { router.replace("/"); return; } if (session.user.role !== "ADMIN") { router.replace("/predictions"); return; } setCurrentUser(session.user); setActiveProjectId(session.user.currentProjectId ?? null); }, [router]); useEffect(() => { setWizardStepIndex((current) => Math.min(current, predictionCards.length)); }, [predictionCards.length]); const loadUsers = async () => { const response = await authenticatedFetch(apiUrl, "/users"); const payload = (await response.json()) as UserListItem[] | { message?: string }; if (!response.ok || !Array.isArray(payload)) { throw new Error((payload as { message?: string }).message ?? "Impossible de charger les utilisateurs"); } setUsers(payload); }; const loadProjects = async () => { const payload = await getMyProjects(); setProjects(payload); if (payload.length === 0) { setNewProjectId(""); setActiveProjectId(null); return null; } const currentProjectId = getCurrentProjectId(); const selectedProject = payload.find((project) => project.id === currentProjectId) ?? payload[0]; if (selectedProject) { setActiveProjectId(selectedProject.id); setNewProjectId((current) => current || selectedProject.id); return selectedProject.id; } return null; }; const loadPredictionAdminData = async (projectId?: string | null) => { const scopedProjectId = projectId ?? getCurrentProjectId(); if (!scopedProjectId) { setPredictionCards([]); setBoardsByBaby({}); setScoreboard([]); setOutcomeDraft({}); setScoresByBabyCard({}); setScoreDraftByBabyCard({}); setWizardStepIndex(0); return; } const project = projects.find((p) => p.id === scopedProjectId); const babyCount = Math.max(1, project?.babyCount ?? 1); const boardPromises = Array.from({ length: babyCount }, (_, i) => getPredictionBoard(scopedProjectId, i + 1), ); const [cards, nextScoreboard, ...boards] = await Promise.all([ getPredictionCards(scopedProjectId), getPredictionScoreboard(scopedProjectId), ...boardPromises, ]); setPredictionCards(cards); setScoreboard(nextScoreboard); const newBoardsByBaby: Record = {}; const allOutcomeDrafts: OutcomeDraft = {}; for (let i = 0; i < boards.length; i++) { const babyIdx = i + 1; newBoardsByBaby[babyIdx] = boards[i]; const draft = initializeOutcomeDraft(cards, boards[i], babyIdx); Object.assign(allOutcomeDrafts, draft); } setBoardsByBaby(newBoardsByBaby); setOutcomeDraft((current) => ({ ...current, ...allOutcomeDrafts })); }; useEffect(() => { if (!isAdmin) { return; } Promise.all([loadUsers(), loadProjects()]) .then(async ([, selectedProjectId]) => { await loadPredictionAdminData(selectedProjectId); }) .catch((error: unknown) => { if (error instanceof Error && error.message.includes("Session manquante")) { clearSession(); setCurrentUser(null); router.replace("/"); } setMessage("Impossible de charger les donnees admin"); }); }, [isAdmin, activeProjectId, router]); useEffect(() => { setScoresByBabyCard({}); setScoreDraftByBabyCard({}); setWizardStepIndex(0); }, [primaryBoard?.game?.status, primaryBoard?.game?.finalized]); useEffect(() => { if (!isGameClosed || isFinalized || isRecapStep || !currentStepCard) { return; } const missingBabies: number[] = []; for (let babyIdx = 1; babyIdx <= activeProjectBabyCount; babyIdx++) { const key = babyCardKey(babyIdx, currentStepCard.id); if (!Object.prototype.hasOwnProperty.call(scoresByBabyCard, key)) { missingBabies.push(babyIdx); } } if (missingBabies.length === 0) { return; } setLoading(true); setPredictionMessage(""); Promise.all( missingBabies.map((babyIdx) => suggestPredictionScores(currentStepCard.id, activeProjectId, babyIdx).then((entries) => ({ babyIdx, entries, })), ), ) .then((results) => { setScoresByBabyCard((current) => { const next = { ...current }; for (const { babyIdx, entries } of results) { next[babyCardKey(babyIdx, currentStepCard.id)] = entries; } return next; }); setScoreDraftByBabyCard((current) => { const next = { ...current }; for (const { babyIdx, entries } of results) { next[babyCardKey(babyIdx, currentStepCard.id)] = createScoreDraft(entries); } return next; }); }) .catch((error) => { setPredictionMessage(error instanceof Error ? error.message : "Erreur de chargement de l'etape"); }) .finally(() => { setLoading(false); }); }, [ activeProjectBabyCount, activeProjectId, currentStepCard, isFinalized, isGameClosed, isRecapStep, scoresByBabyCard, ]); const onCreateUser = async (event: FormEvent) => { event.preventDefault(); setLoading(true); setMessage(""); try { const response = await authenticatedFetch(apiUrl, "/users", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ username: newUsername, displayName: newDisplayName || undefined, password: newPassword, role: newRole, projectId: newRole === "FAMILY" ? newProjectId || undefined : undefined, }), }); const payload = (await response.json()) as { message?: string }; if (!response.ok) { setMessage(payload.message ?? "Creation impossible"); return; } setNewUsername(""); setNewDisplayName(""); setNewPassword(""); setNewRole("FAMILY"); setNewProjectId((current) => current || projects[0]?.id || ""); setMessage("Compte cree"); await loadUsers(); } catch { setMessage("Erreur reseau pendant la creation"); } finally { setLoading(false); } }; const startEdit = (user: UserListItem) => { setEditingUserId(user.id); setEditUsername(user.username); setEditDisplayName(user.displayName ?? ""); setEditPassword(""); setEditRole(user.role); }; const cancelEdit = () => { setEditingUserId(null); setEditUsername(""); setEditDisplayName(""); setEditPassword(""); setEditRole("FAMILY"); }; const onSaveEdit = async (userId: string) => { setLoading(true); setMessage(""); const body: { username?: string; displayName?: string; password?: string; role?: UserRole; } = {}; if (editUsername.trim()) body.username = editUsername.trim(); if (editDisplayName.trim().length >= 2) { body.displayName = editDisplayName.trim(); } if (editPassword.trim()) body.password = editPassword; body.role = editRole; try { const response = await authenticatedFetch(apiUrl, `/users/${userId}`, { method: "PATCH", headers: { "Content-Type": "application/json" }, body: JSON.stringify(body), }); const payload = (await response.json()) as { message?: string }; if (!response.ok) { setMessage(payload.message ?? "Mise a jour impossible"); return; } setMessage("Compte mis a jour"); cancelEdit(); await loadUsers(); } catch { setMessage("Erreur reseau pendant la mise a jour"); } finally { setLoading(false); } }; const onDeleteUser = async (userId: string) => { setLoading(true); setMessage(""); try { const response = await authenticatedFetch(apiUrl, `/users/${userId}`, { method: "DELETE", }); const payload = (await response.json()) as { message?: string }; if (!response.ok) { setMessage(payload.message ?? "Suppression impossible"); return; } setMessage("Compte supprime"); await loadUsers(); } catch { setMessage("Erreur reseau pendant la suppression"); } finally { setLoading(false); } }; const onAssignUserProject = async (userId: string, projectId: string) => { setLoading(true); setMessage(""); try { const response = await authenticatedFetch(apiUrl, `/users/${userId}/project`, { method: "PATCH", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ projectId: projectId || undefined, }), }); const payload = (await response.json()) as { message?: string }; if (!response.ok) { setMessage(payload.message ?? "Affectation de projet impossible"); return; } setMessage("Projet utilisateur mis a jour"); await loadUsers(); } catch { setMessage("Erreur reseau pendant l'affectation projet"); } finally { setLoading(false); } }; const patchOutcomeDraft = ( cardId: string, babyIndex: number, fieldId: string, patch: Partial, ) => { const key = buildOutcomeDraftKey(cardId, babyIndex); setOutcomeDraft((current) => ({ ...current, [key]: { ...(current[key] ?? {}), [fieldId]: { ...(current[key]?.[fieldId] ?? { valueText: "", valueNumber: "", valueDate: "", }), ...patch, }, }, })); }; const onToggleGame = async () => { if (!primaryBoard?.game) { return; } if (primaryBoard.game.status === "CLOSED" && isFinalized) { setPredictionMessage("Concours deja valide: reouverture de La Grande Revelation impossible."); return; } setPredictionMessage(""); setLoading(true); try { if (primaryBoard.game.status === "OPEN") { await closePredictionGame(activeProjectId); setPredictionMessage("La Grande Revelation est cloturee. Passage en mode notation par etapes."); } else { await openPredictionGame(activeProjectId); setPredictionMessage("La Grande Revelation est reouverte. Tu peux encore corriger les valeurs finales."); } await loadPredictionAdminData(); } catch (error) { setPredictionMessage(error instanceof Error ? error.message : "Erreur de changement d'etat du concours"); } finally { setLoading(false); } }; const onSaveOutcomes = async () => { if (!isGameOpen) { setPredictionMessage("La saisie des valeurs finales est fermee."); return; } if (predictionCards.length === 0) { setPredictionMessage("Aucune categorie disponible."); return; } setLoading(true); setPredictionMessage(""); let submittedCards = 0; try { for (let babyIdx = 1; babyIdx <= activeProjectBabyCount; babyIdx++) { for (const card of predictionCards) { const cardDraft = outcomeDraft[buildOutcomeDraftKey(card.id, babyIdx)] ?? {}; const values = card.fields .map((field) => { const draft = cardDraft[field.id] ?? { valueText: "", valueNumber: "", valueDate: "" }; if (card.valueType === "NUMBER") { const parsed = Number(draft.valueNumber); if (Number.isNaN(parsed)) { return null; } return { fieldId: field.id, valueNumber: parsed }; } if (card.valueType === "DATE") { if (!draft.valueDate) { return null; } return { fieldId: field.id, valueDate: draft.valueDate }; } if (!draft.valueText.trim()) { return null; } return { fieldId: field.id, valueText: draft.valueText.trim() }; }) .filter((value): value is NonNullable => value != null); if (values.length === 0) { continue; } await setPredictionOutcomes(card.id, { selectedBabyIndex: babyIdx, values }, activeProjectId); submittedCards += 1; } } if (submittedCards === 0) { setPredictionMessage("Aucune valeur finale detectee a enregistrer."); } else { setPredictionMessage(`${submittedCards} valeur(s) de resultats enregistree(s) pour ${activeProjectBabyCount} bebe(s).`); } await loadPredictionAdminData(); } catch (error) { setPredictionMessage(error instanceof Error ? error.message : "Erreur pendant l'enregistrement des resultats"); } finally { setLoading(false); } }; const onRefreshStepSuggestions = async () => { if (!currentStepCard) { return; } setLoading(true); setPredictionMessage(""); try { const results = await Promise.all( Array.from({ length: activeProjectBabyCount }, (_, i) => i + 1).map((babyIdx) => suggestPredictionScores(currentStepCard.id, activeProjectId, babyIdx).then((entries) => ({ babyIdx, entries, })), ), ); setScoresByBabyCard((current) => { const next = { ...current }; for (const { babyIdx, entries } of results) { next[babyCardKey(babyIdx, currentStepCard.id)] = entries; } return next; }); setScoreDraftByBabyCard((current) => { const next = { ...current }; for (const { babyIdx, entries } of results) { next[babyCardKey(babyIdx, currentStepCard.id)] = createScoreDraft(entries); } return next; }); setPredictionMessage(`Suggestions de points recalculees pour ${currentStepCard.title}.`); } catch (error) { setPredictionMessage(error instanceof Error ? error.message : "Erreur pendant le recalcul des points"); } finally { setLoading(false); } }; const onScoreChange = (babyIdx: number, cardId: string, entryId: string, fieldId: string, value: string) => { const key = babyCardKey(babyIdx, cardId); setScoreDraftByBabyCard((current) => ({ ...current, [key]: { ...(current[key] ?? {}), [entryId]: { ...(current[key]?.[entryId] ?? {}), [fieldId]: value, }, }, })); }; const getStepPointsTotal = (babyIdx: number, entry: PredictionScoringEntry) => { if (!currentStepCard) { return 0; } const key = babyCardKey(babyIdx, currentStepCard.id); const draft = scoreDraftByBabyCard[key]?.[entry.id] ?? {}; return entry.scores.reduce((total, score) => { return total + parsePointsInput(draft[score.fieldId], score.awardedPoints ?? score.suggestedPoints); }, 0); }; const onValidateCurrentStep = async () => { if (!currentStepCard) { return; } let hasEntries = false; for (let babyIdx = 1; babyIdx <= activeProjectBabyCount; babyIdx++) { const entries = scoresByBabyCard[babyCardKey(babyIdx, currentStepCard.id)] ?? []; if (entries.length > 0) { hasEntries = true; break; } } if (!hasEntries) { setPredictionMessage("Aucun participant a noter sur cette categorie."); setWizardStepIndex((current) => Math.min(current + 1, predictionCards.length)); return; } setLoading(true); setPredictionMessage(""); try { for (let babyIdx = 1; babyIdx <= activeProjectBabyCount; babyIdx++) { const key = babyCardKey(babyIdx, currentStepCard.id); const entries = scoresByBabyCard[key] ?? []; for (const entry of entries) { const draftByField = scoreDraftByBabyCard[key]?.[entry.id] ?? {}; const scores = entry.scores.map((score) => ({ fieldId: score.fieldId, awardedPoints: parsePointsInput(draftByField[score.fieldId], score.awardedPoints ?? score.suggestedPoints), })); await validatePredictionScores(entry.id, { scores }); } } setScoresByBabyCard((current) => { const next = { ...current }; for (let babyIdx = 1; babyIdx <= activeProjectBabyCount; babyIdx++) { delete next[babyCardKey(babyIdx, currentStepCard.id)]; } return next; }); setScoreDraftByBabyCard((current) => { const next = { ...current }; for (let babyIdx = 1; babyIdx <= activeProjectBabyCount; babyIdx++) { delete next[babyCardKey(babyIdx, currentStepCard.id)]; } return next; }); await loadPredictionAdminData(); setPredictionMessage(`Categorie ${currentStepCard.title} validee pour ${activeProjectBabyCount} bebe(s).`); setWizardStepIndex((current) => Math.min(current + 1, predictionCards.length)); } catch (error) { setPredictionMessage(error instanceof Error ? error.message : "Erreur pendant la validation des points"); } finally { setLoading(false); } }; const onFinalizeContest = async () => { setLoading(true); setPredictionMessage(""); try { const result = await finalizePredictionGame(activeProjectId); const names = result.winners .map((winner) => winner.displayName ?? winner.username) .join(" / "); setPredictionMessage(`Concours valide. Gagnant(s): ${names}.`); await loadPredictionAdminData(); } catch (error) { setPredictionMessage(error instanceof Error ? error.message : "Erreur pendant la validation finale du concours"); } finally { setLoading(false); } }; const logout = async () => { await logoutSession(apiUrl); setCurrentUser(null); setUsers([]); setPredictionCards([]); setBoardsByBaby({}); setScoreboard([]); setMessage("Deconnecte"); window.location.replace("/"); }; const renderOutcomeCard = (card: PredictionCard, babyIdx: number, boardCard: PredictionBoardCard | null) => { const draftValues = getOutcomeDraftValues(card, outcomeDraft, babyIdx); const weightField = card.code === "legacy_weight" ? card.fields[0] : null; const weightValue = weightField == null ? null : draftValues.find((value) => value.fieldId === weightField.id)?.valueNumber ?? null; return (

{card.title}

{card.description ?

{card.description}

: null}
Resultat final
{weightField ? ( { patchOutcomeDraft(card.id, babyIdx, weightField.id, { valueNumber: nextValueKg.toFixed(3), }); }} /> ) : ( { patchOutcomeDraft(card.id, babyIdx, fieldId, { valueText: value }); }} setNumber={(fieldId, value) => { patchOutcomeDraft(card.id, babyIdx, fieldId, { valueNumber: value == null ? "" : `${value}` }); }} setDate={(fieldId, value) => { patchOutcomeDraft(card.id, babyIdx, fieldId, { valueDate: value }); }} /> )}

Valeur actuellement enregistree: {boardCard?.outcomes.length ? boardCard.outcomes .map((outcome) => formatLabeledValue(outcome.fieldLabel, { valueText: outcome.valueText, valueNumber: outcome.valueNumber, valueDate: outcome.valueDate, }, card.unit), ) .join(" | ") : "Aucune valeur encore sauvegardee"}

); }; return (

Le Juste Poids / Admin

Gestion des comptes famille

Mon profil {currentUser ? ( ) : null}
{!currentUser || !isAdmin ? (

Redirection vers la page de connexion...

) : ( <>
{activeAdminTab === "projects" ? (

Gestion des projets

Cree, clone et selectionne un projet. Ouvre ensuite La Grande Revelation dans l onglet dedie.

Nouveau projet
{projects.length === 0 ? (

Aucun projet pour le moment. Commence par creer le premier projet via le wizard.

) : (
{projects.map((project) => (
{project.name}

{project.babyCount} bebe{project.babyCount > 1 ? "s" : ""} • {project.status}

Configurer les indices
))}
)} {photoModalProjectId ? (() => { const modalProject = projects.find((p) => p.id === photoModalProjectId); return modalProject ? ( { setProjects((prev) => prev.map((p) => p.id === updated.id ? updated : p)); }} onClose={() => setPhotoModalProjectId(null)} /> ) : null; })() : null}
) : null} {activeAdminTab === "users" ? ( <>

Creer un compte

setNewUsername(event.target.value)} />
setNewDisplayName(event.target.value)} />
setNewPassword(event.target.value)} />
{newRole === "FAMILY" ? (
) : null}
{message ?

{message}

: null}

Comptes existants

{users.map((user) => ( ))}
Username Pseudo Role Projet Cree le Actions
{editingUserId === user.id ? ( setEditUsername(event.target.value)} /> ) : ( user.username )} {editingUserId === user.id ? ( setEditDisplayName(event.target.value)} /> ) : ( user.displayName ?? "-" )} {editingUserId === user.id ? ( ) : ( user.role )} {user.role === "FAMILY" ? ( ) : ( Admin scope espace )} {formatDate(user.createdAt)}
{editingUserId === user.id ? ( <> setEditPassword(event.target.value)} /> ) : ( <> )}
) : null} {activeAdminTab === "final" ? (

La Grande Revelation

{!activeProjectId ? (

Selectionne ou cree d abord un projet pour activer La Grande Revelation.

) : ( <>

Projet actif: {activeProjectName ?? "Projet selectionne"} {activeProjectBabyCount > 1 ? ` — ${activeProjectBabyCount} bebes` : ""}

Une fois bebe arrive et ses caracteristiques connues, renseigne ici les resultats finaux. Ensuite, cloture le concours pour lancer la notation et decouvrir qui a gagne.

)} {!activeProjectId ? null : ( <>
Ouvrir les indices de ce projet
Etat: {isGameOpen ? "OUVERT" : isFinalized ? "CLOTURE ET VALIDE" : "CLOTURE"}
{isGameOpen ? ( <>

1. Saisie des valeurs finales

Renseigne les resultats finaux pour {activeProjectBabyCount > 1 ? "chaque bebe" : "le bebe"}.

{activeProjectBabyCount > 1 ? (
{Array.from({ length: activeProjectBabyCount }, (_, i) => i + 1).map((babyIdx) => (

{getBabyLabel(babyIdx)}

{predictionCards.map((card) => renderOutcomeCard( card, babyIdx, boardsByBaby[babyIdx]?.cards.find((c) => c.id === card.id) ?? null, ))}
))}
) : (
{predictionCards.map((card) => renderOutcomeCard( card, 1, boardsByBaby[1]?.cards.find((c) => c.id === card.id) ?? null, ))}
)}
) : null} {isGameClosed && !isFinalized ? (

2. Notation par categorie

Etape {Math.min(wizardStepIndex + 1, totalWorkflowSteps)} / {totalWorkflowSteps}
{!isRecapStep && currentStepCard ? ( <>
Categorie: {currentStepCard.title} {Array.from({ length: activeProjectBabyCount }, (_, i) => i + 1).map((babyIdx) => { const board = boardsByBaby[babyIdx]; const boardCard = board?.cards.find((c) => c.id === currentStepCard.id); return (

{activeProjectBabyCount > 1 ? `${getBabyLabel(babyIdx)}: ` : "Resultat final: "} {boardCard?.outcomes.length ? boardCard.outcomes .map((outcome) => formatLabeledValue(outcome.fieldLabel, { valueText: outcome.valueText, valueNumber: outcome.valueNumber, valueDate: outcome.valueDate, }, currentStepCard.unit), ) .join(" | ") : "Aucun resultat final sauvegarde"}

); })}
{activeProjectBabyCount > 1 ? (
{Array.from({ length: activeProjectBabyCount }, (_, i) => i + 1).map((babyIdx) => { const entries = currentScoringEntriesByBaby[babyIdx] ?? []; const board = boardsByBaby[babyIdx]; const boardCard = board?.cards.find((c) => c.id === currentStepCard.id); const draft = currentScoreDraftByBaby[babyIdx] ?? {}; return (

{getBabyLabel(babyIdx)}

{entries.length === 0 ? (

Aucun participant pour ce bebe.

) : (
{entries.map((entry) => { const boardEntry = boardCard?.entries.find((item) => item.entryId === entry.id) ?? null; const participantName = entry.user.displayName ?? entry.user.username; const currentTotal = totalsByUserId.get(entry.userId) ?? 0; return (
{participantName} Total: {currentTotal} pts

{boardEntry?.values.length ? boardEntry.values .map((value) => formatLabeledValue(value.fieldLabel, { valueText: value.valueText, valueNumber: value.valueNumber, valueDate: value.valueDate, }, currentStepCard.unit), ) .join(" | ") : "Aucune reponse"}

{entry.scores.map((score) => ( ))}

Points: {getStepPointsTotal(babyIdx, entry)}

); })}
)}
); })}
) : ( <> {(currentScoringEntriesByBaby[1] ?? []).length === 0 ? (

Aucun participant sur cette categorie.

) : (
{(currentScoringEntriesByBaby[1] ?? []).map((entry) => { const boardEntry = currentBoardCard?.entries.find((item) => item.entryId === entry.id) ?? null; const participantName = entry.user.displayName ?? entry.user.username; const currentTotal = totalsByUserId.get(entry.userId) ?? 0; const draft = currentScoreDraftByBaby[1] ?? {}; return (
{participantName} Total actuel: {currentTotal} pts

Pronostic: {boardEntry?.values.length ? boardEntry.values .map((value) => formatLabeledValue(value.fieldLabel, { valueText: value.valueText, valueNumber: value.valueNumber, valueDate: value.valueDate, }, currentStepCard.unit), ) .join(" | ") : "Aucune reponse"}

{entry.scores.map((score) => ( ))}

Points sur cette categorie: {getStepPointsTotal(1, entry)}

); })}
)} )}
) : ( <>

3. Recap final des points

Les points sont cumules entre les bebes. Si un participant est le plus proche pour les {activeProjectBabyCount} bebes, il gagne les points x{activeProjectBabyCount}.

Pronostics restants a noter: {remainingEntriesToScore}

{winnerSummary ? (
Gagnant(s) provisoire(s): {winnerSummary.winners .map((winner) => winner.displayName ?? winner.username) .join(" / ")}

Score total: {winnerSummary.points} points (cumul sur {activeProjectBabyCount} bebe{activeProjectBabyCount > 1 ? "s" : ""})

) : (

Pas encore de classement disponible.

)} {scoreboard.map((item, index) => ( ))}
# Participant Total cumule
{index + 1} {item.displayName ?? item.username} {item.totalPoints}
{!canFinalizeContest ? (

Valide tous les pronostics de chaque categorie{activeProjectBabyCount > 1 ? " pour chaque bebe" : ""} avant la validation finale.

) : null} )}
) : null} {isFinalized ? (

Concours valide

{winnerSummary ? (
Gagnant(s): {winnerSummary.winners .map((winner) => winner.displayName ?? winner.username) .join(" / ")}

{winnerSummary.points} points (cumul {activeProjectBabyCount} bebe{activeProjectBabyCount > 1 ? "s" : ""})

) : null} {scoreboard.map((item, index) => ( ))}
# Participant Total cumule
{index + 1} {item.displayName ?? item.username} {item.totalPoints}
) : null} {predictionMessage ?

{predictionMessage}

: null} )}
) : null} )}
); }