253 lines
10 KiB
TypeScript
253 lines
10 KiB
TypeScript
"use client";
|
|
|
|
import { Alert, Box, Button, Card, CardContent, Divider, MenuItem, Stack, TextField, Typography } from "@mui/material";
|
|
import { useEffect, useMemo, useState } from "react";
|
|
import { AppShell } from "@/components/AppShell";
|
|
import { ConfirmDialog } from "@/components/ConfirmDialog";
|
|
import { EmptyState } from "@/components/EmptyState";
|
|
import { PlayerForm, emptyPlayerForm, toPlayerForm } from "@/components/PlayerForm";
|
|
import { SectionCard } from "@/components/SectionCard";
|
|
import { StatusBadge } from "@/components/StatusBadge";
|
|
import { fetchJson } from "@/lib/client";
|
|
import { formatPlayerName } from "@/lib/utils";
|
|
import type { PitchingAppearanceRecord, PlayerInput, PlayerRecord } from "@/lib/types";
|
|
|
|
export function RosterClient() {
|
|
const [players, setPlayers] = useState<PlayerRecord[]>([]);
|
|
const [loading, setLoading] = useState(true);
|
|
const [error, setError] = useState<string | null>(null);
|
|
const [message, setMessage] = useState<string | null>(null);
|
|
const [editingPlayerId, setEditingPlayerId] = useState<string | null>(null);
|
|
const [playerPendingDelete, setPlayerPendingDelete] = useState<PlayerRecord | null>(null);
|
|
const [playerForm, setPlayerForm] = useState<PlayerInput>(emptyPlayerForm());
|
|
const [appearanceForm, setAppearanceForm] = useState({
|
|
playerId: "",
|
|
gameDate: new Date().toISOString().slice(0, 10),
|
|
pitchCount: 20,
|
|
notes: ""
|
|
});
|
|
|
|
const editingPlayer = useMemo(
|
|
() => players.find((player) => player._id === editingPlayerId) ?? null,
|
|
[editingPlayerId, players]
|
|
);
|
|
|
|
async function loadPlayers() {
|
|
setLoading(true);
|
|
setError(null);
|
|
try {
|
|
const response = await fetchJson<PlayerRecord[]>("/api/players");
|
|
setPlayers(response);
|
|
if (response.length > 0 && !appearanceForm.playerId) {
|
|
setAppearanceForm((current) => ({ ...current, playerId: response[0]._id }));
|
|
}
|
|
} catch (loadError) {
|
|
setError(loadError instanceof Error ? loadError.message : "Failed to load roster.");
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
}
|
|
|
|
useEffect(() => {
|
|
void loadPlayers();
|
|
}, []);
|
|
|
|
useEffect(() => {
|
|
if (editingPlayer) {
|
|
setPlayerForm(toPlayerForm(editingPlayer));
|
|
} else {
|
|
setPlayerForm(emptyPlayerForm());
|
|
}
|
|
}, [editingPlayer]);
|
|
|
|
async function submitPlayer() {
|
|
try {
|
|
const url = editingPlayerId ? `/api/players/${editingPlayerId}` : "/api/players";
|
|
const method = editingPlayerId ? "PATCH" : "POST";
|
|
await fetchJson<PlayerRecord>(url, { method, body: JSON.stringify(playerForm) });
|
|
setMessage(editingPlayerId ? "Player updated." : "Player added.");
|
|
setEditingPlayerId(null);
|
|
setPlayerForm(emptyPlayerForm());
|
|
await loadPlayers();
|
|
} catch (submitError) {
|
|
setError(submitError instanceof Error ? submitError.message : "Unable to save player.");
|
|
}
|
|
}
|
|
|
|
async function deletePlayer(player: PlayerRecord) {
|
|
try {
|
|
await fetchJson(`/api/players/${player._id}`, { method: "DELETE" });
|
|
setMessage("Player removed.");
|
|
setPlayerPendingDelete(null);
|
|
await loadPlayers();
|
|
} catch (deleteError) {
|
|
setError(deleteError instanceof Error ? deleteError.message : "Unable to delete player.");
|
|
}
|
|
}
|
|
|
|
async function logAppearance() {
|
|
try {
|
|
await fetchJson<PitchingAppearanceRecord>("/api/pitching-appearances", {
|
|
method: "POST",
|
|
body: JSON.stringify(appearanceForm)
|
|
});
|
|
setMessage("Pitching appearance recorded.");
|
|
await loadPlayers();
|
|
} catch (submitError) {
|
|
setError(submitError instanceof Error ? submitError.message : "Unable to save pitching appearance.");
|
|
}
|
|
}
|
|
|
|
return (
|
|
<AppShell
|
|
title="Roster Management"
|
|
subtitle="Add players, update availability, and keep pitching history current from one screen."
|
|
>
|
|
<ConfirmDialog
|
|
open={playerPendingDelete !== null}
|
|
title="Remove Player?"
|
|
description={
|
|
playerPendingDelete
|
|
? `Remove ${formatPlayerName(playerPendingDelete.firstName, playerPendingDelete.lastName)} from the roster? This also removes their pitching history and saved lineup references.`
|
|
: ""
|
|
}
|
|
confirmLabel="Remove Player"
|
|
onCancel={() => setPlayerPendingDelete(null)}
|
|
onConfirm={() => (playerPendingDelete ? void deletePlayer(playerPendingDelete) : undefined)}
|
|
/>
|
|
|
|
{message ? <Alert severity="success">{message}</Alert> : null}
|
|
{error ? <Alert severity="error">{error}</Alert> : null}
|
|
|
|
<SectionCard
|
|
title={editingPlayerId ? "Edit Player" : "Add Player"}
|
|
description="Keep player details simple so a coach can update them quickly before game time."
|
|
>
|
|
<PlayerForm value={playerForm} onChange={setPlayerForm} onSubmit={() => void submitPlayer()} submitLabel={editingPlayerId ? "Save Changes" : "Add Player"} />
|
|
</SectionCard>
|
|
|
|
<SectionCard title="Pitch Count Log" description="Record the latest outing to keep rest recommendations accurate.">
|
|
<Box sx={{ display: "grid", gridTemplateColumns: { xs: "1fr", md: "repeat(3, 1fr)" }, gap: 2 }}>
|
|
<TextField
|
|
select
|
|
label="Player"
|
|
value={appearanceForm.playerId}
|
|
onChange={(event) => setAppearanceForm((current) => ({ ...current, playerId: event.target.value }))}
|
|
>
|
|
{players.map((player) => (
|
|
<MenuItem key={player._id} value={player._id}>
|
|
{formatPlayerName(player.firstName, player.lastName)}
|
|
</MenuItem>
|
|
))}
|
|
</TextField>
|
|
<TextField
|
|
label="Game Date"
|
|
type="date"
|
|
value={appearanceForm.gameDate}
|
|
onChange={(event) => setAppearanceForm((current) => ({ ...current, gameDate: event.target.value }))}
|
|
slotProps={{ inputLabel: { shrink: true } }}
|
|
/>
|
|
<TextField
|
|
label="Pitches Thrown"
|
|
type="number"
|
|
value={appearanceForm.pitchCount}
|
|
onChange={(event) => setAppearanceForm((current) => ({ ...current, pitchCount: Number(event.target.value) }))}
|
|
/>
|
|
<TextField
|
|
label="Notes"
|
|
multiline
|
|
rows={3}
|
|
value={appearanceForm.notes}
|
|
onChange={(event) => setAppearanceForm((current) => ({ ...current, notes: event.target.value }))}
|
|
sx={{ gridColumn: "1 / -1" }}
|
|
/>
|
|
<Box sx={{ gridColumn: "1 / -1" }}>
|
|
<Button variant="contained" onClick={() => void logAppearance()} disabled={players.length === 0}>
|
|
Save Pitching Appearance
|
|
</Button>
|
|
</Box>
|
|
</Box>
|
|
</SectionCard>
|
|
|
|
<SectionCard title="Current Roster" description="Tap edit for fast changes and check pitcher status at a glance.">
|
|
{loading ? (
|
|
<Typography color="text.secondary">Loading roster...</Typography>
|
|
) : players.length > 0 ? (
|
|
<Box sx={{ display: "grid", gridTemplateColumns: { xs: "1fr", xl: "repeat(2, 1fr)" }, gap: 2 }}>
|
|
{players.map((player) => (
|
|
<Card key={player._id} elevation={0} sx={{ border: "1px solid rgba(15, 38, 47, 0.08)" }}>
|
|
<CardContent sx={{ display: "grid", gap: 2 }}>
|
|
<Box
|
|
sx={{
|
|
display: "flex",
|
|
flexDirection: { xs: "column", sm: "row" },
|
|
justifyContent: "space-between",
|
|
gap: 1.5
|
|
}}
|
|
>
|
|
<Box>
|
|
<Typography variant="h6">
|
|
#{player.jerseyNumber} {formatPlayerName(player.firstName, player.lastName)}
|
|
</Typography>
|
|
<Typography color="text.secondary">
|
|
{player.primaryPosition}
|
|
{player.secondaryPosition ? ` / ${player.secondaryPosition}` : ""}
|
|
{player.tertiaryPosition ? ` / ${player.tertiaryPosition}` : ""}
|
|
</Typography>
|
|
</Box>
|
|
<StatusBadge status={player.pitcherAvailability.status} />
|
|
</Box>
|
|
|
|
<Divider />
|
|
|
|
<Box sx={{ display: "grid", gridTemplateColumns: { xs: "1fr", sm: "repeat(3, 1fr)" }, gap: 1.5 }}>
|
|
<Box>
|
|
<Typography variant="caption" color="text.secondary">
|
|
Bats/Throws
|
|
</Typography>
|
|
<Typography>{player.batsThrows}</Typography>
|
|
</Box>
|
|
<Box>
|
|
<Typography variant="caption" color="text.secondary">
|
|
Status
|
|
</Typography>
|
|
<Typography>{player.isActive ? "Active" : "Inactive"}</Typography>
|
|
</Box>
|
|
<Box>
|
|
<Typography variant="caption" color="text.secondary">
|
|
Pitching
|
|
</Typography>
|
|
<Typography>
|
|
{player.pitcherAvailability.lastPitchedDate
|
|
? `${player.pitcherAvailability.lastPitchCount} pitches on ${player.pitcherAvailability.lastPitchedDate}`
|
|
: "No history"}
|
|
</Typography>
|
|
</Box>
|
|
</Box>
|
|
|
|
{player.notes ? (
|
|
<Box sx={{ p: 1.5, borderRadius: 3, bgcolor: "rgba(15, 92, 77, 0.06)" }}>
|
|
<Typography color="text.secondary">{player.notes}</Typography>
|
|
</Box>
|
|
) : null}
|
|
|
|
<Stack direction="row" spacing={1.25}>
|
|
<Button variant="outlined" color="inherit" onClick={() => setEditingPlayerId(player._id)}>
|
|
Edit
|
|
</Button>
|
|
<Button variant="contained" color="error" onClick={() => setPlayerPendingDelete(player)}>
|
|
Delete
|
|
</Button>
|
|
</Stack>
|
|
</CardContent>
|
|
</Card>
|
|
))}
|
|
</Box>
|
|
) : (
|
|
<EmptyState title="No players yet" description="Add your first player above to start building the roster." />
|
|
)}
|
|
</SectionCard>
|
|
</AppShell>
|
|
);
|
|
}
|