feat: enhance draft reset functionality to include deletion of draft timers

This commit is contained in:
Chris Parsons 2025-10-24 21:41:30 -07:00
parent 4aafd5853b
commit e7baea319e
2 changed files with 97 additions and 29 deletions

View file

@ -13,6 +13,7 @@ import { findAllSportsSeasons } from "~/models/sports-season";
import { findDraftSlotsBySeasonId, setDraftOrder, randomizeDraftOrder } from "~/models/draft-slot";
import { deleteAllDraftPicks } from "~/models/draft-pick";
import { clearAllQueuesForSeason } from "~/models/draft-queue";
import { deleteSeasonTimers } from "~/models/draft-timer";
import type { Route } from "./+types/$leagueId.settings";
import { Button } from "~/components/ui/button";
import { Input } from "~/components/ui/input";
@ -284,11 +285,6 @@ export async function action(args: Route.ActionArgs) {
return { error: "Only admins can reset the draft" };
}
// Check if season status is draft or completed
if (season.status !== "draft" && season.status !== "completed" && season.status !== "active") {
return { error: "Draft can only be reset after it has started" };
}
try {
// Delete all draft picks
await deleteAllDraftPicks(season.id);
@ -296,8 +292,16 @@ export async function action(args: Route.ActionArgs) {
// Clear all draft queues
await clearAllQueuesForSeason(season.id);
// Set season status back to pre_draft (keeps draft order intact)
await updateSeason(season.id, { status: "pre_draft" });
// Delete all draft timers
await deleteSeasonTimers(season.id);
// Set season status back to pre_draft and reset draft state (keeps draft order intact)
await updateSeason(season.id, {
status: "pre_draft",
currentPickNumber: null,
draftStartedAt: null,
draftPaused: false,
});
return { success: true, message: "Draft has been reset successfully. Draft order preserved." };
} catch (error) {
@ -676,7 +680,6 @@ export default function LeagueSettings({ loaderData, actionData }: Route.Compone
mode="single"
selected={draftDate}
onSelect={setDraftDate}
initialFocus
disabled={(date) => date < new Date(new Date().setHours(0, 0, 0, 0))}
/>
</PopoverContent>
@ -1017,11 +1020,11 @@ export default function LeagueSettings({ loaderData, actionData }: Route.Compone
</CardHeader>
<CardContent className="space-y-4">
{/* Reset Draft - Admin Only */}
{isAdmin && season && (season.status === "draft" || season.status === "completed" || season.status === "active") && (
{isAdmin && season && (
<div className="space-y-2">
<h3 className="font-medium">Reset Draft</h3>
<p className="text-sm text-muted-foreground">
Delete all draft picks and reset the season to pre-draft status. The draft order will be preserved.
Delete all draft picks, queues, and timers, and reset the season to pre-draft status. The draft order will be preserved.
</p>
<AlertDialog>
<AlertDialogTrigger asChild>
@ -1033,7 +1036,7 @@ export default function LeagueSettings({ loaderData, actionData }: Route.Compone
<AlertDialogHeader>
<AlertDialogTitle>Reset the draft?</AlertDialogTitle>
<AlertDialogDescription>
This will delete all draft picks and draft queues, and set the season back to pre-draft status.
This will delete all draft picks, draft queues, and draft timers, and set the season back to pre-draft status.
The draft order will remain intact. This action cannot be undone.
</AlertDialogDescription>
</AlertDialogHeader>

View file

@ -1,6 +1,7 @@
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { deleteAllDraftPicks } from '~/models/draft-pick';
import { clearAllQueuesForSeason } from '~/models/draft-queue';
import { deleteSeasonTimers } from '~/models/draft-timer';
import { updateSeason } from '~/models/season';
import { isUserAdminByClerkId } from '~/models/user';
@ -38,6 +39,10 @@ vi.mock('~/models/draft-queue', () => ({
clearAllQueuesForSeason: vi.fn(),
}));
vi.mock('~/models/draft-timer', () => ({
deleteSeasonTimers: vi.fn(),
}));
vi.mock('~/models/season', () => ({
updateSeason: vi.fn(),
findCurrentSeasonWithSports: vi.fn(),
@ -119,6 +124,17 @@ describe('Draft Reset - Delete Operations', () => {
expect(clearAllQueuesForSeason).toHaveBeenCalledTimes(1);
});
it('should delete all draft timers for a season', async () => {
const seasonId = 'season-1';
vi.mocked(deleteSeasonTimers).mockResolvedValue(undefined);
await deleteSeasonTimers(seasonId);
expect(deleteSeasonTimers).toHaveBeenCalledWith(seasonId);
expect(deleteSeasonTimers).toHaveBeenCalledTimes(1);
});
it('should handle errors when deleting draft picks fails', async () => {
const seasonId = 'season-1';
const error = new Error('Database error');
@ -136,6 +152,15 @@ describe('Draft Reset - Delete Operations', () => {
await expect(clearAllQueuesForSeason(seasonId)).rejects.toThrow('Database error');
});
it('should handle errors when deleting timers fails', async () => {
const seasonId = 'season-1';
const error = new Error('Database error');
vi.mocked(deleteSeasonTimers).mockRejectedValue(error);
await expect(deleteSeasonTimers(seasonId)).rejects.toThrow('Database error');
});
});
describe('Draft Reset - Season Status Update', () => {
@ -172,30 +197,30 @@ describe('Draft Reset - Status Validation', () => {
it('should allow reset when season status is draft', () => {
const seasonStatus = 'draft';
const allowedStatuses = ['draft', 'completed', 'active'];
const validStatuses = ['pre_draft', 'draft', 'completed', 'active'];
expect(allowedStatuses.includes(seasonStatus)).toBe(true);
expect(validStatuses.includes(seasonStatus)).toBe(true);
});
it('should allow reset when season status is completed', () => {
const seasonStatus = 'completed';
const allowedStatuses = ['draft', 'completed', 'active'];
const validStatuses = ['pre_draft', 'draft', 'completed', 'active'];
expect(allowedStatuses.includes(seasonStatus)).toBe(true);
expect(validStatuses.includes(seasonStatus)).toBe(true);
});
it('should allow reset when season status is active', () => {
const seasonStatus = 'active';
const allowedStatuses = ['draft', 'completed', 'active'];
const validStatuses = ['pre_draft', 'draft', 'completed', 'active'];
expect(allowedStatuses.includes(seasonStatus)).toBe(true);
expect(validStatuses.includes(seasonStatus)).toBe(true);
});
it('should not allow reset when season status is pre_draft', () => {
it('should allow reset when season status is pre_draft', () => {
const seasonStatus = 'pre_draft';
const allowedStatuses = ['draft', 'completed', 'active'];
const validStatuses = ['pre_draft', 'draft', 'completed', 'active'];
expect(allowedStatuses.includes(seasonStatus)).toBe(false);
expect(validStatuses.includes(seasonStatus)).toBe(true);
});
});
@ -223,14 +248,29 @@ describe('Draft Reset - Complete Workflow', () => {
await clearAllQueuesForSeason(seasonId);
expect(clearAllQueuesForSeason).toHaveBeenCalledWith(seasonId);
// Step 4: Update season status
// Step 4: Delete draft timers
vi.mocked(deleteSeasonTimers).mockResolvedValue(undefined);
await deleteSeasonTimers(seasonId);
expect(deleteSeasonTimers).toHaveBeenCalledWith(seasonId);
// Step 5: Update season status and reset draft state
const mockUpdatedSeason = createMockSeason({ id: seasonId, status: 'pre_draft' });
vi.mocked(updateSeason).mockResolvedValue(mockUpdatedSeason);
const result = await updateSeason(seasonId, { status: 'pre_draft' });
const result = await updateSeason(seasonId, {
status: 'pre_draft',
currentPickNumber: null,
draftStartedAt: null,
draftPaused: false,
});
expect(result.status).toBe('pre_draft');
expect(updateSeason).toHaveBeenCalledWith(seasonId, { status: 'pre_draft' });
expect(updateSeason).toHaveBeenCalledWith(seasonId, {
status: 'pre_draft',
currentPickNumber: null,
draftStartedAt: null,
draftPaused: false,
});
});
it('should not proceed with reset if admin check fails', async () => {
@ -244,6 +284,7 @@ describe('Draft Reset - Complete Workflow', () => {
// Should not call any reset functions
expect(deleteAllDraftPicks).not.toHaveBeenCalled();
expect(clearAllQueuesForSeason).not.toHaveBeenCalled();
expect(deleteSeasonTimers).not.toHaveBeenCalled();
expect(updateSeason).not.toHaveBeenCalled();
});
@ -385,6 +426,10 @@ describe('Draft Reset - Data Integrity', () => {
callOrder.push('clearAllQueuesForSeason');
});
vi.mocked(deleteSeasonTimers).mockImplementation(async () => {
callOrder.push('deleteSeasonTimers');
});
vi.mocked(updateSeason).mockImplementation(async () => {
callOrder.push('updateSeason');
return createMockSeason({ id: seasonId, status: 'pre_draft' });
@ -393,12 +438,19 @@ describe('Draft Reset - Data Integrity', () => {
// Execute in correct order
await deleteAllDraftPicks(seasonId);
await clearAllQueuesForSeason(seasonId);
await updateSeason(seasonId, { status: 'pre_draft' });
await deleteSeasonTimers(seasonId);
await updateSeason(seasonId, {
status: 'pre_draft',
currentPickNumber: null,
draftStartedAt: null,
draftPaused: false,
});
// Verify order
expect(callOrder).toEqual([
'deleteAllDraftPicks',
'clearAllQueuesForSeason',
'deleteSeasonTimers',
'updateSeason',
]);
});
@ -409,17 +461,30 @@ describe('Draft Reset - Data Integrity', () => {
// All operations reference the same season
vi.mocked(deleteAllDraftPicks).mockResolvedValue(undefined);
vi.mocked(clearAllQueuesForSeason).mockResolvedValue(undefined);
vi.mocked(deleteSeasonTimers).mockResolvedValue(undefined);
vi.mocked(updateSeason).mockResolvedValue(
createMockSeason({ id: seasonId, status: 'pre_draft' })
);
await deleteAllDraftPicks(seasonId);
await clearAllQueuesForSeason(seasonId);
await updateSeason(seasonId, { status: 'pre_draft' });
await deleteSeasonTimers(seasonId);
await updateSeason(seasonId, {
status: 'pre_draft',
currentPickNumber: null,
draftStartedAt: null,
draftPaused: false,
});
// All operations used the same season ID
expect(deleteAllDraftPicks).toHaveBeenCalledWith(seasonId);
expect(clearAllQueuesForSeason).toHaveBeenCalledWith(seasonId);
expect(updateSeason).toHaveBeenCalledWith(seasonId, { status: 'pre_draft' });
expect(deleteSeasonTimers).toHaveBeenCalledWith(seasonId);
expect(updateSeason).toHaveBeenCalledWith(seasonId, {
status: 'pre_draft',
currentPickNumber: null,
draftStartedAt: null,
draftPaused: false,
});
});
});