brackt/app/routes/admin._index.tsx

409 lines
15 KiB
TypeScript
Raw Normal View History

import { Link } from "react-router";
Add "Games to Score" widget to admin dashboard (#183) * Add "Games to Score" widget to admin dashboard Shows scoring events for today and tomorrow in a tabbed card, with status badges (Pending/Scored), a scored/total progress counter, and direct "Score" / "View" links to each event's results page. Excludes non-scoring schedule_event types so only actionable events surface. - app/models/scoring-event.ts: add getEventsForDates() model function that queries scoringEvents joined with sportsSeasons/sport for a given list of date strings - app/routes/admin._index.tsx: call getEventsForDates([today, tomorrow]) in the loader and render the GamesToScore component below the stats row - app/models/__tests__/scoring-event-dashboard.test.ts: 8 unit tests covering empty input, shape mapping, multi-date filtering, completed events, and the inArray where conditions https://claude.ai/code/session_014zKK15o5RJfTaodLvnZpF5 * Fix getEventsForDates to include bracket events by game scheduledAt The previous implementation only matched on scoringEvents.eventDate, which is often null for bracket events where individual game times are set on playoffMatchGames.scheduledAt instead. Switched to a two-step query mirroring getUpcomingEventsForDraftedParticipants: 1. selectDistinct event IDs via left joins through playoffMatches → playoffMatchGames, matching where eventDate IN dates OR any game's scheduledAt falls within the date range's timestamp bounds 2. findMany on the matched IDs with sportsSeason + sport relations Updated tests to cover the two-step flow, leftJoin usage, and the scheduledAt timestamp bounds appearing in the where clause. https://claude.ai/code/session_014zKK15o5RJfTaodLvnZpF5 * Document dual game date storage in CLAUDE.md scoringEvents.eventDate and playoffMatchGames.scheduledAt both hold game dates depending on the event type. Added a dedicated section under Important Patterns explaining when each is used and the two-step selectDistinct + findMany pattern required for correct date queries. https://claude.ai/code/session_014zKK15o5RJfTaodLvnZpF5 --------- Co-authored-by: Claude <noreply@anthropic.com>
2026-03-19 19:25:37 -07:00
import { useState } from "react";
import type { Route } from "./+types/admin._index";
import { findAllSports } from "~/models/sport";
import { findAllSportsSeasons } from "~/models/sports-season";
import { findAllSeasonTemplates } from "~/models/season-template";
Add "Games to Score" widget to admin dashboard (#183) * Add "Games to Score" widget to admin dashboard Shows scoring events for today and tomorrow in a tabbed card, with status badges (Pending/Scored), a scored/total progress counter, and direct "Score" / "View" links to each event's results page. Excludes non-scoring schedule_event types so only actionable events surface. - app/models/scoring-event.ts: add getEventsForDates() model function that queries scoringEvents joined with sportsSeasons/sport for a given list of date strings - app/routes/admin._index.tsx: call getEventsForDates([today, tomorrow]) in the loader and render the GamesToScore component below the stats row - app/models/__tests__/scoring-event-dashboard.test.ts: 8 unit tests covering empty input, shape mapping, multi-date filtering, completed events, and the inArray where conditions https://claude.ai/code/session_014zKK15o5RJfTaodLvnZpF5 * Fix getEventsForDates to include bracket events by game scheduledAt The previous implementation only matched on scoringEvents.eventDate, which is often null for bracket events where individual game times are set on playoffMatchGames.scheduledAt instead. Switched to a two-step query mirroring getUpcomingEventsForDraftedParticipants: 1. selectDistinct event IDs via left joins through playoffMatches → playoffMatchGames, matching where eventDate IN dates OR any game's scheduledAt falls within the date range's timestamp bounds 2. findMany on the matched IDs with sportsSeason + sport relations Updated tests to cover the two-step flow, leftJoin usage, and the scheduledAt timestamp bounds appearing in the where clause. https://claude.ai/code/session_014zKK15o5RJfTaodLvnZpF5 * Document dual game date storage in CLAUDE.md scoringEvents.eventDate and playoffMatchGames.scheduledAt both hold game dates depending on the event type. Added a dedicated section under Important Patterns explaining when each is used and the two-step selectDistinct + findMany pattern required for correct date queries. https://claude.ai/code/session_014zKK15o5RJfTaodLvnZpF5 --------- Co-authored-by: Claude <noreply@anthropic.com>
2026-03-19 19:25:37 -07:00
import { getEventsForDates } from "~/models/scoring-event";
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "~/components/ui/card";
import { Button } from "~/components/ui/button";
Add "Games to Score" widget to admin dashboard (#183) * Add "Games to Score" widget to admin dashboard Shows scoring events for today and tomorrow in a tabbed card, with status badges (Pending/Scored), a scored/total progress counter, and direct "Score" / "View" links to each event's results page. Excludes non-scoring schedule_event types so only actionable events surface. - app/models/scoring-event.ts: add getEventsForDates() model function that queries scoringEvents joined with sportsSeasons/sport for a given list of date strings - app/routes/admin._index.tsx: call getEventsForDates([today, tomorrow]) in the loader and render the GamesToScore component below the stats row - app/models/__tests__/scoring-event-dashboard.test.ts: 8 unit tests covering empty input, shape mapping, multi-date filtering, completed events, and the inArray where conditions https://claude.ai/code/session_014zKK15o5RJfTaodLvnZpF5 * Fix getEventsForDates to include bracket events by game scheduledAt The previous implementation only matched on scoringEvents.eventDate, which is often null for bracket events where individual game times are set on playoffMatchGames.scheduledAt instead. Switched to a two-step query mirroring getUpcomingEventsForDraftedParticipants: 1. selectDistinct event IDs via left joins through playoffMatches → playoffMatchGames, matching where eventDate IN dates OR any game's scheduledAt falls within the date range's timestamp bounds 2. findMany on the matched IDs with sportsSeason + sport relations Updated tests to cover the two-step flow, leftJoin usage, and the scheduledAt timestamp bounds appearing in the where clause. https://claude.ai/code/session_014zKK15o5RJfTaodLvnZpF5 * Document dual game date storage in CLAUDE.md scoringEvents.eventDate and playoffMatchGames.scheduledAt both hold game dates depending on the event type. Added a dedicated section under Important Patterns explaining when each is used and the two-step selectDistinct + findMany pattern required for correct date queries. https://claude.ai/code/session_014zKK15o5RJfTaodLvnZpF5 --------- Co-authored-by: Claude <noreply@anthropic.com>
2026-03-19 19:25:37 -07:00
import { Badge } from "~/components/ui/badge";
import { Trophy, Calendar, FolderKanban, ArrowRight, CheckCircle2, Clock, ExternalLink } from "lucide-react";
export function meta(): Route.MetaDescriptors {
return [{ title: "Admin - Brackt" }];
}
Fix oxlint warnings: no-shadow, consistent-function-scoping, no-non-null-assertion, and others (#196) * Fix no-shadow and consistent-function-scoping lint violations Resolves all 11 no-shadow and 16 consistent-function-scoping oxlint warnings and promotes both rules to errors in .oxlintrc.json. no-shadow: renamed Drizzle callback params (sports→s, matches→m, seasons→s) to avoid shadowing outer imports; removed shadowed destructures (eq, inArray) from where callbacks; renamed inner template→bracketTemplate, prev→currentTimers, season→ss, name→teamName (with name: teamName fix to preserve semantics). consistent-function-scoping: moved formatDate, getRankBadge, getMovementIndicator, getPositionBadge, getStatusBadge, toDateStr, elo (×2), weightedPick, sortByMatchNumber (×2) to module scope; moved formatTime (×2), isValidLeagueName, getDraftTimes, makeSeasonQueues to file scope in test files. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * Fix no-non-null-assertion lint violations and promote to error Eliminates all 208 no-non-null-assertion warnings across 38 files. Promotes typescript/no-non-null-assertion from warn to error in .oxlintrc.json. Fix patterns applied: - Map.get(key)! after .has() check → extract with get() + null guard - Map.get(key)! on pre-populated count maps → ?? 0 default - .set(id, map.get(id)! + 1) increment → ?? 0 before adding - participant1Id!/participant2Id! on DB matches → ?? "" fallback - array.find()! in tests → guard + throw or expect().toBeDefined() - bracketTemplateCache.get(id)! → null guard extract - Various nullable field accesses → optional chain or ?? default Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * Fix prefer-add-event-listener, no-unassigned-import, require-module-specifiers Resolves all 9 remaining non-console lint warnings and promotes all three rules to errors in .oxlintrc.json. - prefer-add-event-listener: converted onchange/onclick/onload assignments to addEventListener in useDraftNotifications.ts and admin.data-sync.tsx; stored changeHandler ref for proper cleanup with removeEventListener - no-unassigned-import: configured rule with allow list for legitimate side-effect imports (*.css, @testing-library/jest-dom, @testing-library/cypress/add-commands) - require-module-specifiers: removed redundant `export {}` from cypress/support/e2e.ts (file already has an import) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * Fix TypeScript errors from no-non-null-assertion fixes Two fixes introduced by the non-null assertion cleanup produced type errors: - scoring-event.ts: `?? ""` was wrong type for a participant object map; restructured to explicit null guards so TypeScript can narrow correctly - standings-sync/index.ts: `?? null` after name-match lookup lost the truthy guarantee, causing TS18047 on the write-back block; added `participant &&` guard before accessing its properties Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * Add npm run typecheck as Stop hook in Claude settings Runs a full project typecheck at the end of each Claude turn so type errors surface as feedback before the next message. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-21 10:59:51 -07:00
function toDateStr(d: Date) {
return d.toISOString().split("T")[0];
}
Add "Games to Score" widget to admin dashboard (#183) * Add "Games to Score" widget to admin dashboard Shows scoring events for today and tomorrow in a tabbed card, with status badges (Pending/Scored), a scored/total progress counter, and direct "Score" / "View" links to each event's results page. Excludes non-scoring schedule_event types so only actionable events surface. - app/models/scoring-event.ts: add getEventsForDates() model function that queries scoringEvents joined with sportsSeasons/sport for a given list of date strings - app/routes/admin._index.tsx: call getEventsForDates([today, tomorrow]) in the loader and render the GamesToScore component below the stats row - app/models/__tests__/scoring-event-dashboard.test.ts: 8 unit tests covering empty input, shape mapping, multi-date filtering, completed events, and the inArray where conditions https://claude.ai/code/session_014zKK15o5RJfTaodLvnZpF5 * Fix getEventsForDates to include bracket events by game scheduledAt The previous implementation only matched on scoringEvents.eventDate, which is often null for bracket events where individual game times are set on playoffMatchGames.scheduledAt instead. Switched to a two-step query mirroring getUpcomingEventsForDraftedParticipants: 1. selectDistinct event IDs via left joins through playoffMatches → playoffMatchGames, matching where eventDate IN dates OR any game's scheduledAt falls within the date range's timestamp bounds 2. findMany on the matched IDs with sportsSeason + sport relations Updated tests to cover the two-step flow, leftJoin usage, and the scheduledAt timestamp bounds appearing in the where clause. https://claude.ai/code/session_014zKK15o5RJfTaodLvnZpF5 * Document dual game date storage in CLAUDE.md scoringEvents.eventDate and playoffMatchGames.scheduledAt both hold game dates depending on the event type. Added a dedicated section under Important Patterns explaining when each is used and the two-step selectDistinct + findMany pattern required for correct date queries. https://claude.ai/code/session_014zKK15o5RJfTaodLvnZpF5 --------- Co-authored-by: Claude <noreply@anthropic.com>
2026-03-19 19:25:37 -07:00
function getTodayAndTomorrowDates() {
const today = new Date();
const tomorrow = new Date(today);
tomorrow.setDate(today.getDate() + 1);
return { today: toDateStr(today), tomorrow: toDateStr(tomorrow) };
}
export async function loader() {
Add "Games to Score" widget to admin dashboard (#183) * Add "Games to Score" widget to admin dashboard Shows scoring events for today and tomorrow in a tabbed card, with status badges (Pending/Scored), a scored/total progress counter, and direct "Score" / "View" links to each event's results page. Excludes non-scoring schedule_event types so only actionable events surface. - app/models/scoring-event.ts: add getEventsForDates() model function that queries scoringEvents joined with sportsSeasons/sport for a given list of date strings - app/routes/admin._index.tsx: call getEventsForDates([today, tomorrow]) in the loader and render the GamesToScore component below the stats row - app/models/__tests__/scoring-event-dashboard.test.ts: 8 unit tests covering empty input, shape mapping, multi-date filtering, completed events, and the inArray where conditions https://claude.ai/code/session_014zKK15o5RJfTaodLvnZpF5 * Fix getEventsForDates to include bracket events by game scheduledAt The previous implementation only matched on scoringEvents.eventDate, which is often null for bracket events where individual game times are set on playoffMatchGames.scheduledAt instead. Switched to a two-step query mirroring getUpcomingEventsForDraftedParticipants: 1. selectDistinct event IDs via left joins through playoffMatches → playoffMatchGames, matching where eventDate IN dates OR any game's scheduledAt falls within the date range's timestamp bounds 2. findMany on the matched IDs with sportsSeason + sport relations Updated tests to cover the two-step flow, leftJoin usage, and the scheduledAt timestamp bounds appearing in the where clause. https://claude.ai/code/session_014zKK15o5RJfTaodLvnZpF5 * Document dual game date storage in CLAUDE.md scoringEvents.eventDate and playoffMatchGames.scheduledAt both hold game dates depending on the event type. Added a dedicated section under Important Patterns explaining when each is used and the two-step selectDistinct + findMany pattern required for correct date queries. https://claude.ai/code/session_014zKK15o5RJfTaodLvnZpF5 --------- Co-authored-by: Claude <noreply@anthropic.com>
2026-03-19 19:25:37 -07:00
const { today, tomorrow } = getTodayAndTomorrowDates();
const [sports, sportsSeasons, templates, upcomingEvents] = await Promise.all([
findAllSports(),
findAllSportsSeasons(),
findAllSeasonTemplates(),
Add "Games to Score" widget to admin dashboard (#183) * Add "Games to Score" widget to admin dashboard Shows scoring events for today and tomorrow in a tabbed card, with status badges (Pending/Scored), a scored/total progress counter, and direct "Score" / "View" links to each event's results page. Excludes non-scoring schedule_event types so only actionable events surface. - app/models/scoring-event.ts: add getEventsForDates() model function that queries scoringEvents joined with sportsSeasons/sport for a given list of date strings - app/routes/admin._index.tsx: call getEventsForDates([today, tomorrow]) in the loader and render the GamesToScore component below the stats row - app/models/__tests__/scoring-event-dashboard.test.ts: 8 unit tests covering empty input, shape mapping, multi-date filtering, completed events, and the inArray where conditions https://claude.ai/code/session_014zKK15o5RJfTaodLvnZpF5 * Fix getEventsForDates to include bracket events by game scheduledAt The previous implementation only matched on scoringEvents.eventDate, which is often null for bracket events where individual game times are set on playoffMatchGames.scheduledAt instead. Switched to a two-step query mirroring getUpcomingEventsForDraftedParticipants: 1. selectDistinct event IDs via left joins through playoffMatches → playoffMatchGames, matching where eventDate IN dates OR any game's scheduledAt falls within the date range's timestamp bounds 2. findMany on the matched IDs with sportsSeason + sport relations Updated tests to cover the two-step flow, leftJoin usage, and the scheduledAt timestamp bounds appearing in the where clause. https://claude.ai/code/session_014zKK15o5RJfTaodLvnZpF5 * Document dual game date storage in CLAUDE.md scoringEvents.eventDate and playoffMatchGames.scheduledAt both hold game dates depending on the event type. Added a dedicated section under Important Patterns explaining when each is used and the two-step selectDistinct + findMany pattern required for correct date queries. https://claude.ai/code/session_014zKK15o5RJfTaodLvnZpF5 --------- Co-authored-by: Claude <noreply@anthropic.com>
2026-03-19 19:25:37 -07:00
getEventsForDates([today, tomorrow]),
]);
return {
stats: {
sportsCount: sports.length,
sportsSeasonsCount: sportsSeasons.length,
templatesCount: templates.length,
},
Add "Games to Score" widget to admin dashboard (#183) * Add "Games to Score" widget to admin dashboard Shows scoring events for today and tomorrow in a tabbed card, with status badges (Pending/Scored), a scored/total progress counter, and direct "Score" / "View" links to each event's results page. Excludes non-scoring schedule_event types so only actionable events surface. - app/models/scoring-event.ts: add getEventsForDates() model function that queries scoringEvents joined with sportsSeasons/sport for a given list of date strings - app/routes/admin._index.tsx: call getEventsForDates([today, tomorrow]) in the loader and render the GamesToScore component below the stats row - app/models/__tests__/scoring-event-dashboard.test.ts: 8 unit tests covering empty input, shape mapping, multi-date filtering, completed events, and the inArray where conditions https://claude.ai/code/session_014zKK15o5RJfTaodLvnZpF5 * Fix getEventsForDates to include bracket events by game scheduledAt The previous implementation only matched on scoringEvents.eventDate, which is often null for bracket events where individual game times are set on playoffMatchGames.scheduledAt instead. Switched to a two-step query mirroring getUpcomingEventsForDraftedParticipants: 1. selectDistinct event IDs via left joins through playoffMatches → playoffMatchGames, matching where eventDate IN dates OR any game's scheduledAt falls within the date range's timestamp bounds 2. findMany on the matched IDs with sportsSeason + sport relations Updated tests to cover the two-step flow, leftJoin usage, and the scheduledAt timestamp bounds appearing in the where clause. https://claude.ai/code/session_014zKK15o5RJfTaodLvnZpF5 * Document dual game date storage in CLAUDE.md scoringEvents.eventDate and playoffMatchGames.scheduledAt both hold game dates depending on the event type. Added a dedicated section under Important Patterns explaining when each is used and the two-step selectDistinct + findMany pattern required for correct date queries. https://claude.ai/code/session_014zKK15o5RJfTaodLvnZpF5 --------- Co-authored-by: Claude <noreply@anthropic.com>
2026-03-19 19:25:37 -07:00
upcomingEvents,
today,
tomorrow,
};
}
Add "Games to Score" widget to admin dashboard (#183) * Add "Games to Score" widget to admin dashboard Shows scoring events for today and tomorrow in a tabbed card, with status badges (Pending/Scored), a scored/total progress counter, and direct "Score" / "View" links to each event's results page. Excludes non-scoring schedule_event types so only actionable events surface. - app/models/scoring-event.ts: add getEventsForDates() model function that queries scoringEvents joined with sportsSeasons/sport for a given list of date strings - app/routes/admin._index.tsx: call getEventsForDates([today, tomorrow]) in the loader and render the GamesToScore component below the stats row - app/models/__tests__/scoring-event-dashboard.test.ts: 8 unit tests covering empty input, shape mapping, multi-date filtering, completed events, and the inArray where conditions https://claude.ai/code/session_014zKK15o5RJfTaodLvnZpF5 * Fix getEventsForDates to include bracket events by game scheduledAt The previous implementation only matched on scoringEvents.eventDate, which is often null for bracket events where individual game times are set on playoffMatchGames.scheduledAt instead. Switched to a two-step query mirroring getUpcomingEventsForDraftedParticipants: 1. selectDistinct event IDs via left joins through playoffMatches → playoffMatchGames, matching where eventDate IN dates OR any game's scheduledAt falls within the date range's timestamp bounds 2. findMany on the matched IDs with sportsSeason + sport relations Updated tests to cover the two-step flow, leftJoin usage, and the scheduledAt timestamp bounds appearing in the where clause. https://claude.ai/code/session_014zKK15o5RJfTaodLvnZpF5 * Document dual game date storage in CLAUDE.md scoringEvents.eventDate and playoffMatchGames.scheduledAt both hold game dates depending on the event type. Added a dedicated section under Important Patterns explaining when each is used and the two-step selectDistinct + findMany pattern required for correct date queries. https://claude.ai/code/session_014zKK15o5RJfTaodLvnZpF5 --------- Co-authored-by: Claude <noreply@anthropic.com>
2026-03-19 19:25:37 -07:00
function formatEventTime(eventStartsAt: string | null): string | null {
if (!eventStartsAt) return null;
const d = new Date(eventStartsAt);
return d.toLocaleTimeString([], { hour: "numeric", minute: "2-digit" });
}
function getEventTypeLabel(eventType: string): string {
switch (eventType) {
case "playoff_game": return "Bracket";
case "major_tournament": return "Tournament";
case "final_standings": return "Final Standings";
default: return eventType;
}
}
type DashboardEvent = {
id: string;
name: string;
eventDate: string | null;
eventStartsAt: string | null;
eventType: string;
isComplete: boolean;
completedAt: string | null;
sportsSeasonId: string;
sportsSeasonName: string;
sportName: string;
sportSlug: string | null;
};
function GamesToScore({
events,
today,
tomorrow,
}: {
events: DashboardEvent[];
today: string;
tomorrow: string;
}) {
const [activeTab, setActiveTab] = useState<"today" | "tomorrow">("today");
const todayEvents = events.filter((e) => e.eventDate === today);
const tomorrowEvents = events.filter((e) => e.eventDate === tomorrow);
const displayEvents = activeTab === "today" ? todayEvents : tomorrowEvents;
const scored = displayEvents.filter((e) => e.isComplete).length;
const total = displayEvents.length;
const pending = total - scored;
return (
<Card>
<CardHeader>
<div className="flex items-start justify-between">
<div>
<CardTitle>Games to Score</CardTitle>
<CardDescription>Scoring events that need your attention</CardDescription>
</div>
{total > 0 && (
<div className="text-right">
<div className="text-2xl font-bold text-foreground">
{scored}/{total}
</div>
<p className="text-xs text-muted-foreground">scored</p>
</div>
)}
</div>
{/* Tab selector */}
<div className="flex gap-2 mt-3">
<button
onClick={() => setActiveTab("today")}
className={`px-3 py-1.5 rounded-md text-sm font-medium transition-colors ${
activeTab === "today"
? "bg-primary text-primary-foreground"
: "text-muted-foreground hover:text-foreground hover:bg-muted"
}`}
>
Today
{todayEvents.length > 0 && (
<span className={`ml-1.5 rounded-full px-1.5 py-0.5 text-xs ${
activeTab === "today"
? "bg-primary-foreground/20 text-primary-foreground"
: "bg-muted text-muted-foreground"
}`}>
{todayEvents.filter((e) => !e.isComplete).length || todayEvents.length}
</span>
)}
</button>
<button
onClick={() => setActiveTab("tomorrow")}
className={`px-3 py-1.5 rounded-md text-sm font-medium transition-colors ${
activeTab === "tomorrow"
? "bg-primary text-primary-foreground"
: "text-muted-foreground hover:text-foreground hover:bg-muted"
}`}
>
Tomorrow
{tomorrowEvents.length > 0 && (
<span className={`ml-1.5 rounded-full px-1.5 py-0.5 text-xs ${
activeTab === "tomorrow"
? "bg-primary-foreground/20 text-primary-foreground"
: "bg-muted text-muted-foreground"
}`}>
{tomorrowEvents.length}
</span>
)}
</button>
</div>
</CardHeader>
<CardContent>
{displayEvents.length === 0 ? (
<div className="py-6 text-center text-muted-foreground text-sm">
No scoring events scheduled for {activeTab === "today" ? "today" : "tomorrow"}.
</div>
) : (
<>
{pending > 0 && (
<div className="mb-3 flex items-center gap-2 rounded-md bg-amber-50 dark:bg-amber-950/30 border border-amber-200 dark:border-amber-800 px-3 py-2 text-sm text-amber-800 dark:text-amber-300">
<Clock className="h-4 w-4 shrink-0" />
<span>{pending} event{pending !== 1 ? "s" : ""} still need{pending === 1 ? "s" : ""} scoring</span>
</div>
)}
{pending === 0 && total > 0 && (
<div className="mb-3 flex items-center gap-2 rounded-md bg-green-50 dark:bg-green-950/30 border border-green-200 dark:border-green-800 px-3 py-2 text-sm text-green-800 dark:text-green-300">
<CheckCircle2 className="h-4 w-4 shrink-0" />
<span>All events scored for {activeTab === "today" ? "today" : "tomorrow"}!</span>
</div>
)}
<div className="space-y-2">
{displayEvents.map((event) => (
<div
key={event.id}
className={`flex items-center justify-between rounded-lg border px-3 py-2.5 transition-colors ${
event.isComplete
? "border-border/50 bg-muted/30 opacity-70"
: "border-border bg-card hover:bg-muted/50"
}`}
>
<div className="min-w-0 flex-1">
<div className="flex items-center gap-2 flex-wrap">
<span className="text-sm font-medium truncate">{event.name}</span>
<Badge variant="outline" className="text-xs shrink-0">
{getEventTypeLabel(event.eventType)}
</Badge>
</div>
<div className="flex items-center gap-2 mt-0.5 text-xs text-muted-foreground flex-wrap">
<span className="font-medium text-foreground/70">{event.sportName}</span>
<span>·</span>
<span>{event.sportsSeasonName}</span>
{event.eventStartsAt && (
<>
<span>·</span>
<span>{formatEventTime(event.eventStartsAt)}</span>
</>
)}
</div>
</div>
<div className="flex items-center gap-2 ml-3 shrink-0">
{event.isComplete ? (
<Badge
variant="outline"
className="text-xs border-green-300 text-green-700 dark:border-green-700 dark:text-green-400 bg-green-50 dark:bg-green-950/30"
>
<CheckCircle2 className="h-3 w-3 mr-1" />
Scored
</Badge>
) : (
<Badge
variant="outline"
className="text-xs border-amber-300 text-amber-700 dark:border-amber-700 dark:text-amber-400 bg-amber-50 dark:bg-amber-950/30"
>
Pending
</Badge>
)}
<Button variant="outline" size="sm" asChild className="h-7 text-xs">
<Link
to={`/admin/sports-seasons/${event.sportsSeasonId}/events/${event.id}`}
>
{event.isComplete ? "View" : "Score"}
<ExternalLink className="h-3 w-3 ml-1" />
</Link>
</Button>
</div>
</div>
))}
</div>
</>
)}
</CardContent>
</Card>
);
}
export default function AdminDashboard({ loaderData }: Route.ComponentProps) {
Add "Games to Score" widget to admin dashboard (#183) * Add "Games to Score" widget to admin dashboard Shows scoring events for today and tomorrow in a tabbed card, with status badges (Pending/Scored), a scored/total progress counter, and direct "Score" / "View" links to each event's results page. Excludes non-scoring schedule_event types so only actionable events surface. - app/models/scoring-event.ts: add getEventsForDates() model function that queries scoringEvents joined with sportsSeasons/sport for a given list of date strings - app/routes/admin._index.tsx: call getEventsForDates([today, tomorrow]) in the loader and render the GamesToScore component below the stats row - app/models/__tests__/scoring-event-dashboard.test.ts: 8 unit tests covering empty input, shape mapping, multi-date filtering, completed events, and the inArray where conditions https://claude.ai/code/session_014zKK15o5RJfTaodLvnZpF5 * Fix getEventsForDates to include bracket events by game scheduledAt The previous implementation only matched on scoringEvents.eventDate, which is often null for bracket events where individual game times are set on playoffMatchGames.scheduledAt instead. Switched to a two-step query mirroring getUpcomingEventsForDraftedParticipants: 1. selectDistinct event IDs via left joins through playoffMatches → playoffMatchGames, matching where eventDate IN dates OR any game's scheduledAt falls within the date range's timestamp bounds 2. findMany on the matched IDs with sportsSeason + sport relations Updated tests to cover the two-step flow, leftJoin usage, and the scheduledAt timestamp bounds appearing in the where clause. https://claude.ai/code/session_014zKK15o5RJfTaodLvnZpF5 * Document dual game date storage in CLAUDE.md scoringEvents.eventDate and playoffMatchGames.scheduledAt both hold game dates depending on the event type. Added a dedicated section under Important Patterns explaining when each is used and the two-step selectDistinct + findMany pattern required for correct date queries. https://claude.ai/code/session_014zKK15o5RJfTaodLvnZpF5 --------- Co-authored-by: Claude <noreply@anthropic.com>
2026-03-19 19:25:37 -07:00
const { stats, upcomingEvents, today, tomorrow } = loaderData;
// Serialize dates to strings for the client component
const serializedEvents: DashboardEvent[] = upcomingEvents.map((e) => ({
...e,
eventStartsAt: e.eventStartsAt ? new Date(e.eventStartsAt).toISOString() : null,
completedAt: e.completedAt ? new Date(e.completedAt).toISOString() : null,
}));
return (
<div className="p-8">
<div className="mb-8">
<h1 className="text-3xl font-bold">Admin Dashboard</h1>
<p className="text-muted-foreground mt-2">
Manage sports, seasons, participants, and templates
</p>
</div>
<div className="grid gap-6 md:grid-cols-3 mb-8">
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">Total Sports</CardTitle>
<Trophy className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">{stats.sportsCount}</div>
<p className="text-xs text-muted-foreground mt-1">
Active sports in the system
</p>
</CardContent>
</Card>
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">
Sports Seasons
</CardTitle>
<Calendar className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">{stats.sportsSeasonsCount}</div>
<p className="text-xs text-muted-foreground mt-1">
Across all sports
</p>
</CardContent>
</Card>
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">Templates</CardTitle>
<FolderKanban className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">{stats.templatesCount}</div>
<p className="text-xs text-muted-foreground mt-1">
Season templates available
</p>
</CardContent>
</Card>
</div>
Add "Games to Score" widget to admin dashboard (#183) * Add "Games to Score" widget to admin dashboard Shows scoring events for today and tomorrow in a tabbed card, with status badges (Pending/Scored), a scored/total progress counter, and direct "Score" / "View" links to each event's results page. Excludes non-scoring schedule_event types so only actionable events surface. - app/models/scoring-event.ts: add getEventsForDates() model function that queries scoringEvents joined with sportsSeasons/sport for a given list of date strings - app/routes/admin._index.tsx: call getEventsForDates([today, tomorrow]) in the loader and render the GamesToScore component below the stats row - app/models/__tests__/scoring-event-dashboard.test.ts: 8 unit tests covering empty input, shape mapping, multi-date filtering, completed events, and the inArray where conditions https://claude.ai/code/session_014zKK15o5RJfTaodLvnZpF5 * Fix getEventsForDates to include bracket events by game scheduledAt The previous implementation only matched on scoringEvents.eventDate, which is often null for bracket events where individual game times are set on playoffMatchGames.scheduledAt instead. Switched to a two-step query mirroring getUpcomingEventsForDraftedParticipants: 1. selectDistinct event IDs via left joins through playoffMatches → playoffMatchGames, matching where eventDate IN dates OR any game's scheduledAt falls within the date range's timestamp bounds 2. findMany on the matched IDs with sportsSeason + sport relations Updated tests to cover the two-step flow, leftJoin usage, and the scheduledAt timestamp bounds appearing in the where clause. https://claude.ai/code/session_014zKK15o5RJfTaodLvnZpF5 * Document dual game date storage in CLAUDE.md scoringEvents.eventDate and playoffMatchGames.scheduledAt both hold game dates depending on the event type. Added a dedicated section under Important Patterns explaining when each is used and the two-step selectDistinct + findMany pattern required for correct date queries. https://claude.ai/code/session_014zKK15o5RJfTaodLvnZpF5 --------- Co-authored-by: Claude <noreply@anthropic.com>
2026-03-19 19:25:37 -07:00
{/* Games to Score widget — full width */}
<div className="mb-6">
<GamesToScore events={serializedEvents} today={today} tomorrow={tomorrow} />
</div>
<div className="grid gap-6 md:grid-cols-2">
<Card>
<CardHeader>
<CardTitle>Quick Actions</CardTitle>
<CardDescription>Common administrative tasks</CardDescription>
</CardHeader>
<CardContent className="space-y-2">
<Button variant="outline" className="w-full justify-between" asChild>
<Link to="/admin/sports/new">
Create New Sport
<ArrowRight className="h-4 w-4" />
</Link>
</Button>
<Button variant="outline" className="w-full justify-between" asChild>
<Link to="/admin/sports-seasons/new">
Create Sports Season
<ArrowRight className="h-4 w-4" />
</Link>
</Button>
<Button variant="outline" className="w-full justify-between" asChild>
<Link to="/admin/templates/new">
Create Season Template
<ArrowRight className="h-4 w-4" />
</Link>
</Button>
<Button variant="outline" className="w-full justify-between" asChild>
<Link to="/admin/standings-snapshots">
Manage Standings Snapshots
<ArrowRight className="h-4 w-4" />
</Link>
</Button>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle>Getting Started</CardTitle>
<CardDescription>Set up your sports system</CardDescription>
</CardHeader>
<CardContent className="space-y-3">
<div className="flex items-start space-x-3">
<div className="flex h-6 w-6 shrink-0 items-center justify-center rounded-full bg-primary text-primary-foreground text-xs font-medium">
1
</div>
<div className="space-y-1">
<p className="text-sm font-medium">Create Sports</p>
<p className="text-xs text-muted-foreground">
Add sports like NFL, NBA, Golf, etc.
</p>
</div>
</div>
<div className="flex items-start space-x-3">
<div className="flex h-6 w-6 shrink-0 items-center justify-center rounded-full bg-primary text-primary-foreground text-xs font-medium">
2
</div>
<div className="space-y-1">
<p className="text-sm font-medium">Create Sports Seasons</p>
<p className="text-xs text-muted-foreground">
Add specific seasons for each sport
</p>
</div>
</div>
<div className="flex items-start space-x-3">
<div className="flex h-6 w-6 shrink-0 items-center justify-center rounded-full bg-primary text-primary-foreground text-xs font-medium">
3
</div>
<div className="space-y-1">
<p className="text-sm font-medium">Add Participants</p>
<p className="text-xs text-muted-foreground">
Add teams or players to each sports season
</p>
</div>
</div>
<div className="flex items-start space-x-3">
<div className="flex h-6 w-6 shrink-0 items-center justify-center rounded-full bg-primary text-primary-foreground text-xs font-medium">
4
</div>
<div className="space-y-1">
<p className="text-sm font-medium">Create Templates</p>
<p className="text-xs text-muted-foreground">
Bundle sports seasons into templates for leagues
</p>
</div>
</div>
</CardContent>
</Card>
</div>
</div>
);
}