feat: add bulk participant creation and expected value field
This commit is contained in:
parent
fdfce98d87
commit
0e68bd2037
9 changed files with 1171 additions and 27 deletions
|
|
@ -96,6 +96,7 @@ export async function copyParticipantsFromSeason(
|
|||
name: p.name,
|
||||
shortName: p.shortName,
|
||||
externalId: p.externalId,
|
||||
expectedValue: p.expectedValue,
|
||||
}))
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -80,6 +80,11 @@ export async function findAllSportsSeasons(): Promise<SportsSeason[]> {
|
|||
orderBy: (sportsSeasons, { desc, asc }) => [desc(sportsSeasons.year), asc(sportsSeasons.name)],
|
||||
with: {
|
||||
sport: true,
|
||||
participants: {
|
||||
columns: {
|
||||
id: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
|
@ -128,6 +133,7 @@ export async function copyParticipantsFromPreviousSeason(
|
|||
name: p.name,
|
||||
shortName: p.shortName,
|
||||
externalId: p.externalId,
|
||||
expectedValue: p.expectedValue,
|
||||
}))
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import {
|
|||
import { Button } from "~/components/ui/button";
|
||||
import { Input } from "~/components/ui/input";
|
||||
import { Label } from "~/components/ui/label";
|
||||
import { Textarea } from "~/components/ui/textarea";
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
|
|
@ -54,9 +55,43 @@ export async function action({ request, params }: Route.ActionArgs) {
|
|||
return { success: true };
|
||||
}
|
||||
|
||||
// Add participant
|
||||
// Bulk add participants
|
||||
if (intent === "bulk") {
|
||||
const bulkNames = formData.get("bulkNames");
|
||||
|
||||
if (typeof bulkNames !== "string" || !bulkNames.trim()) {
|
||||
return { error: "Please enter at least one participant name" };
|
||||
}
|
||||
|
||||
const names = bulkNames
|
||||
.split("\n")
|
||||
.map(name => name.trim())
|
||||
.filter(name => name.length > 0);
|
||||
|
||||
if (names.length === 0) {
|
||||
return { error: "Please enter at least one participant name" };
|
||||
}
|
||||
|
||||
try {
|
||||
for (const name of names) {
|
||||
await createParticipant({
|
||||
sportsSeasonId: params.id,
|
||||
name,
|
||||
shortName: null,
|
||||
externalId: null,
|
||||
expectedValue: 0,
|
||||
});
|
||||
}
|
||||
|
||||
return { success: true, count: names.length };
|
||||
} catch (error) {
|
||||
console.error("Error creating participants:", error);
|
||||
return { error: "Failed to add participants. Please try again." };
|
||||
}
|
||||
}
|
||||
|
||||
// Add single participant
|
||||
const name = formData.get("name");
|
||||
const shortName = formData.get("shortName");
|
||||
|
||||
if (typeof name !== "string" || !name.trim()) {
|
||||
return { error: "Participant name is required" };
|
||||
|
|
@ -66,8 +101,9 @@ export async function action({ request, params }: Route.ActionArgs) {
|
|||
await createParticipant({
|
||||
sportsSeasonId: params.id,
|
||||
name: name.trim(),
|
||||
shortName: typeof shortName === "string" && shortName.trim() ? shortName.trim() : null,
|
||||
shortName: null,
|
||||
externalId: null,
|
||||
expectedValue: 0,
|
||||
});
|
||||
|
||||
return { success: true };
|
||||
|
|
@ -79,6 +115,9 @@ export async function action({ request, params }: Route.ActionArgs) {
|
|||
|
||||
export default function ManageParticipants({ loaderData, actionData }: Route.ComponentProps) {
|
||||
const { sportsSeason, participants } = loaderData;
|
||||
|
||||
// Use success state to reset forms by changing the key
|
||||
const formKey = actionData?.success ? Date.now() : 'static';
|
||||
|
||||
return (
|
||||
<div className="p-8">
|
||||
|
|
@ -96,7 +135,7 @@ export default function ManageParticipants({ loaderData, actionData }: Route.Com
|
|||
</p>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-6 md:grid-cols-2">
|
||||
<div className="grid gap-6 md:grid-cols-2 mb-6">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Add Participant</CardTitle>
|
||||
|
|
@ -105,11 +144,9 @@ export default function ManageParticipants({ loaderData, actionData }: Route.Com
|
|||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<Form method="post" className="space-y-4">
|
||||
<Form method="post" className="space-y-4" key={`single-${formKey}`}>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="name">
|
||||
{sportsSeason.sport.type === "team" ? "Team" : "Player"} Name
|
||||
</Label>
|
||||
<Label htmlFor="name">Participant Name</Label>
|
||||
<Input
|
||||
id="name"
|
||||
name="name"
|
||||
|
|
@ -119,23 +156,13 @@ export default function ManageParticipants({ loaderData, actionData }: Route.Com
|
|||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="shortName">Short Name (Optional)</Label>
|
||||
<Input
|
||||
id="shortName"
|
||||
name="shortName"
|
||||
type="text"
|
||||
placeholder={sportsSeason.sport.type === "team" ? "e.g., KC" : "e.g., T. Woods"}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{actionData?.error && (
|
||||
{actionData?.error && !actionData?.count && (
|
||||
<div className="bg-destructive/15 text-destructive px-4 py-3 rounded-md text-sm">
|
||||
{actionData.error}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{actionData?.success && (
|
||||
{actionData?.success && !actionData?.count && (
|
||||
<div className="bg-green-500/15 text-green-700 dark:text-green-400 px-4 py-3 rounded-md text-sm">
|
||||
Participant added successfully!
|
||||
</div>
|
||||
|
|
@ -150,6 +177,53 @@ export default function ManageParticipants({ loaderData, actionData }: Route.Com
|
|||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Bulk Add Participants</CardTitle>
|
||||
<CardDescription>
|
||||
Add multiple participants at once (one per line)
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<Form method="post" className="space-y-4" key={`bulk-${formKey}`}>
|
||||
<input type="hidden" name="intent" value="bulk" />
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="bulkNames">Participant Names</Label>
|
||||
<Textarea
|
||||
id="bulkNames"
|
||||
name="bulkNames"
|
||||
placeholder={sportsSeason.sport.type === "team"
|
||||
? "Kansas City Chiefs\nBuffalo Bills\nSan Francisco 49ers"
|
||||
: "Tiger Woods\nRory McIlroy\nJon Rahm"}
|
||||
rows={8}
|
||||
className="font-mono text-sm"
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Enter one participant name per line
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{actionData?.error && actionData?.count === undefined && (
|
||||
<div className="bg-destructive/15 text-destructive px-4 py-3 rounded-md text-sm">
|
||||
{actionData.error}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{actionData?.success && actionData?.count && (
|
||||
<div className="bg-green-500/15 text-green-700 dark:text-green-400 px-4 py-3 rounded-md text-sm">
|
||||
{actionData.count} participant{actionData.count !== 1 ? 's' : ''} added successfully!
|
||||
</div>
|
||||
)}
|
||||
|
||||
<Button type="submit" className="w-full">
|
||||
<Plus className="mr-2 h-4 w-4" />
|
||||
Add All Participants
|
||||
</Button>
|
||||
</Form>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>All Participants</CardTitle>
|
||||
<CardDescription>
|
||||
|
|
@ -167,7 +241,6 @@ export default function ManageParticipants({ loaderData, actionData }: Route.Com
|
|||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>Name</TableHead>
|
||||
<TableHead>Short Name</TableHead>
|
||||
<TableHead className="w-[50px]"></TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
|
|
@ -175,9 +248,6 @@ export default function ManageParticipants({ loaderData, actionData }: Route.Com
|
|||
{participants.map((participant) => (
|
||||
<TableRow key={participant.id}>
|
||||
<TableCell className="font-medium">{participant.name}</TableCell>
|
||||
<TableCell className="text-muted-foreground">
|
||||
{participant.shortName || "-"}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Form method="post">
|
||||
<input type="hidden" name="intent" value="delete" />
|
||||
|
|
@ -200,7 +270,6 @@ export default function ManageParticipants({ loaderData, actionData }: Route.Com
|
|||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -22,8 +22,15 @@ import { Badge } from "~/components/ui/badge";
|
|||
|
||||
export async function loader() {
|
||||
const sportsSeasons = await findAllSportsSeasons();
|
||||
// Type assertion since we know the sport relation is included from the model
|
||||
return { sportsSeasons: sportsSeasons as Array<typeof sportsSeasons[0] & { sport: { id: string; name: string; type: string; slug: string } }> };
|
||||
// Type assertion since we know the sport and participants relations are included from the model
|
||||
return {
|
||||
sportsSeasons: sportsSeasons as Array<
|
||||
typeof sportsSeasons[0] & {
|
||||
sport: { id: string; name: string; type: string; slug: string };
|
||||
participants: Array<{ id: string }>;
|
||||
}
|
||||
>
|
||||
};
|
||||
}
|
||||
|
||||
export default function AdminSportsSeasons({ loaderData }: Route.ComponentProps) {
|
||||
|
|
@ -77,6 +84,7 @@ export default function AdminSportsSeasons({ loaderData }: Route.ComponentProps)
|
|||
<TableHead>Year</TableHead>
|
||||
<TableHead>Status</TableHead>
|
||||
<TableHead>Scoring Type</TableHead>
|
||||
<TableHead>Participants</TableHead>
|
||||
<TableHead className="text-right">Actions</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
|
|
@ -102,6 +110,9 @@ export default function AdminSportsSeasons({ loaderData }: Route.ComponentProps)
|
|||
<TableCell className="text-muted-foreground capitalize">
|
||||
{season.scoringType.replace("_", " ")}
|
||||
</TableCell>
|
||||
<TableCell className="text-muted-foreground">
|
||||
{season.participants.length}
|
||||
</TableCell>
|
||||
<TableCell className="text-right">
|
||||
<div className="flex items-center justify-end gap-2">
|
||||
<Button variant="ghost" size="sm" asChild>
|
||||
|
|
|
|||
|
|
@ -27,6 +27,7 @@ interface ExportData {
|
|||
name: string;
|
||||
shortName: string | null;
|
||||
externalId: string | null;
|
||||
expectedValue: number;
|
||||
}>;
|
||||
seasonTemplates: Array<{
|
||||
name: string;
|
||||
|
|
@ -114,6 +115,7 @@ export async function exportSportsDataToJSON(): Promise<ExportData> {
|
|||
name: participant.name,
|
||||
shortName: participant.shortName,
|
||||
externalId: participant.externalId,
|
||||
expectedValue: participant.expectedValue,
|
||||
})),
|
||||
seasonTemplates: seasonTemplates.map((template) => ({
|
||||
name: template.name,
|
||||
|
|
@ -262,6 +264,7 @@ export async function importSportsDataFromJSON(
|
|||
name: participant.name,
|
||||
shortName: participant.shortName,
|
||||
externalId: participant.externalId,
|
||||
expectedValue: participant.expectedValue ?? 0,
|
||||
});
|
||||
created++;
|
||||
} else {
|
||||
|
|
|
|||
|
|
@ -142,6 +142,7 @@ export const participants = pgTable("participants", {
|
|||
name: varchar("name", { length: 255 }).notNull(),
|
||||
shortName: varchar("short_name", { length: 100 }),
|
||||
externalId: varchar("external_id", { length: 255 }),
|
||||
expectedValue: integer("expected_value").notNull().default(0),
|
||||
createdAt: timestamp("created_at").defaultNow().notNull(),
|
||||
updatedAt: timestamp("updated_at").defaultNow().notNull(),
|
||||
});
|
||||
|
|
|
|||
1
drizzle/0014_lowly_gateway.sql
Normal file
1
drizzle/0014_lowly_gateway.sql
Normal file
|
|
@ -0,0 +1 @@
|
|||
ALTER TABLE "participants" ADD COLUMN "expected_value" integer DEFAULT 0 NOT NULL;
|
||||
1045
drizzle/meta/0014_snapshot.json
Normal file
1045
drizzle/meta/0014_snapshot.json
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -99,6 +99,13 @@
|
|||
"when": 1760543654915,
|
||||
"tag": "0013_pink_the_order",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 14,
|
||||
"version": "7",
|
||||
"when": 1760588333462,
|
||||
"tag": "0014_lowly_gateway",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue