2025-10-12 21:54:49 -07:00
|
|
|
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";
|
2025-10-12 21:54:49 -07:00
|
|
|
import type { Route } from "./+types/admin._index";
|
2026-03-10 12:10:52 -07:00
|
|
|
|
2025-10-12 21:54:49 -07:00
|
|
|
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";
|
2025-10-12 21:54:49 -07:00
|
|
|
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";
|
2025-10-12 21:54:49 -07:00
|
|
|
|
2026-03-10 12:10:52 -07:00
|
|
|
export function meta(): Route.MetaDescriptors {
|
|
|
|
|
return [{ title: "Admin - Brackt" }];
|
|
|
|
|
}
|
|
|
|
|
|
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);
|
|
|
|
|
const toDateStr = (d: Date) => d.toISOString().split("T")[0];
|
|
|
|
|
return { today: toDateStr(today), tomorrow: toDateStr(tomorrow) };
|
|
|
|
|
}
|
|
|
|
|
|
2025-10-12 21:54:49 -07:00
|
|
|
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([
|
2025-10-12 21:54:49 -07:00
|
|
|
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]),
|
2025-10-12 21:54:49 -07:00
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
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,
|
2025-10-12 21:54:49 -07:00
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
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>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2025-10-12 21:54:49 -07:00
|
|
|
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,
|
|
|
|
|
}));
|
2025-10-12 21:54:49 -07:00
|
|
|
|
|
|
|
|
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>
|
|
|
|
|
|
2025-10-12 21:54:49 -07:00
|
|
|
<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>
|
2025-11-14 09:15:58 -08:00
|
|
|
<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>
|
2025-10-12 21:54:49 -07:00
|
|
|
</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>
|
|
|
|
|
);
|
|
|
|
|
}
|