Updating
This commit is contained in:
@@ -1,9 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { FileText, Trash2 } from "lucide-react";
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { AppShell } from "@/components/AppShell";
|
||||
import { ConfirmDialog } from "@/components/ConfirmDialog";
|
||||
import { EmptyState } from "@/components/EmptyState";
|
||||
import { SectionCard } from "@/components/SectionCard";
|
||||
import { Alert } from "@/components/ui/alert";
|
||||
@@ -15,6 +13,7 @@ import { fetchJson } from "@/lib/client";
|
||||
import type { GamePlayerStatsInput, GameRecord, LineupRecord } from "@/lib/types";
|
||||
|
||||
const statFields = [
|
||||
["pitchCount", "Pitches"],
|
||||
["atBats", "AB"],
|
||||
["runs", "R"],
|
||||
["hits", "H"],
|
||||
@@ -36,13 +35,12 @@ export function GameScoringClient() {
|
||||
const [gameDate, setGameDate] = useState(new Date().toISOString().slice(0, 10));
|
||||
const [lineups, setLineups] = useState<LineupRecord[]>([]);
|
||||
const [selectedLineupId, setSelectedLineupId] = useState("");
|
||||
const [savedGames, setSavedGames] = useState<GameRecord[]>([]);
|
||||
const [gamePendingDelete, setGamePendingDelete] = useState<GameRecord | null>(null);
|
||||
const [opponent, setOpponent] = useState("");
|
||||
const [teamScore, setTeamScore] = useState<string>("");
|
||||
const [opponentScore, setOpponentScore] = useState<string>("");
|
||||
const [notes, setNotes] = useState("");
|
||||
const [playerStats, setPlayerStats] = useState<Record<string, GamePlayerStatsInput>>({});
|
||||
const [isScoring, setIsScoring] = useState(false);
|
||||
const [message, setMessage] = useState<string | null>(null);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
@@ -50,27 +48,21 @@ export function GameScoringClient() {
|
||||
try {
|
||||
const response = await fetchJson<LineupRecord[]>("/api/lineups");
|
||||
setLineups(response);
|
||||
setSelectedLineupId((current) => (current && response.some((lineup) => lineup._id === current) ? current : pickDefaultLineupId(gameDate, response)));
|
||||
setSelectedLineupId((current) =>
|
||||
current && response.some((lineup) => lineup._id === current)
|
||||
? current
|
||||
: pickDefaultLineupId(gameDate, response)
|
||||
);
|
||||
} catch (loadError) {
|
||||
setError(loadError instanceof Error ? loadError.message : "Failed to load lineups.");
|
||||
}
|
||||
}
|
||||
|
||||
async function loadSavedGames(targetDate: string) {
|
||||
try {
|
||||
const response = await fetchJson<GameRecord[]>(`/api/games?gameDate=${targetDate}`);
|
||||
setSavedGames(response);
|
||||
} catch (loadError) {
|
||||
setError(loadError instanceof Error ? loadError.message : "Failed to load games.");
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
void loadLineups();
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
void loadSavedGames(gameDate);
|
||||
setSelectedLineupId((current) => {
|
||||
if (current && lineups.some((lineup) => lineup._id === current)) {
|
||||
return current;
|
||||
@@ -88,6 +80,7 @@ export function GameScoringClient() {
|
||||
useEffect(() => {
|
||||
if (!selectedLineup) {
|
||||
setPlayerStats({});
|
||||
setIsScoring(false);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -104,7 +97,12 @@ export function GameScoringClient() {
|
||||
);
|
||||
}, [selectedLineup]);
|
||||
|
||||
function updatePlayerStat(playerId: string, playerName: string, field: keyof Omit<GamePlayerStatsInput, "playerId" | "playerName">, value: string) {
|
||||
function updatePlayerStat(
|
||||
playerId: string,
|
||||
playerName: string,
|
||||
field: keyof Omit<GamePlayerStatsInput, "playerId" | "playerName">,
|
||||
value: string
|
||||
) {
|
||||
setPlayerStats((current) => ({
|
||||
...current,
|
||||
[playerId]: {
|
||||
@@ -130,7 +128,9 @@ export function GameScoringClient() {
|
||||
teamScore: teamScore === "" ? null : Number(teamScore),
|
||||
opponentScore: opponentScore === "" ? null : Number(opponentScore),
|
||||
notes,
|
||||
playerStats: selectedLineup.battingOrder.map((slot) => playerStats[slot.playerId] ?? { playerId: slot.playerId, playerName: slot.playerName })
|
||||
playerStats: selectedLineup.battingOrder.map(
|
||||
(slot) => playerStats[slot.playerId] ?? { playerId: slot.playerId, playerName: slot.playerName }
|
||||
)
|
||||
};
|
||||
|
||||
const saved = await fetchJson<GameRecord>("/api/games", {
|
||||
@@ -144,59 +144,47 @@ export function GameScoringClient() {
|
||||
setOpponentScore("");
|
||||
setNotes("");
|
||||
setPlayerStats({});
|
||||
await loadSavedGames(gameDate);
|
||||
setIsScoring(false);
|
||||
} catch (saveError) {
|
||||
setError(saveError instanceof Error ? saveError.message : "Unable to save game.");
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteGame(game: GameRecord) {
|
||||
if (!game._id) return;
|
||||
|
||||
try {
|
||||
await fetchJson(`/api/games/${game._id}`, { method: "DELETE" });
|
||||
setMessage(`Deleted game #${game.gameNumber}.`);
|
||||
setGamePendingDelete(null);
|
||||
await loadSavedGames(gameDate);
|
||||
} catch (deleteError) {
|
||||
setError(deleteError instanceof Error ? deleteError.message : "Unable to delete game.");
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<AppShell
|
||||
title="Score Game"
|
||||
subtitle="Pick the lineup used, enter the results, and roll the stats into each player automatically."
|
||||
subtitle="Choose the lineup used, then open the scorecard only when you're ready to enter the game results."
|
||||
>
|
||||
<ConfirmDialog
|
||||
open={gamePendingDelete !== null}
|
||||
title="Delete Saved Game?"
|
||||
description={
|
||||
gamePendingDelete
|
||||
? `Delete game #${gamePendingDelete.gameNumber} for ${gamePendingDelete.gameDate}? Player stats from that game will be reversed.`
|
||||
: ""
|
||||
}
|
||||
confirmLabel="Delete Game"
|
||||
onCancel={() => setGamePendingDelete(null)}
|
||||
onConfirm={() => (gamePendingDelete ? void deleteGame(gamePendingDelete) : undefined)}
|
||||
/>
|
||||
|
||||
{message ? <Alert tone="success">{message}</Alert> : null}
|
||||
{error ? <Alert tone="error">{error}</Alert> : null}
|
||||
|
||||
<SectionCard
|
||||
title="Game Setup"
|
||||
description="Choose the lineup used in the game. The latest saved lineup is selected by default."
|
||||
description="The most recent saved lineup is selected by default."
|
||||
>
|
||||
{lineups.length > 0 ? (
|
||||
<div className="grid gap-4 md:grid-cols-2">
|
||||
<label>
|
||||
<span className="field-label">Game Date</span>
|
||||
<Input type="date" value={gameDate} onChange={(event) => setGameDate(event.target.value)} />
|
||||
<Input
|
||||
type="date"
|
||||
value={gameDate}
|
||||
onChange={(event) => {
|
||||
setGameDate(event.target.value);
|
||||
setIsScoring(false);
|
||||
}}
|
||||
/>
|
||||
</label>
|
||||
<label>
|
||||
<span className="field-label">Lineup Used</span>
|
||||
<select className="field-select" value={selectedLineupId} onChange={(event) => setSelectedLineupId(event.target.value)}>
|
||||
<select
|
||||
className="field-select"
|
||||
value={selectedLineupId}
|
||||
onChange={(event) => {
|
||||
setSelectedLineupId(event.target.value);
|
||||
setIsScoring(false);
|
||||
}}
|
||||
>
|
||||
{lineups.map((lineup) => (
|
||||
<option key={lineup._id} value={lineup._id}>
|
||||
{lineup.gameDate} · Lineup #{lineup.lineupNumber}
|
||||
@@ -222,6 +210,14 @@ export function GameScoringClient() {
|
||||
<span className="field-label">Game Notes</span>
|
||||
<Textarea rows={3} value={notes} onChange={(event) => setNotes(event.target.value)} />
|
||||
</label>
|
||||
<div className="md:col-span-2 flex flex-wrap gap-2">
|
||||
<Button onClick={() => setIsScoring(true)}>Start Scorecard</Button>
|
||||
{isScoring ? (
|
||||
<Button variant="secondary" onClick={() => setIsScoring(false)}>
|
||||
Hide Scorecard
|
||||
</Button>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<EmptyState
|
||||
@@ -231,14 +227,18 @@ export function GameScoringClient() {
|
||||
)}
|
||||
</SectionCard>
|
||||
|
||||
{selectedLineup ? (
|
||||
{selectedLineup && isScoring ? (
|
||||
<SectionCard
|
||||
title="Player Results"
|
||||
description="Stat entry is optional. Leave any field blank if you do not want to track it for that player."
|
||||
>
|
||||
<div className="grid gap-3">
|
||||
{selectedLineup.battingOrder.map((slot, index) => {
|
||||
const statLine = playerStats[slot.playerId] ?? { playerId: slot.playerId, playerName: slot.playerName };
|
||||
const statLine = playerStats[slot.playerId] ?? {
|
||||
playerId: slot.playerId,
|
||||
playerName: slot.playerName
|
||||
};
|
||||
|
||||
return (
|
||||
<Card key={slot.playerId}>
|
||||
<CardContent className="grid gap-3 pt-4">
|
||||
@@ -251,7 +251,7 @@ export function GameScoringClient() {
|
||||
<p className="text-xs text-muted-foreground">Batting order slot {index + 1}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid grid-cols-3 gap-2 sm:grid-cols-5">
|
||||
<div className="grid grid-cols-3 gap-2 sm:grid-cols-5 lg:grid-cols-6">
|
||||
{statFields.map(([field, label]) => (
|
||||
<label key={field}>
|
||||
<span className="field-label">{label}</span>
|
||||
@@ -259,7 +259,9 @@ export function GameScoringClient() {
|
||||
type="number"
|
||||
min={0}
|
||||
value={statLine[field] ?? ""}
|
||||
onChange={(event) => updatePlayerStat(slot.playerId, slot.playerName, field, event.target.value)}
|
||||
onChange={(event) =>
|
||||
updatePlayerStat(slot.playerId, slot.playerName, field, event.target.value)
|
||||
}
|
||||
/>
|
||||
</label>
|
||||
))}
|
||||
@@ -274,48 +276,6 @@ export function GameScoringClient() {
|
||||
</div>
|
||||
</SectionCard>
|
||||
) : null}
|
||||
|
||||
<SectionCard
|
||||
title="Saved Games"
|
||||
description="Previously saved games for the selected date can be deleted."
|
||||
>
|
||||
{savedGames.length > 0 ? (
|
||||
<div className="grid gap-3">
|
||||
{savedGames.map((game) => (
|
||||
<Card key={game._id}>
|
||||
<CardContent className="flex items-start justify-between gap-3 pt-4">
|
||||
<div className="flex items-start gap-3">
|
||||
<div className="mt-0.5 rounded-full bg-muted p-2 text-muted-foreground">
|
||||
<FileText className="h-4 w-4" />
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="text-sm font-semibold">
|
||||
{game.gameDate} · Game #{game.gameNumber}
|
||||
</h3>
|
||||
<p className="mt-1 text-xs text-muted-foreground">
|
||||
Lineup #{game.lineupNumber ?? "?"}
|
||||
{game.opponent ? ` · vs ${game.opponent}` : ""}
|
||||
{game.teamScore !== null && game.teamScore !== undefined && game.opponentScore !== null && game.opponentScore !== undefined
|
||||
? ` · ${game.teamScore}-${game.opponentScore}`
|
||||
: ""}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<Button size="sm" variant="destructive" className="gap-1" onClick={() => setGamePendingDelete(game)}>
|
||||
<Trash2 className="h-3.5 w-3.5" />
|
||||
Delete
|
||||
</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<EmptyState
|
||||
title="No saved games"
|
||||
description="Score and save a game to keep a dated history here."
|
||||
/>
|
||||
)}
|
||||
</SectionCard>
|
||||
</AppShell>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user