This commit is contained in:
Zeeshaun
2026-05-06 15:33:36 +00:00
parent 0de593c7d3
commit 9e56dae161
21 changed files with 1663 additions and 955 deletions
+128 -95
View File
@@ -1,7 +1,9 @@
"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";
@@ -16,6 +18,7 @@ export function RosterClient() {
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: "",
@@ -72,13 +75,10 @@ export function RosterClient() {
}
async function deletePlayer(player: PlayerRecord) {
if (!window.confirm(`Remove ${formatPlayerName(player.firstName, player.lastName)} from the roster?`)) {
return;
}
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.");
@@ -103,8 +103,21 @@ export function RosterClient() {
title="Roster Management"
subtitle="Add players, update availability, and keep pitching history current from one screen."
>
{message ? <div className="banner banner-success">{message}</div> : null}
{error ? <div className="banner banner-error">{error}</div> : null}
<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"}
@@ -114,104 +127,124 @@ export function RosterClient() {
</SectionCard>
<SectionCard title="Pitch Count Log" description="Record the latest outing to keep rest recommendations accurate.">
<div className="form-grid">
<label>
<span>Player</span>
<select
value={appearanceForm.playerId}
onChange={(event) => setAppearanceForm((current) => ({ ...current, playerId: event.target.value }))}
>
{players.map((player) => (
<option key={player._id} value={player._id}>
{formatPlayerName(player.firstName, player.lastName)}
</option>
))}
</select>
</label>
<label>
<span>Game Date</span>
<input
type="date"
value={appearanceForm.gameDate}
onChange={(event) => setAppearanceForm((current) => ({ ...current, gameDate: event.target.value }))}
/>
</label>
<label>
<span>Pitches Thrown</span>
<input
type="number"
value={appearanceForm.pitchCount}
onChange={(event) => setAppearanceForm((current) => ({ ...current, pitchCount: Number(event.target.value) }))}
/>
</label>
<label className="full-width">
<span>Notes</span>
<textarea
rows={3}
value={appearanceForm.notes}
onChange={(event) => setAppearanceForm((current) => ({ ...current, notes: event.target.value }))}
/>
</label>
<div className="full-width">
<button className="button" onClick={() => void logAppearance()} disabled={players.length === 0}>
<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>
</div>
</div>
</Button>
</Box>
</Box>
</SectionCard>
<SectionCard title="Current Roster" description="Tap edit for fast changes and check pitcher status at a glance.">
{loading ? (
<p className="muted-copy">Loading roster...</p>
<Typography color="text.secondary">Loading roster...</Typography>
) : players.length > 0 ? (
<div className="card-grid">
<Box sx={{ display: "grid", gridTemplateColumns: { xs: "1fr", xl: "repeat(2, 1fr)" }, gap: 2 }}>
{players.map((player) => (
<article key={player._id} className="player-card">
<div className="player-card-head">
<div>
<h3>
#{player.jerseyNumber} {formatPlayerName(player.firstName, player.lastName)}
</h3>
<p>
{player.primaryPosition}
{player.secondaryPosition ? ` / ${player.secondaryPosition}` : ""}
{player.tertiaryPosition ? ` / ${player.tertiaryPosition}` : ""}
</p>
</div>
<StatusBadge status={player.pitcherAvailability.status} />
</div>
<dl className="player-meta">
<div>
<dt>Bats/Throws</dt>
<dd>{player.batsThrows}</dd>
</div>
<div>
<dt>Status</dt>
<dd>{player.isActive ? "Active" : "Inactive"}</dd>
</div>
<div>
<dt>Pitching</dt>
<dd>
{player.pitcherAvailability.lastPitchedDate
? `${player.pitcherAvailability.lastPitchCount} pitches on ${player.pitcherAvailability.lastPitchedDate}`
: "No history"}
</dd>
</div>
</dl>
{player.notes ? <p className="player-notes">{player.notes}</p> : null}
<div className="action-row">
<button className="button button-secondary" onClick={() => setEditingPlayerId(player._id)}>
Edit
</button>
<button className="button button-danger" onClick={() => void deletePlayer(player)}>
Delete
</button>
</div>
</article>
<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>
))}
</div>
</Box>
) : (
<EmptyState title="No players yet" description="Add your first player above or load sample data from the dashboard." />
<EmptyState title="No players yet" description="Add your first player above to start building the roster." />
)}
</SectionCard>
</AppShell>