* Redesign autodraft queue system with three-state control and queue-only constraint Core Logic & Database: - Add `queue_only` boolean column to `autodraft_settings` (migration 0031) - Rename autodraft UI states: Off / Next Pick / All Picks (while_on mode maps to All Picks) - `autoPickForTeam`: respects new `queueOnly` param — skips EV fallback when enabled - `executeAutoPick`: auto-disables autodraft + emits socket event when queue empties with queueOnly ON (AC3) - `autodraft-updated` socket event now includes `queueOnly` field Mobile UI Overhaul: - Rename "Lobby" tab → "Available" (AC6) - Add new "Queue" tab to mobile bottom nav with drag-reorder, per-item Draft buttons, and autodraft controls (AC5) - Controls tab retains commissioner tools, notifications, exit; queue controls moved to Queue tab - Turn indicator appears on both Available and Queue tabs Components: - `AutodraftSettings`: replaces toggle+radio with three-state button group (Off | Next Pick | All Picks) + "Only autodraft from queue" switch (AC1, AC2) - `QueueSection`: adds `canPick` prop + per-item Draft buttons for instant drafting when on the clock Desktop (AC4): - Sidebar QueueSection unchanged in position; gains same three-state controls and Draft buttons Tests (AC7): - `autodraft.test.ts`: updated for queueOnly field and socket event shape - `timer-autodraft.test.ts`: new tests for queue-only constraint, auto-shutoff transitions, and all three autodraft states https://claude.ai/code/session_01PYhJicAStoJ2u6q6dV1naB * fix: remove erroneous ?? fallbacks in autodraft socket emissions and add autoPickForTeam tests - Remove `?? true` default on queueOnly in queue-empty auto-disable socket emit (line 488) — was dead code since the column is NOT NULL, but semantically wrong and would have caused client-side UI desync if the type ever relaxed - Remove `?? false` default on the next_pick auto-disable path for consistency - Add app/models/__tests__/auto-pick.test.ts with 6 tests covering the queueOnly constraint: empty queue, all items drafted, partial queue skip, and EV fallback Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix: add missing queueOnly prop to AutodraftSettings test fixtures Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * test: rewrite AutodraftSettings tests for three-state button group UI The component was redesigned from a switch + radio buttons to Off/Next Pick/All Picks buttons with a separate queue-only Switch toggle. Updated 17 stale tests and added 5 new tests covering the queue-only toggle and the All Picks/Off button interactions. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude <noreply@anthropic.com>
159 lines
5 KiB
TypeScript
159 lines
5 KiB
TypeScript
import { useState, useEffect } from "react";
|
|
import { Switch } from "~/components/ui/switch";
|
|
import { Label } from "~/components/ui/label";
|
|
import { Button } from "~/components/ui/button";
|
|
|
|
type AutodraftMode = "next_pick" | "while_on";
|
|
|
|
// The three visible states map to isEnabled + mode combinations:
|
|
// "off" → isEnabled: false
|
|
// "next_pick" → isEnabled: true, mode: "next_pick"
|
|
// "all_picks" → isEnabled: true, mode: "while_on"
|
|
type AutodraftState = "off" | "next_pick" | "all_picks";
|
|
|
|
function toAutodraftState(isEnabled: boolean, mode: AutodraftMode): AutodraftState {
|
|
if (!isEnabled) return "off";
|
|
return mode === "next_pick" ? "next_pick" : "all_picks";
|
|
}
|
|
|
|
interface AutodraftSettingsProps {
|
|
seasonId: string;
|
|
teamId: string;
|
|
isEnabled: boolean;
|
|
mode: AutodraftMode;
|
|
queueOnly: boolean;
|
|
isMyTurn: boolean;
|
|
onUpdate: (isEnabled: boolean, mode: AutodraftMode, queueOnly: boolean) => void;
|
|
}
|
|
|
|
export function AutodraftSettings({
|
|
seasonId,
|
|
teamId,
|
|
isEnabled,
|
|
mode,
|
|
queueOnly,
|
|
isMyTurn,
|
|
onUpdate,
|
|
}: AutodraftSettingsProps) {
|
|
const [localState, setLocalState] = useState<AutodraftState>(toAutodraftState(isEnabled, mode));
|
|
const [localQueueOnly, setLocalQueueOnly] = useState(queueOnly);
|
|
const [isUpdating, setIsUpdating] = useState(false);
|
|
|
|
// Sync local state with props when they change (from socket events)
|
|
useEffect(() => {
|
|
setLocalState(toAutodraftState(isEnabled, mode));
|
|
}, [isEnabled, mode]);
|
|
|
|
useEffect(() => {
|
|
setLocalQueueOnly(queueOnly);
|
|
}, [queueOnly]);
|
|
|
|
const sendUpdate = async (newState: AutodraftState, newQueueOnly: boolean) => {
|
|
if (isUpdating) return;
|
|
setIsUpdating(true);
|
|
|
|
const newEnabled = newState !== "off";
|
|
const newMode: AutodraftMode = newState === "all_picks" ? "while_on" : "next_pick";
|
|
|
|
try {
|
|
const formData = new FormData();
|
|
formData.append("seasonId", seasonId);
|
|
formData.append("teamId", teamId);
|
|
formData.append("isEnabled", newEnabled.toString());
|
|
formData.append("mode", newMode);
|
|
formData.append("queueOnly", newQueueOnly.toString());
|
|
|
|
const response = await fetch("/api/autodraft/update", {
|
|
method: "POST",
|
|
body: formData,
|
|
});
|
|
|
|
if (response.ok) {
|
|
onUpdate(newEnabled, newMode, newQueueOnly);
|
|
} else {
|
|
// Revert on error
|
|
setLocalState(toAutodraftState(isEnabled, mode));
|
|
setLocalQueueOnly(queueOnly);
|
|
console.error("Failed to update autodraft settings");
|
|
}
|
|
} catch (error) {
|
|
// Revert on error
|
|
setLocalState(toAutodraftState(isEnabled, mode));
|
|
setLocalQueueOnly(queueOnly);
|
|
console.error("Error updating autodraft settings:", error);
|
|
} finally {
|
|
setIsUpdating(false);
|
|
}
|
|
};
|
|
|
|
const handleStateChange = (newState: AutodraftState) => {
|
|
if (isMyTurn || isUpdating) return;
|
|
setLocalState(newState);
|
|
sendUpdate(newState, localQueueOnly);
|
|
};
|
|
|
|
const handleQueueOnlyChange = async (checked: boolean) => {
|
|
if (isMyTurn || isUpdating) return;
|
|
setLocalQueueOnly(checked);
|
|
sendUpdate(localState, checked);
|
|
};
|
|
|
|
const isDisabled = isMyTurn || isUpdating;
|
|
|
|
return (
|
|
<div className="border-t pt-4 mt-4">
|
|
<Label className="text-sm font-semibold mb-3 block">Autodraft</Label>
|
|
|
|
{/* Three-state button group */}
|
|
<div className="flex rounded-lg border overflow-hidden mb-3">
|
|
{(["off", "next_pick", "all_picks"] as AutodraftState[]).map((state) => {
|
|
const labels: Record<AutodraftState, string> = {
|
|
off: "Off",
|
|
next_pick: "Next Pick",
|
|
all_picks: "All Picks",
|
|
};
|
|
const isActive = localState === state;
|
|
return (
|
|
<button
|
|
key={state}
|
|
type="button"
|
|
disabled={isDisabled}
|
|
onClick={() => handleStateChange(state)}
|
|
className={`flex-1 py-1.5 text-xs font-medium transition-colors ${
|
|
isActive
|
|
? "bg-electric text-background"
|
|
: "bg-transparent text-muted-foreground hover:bg-muted/60"
|
|
} ${isDisabled ? "opacity-50 cursor-not-allowed" : "cursor-pointer"}`}
|
|
>
|
|
{labels[state]}
|
|
</button>
|
|
);
|
|
})}
|
|
</div>
|
|
|
|
{/* Queue-only toggle — only visible when autodraft is active */}
|
|
{localState !== "off" && (
|
|
<div className="flex items-center justify-between">
|
|
<Label
|
|
htmlFor="queue-only-switch"
|
|
className={`text-xs ${isDisabled ? "text-muted-foreground" : "cursor-pointer"}`}
|
|
>
|
|
Only autodraft from queue
|
|
</Label>
|
|
<Switch
|
|
id="queue-only-switch"
|
|
checked={localQueueOnly}
|
|
onCheckedChange={handleQueueOnlyChange}
|
|
disabled={isDisabled}
|
|
/>
|
|
</div>
|
|
)}
|
|
|
|
{isMyTurn && (
|
|
<p className="text-xs text-muted-foreground mt-2">
|
|
Autodraft settings are disabled during your pick
|
|
</p>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|