claude/sports-templates-conversion-ILWAM #71

Merged
chrisp merged 3 commits from claude/sports-templates-conversion-ILWAM into main 2026-06-04 21:31:49 +00:00
15 changed files with 12555 additions and 165 deletions

View file

@ -1,6 +1,8 @@
import { eq, and } from "drizzle-orm"; import { eq, and } from "drizzle-orm";
import { database } from "~/database/context"; import { database } from "~/database/context";
import * as schema from "~/database/schema"; import * as schema from "~/database/schema";
import { findDraftableSportsSeasonBySportId } from "~/models/sports-season";
import { logger } from "~/lib/logger";
export type SeasonSport = typeof schema.seasonSports.$inferSelect; export type SeasonSport = typeof schema.seasonSports.$inferSelect;
export type NewSeasonSport = typeof schema.seasonSports.$inferInsert; export type NewSeasonSport = typeof schema.seasonSports.$inferInsert;
@ -89,7 +91,6 @@ export async function applySportsFromTemplate(
): Promise<SeasonSport[]> { ): Promise<SeasonSport[]> {
const db = database(); const db = database();
// Get all sports from the template
const templateSports = await db.query.seasonTemplateSports.findMany({ const templateSports = await db.query.seasonTemplateSports.findMany({
where: eq(schema.seasonTemplateSports.templateId, templateId), where: eq(schema.seasonTemplateSports.templateId, templateId),
}); });
@ -98,12 +99,26 @@ export async function applySportsFromTemplate(
return []; return [];
} }
// Link them to the season const resolved = await Promise.all(
templateSports.map(async (ts) => {
const season = await findDraftableSportsSeasonBySportId(ts.sportId);
if (!season) {
logger.warn(`No draftable season found for sport ${ts.sportId} in template ${templateId}`);
}
return season ?? null;
})
);
const sportsSeasonIds = resolved
.filter((s): s is NonNullable<typeof s> => s !== null)
.map((s) => s.id);
if (sportsSeasonIds.length === 0) {
return [];
}
return await linkMultipleSportsToSeason( return await linkMultipleSportsToSeason(
templateSports.map((ts) => ({ sportsSeasonIds.map((sportsSeasonId) => ({ seasonId, sportsSeasonId }))
seasonId,
sportsSeasonId: ts.sportsSeasonId,
}))
); );
} }

View file

@ -7,14 +7,14 @@ export type NewSeasonTemplateSport = typeof schema.seasonTemplateSports.$inferIn
export async function addSportToTemplate( export async function addSportToTemplate(
templateId: string, templateId: string,
sportsSeasonId: string sportId: string
): Promise<SeasonTemplateSport> { ): Promise<SeasonTemplateSport> {
const db = database(); const db = database();
const [link] = await db const [link] = await db
.insert(schema.seasonTemplateSports) .insert(schema.seasonTemplateSports)
.values({ .values({
templateId, templateId,
sportsSeasonId, sportId,
}) })
.returning(); .returning();
return link; return link;
@ -32,7 +32,7 @@ export async function addMultipleSportsToTemplate(
export async function removeSportFromTemplate( export async function removeSportFromTemplate(
templateId: string, templateId: string,
sportsSeasonId: string sportId: string
): Promise<void> { ): Promise<void> {
const db = database(); const db = database();
await db await db
@ -40,7 +40,7 @@ export async function removeSportFromTemplate(
.where( .where(
and( and(
eq(schema.seasonTemplateSports.templateId, templateId), eq(schema.seasonTemplateSports.templateId, templateId),
eq(schema.seasonTemplateSports.sportsSeasonId, sportsSeasonId) eq(schema.seasonTemplateSports.sportId, sportId)
) )
); );
} }
@ -52,28 +52,23 @@ export async function findSeasonTemplateSportsByTemplateId(
return await db.query.seasonTemplateSports.findMany({ return await db.query.seasonTemplateSports.findMany({
where: eq(schema.seasonTemplateSports.templateId, templateId), where: eq(schema.seasonTemplateSports.templateId, templateId),
with: { with: {
sportsSeason: { sport: true,
with: {
sport: true,
},
},
}, },
}); });
} }
export async function findSeasonTemplateSportsBySportsSeasonId( export async function findSeasonTemplateSportsBySportId(
sportsSeasonId: string sportId: string
): Promise<SeasonTemplateSport[]> { ): Promise<SeasonTemplateSport[]> {
const db = database(); const db = database();
return await db.query.seasonTemplateSports.findMany({ return await db.query.seasonTemplateSports.findMany({
where: eq(schema.seasonTemplateSports.sportsSeasonId, sportsSeasonId), where: eq(schema.seasonTemplateSports.sportId, sportId),
with: { with: {
template: true, template: true,
}, },
}); });
} }
export async function deleteSeasonTemplateSport(id: string): Promise<void> { export async function deleteSeasonTemplateSport(id: string): Promise<void> {
const db = database(); const db = database();
await db.delete(schema.seasonTemplateSports).where(eq(schema.seasonTemplateSports.id, id)); await db.delete(schema.seasonTemplateSports).where(eq(schema.seasonTemplateSports.id, id));

View file

@ -29,18 +29,14 @@ export async function findSeasonTemplateById(id: string): Promise<SeasonTemplate
}); });
} }
export async function findSeasonTemplateWithSportsSeasons(id: string) { export async function findSeasonTemplateWithSports(id: string) {
const db = database(); const db = database();
return await db.query.seasonTemplates.findFirst({ return await db.query.seasonTemplates.findFirst({
where: eq(schema.seasonTemplates.id, id), where: eq(schema.seasonTemplates.id, id),
with: { with: {
seasonTemplateSports: { seasonTemplateSports: {
with: { with: {
sportsSeason: { sport: true,
with: {
sport: true,
},
},
}, },
}, },
}, },
@ -51,14 +47,6 @@ export async function findActiveSeasonTemplates(): Promise<SeasonTemplate[]> {
const db = database(); const db = database();
return await db.query.seasonTemplates.findMany({ return await db.query.seasonTemplates.findMany({
where: eq(schema.seasonTemplates.isActive, true), where: eq(schema.seasonTemplates.isActive, true),
orderBy: (templates, { desc }) => [desc(templates.year)],
});
}
export async function findSeasonTemplatesByYear(year: number): Promise<SeasonTemplate[]> {
const db = database();
return await db.query.seasonTemplates.findMany({
where: eq(schema.seasonTemplates.year, year),
orderBy: (templates, { asc }) => [asc(templates.name)], orderBy: (templates, { asc }) => [asc(templates.name)],
}); });
} }
@ -66,7 +54,7 @@ export async function findSeasonTemplatesByYear(year: number): Promise<SeasonTem
export async function findAllSeasonTemplates(): Promise<SeasonTemplate[]> { export async function findAllSeasonTemplates(): Promise<SeasonTemplate[]> {
const db = database(); const db = database();
return await db.query.seasonTemplates.findMany({ return await db.query.seasonTemplates.findMany({
orderBy: (templates, { desc }) => [desc(templates.year)], orderBy: (templates, { asc }) => [asc(templates.name)],
}); });
} }

View file

@ -151,6 +151,24 @@ export async function findDraftableSportsSeasons() {
})); }));
} }
export async function findDraftableSportsSeasonBySportId(sportId: string) {
const db = database();
const today = sql`CURRENT_DATE`;
return await db.query.sportsSeasons.findFirst({
where: (ss, ops) =>
ops.and(
ops.eq(ss.sportId, sportId),
lte(ss.draftOn, today),
gte(ss.draftOff, today),
isNull(ss.fantasySeasonId)
),
orderBy: (ss, { desc }) => [desc(ss.year), desc(ss.draftOn)],
with: {
sport: true,
},
});
}
export async function updateSportsSeason( export async function updateSportsSeason(
id: string, id: string,
data: Partial<NewSportsSeason> data: Partial<NewSportsSeason>

View file

@ -4,7 +4,7 @@ import type { Route } from "./+types/admin.templates.$id";
import { logger } from "~/lib/logger"; import { logger } from "~/lib/logger";
import { import {
findSeasonTemplateWithSportsSeasons, findSeasonTemplateWithSports,
updateSeasonTemplate, updateSeasonTemplate,
deleteSeasonTemplate, deleteSeasonTemplate,
setSeasonTemplateActive setSeasonTemplateActive
@ -13,7 +13,7 @@ import {
addSportToTemplate, addSportToTemplate,
removeSportFromTemplate removeSportFromTemplate
} from "~/models/season-template-sport"; } from "~/models/season-template-sport";
import { findAllSportsSeasons } from "~/models/sports-season"; import { findAllSports } from "~/models/sport";
import { Button } from "~/components/ui/button"; import { Button } from "~/components/ui/button";
import { Input } from "~/components/ui/input"; import { Input } from "~/components/ui/input";
import { Label } from "~/components/ui/label"; import { Label } from "~/components/ui/label";
@ -51,19 +51,15 @@ export function meta({ data }: Route.MetaArgs): Route.MetaDescriptors {
} }
export async function loader({ params }: Route.LoaderArgs) { export async function loader({ params }: Route.LoaderArgs) {
const template = await findSeasonTemplateWithSportsSeasons(params.id); const template = await findSeasonTemplateWithSports(params.id);
if (!template) { if (!template) {
throw new Response("Template not found", { status: 404 }); throw new Response("Template not found", { status: 404 });
} }
const allSportsSeasons = await findAllSportsSeasons(); const allSports = await findAllSports();
// Type assertion since we know the sport relation is included return { template, allSports };
return {
template,
allSportsSeasons: allSportsSeasons as Array<typeof allSportsSeasons[0] & { sport: { id: string; name: string; type: string; slug: string } }>
};
} }
export async function action({ request, params }: Route.ActionArgs) { export async function action({ request, params }: Route.ActionArgs) {
@ -82,45 +78,34 @@ export async function action({ request, params }: Route.ActionArgs) {
} }
if (intent === "add-sport") { if (intent === "add-sport") {
const sportsSeasonId = formData.get("sportsSeasonId"); const sportId = formData.get("sportId");
if (typeof sportsSeasonId === "string") { if (typeof sportId === "string") {
await addSportToTemplate(params.id, sportsSeasonId); await addSportToTemplate(params.id, sportId);
} }
return { success: true }; return { success: true };
} }
if (intent === "remove-sport") { if (intent === "remove-sport") {
const sportsSeasonId = formData.get("sportsSeasonId"); const sportId = formData.get("sportId");
if (typeof sportsSeasonId === "string") { if (typeof sportId === "string") {
await removeSportFromTemplate(params.id, sportsSeasonId); await removeSportFromTemplate(params.id, sportId);
} }
return { success: true }; return { success: true };
} }
// Update template // Update template
const name = formData.get("name"); const name = formData.get("name");
const year = formData.get("year");
const description = formData.get("description"); const description = formData.get("description");
if (typeof name !== "string" || !name.trim()) { if (typeof name !== "string" || !name.trim()) {
return { error: "Template name is required" }; return { error: "Template name is required" };
} }
if (typeof year !== "string") {
return { error: "Year is required" };
}
const yearNum = parseInt(year, 10);
if (isNaN(yearNum) || yearNum < 2000 || yearNum > 2100) {
return { error: "Year must be between 2000 and 2100" };
}
try { try {
await updateSeasonTemplate(params.id, { await updateSeasonTemplate(params.id, {
name: name.trim(), name: name.trim(),
year: yearNum,
description: typeof description === "string" && description.trim() ? description.trim() : null, description: typeof description === "string" && description.trim() ? description.trim() : null,
}); });
@ -132,15 +117,15 @@ export async function action({ request, params }: Route.ActionArgs) {
} }
export default function EditTemplate({ loaderData, actionData }: Route.ComponentProps) { export default function EditTemplate({ loaderData, actionData }: Route.ComponentProps) {
const { template, allSportsSeasons } = loaderData; const { template, allSports } = loaderData;
const [selectedSportId, setSelectedSportId] = useState(""); const [selectedSportId, setSelectedSportId] = useState("");
const includedSeasonIds = new Set( const includedSportIds = new Set(
template.seasonTemplateSports.map((s) => s.sportsSeasonId) template.seasonTemplateSports.map((s) => s.sportId)
); );
const availableSportsSeasons = allSportsSeasons.filter( const availableSports = allSports.filter(
(s) => !includedSeasonIds.has(s.id) (s) => !includedSportIds.has(s.id)
); );
return ( return (
@ -170,19 +155,6 @@ export default function EditTemplate({ loaderData, actionData }: Route.Component
/> />
</div> </div>
<div className="space-y-2">
<Label htmlFor="year">Year</Label>
<Input
id="year"
name="year"
type="number"
min="2000"
max="2100"
defaultValue={template.year}
required
/>
</div>
<div className="space-y-2"> <div className="space-y-2">
<Label htmlFor="description">Description (Optional)</Label> <Label htmlFor="description">Description (Optional)</Label>
<Textarea <Textarea
@ -236,9 +208,9 @@ export default function EditTemplate({ loaderData, actionData }: Route.Component
<Card> <Card>
<CardHeader> <CardHeader>
<CardTitle>Sports Seasons</CardTitle> <CardTitle>Sports</CardTitle>
<CardDescription> <CardDescription>
Add sports seasons to this template Add sports to this template. The active season for each sport will be used automatically when a league is created.
</CardDescription> </CardDescription>
</CardHeader> </CardHeader>
<CardContent className="space-y-4"> <CardContent className="space-y-4">
@ -249,17 +221,12 @@ export default function EditTemplate({ loaderData, actionData }: Route.Component
key={templateSport.id} key={templateSport.id}
className="flex items-center justify-between p-3 border rounded-lg" className="flex items-center justify-between p-3 border rounded-lg"
> >
<div> <p className="font-medium">
<p className="font-medium"> {templateSport.sport.name}
{templateSport.sportsSeason.sport.name} - {templateSport.sportsSeason.name} </p>
</p>
<p className="text-sm text-muted-foreground">
{templateSport.sportsSeason.year} {templateSport.sportsSeason.scoringType.replace("_", " ")}
</p>
</div>
<Form method="post"> <Form method="post">
<input type="hidden" name="intent" value="remove-sport" /> <input type="hidden" name="intent" value="remove-sport" />
<input type="hidden" name="sportsSeasonId" value={templateSport.sportsSeasonId} /> <input type="hidden" name="sportId" value={templateSport.sportId} />
<Button <Button
type="submit" type="submit"
variant="ghost" variant="ghost"
@ -274,22 +241,22 @@ export default function EditTemplate({ loaderData, actionData }: Route.Component
</div> </div>
)} )}
{availableSportsSeasons.length > 0 ? ( {availableSports.length > 0 ? (
<Form method="post" className="flex gap-2"> <Form method="post" className="flex gap-2">
<input type="hidden" name="intent" value="add-sport" /> <input type="hidden" name="intent" value="add-sport" />
<Select <Select
name="sportsSeasonId" name="sportId"
value={selectedSportId} value={selectedSportId}
onValueChange={setSelectedSportId} onValueChange={setSelectedSportId}
required required
> >
<SelectTrigger className="flex-1"> <SelectTrigger className="flex-1">
<SelectValue placeholder="Select a sports season" /> <SelectValue placeholder="Select a sport" />
</SelectTrigger> </SelectTrigger>
<SelectContent> <SelectContent>
{availableSportsSeasons.map((season) => ( {availableSports.map((sport) => (
<SelectItem key={season.id} value={season.id}> <SelectItem key={sport.id} value={sport.id}>
{season.sport.name} - {season.name} ({season.year}) {sport.name}
</SelectItem> </SelectItem>
))} ))}
</SelectContent> </SelectContent>
@ -300,7 +267,7 @@ export default function EditTemplate({ loaderData, actionData }: Route.Component
</Form> </Form>
) : ( ) : (
<p className="text-sm text-muted-foreground text-center py-4"> <p className="text-sm text-muted-foreground text-center py-4">
All available sports seasons have been added All available sports have been added
</p> </p>
)} )}
</CardContent> </CardContent>

View file

@ -22,27 +22,15 @@ export function meta(): Route.MetaDescriptors {
export async function action({ request }: Route.ActionArgs) { export async function action({ request }: Route.ActionArgs) {
const formData = await request.formData(); const formData = await request.formData();
const name = formData.get("name"); const name = formData.get("name");
const year = formData.get("year");
const description = formData.get("description"); const description = formData.get("description");
// Validation
if (typeof name !== "string" || !name.trim()) { if (typeof name !== "string" || !name.trim()) {
return { error: "Template name is required" }; return { error: "Template name is required" };
} }
if (typeof year !== "string") {
return { error: "Year is required" };
}
const yearNum = parseInt(year, 10);
if (isNaN(yearNum) || yearNum < 2000 || yearNum > 2100) {
return { error: "Year must be between 2000 and 2100" };
}
try { try {
const template = await createSeasonTemplate({ const template = await createSeasonTemplate({
name: name.trim(), name: name.trim(),
year: yearNum,
description: typeof description === "string" && description.trim() ? description.trim() : null, description: typeof description === "string" && description.trim() ? description.trim() : null,
isActive: true, isActive: true,
}); });
@ -55,15 +43,13 @@ export async function action({ request }: Route.ActionArgs) {
} }
export default function NewTemplate({ actionData }: Route.ComponentProps) { export default function NewTemplate({ actionData }: Route.ComponentProps) {
const currentYear = new Date().getFullYear();
return ( return (
<div className="p-8"> <div className="p-8">
<div className="max-w-2xl"> <div className="max-w-2xl">
<div className="mb-6"> <div className="mb-6">
<h1 className="text-3xl font-bold">Create Season Template</h1> <h1 className="text-3xl font-bold">Create Season Template</h1>
<p className="text-muted-foreground mt-1"> <p className="text-muted-foreground mt-1">
Bundle sports seasons together for league commissioners Bundle sports together for league commissioners
</p> </p>
</div> </div>
@ -82,7 +68,7 @@ export default function NewTemplate({ actionData }: Route.ComponentProps) {
id="name" id="name"
name="name" name="name"
type="text" type="text"
placeholder="e.g., 2025 Full Season, 2025 Playoffs Only" placeholder="e.g., Full Season, Playoffs Only"
required required
/> />
<p className="text-sm text-muted-foreground"> <p className="text-sm text-muted-foreground">
@ -90,25 +76,12 @@ export default function NewTemplate({ actionData }: Route.ComponentProps) {
</p> </p>
</div> </div>
<div className="space-y-2">
<Label htmlFor="year">Year</Label>
<Input
id="year"
name="year"
type="number"
min="2000"
max="2100"
defaultValue={currentYear}
required
/>
</div>
<div className="space-y-2"> <div className="space-y-2">
<Label htmlFor="description">Description (Optional)</Label> <Label htmlFor="description">Description (Optional)</Label>
<Textarea <Textarea
id="description" id="description"
name="description" name="description"
placeholder="Describe what sports seasons are included in this template" placeholder="Describe what sports are included in this template"
rows={3} rows={3}
/> />
</div> </div>
@ -138,7 +111,7 @@ export default function NewTemplate({ actionData }: Route.ComponentProps) {
<CardContent className="text-sm text-foreground/80"> <CardContent className="text-sm text-foreground/80">
<p>After creating the template, you'll be able to:</p> <p>After creating the template, you'll be able to:</p>
<ul className="list-disc list-inside mt-2 space-y-1"> <ul className="list-disc list-inside mt-2 space-y-1">
<li>Add sports seasons to the template</li> <li>Add sports to the template</li>
<li>Mark which sports are required vs optional</li> <li>Mark which sports are required vs optional</li>
<li>Set the number of flex spots</li> <li>Set the number of flex spots</li>
</ul> </ul>

View file

@ -77,7 +77,6 @@ export default function AdminTemplates({ loaderData }: Route.ComponentProps) {
<TableHeader> <TableHeader>
<TableRow> <TableRow>
<TableHead>Name</TableHead> <TableHead>Name</TableHead>
<TableHead>Year</TableHead>
<TableHead>Status</TableHead> <TableHead>Status</TableHead>
<TableHead>Description</TableHead> <TableHead>Description</TableHead>
<TableHead className="text-right">Actions</TableHead> <TableHead className="text-right">Actions</TableHead>
@ -87,7 +86,6 @@ export default function AdminTemplates({ loaderData }: Route.ComponentProps) {
{templates.map((template) => ( {templates.map((template) => (
<TableRow key={template.id}> <TableRow key={template.id}>
<TableCell className="font-medium">{template.name}</TableCell> <TableCell className="font-medium">{template.name}</TableCell>
<TableCell>{template.year}</TableCell>
<TableCell> <TableCell>
<Badge variant={template.isActive ? "default" : "secondary"}> <Badge variant={template.isActive ? "default" : "secondary"}>
{template.isActive ? "Active" : "Inactive"} {template.isActive ? "Active" : "Inactive"}

View file

@ -10,7 +10,7 @@ import { createCommissioner } from "~/models/commissioner";
import { createSeason } from "~/models/season"; import { createSeason } from "~/models/season";
import { linkMultipleSportsToSeason } from "~/models/season-sport"; import { linkMultipleSportsToSeason } from "~/models/season-sport";
import { createManyTeams } from "~/models/team"; import { createManyTeams } from "~/models/team";
import { findActiveSeasonTemplates, findSeasonTemplateWithSportsSeasons } from "~/models/season-template"; import { findActiveSeasonTemplates, findSeasonTemplateWithSports } from "~/models/season-template";
import { findDraftableSportsSeasons } from "~/models/sports-season"; import { findDraftableSportsSeasons } from "~/models/sports-season";
import { findUserById, updateUser } from "~/models/user"; import { findUserById, updateUser } from "~/models/user";
import { generateUniqueTeamNames, prependOwnerToTeamName } from "~/utils/team-names"; import { generateUniqueTeamNames, prependOwnerToTeamName } from "~/utils/team-names";
@ -57,7 +57,6 @@ type SportSeason = {
type Template = { type Template = {
id: string; id: string;
name: string; name: string;
year: number;
description: string | null; description: string | null;
sportsSeasonIds: string[]; sportsSeasonIds: string[];
}; };
@ -104,14 +103,18 @@ export async function loader(args: Route.LoaderArgs) {
const templates = await findActiveSeasonTemplates(); const templates = await findActiveSeasonTemplates();
const allSportsSeasons = await findDraftableSportsSeasons(); const allSportsSeasons = await findDraftableSportsSeasons();
const draftableSeasonBySportId = new Map(
allSportsSeasons.map((s) => [s.sportId, s])
);
const templatesWithSports = await Promise.all( const templatesWithSports = await Promise.all(
templates.map(async (template) => { templates.map(async (template) => {
const t = await findSeasonTemplateWithSportsSeasons(template.id); const t = await findSeasonTemplateWithSports(template.id);
return { return {
...template, ...template,
sportsSeasonIds: t?.seasonTemplateSports?.map( sportsSeasonIds: (t?.seasonTemplateSports ?? [])
(ts: { sportsSeason: { id: string } }) => ts.sportsSeason.id .map((ts: { sportId: string }) => draftableSeasonBySportId.get(ts.sportId)?.id)
) ?? [], .filter((id): id is string => id !== null && id !== undefined),
}; };
}) })
); );

View file

@ -80,14 +80,11 @@ const ImportDataSchema = z.object({
seasonTemplates: z.array( seasonTemplates: z.array(
z.object({ z.object({
name: z.string(), name: z.string(),
year: z.number(),
description: z.string().nullable(), description: z.string().nullable(),
isActive: z.boolean(), isActive: z.boolean(),
sports: z.array( sports: z.array(
z.object({ z.object({
sportSlug: z.string(), sportSlug: z.string(),
seasonName: z.string(),
seasonYear: z.number(),
}) })
), ),
}) })
@ -133,10 +130,10 @@ export async function exportSportsDataToJSON(): Promise<ExportData> {
db.query.seasonTemplates.findMany({ db.query.seasonTemplates.findMany({
with: { with: {
seasonTemplateSports: { seasonTemplateSports: {
with: { sportsSeason: { with: { sport: true } } }, with: { sport: true },
}, },
}, },
orderBy: (t, { desc }) => [desc(t.year)], orderBy: (t, { asc }) => [asc(t.name)],
}), }),
]); ]);
@ -197,13 +194,10 @@ export async function exportSportsDataToJSON(): Promise<ExportData> {
})), })),
seasonTemplates: seasonTemplates.map((t) => ({ seasonTemplates: seasonTemplates.map((t) => ({
name: t.name, name: t.name,
year: t.year,
description: t.description, description: t.description,
isActive: t.isActive, isActive: t.isActive,
sports: t.seasonTemplateSports.map((ts) => ({ sports: t.seasonTemplateSports.map((ts) => ({
sportSlug: ts.sportsSeason.sport.slug, sportSlug: ts.sport.slug,
seasonName: ts.sportsSeason.name,
seasonYear: ts.sportsSeason.year,
})), })),
})), })),
}; };
@ -529,10 +523,7 @@ export async function importSportsDataFromJSON(
for (const template of importData.seasonTemplates) { for (const template of importData.seasonTemplates) {
const existing = await tx.query.seasonTemplates.findFirst({ const existing = await tx.query.seasonTemplates.findFirst({
where: and( where: eq(schema.seasonTemplates.name, template.name),
eq(schema.seasonTemplates.name, template.name),
eq(schema.seasonTemplates.year, template.year)
),
}); });
let templateId: string; let templateId: string;
@ -561,7 +552,6 @@ export async function importSportsDataFromJSON(
.insert(schema.seasonTemplates) .insert(schema.seasonTemplates)
.values({ .values({
name: template.name, name: template.name,
year: template.year,
description: template.description, description: template.description,
isActive: template.isActive, isActive: template.isActive,
}) })
@ -571,12 +561,11 @@ export async function importSportsDataFromJSON(
} }
for (const templateSport of template.sports) { for (const templateSport of template.sports) {
const seasonKey = `${templateSport.sportSlug}:${templateSport.seasonName}:${templateSport.seasonYear}`; const sportId = sportIdMap.get(templateSport.sportSlug);
const sportsSeasonId = sportsSeasonIdMap.get(seasonKey); if (sportId) {
if (sportsSeasonId) {
await tx.insert(schema.seasonTemplateSports).values({ await tx.insert(schema.seasonTemplateSports).values({
templateId, templateId,
sportsSeasonId, sportId,
}); });
} }
} }

View file

@ -190,11 +190,12 @@ export const seasonTemplates = pgTable("season_templates", {
id: uuid("id").primaryKey().defaultRandom(), id: uuid("id").primaryKey().defaultRandom(),
name: varchar("name", { length: 255 }).notNull(), name: varchar("name", { length: 255 }).notNull(),
description: text("description"), description: text("description"),
year: integer("year").notNull(),
isActive: boolean("is_active").notNull().default(true), isActive: boolean("is_active").notNull().default(true),
createdAt: timestamp("created_at").defaultNow().notNull(), createdAt: timestamp("created_at").defaultNow().notNull(),
updatedAt: timestamp("updated_at").defaultNow().notNull(), updatedAt: timestamp("updated_at").defaultNow().notNull(),
}); }, (t) => ({
uniqueName: uniqueIndex("season_templates_name_unique").on(t.name),
}));
export const seasons = pgTable("seasons", { export const seasons = pgTable("seasons", {
id: uuid("id").primaryKey().defaultRandom(), id: uuid("id").primaryKey().defaultRandom(),
@ -567,11 +568,13 @@ export const seasonTemplateSports = pgTable("season_template_sports", {
templateId: uuid("template_id") templateId: uuid("template_id")
.notNull() .notNull()
.references(() => seasonTemplates.id, { onDelete: "cascade" }), .references(() => seasonTemplates.id, { onDelete: "cascade" }),
sportsSeasonId: uuid("sports_season_id") sportId: uuid("sport_id")
.notNull() .notNull()
.references(() => sportsSeasons.id, { onDelete: "cascade" }), .references(() => sports.id, { onDelete: "cascade" }),
createdAt: timestamp("created_at").defaultNow().notNull(), createdAt: timestamp("created_at").defaultNow().notNull(),
}); }, (t) => ({
uniqueTemplateSport: uniqueIndex("season_template_sports_template_sport_unique").on(t.templateId, t.sportId),
}));
export const seasonSports = pgTable("season_sports", { export const seasonSports = pgTable("season_sports", {
id: uuid("id").primaryKey().defaultRandom(), id: uuid("id").primaryKey().defaultRandom(),
@ -974,6 +977,7 @@ export const sportsRelations = relations(sports, ({ many }) => ({
sportsSeasons: many(sportsSeasons), sportsSeasons: many(sportsSeasons),
tournaments: many(tournaments), tournaments: many(tournaments),
participants: many(participants), participants: many(participants),
seasonTemplateSports: many(seasonTemplateSports),
})); }));
export const sportsSeasonsRelations = relations(sportsSeasons, ({ one, many }) => ({ export const sportsSeasonsRelations = relations(sportsSeasons, ({ one, many }) => ({
@ -988,7 +992,6 @@ export const sportsSeasonsRelations = relations(sportsSeasons, ({ one, many }) =
participants: many(seasonParticipants), participants: many(seasonParticipants),
simulatorConfig: one(sportsSeasonSimulatorConfigs), simulatorConfig: one(sportsSeasonSimulatorConfigs),
simulatorInputs: many(seasonParticipantSimulatorInputs), simulatorInputs: many(seasonParticipantSimulatorInputs),
seasonTemplateSports: many(seasonTemplateSports),
seasonSports: many(seasonSports), seasonSports: many(seasonSports),
participantResults: many(seasonParticipantResults), participantResults: many(seasonParticipantResults),
regularSeasonStandings: many(regularSeasonStandings), regularSeasonStandings: many(regularSeasonStandings),
@ -1092,9 +1095,9 @@ export const seasonTemplateSportsRelations = relations(seasonTemplateSports, ({
fields: [seasonTemplateSports.templateId], fields: [seasonTemplateSports.templateId],
references: [seasonTemplates.id], references: [seasonTemplates.id],
}), }),
sportsSeason: one(sportsSeasons, { sport: one(sports, {
fields: [seasonTemplateSports.sportsSeasonId], fields: [seasonTemplateSports.sportId],
references: [sportsSeasons.id], references: [sports.id],
}), }),
})); }));

View file

@ -0,0 +1,13 @@
TRUNCATE TABLE "season_template_sports";
--> statement-breakpoint
ALTER TABLE "season_template_sports" DROP CONSTRAINT "season_template_sports_sports_season_id_sports_seasons_id_fk";
--> statement-breakpoint
ALTER TABLE "season_template_sports" ADD COLUMN "sport_id" uuid NOT NULL;--> statement-breakpoint
DO $$ BEGIN
ALTER TABLE "season_template_sports" ADD CONSTRAINT "season_template_sports_sport_id_sports_id_fk" FOREIGN KEY ("sport_id") REFERENCES "public"."sports"("id") ON DELETE cascade ON UPDATE no action;
EXCEPTION
WHEN duplicate_object THEN null;
END $$;
--> statement-breakpoint
ALTER TABLE "season_template_sports" DROP COLUMN IF EXISTS "sports_season_id";--> statement-breakpoint
ALTER TABLE "season_templates" DROP COLUMN IF EXISTS "year";

View file

@ -0,0 +1,2 @@
CREATE UNIQUE INDEX IF NOT EXISTS "season_template_sports_template_sport_unique" ON "season_template_sports" USING btree ("template_id","sport_id");--> statement-breakpoint
CREATE UNIQUE INDEX IF NOT EXISTS "season_templates_name_unique" ON "season_templates" USING btree ("name");

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -799,6 +799,20 @@
"when": 1779579877169, "when": 1779579877169,
"tag": "0113_bent_banshee", "tag": "0113_bent_banshee",
"breakpoints": true "breakpoints": true
},
{
"idx": 114,
"version": "7",
"when": 1780595525976,
"tag": "0114_warm_swarm",
"breakpoints": true
},
{
"idx": 115,
"version": "7",
"when": 1780596702982,
"tag": "0115_spooky_spot",
"breakpoints": true
} }
] ]
} }