import { auth } from "~/lib/auth.server"; import { eq, and } from "drizzle-orm"; import { database } from "~/database/context"; import * as schema from "~/database/schema"; import { isCommissioner } from "~/models/commissioner"; import { logCommissionerAction } from "~/models/audit-log"; import { getSocketIO } from "../../../server/socket"; import { rescheduleTimer } from "../../../server/timer"; import { logger } from "~/lib/logger"; import type { ActionFunctionArgs } from "react-router"; export async function action(args: ActionFunctionArgs) { const { request } = args; const session = await auth.api.getSession({ headers: args.request.headers }); const userId = session?.user.id ?? null; if (!userId) { return Response.json({ error: "Unauthorized" }, { status: 401 }); } const formData = await request.formData(); const seasonId = formData.get("seasonId") as string; const teamId = formData.get("teamId") as string; const adjustmentRaw = formData.get("adjustment"); const adjustment = adjustmentRaw !== null ? parseInt(adjustmentRaw as string, 10) : NaN; if (!seasonId || !teamId || isNaN(adjustment)) { return Response.json({ error: "seasonId, teamId, and adjustment are required" }, { status: 400 }); } const db = database(); const season = await db.query.seasons.findFirst({ where: eq(schema.seasons.id, seasonId), }); if (!season) { return Response.json({ error: "Season not found" }, { status: 404 }); } if (season.status !== "draft") { return Response.json({ error: "Draft is not currently active" }, { status: 409 }); } if (!(await isCommissioner(season.leagueId, userId))) { return Response.json( { error: "Only commissioners can adjust time banks" }, { status: 403 } ); } const [currentTimer] = await db .select() .from(schema.draftTimers) .where( and( eq(schema.draftTimers.seasonId, seasonId), eq(schema.draftTimers.teamId, teamId) ) ); let newTime: number; let isOnClock = false; if (!currentTimer) { if (adjustment <= 0) { return Response.json({ error: "Timer not found for this team" }, { status: 404 }); } newTime = adjustment; await db.insert(schema.draftTimers).values({ seasonId, teamId, timeRemaining: newTime, }); } else { const now = Date.now(); if (currentTimer.picksExpiresAt && currentTimer.picksExpiresAt.getTime() > now) { // Team is on the clock — base the adjustment on the live expiry, not the stale timeRemaining. isOnClock = true; const msRemaining = currentTimer.picksExpiresAt.getTime() - now; const currentRemaining = Math.max(0, Math.ceil(msRemaining / 1000)); newTime = currentRemaining + adjustment; if (newTime <= 0) { return Response.json({ error: "Adjustment would reduce the time bank to zero or below" }, { status: 400 }); } const newExpiresAt = new Date(now + newTime * 1000); await db .update(schema.draftTimers) .set({ timeRemaining: newTime, picksExpiresAt: newExpiresAt, updatedAt: new Date() }) .where(eq(schema.draftTimers.id, currentTimer.id)); } else { newTime = currentTimer.timeRemaining + adjustment; if (newTime <= 0) { return Response.json({ error: "Adjustment would reduce the time bank to zero or below" }, { status: 400 }); } await db .update(schema.draftTimers) .set({ timeRemaining: newTime, updatedAt: new Date() }) .where(eq(schema.draftTimers.id, currentTimer.id)); } } const team = await db.query.teams.findFirst({ where: eq(schema.teams.id, teamId), }); await logCommissionerAction({ seasonId, leagueId: season.leagueId, actorUserId: userId, action: "time_bank_edited", affectedTeamIds: [teamId], details: { teamId, teamName: team?.name ?? teamId, adjustment, newTimeRemaining: newTime, }, }); if (isOnClock) { // Reschedule cancels the old setTimeout and re-reads picksExpiresAt from the DB, // then emits timer-pick-started with the new expiresAt so all clients update their countdown. try { await rescheduleTimer(seasonId); } catch (err) { logger.error("[AdjustTimeBank] rescheduleTimer failed:", err); } } else { // Off-clock team: just push the updated bank to all clients. try { getSocketIO().to(`draft-${seasonId}`).emit("timer-bank-updated", { teamId, timeRemaining: newTime }); } catch (error) { logger.error("Socket.IO error:", error); } } return Response.json({ success: true, timeRemaining: newTime }); }