Updating
This commit is contained in:
@@ -0,0 +1,249 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { AppShell } from "@/components/AppShell";
|
||||
import { EmptyState } from "@/components/EmptyState";
|
||||
import { SectionCard } from "@/components/SectionCard";
|
||||
import { StatusBadge } from "@/components/StatusBadge";
|
||||
import { FIELD_POSITIONS } from "@/lib/constants";
|
||||
import { fetchJson } from "@/lib/client";
|
||||
import { formatPlayerName } from "@/lib/utils";
|
||||
import type { GeneratedLineup, LineupRecord, PlayerRecord } from "@/lib/types";
|
||||
|
||||
export function LineupClient() {
|
||||
const [gameDate, setGameDate] = useState(new Date().toISOString().slice(0, 10));
|
||||
const [players, setPlayers] = useState<PlayerRecord[]>([]);
|
||||
const [selectedPlayerIds, setSelectedPlayerIds] = useState<string[]>([]);
|
||||
const [startingPitcherId, setStartingPitcherId] = useState<string>("");
|
||||
const [showAllPitchers, setShowAllPitchers] = useState(false);
|
||||
const [generated, setGenerated] = useState<GeneratedLineup | null>(null);
|
||||
const [message, setMessage] = useState<string | null>(null);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
async function loadPlayers(targetDate: string) {
|
||||
try {
|
||||
const response = await fetchJson<PlayerRecord[]>(`/api/players?gameDate=${targetDate}`);
|
||||
setPlayers(response);
|
||||
const activeIds = response.filter((player) => player.isActive).map((player) => player._id);
|
||||
setSelectedPlayerIds(activeIds);
|
||||
const firstEligible = response.find((player) => player.pitcherAvailability.status === "available");
|
||||
setStartingPitcherId(firstEligible?._id ?? response[0]?._id ?? "");
|
||||
} catch (loadError) {
|
||||
setError(loadError instanceof Error ? loadError.message : "Failed to load players.");
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
void loadPlayers(gameDate);
|
||||
}, [gameDate]);
|
||||
|
||||
const selectablePitchers = useMemo(
|
||||
() =>
|
||||
players.filter((player) =>
|
||||
selectedPlayerIds.includes(player._id) &&
|
||||
(showAllPitchers || player.pitcherAvailability.status !== "needs-rest")
|
||||
),
|
||||
[players, selectedPlayerIds, showAllPitchers]
|
||||
);
|
||||
|
||||
const selectedPlayers = useMemo(
|
||||
() => players.filter((player) => selectedPlayerIds.includes(player._id)),
|
||||
[players, selectedPlayerIds]
|
||||
);
|
||||
|
||||
function updateGenerated(next: GeneratedLineup | null) {
|
||||
setGenerated(next);
|
||||
}
|
||||
|
||||
async function generate() {
|
||||
try {
|
||||
const response = await fetchJson<GeneratedLineup>("/api/lineups/generate", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({
|
||||
gameDate,
|
||||
playerIds: selectedPlayerIds,
|
||||
startingPitcherId: startingPitcherId || null
|
||||
})
|
||||
});
|
||||
setGenerated(response);
|
||||
setMessage("Suggested lineup generated.");
|
||||
} catch (generateError) {
|
||||
setError(generateError instanceof Error ? generateError.message : "Unable to generate lineup.");
|
||||
}
|
||||
}
|
||||
|
||||
async function save() {
|
||||
if (!generated) return;
|
||||
|
||||
try {
|
||||
await fetchJson<LineupRecord>("/api/lineups", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({
|
||||
...generated,
|
||||
playerIds: selectedPlayerIds
|
||||
})
|
||||
});
|
||||
setMessage("Lineup saved.");
|
||||
} catch (saveError) {
|
||||
setError(saveError instanceof Error ? saveError.message : "Unable to save lineup.");
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<AppShell
|
||||
title="Generate Lineup"
|
||||
subtitle="Choose the active group, pick an eligible starter, then adjust the suggested lineup as needed."
|
||||
>
|
||||
{message ? <div className="banner banner-success">{message}</div> : null}
|
||||
{error ? <div className="banner banner-error">{error}</div> : null}
|
||||
|
||||
<SectionCard title="Game Setup" description="Start with the players who are available today.">
|
||||
<div className="form-grid">
|
||||
<label>
|
||||
<span>Game Date</span>
|
||||
<input type="date" value={gameDate} onChange={(event) => setGameDate(event.target.value)} />
|
||||
</label>
|
||||
<label>
|
||||
<span>Starting Pitcher</span>
|
||||
<select value={startingPitcherId} onChange={(event) => setStartingPitcherId(event.target.value)}>
|
||||
{selectablePitchers.map((player) => (
|
||||
<option key={player._id} value={player._id}>
|
||||
{formatPlayerName(player.firstName, player.lastName)}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</label>
|
||||
<label className="checkbox-row">
|
||||
<input type="checkbox" checked={showAllPitchers} onChange={(event) => setShowAllPitchers(event.target.checked)} />
|
||||
<span>Show pitchers who need rest</span>
|
||||
</label>
|
||||
<div className="full-width action-row">
|
||||
<button className="button" disabled={selectedPlayerIds.length === 0} onClick={() => void generate()}>
|
||||
Generate Suggested Lineup
|
||||
</button>
|
||||
<button className="button button-secondary" disabled={!generated} onClick={() => void save()}>
|
||||
Save Lineup
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="picker-grid">
|
||||
{players.map((player) => (
|
||||
<label key={player._id} className="picker-card">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={selectedPlayerIds.includes(player._id)}
|
||||
onChange={(event) =>
|
||||
setSelectedPlayerIds((current) =>
|
||||
event.target.checked ? [...current, player._id] : current.filter((id) => id !== player._id)
|
||||
)
|
||||
}
|
||||
/>
|
||||
<div>
|
||||
<strong>{formatPlayerName(player.firstName, player.lastName)}</strong>
|
||||
<p>
|
||||
#{player.jerseyNumber} {player.primaryPosition}
|
||||
</p>
|
||||
</div>
|
||||
<StatusBadge status={player.pitcherAvailability.status} />
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
</SectionCard>
|
||||
|
||||
{generated ? (
|
||||
<>
|
||||
<SectionCard title="Batting Order" description="Adjust any slot manually before saving.">
|
||||
<div className="list-stack">
|
||||
{generated.battingOrder.map((slot, index) => (
|
||||
<div key={`${slot.playerId}-${index}`} className="list-row">
|
||||
<div>
|
||||
<h3>Spot {index + 1}</h3>
|
||||
</div>
|
||||
<select
|
||||
value={slot.playerId}
|
||||
onChange={(event) => {
|
||||
const nextPlayer = selectedPlayers.find((player) => player._id === event.target.value);
|
||||
if (!nextPlayer) return;
|
||||
updateGenerated({
|
||||
...generated,
|
||||
battingOrder: generated.battingOrder.map((entry, entryIndex) =>
|
||||
entryIndex === index
|
||||
? {
|
||||
...entry,
|
||||
playerId: nextPlayer._id,
|
||||
playerName: formatPlayerName(nextPlayer.firstName, nextPlayer.lastName)
|
||||
}
|
||||
: entry
|
||||
)
|
||||
});
|
||||
}}
|
||||
>
|
||||
{selectedPlayers.map((player) => (
|
||||
<option key={player._id} value={player._id}>
|
||||
{formatPlayerName(player.firstName, player.lastName)}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</SectionCard>
|
||||
|
||||
<SectionCard title="Defensive Alignment" description="Assignments follow position preferences, but every field is editable.">
|
||||
<div className="list-stack">
|
||||
{FIELD_POSITIONS.map((position) => {
|
||||
const assignment = generated.defensiveAssignments.find((entry) => entry.position === position);
|
||||
return (
|
||||
<div key={position} className="list-row">
|
||||
<div>
|
||||
<h3>{position}</h3>
|
||||
</div>
|
||||
<select
|
||||
value={assignment?.playerId ?? ""}
|
||||
onChange={(event) => {
|
||||
const nextPlayer = selectedPlayers.find((player) => player._id === event.target.value);
|
||||
if (!nextPlayer) return;
|
||||
const nextAssignments = generated.defensiveAssignments.filter((entry) => entry.position !== position);
|
||||
nextAssignments.push({
|
||||
playerId: nextPlayer._id,
|
||||
playerName: formatPlayerName(nextPlayer.firstName, nextPlayer.lastName),
|
||||
position
|
||||
});
|
||||
updateGenerated({
|
||||
...generated,
|
||||
defensiveAssignments: nextAssignments
|
||||
});
|
||||
}}
|
||||
>
|
||||
<option value="">Unassigned</option>
|
||||
{selectedPlayers.map((player) => (
|
||||
<option key={player._id} value={player._id}>
|
||||
{formatPlayerName(player.firstName, player.lastName)}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
{generated.notes.length > 0 ? (
|
||||
<div className="note-block">
|
||||
{generated.notes.map((note) => (
|
||||
<p key={note}>{note}</p>
|
||||
))}
|
||||
</div>
|
||||
) : null}
|
||||
</SectionCard>
|
||||
</>
|
||||
) : (
|
||||
<SectionCard title="No Lineup Yet" description="Generate a lineup once your game-day roster is selected.">
|
||||
<EmptyState
|
||||
title="Waiting on lineup"
|
||||
description="Select your active players, choose a starter, and generate a suggested batting order and field setup."
|
||||
/>
|
||||
</SectionCard>
|
||||
)}
|
||||
</AppShell>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user