AnayShukla's picture
updates
72e647c
import React, { useState, useEffect, useMemo, useContext, useRef } from "react";
import { createPortal } from "react-dom";
import { Search, Loader2, RotateCcw, Shield, Settings, Zap, Plus, Copy, Trash2 } from "lucide-react";
import { DndContext, DragOverlay, closestCenter, PointerSensor, useSensor, useSensors } from "@dnd-kit/core";
import { PlayerContext } from "../PlayerContext";
import { CHIP_CONFIG, getPlayerPrice, normalizeBenchGkFirst,getOptimalLayout } from "../utils/fplLogic";
import { useFplSolverApi } from "../hooks/useFplSolverApi";
import { SolverOutputPanel } from "./SolverOutputPanel";
import { PitchView } from "./PitchView";
import { PlayerEditModal, PlayerSearchModal } from "./PlayerModals";
import { PlayerCardVisual } from "./PlayerCardVisual";
import { TabsPanel } from "./TabsPanel";
import { AdvancedSettingsModal, DEFAULT_SETTINGS } from "./AdvancedSettingsModal";
import { ActiveMovesPanel } from "./ActiveMovesPanel";
import { DraftsComparisonTable } from "./DraftsComparisonTable";
export default function Solver() {
const {
globalPlayers, updatePlayerStat, isLoadingDB, teamId, setTeamId, teamData, setTeamData, availableGWs, setAvailableGWs, horizon, setHorizon, activeGW, setActiveGW, captainId, setCaptainId, viceId, setViceId, initialSquadIds, setInitialSquadIds, isLoggedIn, userProfile, setUserProfile, manualOverrides, setManualOverrides, highlightTransferIds, setHighlightTransferIds, transfersByGw, setTransfersByGw, chipsByGw, setChipsByGw, baselineItb, setBaselineItb, baselineFt, setBaselineFt, availableFts, setAvailableFts, itb, setItb, HIT_COST, ftAtStartOfGw, quickSettings, setQuickSettings, advancedSettings, setAdvancedSettings, numSims, setNumSims, comprehensiveSettings, setComprehensiveSettings, appliedPlanSummary, setAppliedPlanSummary, solverApplySnapshot, setSolverApplySnapshot, solverTransferPairs, setSolverTransferPairs, solveElapsedSec, setSolveElapsedSec, drafts, setDrafts, activeDraftId, setActiveDraftId, fixtureOverrides, sessionEdits
} = useContext(PlayerContext);
// --- THE PRISTINE VAULT ---
const pristineSquadRef = useRef({});
useEffect(() => {
if (teamData.length > 0 && globalPlayers.length > 0 && availableGWs?.length > 0) {
setTeamData(prevTeam => {
let hasChanges = false;
const newTeam = prevTeam.map(p => {
if (p.isBlank) return p;
const fresh = globalPlayers.find(g => String(g.ID) === String(p.ID));
if (!fresh) return p;
let needsUpdate = false;
for (const gw of availableGWs) {
if (p[`${gw}_Pts`] !== fresh[`${gw}_Pts`] || p[`${gw}_xMins`] !== fresh[`${gw}_xMins`]) {
needsUpdate = true;
break;
}
}
if (needsUpdate) {
hasChanges = true;
// Inherits the fresh stats while fiercely protecting the locked financial data
return { ...p, ...fresh, purchase_price: p.purchase_price, selling_price: p.selling_price, Price: p.Price, now_cost: p.now_cost };
}
return p;
});
return hasChanges ? newTeam : prevTeam;
});
}
}, [globalPlayers, availableGWs]);
const [pendingAutoReset, setPendingAutoReset] = useState(false);
const lastOverridesRef = useRef(fixtureOverrides);
// Watches for fixture changes and queues an auto-reset for when the math finishes
useEffect(() => {
// THE FIX: Stringify the objects so React compares the actual data, not the memory address!
const currentStr = JSON.stringify(fixtureOverrides || {});
const lastStr = JSON.stringify(lastOverridesRef.current || {});
if (lastStr !== currentStr) {
lastOverridesRef.current = fixtureOverrides;
setPendingAutoReset(true);
}
}, [fixtureOverrides]);
// --- STRICT VAULT-BASED PLAYER FACTORY ---
const hydratePlayer = (id, knownPristineData = null) => {
const globalMatch = globalPlayers.find((p) => String(p.ID) === String(id));
if (!globalMatch) return null;
// 1. Trust explicit overrides (like when clicking 'undo transfer')
if (knownPristineData && typeof knownPristineData === "object" && knownPristineData.purchase_price !== undefined) {
const hydrated = {
...globalMatch, // Absolute source of truth for stats
purchase_price: knownPristineData.purchase_price,
selling_price: knownPristineData.selling_price,
multiplier: knownPristineData.multiplier,
is_captain: knownPristineData.is_captain,
is_vice_captain: knownPristineData.is_vice_captain
};
hydrated.now_cost = globalMatch.now_cost !== undefined ? globalMatch.now_cost : globalMatch.Price;
hydrated.Price = hydrated.selling_price !== undefined ? hydrated.selling_price : getPlayerPrice(hydrated);
return hydrated;
}
const marketCost = globalMatch.now_cost !== undefined ? globalMatch.now_cost : globalMatch.Price;
const lockedBaselinePlayer = pristineSquadRef.current[id];
// 2. CHECK THE CHAIN: Was this player sold in any previous gameweek?
let isChainBroken = false;
if (lockedBaselinePlayer && availableGWs && availableGWs.length > 0) {
const pastGWs = availableGWs.filter(g => g < activeGW).sort((a, b) => a - b);
for (const gw of pastGWs) {
if (chipsByGw[gw] === "fh") continue;
const mLock = manualOverrides[gw];
if (mLock?.manualTransfers && Object.values(mLock.manualTransfers).some(p => String(p?.ID) === String(id))) {
isChainBroken = true; break;
}
const sPairs = solverTransferPairs[gw];
if (sPairs && Object.values(sPairs).some(pair => String(pair.outPlayer?.ID) === String(id))) {
isChainBroken = true; break;
}
}
} else {
isChainBroken = true;
}
// 3. APPLY THE LOGIC
let finalPurchasePrice, finalSellingPrice;
if (lockedBaselinePlayer && !isChainBroken) {
finalPurchasePrice = lockedBaselinePlayer.purchase_price;
finalSellingPrice = lockedBaselinePlayer.selling_price !== undefined ? lockedBaselinePlayer.selling_price : getPlayerPrice(lockedBaselinePlayer);
} else {
finalPurchasePrice = marketCost;
finalSellingPrice = marketCost;
}
const hydrated = {
...globalMatch, // Fresh stats unconditionally overlay everything!
...(lockedBaselinePlayer && !isChainBroken ? {
purchase_price: lockedBaselinePlayer.purchase_price,
selling_price: lockedBaselinePlayer.selling_price,
multiplier: lockedBaselinePlayer.multiplier,
is_captain: lockedBaselinePlayer.is_captain,
is_vice_captain: lockedBaselinePlayer.is_vice_captain
} : {}),
purchase_price: finalPurchasePrice,
selling_price: finalSellingPrice,
Price: finalSellingPrice,
now_cost: marketCost
};
return hydrated;
};
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState(null);
const [fixtures, setFixtures] = useState([]);
const [isFixturesLoaded, setIsFixturesLoaded] = useState(false);
const [activeDragPlayer, setActiveDragPlayer] = useState(null);
const [selectedPlayer, setSelectedPlayer] = useState(null);
const [searchQuery, setSearchQuery] = useState("");
const [sortConfig, setSortConfig] = useState({ key: "ev", direction: "desc" });
const [showIdPrompt, setShowIdPrompt] = useState(false);
// --- DEFAULT ID ONBOARDING STATE ---
const [showInitialIdPrompt, setShowInitialIdPrompt] = useState(false);
const [initialIdInput, setInitialIdInput] = useState("");
// Trigger popup if logged in but no default ID is set
useEffect(() => {
// THE FIX: Wait until the profile is fully hydrated (checking for .email)
// and the site has finished its initial load (!isLoadingDB) before prompting.
if (isLoggedIn && userProfile && userProfile.email && !userProfile.defaultTeamId && !isLoadingDB) {
setShowInitialIdPrompt(true);
} else {
setShowInitialIdPrompt(false);
}
}, [isLoggedIn, userProfile, isLoadingDB]);
const handleSaveInitialId = () => {
const parsedId = parseInt(initialIdInput);
if (!parsedId) return;
const token = localStorage.getItem('fpl_token');
if (token) {
fetch('https://anayshukla-fpl-solver.hf.space/api/auth/save_session', {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${token}` },
body: JSON.stringify({ default_team_id: parsedId })
});
setUserProfile(prev => ({ ...prev, defaultTeamId: parsedId }));
setTeamId(String(parsedId)); // Auto-load the ID for them
setShowInitialIdPrompt(false);
}
};
const [pendingTeamId, setPendingTeamId] = useState(null);
const [lastLoadedId, setLastLoadedId] = useState(teamData.length > 0 ? teamId : null);
const [solverTab, setSolverTab] = useState("solver");
const [showAdvancedSettings, setShowAdvancedSettings] = useState(false);
const [banSearch, setBanSearch] = useState("");
const [lockSearch, setLockSearch] = useState("");
const [chipSolveOptions, setChipSolveOptions] = useState({ wc: [], fh: [], bb: [], tc: [] });
const [showDraftMenu, setShowDraftMenu] = useState(false);
const [sensTimer, setSensTimer] = useState(0);
const [chipSolveTimer, setChipSolveTimer] = useState(0);
const abortControllerRef = useRef(null);
const sensors = useSensors(useSensor(PointerSensor, { activationConstraint: { distance: 5 } }));
const {
isSolving, isChipSolving, isRunningSens, pendingSolutions, setPendingSolutions, chipSolveSolutions, setChipSolveSolutions, sensResults, setSensResults, sensViewGw, setSensViewGw, handleSolve: apiHandleSolve, handleChipSolve: apiHandleChipSolve, handleSensAnalysis: apiHandleSensAnalysis, loadSettingsFromCloud, saveSettingsToCloud
} = useFplSolverApi(abortControllerRef);
useEffect(() => {
let interval;
if (isRunningSens) { interval = setInterval(() => setSensTimer((t) => t + 1), 1000); } else { setSensTimer(0); }
return () => clearInterval(interval);
}, [isRunningSens]);
useEffect(() => {
let interval;
if (isChipSolving) { interval = setInterval(() => setChipSolveTimer((t) => t + 1), 1000); } else { setChipSolveTimer(0); }
return () => clearInterval(interval);
}, [isChipSolving]);
const maxAvailableHorizon = useMemo(() => (availableGWs.length ? Math.min(10, availableGWs.length) : 10), [availableGWs]);
const horizonGWs = useMemo(() => (availableGWs.length ? availableGWs.slice(0, horizon) : []), [availableGWs, horizon]);
const playerCardGWs = useMemo(() => {
if (!horizonGWs.length || !activeGW) return [];
const idx = horizonGWs.indexOf(activeGW);
return idx === -1 ? [] : horizonGWs.slice(idx).slice(0, 3);
}, [horizonGWs, activeGW]);
const solveGWs = useMemo(() => {
if (!horizonGWs.length || !activeGW) return horizonGWs;
const idx = horizonGWs.indexOf(activeGW);
return idx === -1 ? horizonGWs : horizonGWs.slice(idx);
}, [horizonGWs, activeGW]);
const solveGWLabel = useMemo(() => {
if (!solveGWs.length) return "";
return solveGWs.length === 1 ? `GW${solveGWs[0]}` : `GW${solveGWs[0]}${solveGWs[solveGWs.length - 1]}`;
}, [solveGWs]);
const ownedPlayerIds = useMemo(() => new Set(teamData.filter((p) => !p.isBlank).map((p) => p.ID)), [teamData]);
const hitsThisGw = useMemo(() => {
const T = transfersByGw[activeGW]?.count || 0;
const chip = chipsByGw[activeGW];
if (chip === "wc" || chip === "fh") return 0;
const startFt = ftAtStartOfGw(activeGW, availableGWs, baselineFt, transfersByGw, chipsByGw);
return Math.max(0, T - startFt);
}, [activeGW, availableGWs, baselineFt, transfersByGw, chipsByGw]);
// 1. Standard state (can default to your hardcoded defaults initially)
const [isCloudLoaded, setIsCloudLoaded] = useState(false);
// 1. Fetch from Cloud on Mount / Login
useEffect(() => {
if (teamId && !isCloudLoaded) {
loadSettingsFromCloud(teamId).then((cloudData) => {
if (cloudData) {
if (cloudData.quick) {
setQuickSettings(prev => ({ ...prev, ...cloudData.quick }));
}
// THE FIX: Use setComprehensiveSettings!
if (cloudData.advanced) {
setComprehensiveSettings(prev => ({ ...prev, ...cloudData.advanced }));
}
}
setIsCloudLoaded(true);
});
}
}, [teamId]);
// 2. Save to Cloud (DEBOUNCED)
useEffect(() => {
if (teamId && isCloudLoaded) {
const timerId = setTimeout(() => {
// THE FIX: Pass comprehensiveSettings instead of advancedSettings!
saveSettingsToCloud(teamId, quickSettings, comprehensiveSettings);
}, 500);
return () => clearTimeout(timerId);
}
// THE FIX: Watch comprehensiveSettings in the dependency array!
}, [quickSettings, comprehensiveSettings, teamId, isCloudLoaded]);
const getValidLayout = (players, gw) => {
if (!players || players.length !== 15) return null;
const getEV = (p) => p.isBlank ? -1000 : (Number(p[`${gw}_Pts`]) || 0);
let gks = players.filter((p) => p.Pos === "G").sort((a, b) => getEV(b) - getEV(a));
let defs = players.filter((p) => p.Pos === "D").sort((a, b) => getEV(b) - getEV(a));
let mids = players.filter((p) => p.Pos === "M").sort((a, b) => getEV(b) - getEV(a));
let fwds = players.filter((p) => p.Pos === "F").sort((a, b) => getEV(b) - getEV(a));
const starters = [];
if (gks.length) starters.push(gks.shift());
starters.push(...defs.splice(0, 3), ...mids.splice(0, 2), ...fwds.splice(0, 1));
const remaining = [...defs, ...mids, ...fwds].sort((a, b) => getEV(b) - getEV(a));
starters.push(...remaining.splice(0, 11 - starters.length));
const finalStarters = [
...starters.filter((p) => p.Pos === "G"),
...starters.filter((p) => p.Pos === "D"),
...starters.filter((p) => p.Pos === "M"),
...starters.filter((p) => p.Pos === "F"),
];
const benchGk = gks.length ? gks[0] : null;
const benchRest = remaining.sort((a, b) => getEV(b) - getEV(a));
const bench = benchGk ? [benchGk, ...benchRest] : benchRest;
const topStarters = [...finalStarters].sort((a, b) => getEV(b) - getEV(a));
return { optimalArray: [...finalStarters, ...bench], cap: topStarters[0]?.ID, vice: topStarters[1]?.ID };
};
// Like getValidLayout but penalises players whose EV should be discounted due to
// opposing-play clashes (two squad members facing each other in a fixture).
// Falls back to getValidLayout when fixtures/settings are unavailable.
const getValidLayoutWithPenalty = (players, gw, fixturesArr = fixtures, settings = comprehensiveSettings) => {
if (!players || players.length !== 15) return null;
const penalty = Number(settings?.opposing_play_penalty ?? DEFAULT_SETTINGS.opposing_play_penalty);
// Build a map: teamId -> list of player IDs from this squad playing that GW
const gwFixtures = (fixturesArr || []).filter(f => Number(f.event || f.gw) === Number(gw));
// For each player collect their team identifier (numeric or string)
const getTeamId = (p) => p.team || p.Team || p.team_id;
// Build a set of opposing-team pairs from GW fixtures that involve squad players
// Returns adjusted EV for a player = raw EV - (penalty * number of opponents in squad)
const getAdjustedEV = (p) => {
if (p.isBlank) return -1000;
const rawEV = Number(p[`${gw}_Pts`]) || 0;
if (!penalty || !gwFixtures.length) return rawEV;
const pTeam = getTeamId(p);
if (!pTeam) return rawEV;
// Find fixtures this player's team is in, then count squad opponents
let opponentCount = 0;
gwFixtures.forEach(fix => {
const h = fix.team_h || fix.home_team;
const a = fix.team_a || fix.away_team;
// Is this player's team in this fixture?
if (String(pTeam) === String(h) || String(pTeam) === String(a)) {
const opponentTeam = String(pTeam) === String(h) ? String(a) : String(h);
// Count squad players on the opposing team
players.forEach(other => {
if (other.ID !== p.ID && !other.isBlank) {
const otherTeam = String(getTeamId(other));
if (otherTeam === opponentTeam) opponentCount++;
}
});
}
});
return rawEV - penalty * opponentCount;
};
let gks = players.filter((p) => p.Pos === "G").sort((a, b) => getAdjustedEV(b) - getAdjustedEV(a));
let defs = players.filter((p) => p.Pos === "D").sort((a, b) => getAdjustedEV(b) - getAdjustedEV(a));
let mids = players.filter((p) => p.Pos === "M").sort((a, b) => getAdjustedEV(b) - getAdjustedEV(a));
let fwds = players.filter((p) => p.Pos === "F").sort((a, b) => getAdjustedEV(b) - getAdjustedEV(a));
const starters = [];
if (gks.length) starters.push(gks.shift());
starters.push(...defs.splice(0, 3), ...mids.splice(0, 2), ...fwds.splice(0, 1));
const remaining = [...defs, ...mids, ...fwds].sort((a, b) => getAdjustedEV(b) - getAdjustedEV(a));
starters.push(...remaining.splice(0, 11 - starters.length));
const finalStarters = [
...starters.filter((p) => p.Pos === "G"),
...starters.filter((p) => p.Pos === "D"),
...starters.filter((p) => p.Pos === "M"),
...starters.filter((p) => p.Pos === "F"),
];
const benchGk = gks.length ? gks[0] : null;
const benchRest = remaining.sort((a, b) => getAdjustedEV(b) - getAdjustedEV(a));
const bench = benchGk ? [benchGk, ...benchRest] : benchRest;
const topStarters = [...finalStarters].sort((a, b) => getAdjustedEV(b) - getAdjustedEV(a));
return { optimalArray: [...finalStarters, ...bench], cap: topStarters[0]?.ID, vice: topStarters[1]?.ID };
};
const derivedItb = useMemo(() => {
let currentBank = baselineItb;
if (!availableGWs || availableGWs.length === 0) return currentBank;
for (let gw = availableGWs[0]; gw <= activeGW; gw++) {
if (gw < activeGW && chipsByGw[gw] === "fh") continue;
if (transfersByGw[gw]) currentBank += transfersByGw[gw].netDelta || 0;
}
return currentBank;
}, [activeGW, availableGWs, baselineItb, transfersByGw, chipsByGw]);
const currentRemainingFts = useMemo(() => {
if (!availableGWs || availableGWs.length === 0) return baselineFt;
const startingFts = ftAtStartOfGw(activeGW, availableGWs, baselineFt, transfersByGw, chipsByGw);
const usedThisWeek = transfersByGw[activeGW]?.count || 0;
return Math.max(0, startingFts - usedThisWeek);
}, [activeGW, availableGWs, baselineFt, transfersByGw, chipsByGw, ftAtStartOfGw]);
useEffect(() => {
fetch("https://anayshukla-fpl-solver.hf.space/api/fixtures")
.then((res) => res.json())
.then(data => { setFixtures(data); setIsFixturesLoaded(true); })
.catch(() => { setIsFixturesLoaded(true); });
fetch("https://anayshukla-fpl-solver.hf.space/api/solver/default-settings").then((r) => (r.ok ? r.json() : {})).then((d) => {
if (d && typeof d === "object") {
setComprehensiveSettings(prev => ({ ...d, ...prev }));
}
}).catch(() => { });
}, []);
useEffect(() => { setItb(derivedItb); setAvailableFts(currentRemainingFts); }, [derivedItb, currentRemainingFts, setItb, setAvailableFts]);
useEffect(() => { if (horizon > maxAvailableHorizon && maxAvailableHorizon > 0) setHorizon(maxAvailableHorizon); }, [maxAvailableHorizon, horizon]);
useEffect(() => {
if (!isSolving) { setSolveElapsedSec(0); return; }
const t0 = Date.now();
const id = setInterval(() => setSolveElapsedSec(Math.floor((Date.now() - t0) / 1000)), 250);
const prev = document.body.style.overflow;
document.body.style.overflow = "hidden";
return () => { clearInterval(id); document.body.style.overflow = prev; };
}, [isSolving]);
const hasAutoLoadedAfterLogin = useRef(false);
useEffect(() => {
if (isLoggedIn && userProfile.defaultTeamId && String(userProfile.defaultTeamId) === String(teamId) && !isLoading) {
if (!hasAutoLoadedAfterLogin.current) {
hasAutoLoadedAfterLogin.current = true;
fetchTeam(null, teamData.length > 0);
}
}
if (!isLoggedIn) {
hasAutoLoadedAfterLogin.current = false; // reset on logout
}
}, [isLoggedIn, userProfile.defaultTeamId, teamId]);
const fetchTeam = async (e, preserveState = false) => {
// If manually clicked by user, always wipe the slate clean
if (e) { e.preventDefault(); setManualOverrides({}); preserveState = false; }
if (!teamId) return;
setIsLoading(true); setError(null);
try {
const token = localStorage.getItem('fpl_token');
const res = await fetch(`https://anayshukla-fpl-solver.hf.space/api/manager/${teamId}`, {
headers: token ? { 'Authorization': `Bearer ${token}` } : {}
});
if (!res.ok) throw new Error("Could not fetch team.");
const data = await res.json();
if (data.picks && data.picks.length > 0) {
// 1. ALWAYS populate the strict baseline vault and logic
pristineSquadRef.current = {};
data.picks.forEach(p => {
pristineSquadRef.current[p.ID] = { ...p };
});
setBaselineItb(data.in_the_bank || 0);
setBaselineFt(typeof data.free_transfers === "number" ? data.free_transfers : 1);
setInitialSquadIds(data.picks.map((p) => p.ID));
const gws = Object.keys(data.picks[0]).filter((k) => k.includes("_Pts")).map((k) => parseInt(k.split("_")[0])).sort((a, b) => a - b);
setAvailableGWs(gws);
// 2. ONLY overwrite the squad arrays if we are NOT loading from a saved DB Draft
if (!preserveState) {
setTransfersByGw({}); setHighlightTransferIds({}); setSolverTransferPairs({}); setSolverApplySnapshot(null); setChipsByGw({}); setChipSolveSolutions([]);
setActiveGW(gws[0]);
// THE HYDRATION FIX: Merge the raw FPL picks with the rich Python math BEFORE rendering the pitch!
const hydratedPicks = data.picks.map(p => {
const gMatch = globalPlayers.find(g => String(g.ID) === String(p.ID));
if (gMatch) {
// Attach all EV, Fixture, and Price data instantly
return { ...gMatch, ...p, Price: p.selling_price !== undefined ? p.selling_price : gMatch.Price };
}
return p;
});
// Now getValidLayoutWithPenalty has the EV data + opposing-play penalty to sort properly!
const opt = getValidLayoutWithPenalty(hydratedPicks, gws[0], fixtures, comprehensiveSettings);
if (opt) { setTeamData(opt.optimalArray); setCaptainId(opt.cap); setViceId(opt.vice); }
else { setTeamData(hydratedPicks); }
} else {
// If preserving state, just ensure activeGW doesn't break if the draft lacked it
if (!activeGW) setActiveGW(gws[0]);
}
setLastLoadedId(teamId);
if (isLoggedIn && userProfile.defaultTeamId !== parseInt(teamId)) { setPendingTeamId(parseInt(teamId)); setShowIdPrompt(true); }
}
} catch (err) { setError(err.message); } finally { setIsLoading(false); }
};
useEffect(() => {
if (!teamData.length || !activeGW || teamData.some((p) => p.isBlank && !String(p.ID).startsWith("blank_"))) return;
const gwLock = manualOverrides[activeGW];
if (gwLock && gwLock.ids) {
let reconstructed = gwLock.ids.map((id) => {
if (String(id).startsWith("blank_")) {
const replaced = gwLock.manualTransfers?.[id];
return { ID: id, isBlank: true, Pos: replaced?.Pos || "M", Name: "", Team: "", Price: 0, replacedPlayer: replaced };
}
let found = hydratePlayer(id);
if (found && gwLock.manualTransfers && gwLock.manualTransfers[id]) {
found.replacedPlayer = gwLock.manualTransfers[id];
}
return found;
}).filter(Boolean);
if (reconstructed.length !== 15) {
if (globalPlayers.length > 0) {
setManualOverrides((prev) => { const n = { ...prev }; delete n[activeGW]; return n; });
}
return;
}
// THE FIX: Auto-optimize lineup safely AFTER global EVs finish recalculating!
if (pendingAutoReset) {
const opt = getValidLayoutWithPenalty(reconstructed, activeGW);
if (opt) {
setManualOverrides((prev) => ({ ...prev, [activeGW]: { ...gwLock, ids: opt.optimalArray.map((p) => p.ID), cap: opt.cap, vice: opt.vice } }));
setTeamData(opt.optimalArray);
setCaptainId(opt.cap);
setViceId(opt.vice);
}
setPendingAutoReset(false);
return;
}
let needsSub = false;
const getEV = (p) => Number(p[`${activeGW}_Pts`]) || 0;
for (let i = 0; i < 11; i++) {
const starter = reconstructed[i];
if (starter.isBlank || (getEV(starter) === 0 && (!gwLock.forcedZeros || !gwLock.forcedZeros.includes(starter.ID)))) {
const bestBenchIdx = [11, 12, 13, 14].find((bIdx) => {
const bPlayer = reconstructed[bIdx];
if (bPlayer.isBlank || getEV(bPlayer) <= 0) return false;
const tempStarters = [...reconstructed.slice(0, 11)];
tempStarters[i] = bPlayer;
const counts = { G: 0, D: 0, M: 0, F: 0 };
tempStarters.forEach(p => { if (p.Pos) counts[p.Pos]++; });
if (counts.G !== 1 || counts.D < 3 || counts.M < 2 || counts.F < 1) return false;
return true;
});
if (bestBenchIdx) {
const temp = reconstructed[i];
reconstructed[i] = reconstructed[bestBenchIdx];
reconstructed[bestBenchIdx] = temp;
needsSub = true;
}
}
}
if (needsSub) {
setManualOverrides((prev) => ({ ...prev, [activeGW]: { ...gwLock, ids: reconstructed.map((p) => p.ID) } }));
}
setTeamData(reconstructed);
setCaptainId(gwLock.cap);
setViceId(gwLock.vice);
} else {
let deterministicIds = [...initialSquadIds];
if (availableGWs && availableGWs.length > 0) {
for (let gw = availableGWs[0]; gw < activeGW; gw++) {
if (manualOverrides[gw] && chipsByGw[gw] !== "fh") deterministicIds = manualOverrides[gw].ids;
}
}
const deterministicSquad = deterministicIds.map(id => hydratePlayer(id)).filter(Boolean);
const opt = getValidLayoutWithPenalty(deterministicSquad, activeGW);
if (opt) {
setTeamData(opt.optimalArray);
setCaptainId(opt.cap);
setViceId(opt.vice);
}
}
}, [globalPlayers, activeGW, teamData.length, manualOverrides, pendingAutoReset]);
const activeGwEV = useMemo(() => {
if (!teamData.length || !activeGW) return 0;
const chip = chipsByGw[activeGW];
const capMult = chip === "tc" ? 3 : 2;
let total = 0;
teamData.slice(0, 11).forEach((p) => { if (!p.isBlank) total += (Number(p[`${activeGW}_Pts`]) || 0) * (p.ID === captainId ? capMult : 1); });
let ofIdx = 0;
teamData.slice(11, 15).forEach((p) => {
if (!p.isBlank) {
if (chip === "bb") {
total += (Number(p[`${activeGW}_Pts`]) || 0);
} else {
// THE FIX: Read dynamically from the UI settings!
const rawBw = comprehensiveSettings?.bench_weights || { 0: 0.03, 1: 0.21, 2: 0.06, 3: 0.002 };
const gkWeight = Number(rawBw[0] || 0.03);
const outWeights = [Number(rawBw[1] || 0.21), Number(rawBw[2] || 0.06), Number(rawBw[3] || 0.002)];
if (p.Pos === "G") {
total += (Number(p[`${activeGW}_Pts`]) || 0) * gkWeight;
} else {
const bw = outWeights[ofIdx] || 0.02;
total += (Number(p[`${activeGW}_Pts`]) || 0) * bw;
ofIdx++;
}
}
}
});
return total - hitsThisGw * HIT_COST;
}, [teamData, activeGW, captainId, hitsThisGw, chipsByGw]);
const horizonEvData = useMemo(() => {
if (!teamData.length || !horizonGWs.length) return { total: 0, breakdown: {} };
let total = 0;
const breakdown = {};
// Helper to get the actual squad that existed at the start of a specific GW
const getSquadForGw = (targetGw) => {
// If it's the active GW or in the future, the current teamData is our baseline
if (targetGw >= activeGW) return teamData;
// If it's in the past, rebuild it from the permanent chain
let deterministicIds = [...initialSquadIds];
if (availableGWs && availableGWs.length > 0) {
for (let gw = availableGWs[0]; gw <= targetGw; gw++) {
if (manualOverrides[gw] && chipsByGw[gw] !== "fh") {
deterministicIds = manualOverrides[gw].ids;
}
}
}
return deterministicIds.map(id => hydratePlayer(id)).filter(Boolean);
};
horizonGWs.forEach((gw) => {
let gwPts = 0;
const gwChip = chipsByGw[gw];
const gwCapMult = gwChip === "tc" ? 3 : 2;
const applyBenchMath = (benchSlice) => {
let ofIdx = 0;
benchSlice.forEach((p) => {
if (!p.isBlank) {
if (gwChip === "bb") {
gwPts += (Number(p[`${gw}_Pts`]) || 0);
} else {
// THE FIX: Dynamic settings for horizon EV too!
const rawBw = comprehensiveSettings?.bench_weights || { 0: 0.03, 1: 0.21, 2: 0.06, 3: 0.002 };
const gkWeight = Number(rawBw[0] || 0.03);
const outWeights = [Number(rawBw[1] || 0.21), Number(rawBw[2] || 0.06), Number(rawBw[3] || 0.002)];
if (p.Pos === "G") {
gwPts += (Number(p[`${gw}_Pts`]) || 0) * gkWeight;
} else {
const bw = outWeights[ofIdx] || 0.02;
gwPts += (Number(p[`${gw}_Pts`]) || 0) * bw;
ofIdx++;
}
}
}
});
};
// THE FIX: Use the time-accurate squad for this specific GW
const gwSpecificSquad = getSquadForGw(gw);
if (gw === activeGW) {
gwSpecificSquad.slice(0, 11).forEach((p) => { if (!p.isBlank) gwPts += (Number(p[`${gw}_Pts`]) || 0) * (p.ID === captainId ? gwCapMult : 1); });
applyBenchMath(gwSpecificSquad.slice(11, 15));
} else {
const gwLock = manualOverrides[gw];
if (gwLock && gwLock.ids) {
const reconstructed = gwLock.ids.map((id) => gwSpecificSquad.find((p) => String(p.ID) === String(id)) || globalPlayers.find((p) => String(p.ID) === String(id))).filter(Boolean);
if (reconstructed.length === 15) {
reconstructed.slice(0, 11).forEach((p) => { if (!p.isBlank) gwPts += (Number(p[`${gw}_Pts`]) || 0) * (p.ID === gwLock.cap ? gwCapMult : 1); });
applyBenchMath(reconstructed.slice(11, 15));
} else {
const opt = getValidLayout(gwSpecificSquad, gw);
if (opt) {
opt.optimalArray.slice(0, 11).forEach((p) => { gwPts += (Number(p[`${gw}_Pts`]) || 0) * (p.ID === opt.cap ? gwCapMult : 1); });
applyBenchMath(opt.optimalArray.slice(11, 15));
}
}
} else {
const opt = getValidLayout(gwSpecificSquad, gw);
if (opt) {
opt.optimalArray.slice(0, 11).forEach((p) => { gwPts += (Number(p[`${gw}_Pts`]) || 0) * (p.ID === opt.cap ? gwCapMult : 1); });
applyBenchMath(opt.optimalArray.slice(11, 15));
}
}
}
const ftStart = ftAtStartOfGw(gw, availableGWs, baselineFt, transfersByGw, chipsByGw);
const T = transfersByGw[gw]?.count ?? 0;
const isChipFree = gwChip === "wc" || gwChip === "fh";
const hits = isChipFree ? 0 : Math.max(0, T - ftStart);
const ev = gwPts - hits * HIT_COST;
total += ev;
breakdown[gw] = { ev, chip: gwChip, hits, ftStart, moves: T, isChipFree };
});
return { total, breakdown };
}, [teamData, horizonGWs, activeGW, captainId, manualOverrides, baselineFt, transfersByGw, chipsByGw, globalPlayers, initialSquadIds, availableGWs]);
const horizonEV = horizonEvData.total;
// Sync the breakdown to the active draft automatically
useEffect(() => {
if (Object.keys(horizonEvData.breakdown).length === 0) return;
setDrafts(prev => {
const activeIdx = prev.findIndex(d => d.id === activeDraftId);
if (activeIdx === -1) return prev;
const currentCached = prev[activeIdx].cachedEvs;
if (JSON.stringify(currentCached) === JSON.stringify(horizonEvData.breakdown)) return prev;
const next = [...prev];
next[activeIdx] = { ...next[activeIdx], cachedEvs: horizonEvData.breakdown };
return next;
});
}, [horizonEvData.breakdown, activeDraftId, setDrafts]);
// --- SOLVER API TRIGGERS & BASELINE ENGINE ---
const getSolverStartingState = () => {
const startGW = solveGWs[0];
const startIndex = availableGWs.indexOf(startGW);
// 1. Get Starting Squad (The exact squad going INTO the solve horizon, before current manual moves)
let startingIds = initialSquadIds;
if (startIndex > 0) {
const prevGw = availableGWs[startIndex - 1];
startingIds = manualOverrides[prevGw]?.ids || initialSquadIds;
}
const startingSquad = startingIds.map(id => {
// Try to keep exact FPL prices from current teamData
const existing = teamData.find(t => String(t.ID) === String(id));
if (existing) return existing;
const g = globalPlayers.find(x => String(x.ID) === String(id));
return g ? { ...g, Price: getPlayerPrice(g) } : null;
}).filter(Boolean);
// 2. Get Starting ITB (Bank BEFORE the current gameweek's moves)
let startingItb = baselineItb;
for (let i = 0; i < startIndex; i++) {
const gw = availableGWs[i];
if (chipsByGw[gw] === "fh") continue;
if (transfersByGw[gw]) startingItb += transfersByGw[gw].netDelta || 0;
}
// 3. Get Starting FTs (FTs going INTO the horizon)
const startingFts = ftAtStartOfGw(startGW, availableGWs, baselineFt, transfersByGw, chipsByGw);
// 4. Extract Manual Moves as Booked Transfers
const bookedTransfers = [];
solveGWs.forEach(gw => {
const lock = manualOverrides[gw];
if (lock && lock.manualTransfers) {
Object.entries(lock.manualTransfers).forEach(([inId, outPlayer]) => {
if (!String(inId).startsWith("blank_") && outPlayer) {
bookedTransfers.push({
gw: Number(gw),
transfer_in: Number(inId),
transfer_out: Number(outPlayer.ID)
});
}
});
}
});
return { startingSquad, startingItb, startingFts, bookedTransfers };
};
const getActiveCompSettings = (bookedTransfers) => {
let payload;
// If OFF: Send the absolute baseline defaults from our frontend UI + the manual moves
if (!comprehensiveSettings.enabled) {
payload = { ...DEFAULT_SETTINGS, booked_transfers: bookedTransfers };
}
// If ON: Send the user's custom edited settings + the manual moves
else {
payload = { ...comprehensiveSettings, booked_transfers: bookedTransfers };
}
// Safety check: If they aren't using the advanced FT list, strip it so backend uses the flat value
if (!payload.use_ft_value_list) {
delete payload.ft_value_list;
}
return payload;
};
// --- THE XMINS FILTER ENGINE ---
// Replicates open-fpl-solver logic BEFORE sending the payload to Python
const getFilteredGlobalPlayers = (startingSquad, bookedTransfers) => {
const activeSettings = getActiveCompSettings(bookedTransfers);
const xminLbPerGw = activeSettings.xmin_lb || 0;
// If the setting is 0 or disabled, skip filtering
if (xminLbPerGw <= 0) return globalPlayers;
// Multiply the input by the horizon length to get the total threshold
const totalXminThreshold = xminLbPerGw * horizonGWs.length;
// Build the "safe_players" array (Current squad + anyone you manually locked in)
const safePlayers = new Set(startingSquad.map(p => String(p.ID)));
bookedTransfers.forEach(bt => {
safePlayers.add(String(bt.transfer_in));
safePlayers.add(String(bt.transfer_out));
});
// Execute the filter: (total_min >= xmin_lb) | (ID in safe_players)
return globalPlayers.filter(p => {
if (safePlayers.has(String(p.ID))) return true;
let totalMins = 0;
horizonGWs.forEach(gw => {
totalMins += (Number(p[`${gw}_xMins`]) || 0);
});
return totalMins >= totalXminThreshold;
});
};
const runMainSolver = () => {
const { startingSquad, startingItb, startingFts, bookedTransfers } = getSolverStartingState();
// THE FIX: Calculate apples-to-apples baseline EV for the active window, and the past locked EV
const lockedBaselineEv = solveGWs.reduce((sum, gw) => sum + (horizonEvData.breakdown[gw]?.ev || 0), 0);
const pastBaselineEv = horizonGWs.filter(gw => !solveGWs.includes(gw)).reduce((sum, gw) => sum + (horizonEvData.breakdown[gw]?.ev || 0), 0);
apiHandleSolve({
teamId, solveGWs, horizonGWs, teamData: startingSquad,
globalPlayers: getFilteredGlobalPlayers(startingSquad, bookedTransfers),
itb: startingItb, availableFts: startingFts, advancedSettings, quickSettings, chipsByGw,
comprehensiveSettings: getActiveCompSettings(bookedTransfers),
lockedBaselineEv, pastBaselineEv // <-- INJECTED HERE
});
};
const runSensAnalysis = () => {
const { startingSquad, startingItb, startingFts, bookedTransfers } = getSolverStartingState();
const lockedBaselineEv = solveGWs.reduce((sum, gw) => sum + (horizonEvData.breakdown[gw]?.ev || 0), 0);
const pastBaselineEv = horizonGWs.filter(gw => !solveGWs.includes(gw)).reduce((sum, gw) => sum + (horizonEvData.breakdown[gw]?.ev || 0), 0);
apiHandleSensAnalysis({
teamId, solveGWs, horizonGWs, teamData: startingSquad,
globalPlayers: getFilteredGlobalPlayers(startingSquad, bookedTransfers),
itb: startingItb, availableFts: startingFts, advancedSettings, quickSettings, chipsByGw,
comprehensiveSettings: getActiveCompSettings(bookedTransfers), numSims,
lockedBaselineEv, pastBaselineEv // <-- INJECTED HERE
});
};
const runChipSolve = () => {
const { startingSquad, startingItb, startingFts, bookedTransfers } = getSolverStartingState();
const lockedBaselineEv = solveGWs.reduce((sum, gw) => sum + (horizonEvData.breakdown[gw]?.ev || 0), 0);
const pastBaselineEv = horizonGWs.filter(gw => !solveGWs.includes(gw)).reduce((sum, gw) => sum + (horizonEvData.breakdown[gw]?.ev || 0), 0);
apiHandleChipSolve({
teamId, horizonGWs, teamData: startingSquad,
globalPlayers: getFilteredGlobalPlayers(startingSquad, bookedTransfers),
itb: startingItb, availableFts: startingFts, advancedSettings,
comprehensiveSettings: getActiveCompSettings(bookedTransfers), chipSolveOptions,
lockedBaselineEv, pastBaselineEv // <-- INJECTED HERE
});
};
// --- MULTIVERSE DRAFT HANDLERS ---
const handleCloneDraft = () => {
if (drafts.length >= 5) { alert("Maximum of 5 realities allowed."); return; }
const currentDraft = drafts.find(d => d.id === activeDraftId);
const newId = `draft_${Date.now()}`;
// THE FIX: Deep clone ALL objects to stop timelines from sharing memory
const newDraft = {
...currentDraft,
id: newId,
name: `${currentDraft.name} (Copy)`,
fixtureOverrides: JSON.parse(JSON.stringify(currentDraft.fixtureOverrides || {})),
sessionEdits: JSON.parse(JSON.stringify(currentDraft.sessionEdits || {})),
manualOverrides: JSON.parse(JSON.stringify(currentDraft.manualOverrides || {})),
transfersByGw: JSON.parse(JSON.stringify(currentDraft.transfersByGw || {})),
highlightTransferIds: JSON.parse(JSON.stringify(currentDraft.highlightTransferIds || {})),
solverTransferPairs: JSON.parse(JSON.stringify(currentDraft.solverTransferPairs || {})),
chipsByGw: JSON.parse(JSON.stringify(currentDraft.chipsByGw || {})),
cachedEvs: JSON.parse(JSON.stringify(currentDraft.cachedEvs || {}))
};
setDrafts(prev => [...prev, newDraft]);
setActiveDraftId(newId);
};
const handleNewDraft = () => {
if (drafts.length >= 5) { alert("Maximum of 5 realities allowed."); return; }
const newId = `draft_${Date.now()}`;
const startGW = availableGWs[0] || activeGW;
const pristineSquad = initialSquadIds.map(id => hydratePlayer(id)).filter(Boolean);
const opt = getValidLayoutWithPenalty(pristineSquad, startGW);
const finalSquad = opt ? opt.optimalArray : pristineSquad;
const newDraft = {
id: newId, name: `Draft ${drafts.length + 1}`, teamData: finalSquad, horizon: horizon, activeGW: startGW, captainId: opt ? opt.cap : null, viceId: opt ? opt.vice : null, solverTransferPairs: {}, solverApplySnapshot: null, appliedPlanSummary: null, hitsThisGw: 0, highlightTransferIds: {}, transfersByGw: {}, chipsByGw: {}, manualOverrides: {}, fixtureOverrides: {}, sessionEdits: {}, cachedEvs: {}
};
setDrafts(prev => [...prev, newDraft]);
setActiveDraftId(newId);
};
// --- TIMELINE WIPING HELPER ---
// If you manually edit the pitch, any "future" moves planned by the solver MUST be
// wiped out so that the timeline correctly cascades forward!
const clearFuture = (prev) => {
return prev; // 👈 FIXED: We no longer wipe out future gameweek plans!
};
const applySolution = (sol) => {
setSolverApplySnapshot({
teamData: [...teamData], availableFts, transfersByGw: { ...transfersByGw }, manualOverrides: { ...manualOverrides }, baselineItb, baselineFt
});
const newOverrides = { ...manualOverrides };
const newTransfersByGw = { ...transfersByGw };
const newChipsByGw = { ...chipsByGw };
const newHighlights = { ...highlightTransferIds };
const newPairs = { ...solverTransferPairs };
let runningItb = baselineItb;
const startIndex = availableGWs.indexOf(sol.horizon_gws[0]);
if (startIndex > 0) {
for (let i = 0; i < startIndex; i++) {
const gw = availableGWs[i];
if (chipsByGw[gw] === "fh") continue;
if (transfersByGw[gw]) runningItb += transfersByGw[gw].netDelta || 0;
}
}
const fixedPlanForSummary = [];
sol.plan.forEach(gwPlan => {
const gw = gwPlan.gw;
const getPts = (id) => { const p = globalPlayers.find(x => String(x.ID) === String(id)); return p ? (Number(p[`${gw}_Pts`]) || 0) : 0; };
const posOrder = { G: 1, D: 2, M: 3, F: 4 };
const getPos = (id) => { const p = globalPlayers.find(x => String(x.ID) === String(id)); return p ? posOrder[p.Pos] || 5 : 5; };
let activeLineup = [...gwPlan.lineup];
let activeBench = [...gwPlan.bench];
const isSymmetricChip = sol.chips_used && (sol.chips_used[String(gw)] === "bb" || sol.chips_used[String(gw)] === "fh");
// BUG 2 FIX: Auto-optimize the BB lineup visually so best players start,
// using the opposing-play penalty for accurate EV ranking.
if (isSymmetricChip) {
const all15 = [...activeLineup, ...activeBench];
const pObjs = all15.map(id => {
const p = globalPlayers.find(x => String(x.ID) === String(id));
return p ? { ...p } : { ID: id, Pos: 'M' };
}).filter(Boolean);
const opt = getValidLayoutWithPenalty(pObjs, gw);
if (opt) {
const starterSet = new Set(opt.optimalArray.slice(0, 11).map(p => String(p.ID)));
activeLineup = opt.optimalArray.slice(0, 11).map(p => p.ID);
activeBench = opt.optimalArray.slice(11).map(p => p.ID);
}
}
const sortedLineup = activeLineup.sort((a, b) => {
const posDiff = getPos(a) - getPos(b);
if (posDiff !== 0) return posDiff;
return getPts(b) - getPts(a);
});
newOverrides[gw] = { ids: [...sortedLineup, ...activeBench], cap: gwPlan.captain, vice: gwPlan.vice_captain, forcedZeros: [] };
if (sol.chips_used && sol.chips_used[String(gw)]) newChipsByGw[gw] = sol.chips_used[String(gw)];
let currentNetDelta = 0; // Track this GW's net delta
if (gwPlan.transfers_in.length > 0 || gwPlan.transfers_out.length > 0) {
// THE RE-APPLY FIX: Use hydratePlayer to pull the permanent locked squad prices, NEVER the current UI teamData!
currentNetDelta = gwPlan.transfers_out.reduce((sum, id) => {
const p = hydratePlayer(id);
return sum + (p ? p.Price : 0);
}, 0) - gwPlan.transfers_in.reduce((sum, id) => {
const p = hydratePlayer(id);
return sum + (p ? p.now_cost : 0);
}, 0);
newTransfersByGw[gw] = { count: gwPlan.transfers_in.length, hits: gwPlan.hits, netDelta: currentNetDelta, inIds: gwPlan.transfers_in, outIds: gwPlan.transfers_out };
newHighlights[gw] = [...gwPlan.transfers_in];
const newManualTransfersForGw = {};
gwPlan.transfers_in.forEach((inId, idx) => {
const outId = gwPlan.transfers_out[idx];
const outP = hydratePlayer(outId); // Always fetches the accurate original player!
if (outP) newManualTransfersForGw[inId] = outP;
});
// THE FIX: Delete the solver memory so it doesn't double-render alongside the manual memory!
delete newPairs[gw];
// CLEAN FIX: Attach the transfers directly to the master object we are building
newOverrides[gw] = {
...newOverrides[gw],
manualTransfers: {
...(newOverrides[gw]?.manualTransfers || {}),
...newManualTransfersForGw
}
};
if (gwPlan.chip) newChipsByGw[gw] = gwPlan.chip;
} else {
delete newTransfersByGw[gw];
delete newHighlights[gw];
delete newPairs[gw];
}
let activeItbThisGw = runningItb + currentNetDelta;
if (gwPlan.chip !== "fh") {
runningItb += currentNetDelta;
}
fixedPlanForSummary.push({ ...gwPlan, itb: activeItbThisGw });
});
setManualOverrides(newOverrides); setTransfersByGw(newTransfersByGw); setChipsByGw(newChipsByGw); setHighlightTransferIds(newHighlights); setSolverTransferPairs(newPairs);
setAppliedPlanSummary({
horizon: `GW${sol.horizon_gws[0]} - GW${sol.horizon_gws[sol.horizon_gws.length - 1]}`,
ev: sol.ev,
objectiveScore: sol.objective_score,
plan: fixedPlanForSummary,
lockedBaselineEv: horizonEV,
transfers: sol.plan.map(p => ({
gw: p.gw, chip: p.chip, itb: p.itb, hits: p.hits, ft_at_start: p.ft_at_start,
outs: p.transfers_out.map(id => globalPlayers.find(x => x.ID === id)?.Name || id),
ins: p.transfers_in.map(id => globalPlayers.find(x => x.ID === id)?.Name || id)
}))
});
if (sol.plan.length > 0) {
const activePlan = sol.plan.find(p => p.gw === activeGW) || sol.plan[0];
let nextSquad = [...teamData];
if (activePlan.transfers_in.length > 0) {
activePlan.transfers_in.forEach((inId, idx) => {
const pIn = globalPlayers.find(p => String(p.ID) === String(inId));
const outIndex = nextSquad.findIndex(p => String(p.ID) === String(activePlan.transfers_out[idx]));
if (outIndex !== -1 && pIn) nextSquad[outIndex] = { ...pIn, Price: getPlayerPrice(pIn) };
});
} else {
nextSquad = [...activePlan.lineup, ...activePlan.bench].map(id => {
const existing = teamData.find(t => String(t.ID) === String(id));
const hydrated = hydratePlayer(id);
if (existing && hydrated) return { ...hydrated, replacedPlayer: existing.replacedPlayer };
return hydrated;
}).filter(Boolean);
}
const getPts = (p) => Number(p[`${activePlan.gw}_Pts`]) || 0;
const finalLineup = activePlan.lineup.map(id => nextSquad.find(p => String(p.ID) === String(id))).filter(Boolean);
const finalBench = activePlan.bench.map(id => nextSquad.find(p => String(p.ID) === String(id))).filter(Boolean);
const sortedStarters = [
...finalLineup.filter(p => p.Pos === "G").sort((a, b) => getPts(b) - getPts(a)),
...finalLineup.filter(p => p.Pos === "D").sort((a, b) => getPts(b) - getPts(a)),
...finalLineup.filter(p => p.Pos === "M").sort((a, b) => getPts(b) - getPts(a)),
...finalLineup.filter(p => p.Pos === "F").sort((a, b) => getPts(b) - getPts(a)),
];
const sortedBench = [
...finalBench.filter(p => p.Pos === "G"),
...finalBench.filter(p => p.Pos !== "G").sort((a, b) => getPts(b) - getPts(a))
];
setTeamData([...sortedStarters, ...sortedBench]);
setCaptainId(activePlan.captain); setViceId(activePlan.vice_captain);
}
setPendingSolutions([]);
};
const updateFutureTimelines = (oldSquad, newSquad, currentOverrides, currentTransfers, currentPairs, customMapping = null) => {
let mapping = {};
let removedIds = [];
let addedIds = [];
if (customMapping) {
Object.keys(customMapping).forEach(k => {
mapping[String(k)] = String(customMapping[k]);
removedIds.push(String(k));
addedIds.push(String(customMapping[k]));
});
} else {
const oldIds = oldSquad.map(p => String(p.ID));
const newIds = newSquad.map(p => String(p.ID));
removedIds = oldIds.filter(id => !newIds.includes(id) && !id.startsWith("blank_"));
addedIds = newIds.filter(id => !oldIds.includes(id) && !id.startsWith("blank_"));
for (let i = 0; i < Math.min(removedIds.length, addedIds.length); i++) {
mapping[removedIds[i]] = addedIds[i];
}
}
const nextOverrides = { ...currentOverrides };
const nextTransfers = { ...currentTransfers };
const nextPairs = { ...currentPairs };
for (let gw = activeGW + 1; gw <= Math.max(...(availableGWs || [])); gw++) {
// 1. SURGICAL SCRUB: Remove redundant "Buy" plans to kill the ghost button,
// but DO NOT filter "outIds" so the Y->Z to X->Z cascade survives perfectly!
if (nextTransfers[gw]) {
nextTransfers[gw].inIds = (nextTransfers[gw].inIds || []).filter(id => !addedIds.includes(String(id)));
nextTransfers[gw].count = nextTransfers[gw].inIds.length;
if (nextTransfers[gw].count === 0) delete nextTransfers[gw];
}
if (nextOverrides[gw]) {
const lock = nextOverrides[gw];
const updatedIds = lock.ids.map(id => mapping[String(id)] || String(id));
// Anti-Time-Paradox: Only wipe the GW if the cascade creates literal duplicate players
const uniqueIds = new Set(updatedIds);
if (uniqueIds.size !== updatedIds.length) {
delete nextOverrides[gw];
delete nextTransfers[gw];
delete nextPairs[gw];
// THE FIX: Plunge the timeline into darkness so the UI doesn't glow for a deleted GW!
setHighlightTransferIds(prev => { const n = { ...prev }; delete n[gw]; return n; });
setTransfersByGw(prev => { const n = { ...prev }; delete n[gw]; return n; });
continue;
}
const updatedTransfers = {};
if (lock.manualTransfers) {
for (const [inId, outPlayer] of Object.entries(lock.manualTransfers)) {
// KILL OBSOLETE MOVES & GLOWS: Skip this move if the player is already naturally in the incoming squad
if (addedIds.includes(String(inId)) || newSquad.some(p => String(p.ID) === String(inId))) {
// FIX: highlightTransferIds is an object of arrays. Target the specific GW array.
setHighlightTransferIds(prev => ({
...prev,
[gw]: Array.from(prev[gw] || []).filter(id => String(id) !== String(inId))
}));
// FIX: transfersByGw is an object of objects. Safely reduce the count.
setTransfersByGw(prev => {
const currentGwTransfers = prev[gw];
if (!currentGwTransfers) return prev;
const newInIds = Array.from(currentGwTransfers.inIds || []).filter(id => String(id) !== String(inId));
const newCount = Math.max(0, (currentGwTransfers.count || 1) - 1);
if (newCount === 0) {
const next = { ...prev };
delete next[gw];
return next;
}
return { ...prev, [gw]: { ...currentGwTransfers, inIds: newInIds, count: newCount } };
});
continue;
}
let newOutPlayer = outPlayer;
const outIdStr = String(outPlayer?.ID);
if (outPlayer && mapping[outIdStr]) {
const mappedId = mapping[outIdStr];
let mappedP = globalPlayers.find(p => String(p.ID) === mappedId) || newSquad.find(p => String(p.ID) === mappedId);
if (mappedP) newOutPlayer = { ...mappedP, Price: getPlayerPrice(mappedP) };
}
updatedTransfers[mapping[String(inId)] || String(inId)] = newOutPlayer;
}
}
// --- RE-OPTIMIZE LINEUP FOR FUTURE GAMEWEEKS ---
// Instantly sub out the cascaded player if their EV is bad in this future gameweek
const reconstructedSquad = updatedIds.map(id => {
if (String(id).startsWith("blank_")) {
const replaced = updatedTransfers[id];
return { ID: id, isBlank: true, Pos: replaced?.Pos || "M", Name: "", Team: "", Price: 0, replacedPlayer: replaced };
}
return hydratePlayer(id);
}).filter(Boolean);
const opt = getValidLayoutWithPenalty(reconstructedSquad, gw);
nextOverrides[gw] = {
...lock,
ids: opt ? opt.optimalArray.map(p => p.ID) : updatedIds,
manualTransfers: updatedTransfers,
cap: opt ? opt.cap : mapping[String(lock.cap)] || lock.cap,
vice: opt ? opt.vice : mapping[String(lock.vice)] || lock.vice
};
}
if (nextPairs[gw]) {
const updatedGwPairs = {};
for (const [inId, pairData] of Object.entries(nextPairs[gw])) {
// KILL GHOST BUTTON & GLOW: Skip this solver memory if the player naturally returns to squad
if (addedIds.includes(String(inId)) || newSquad.some(p => String(p.ID) === String(inId))) {
setHighlightTransferIds(prev => ({ ...prev, [gw]: Array.from(prev[gw] || []).filter(id => String(id) !== String(inId)) }));
setTransfersByGw(prev => {
const currentGwTransfers = prev[gw];
if (!currentGwTransfers) return prev;
const newInIds = Array.from(currentGwTransfers.inIds || []).filter(id => String(id) !== String(inId));
const newCount = Math.max(0, (currentGwTransfers.count || 1) - 1);
if (newCount === 0) { const next = { ...prev }; delete next[gw]; return next; }
return { ...prev, [gw]: { ...currentGwTransfers, inIds: newInIds, count: newCount } };
});
continue;
}
let newOut = pairData.outPlayer;
const outIdStr = String(newOut?.ID);
if (newOut && mapping[outIdStr]) {
const mappedId = mapping[outIdStr];
let mappedP = globalPlayers.find(p => String(p.ID) === mappedId) || newSquad.find(p => String(p.ID) === mappedId);
if (mappedP) newOut = { ...mappedP, Price: getPlayerPrice(mappedP) };
}
updatedGwPairs[mapping[String(inId)] || String(inId)] = { outPlayer: newOut };
}
if (Object.keys(updatedGwPairs).length === 0) {
delete nextPairs[gw];
} else {
nextPairs[gw] = updatedGwPairs;
}
}
}
return { nextOverrides, nextTransfers, nextPairs };
};
const handleDragStart = (event) => setActiveDragPlayer(event.active.data.current.player);
const isValidSwap = (p1, p2) => {
if (!p1 || !p2 || p1.isBlank || p2.isBlank) return false;
if (p1.ID === p2.ID) return true;
if (p1.Pos === "G" && p2.Pos !== "G") return false;
if (p1.Pos !== "G" && p2.Pos === "G") return false;
const currentStarters = teamData.slice(0, 11);
const isP1Starter = currentStarters.some((p) => p.ID === p1.ID);
const isP2Starter = currentStarters.some((p) => p.ID === p2.ID);
if (isP1Starter === isP2Starter) return true;
const newStarters = currentStarters.filter((p) => p.ID !== p1.ID && p.ID !== p2.ID);
newStarters.push(isP1Starter ? p2 : p1);
const counts = { G: 0, D: 0, M: 0, F: 0 };
newStarters.forEach((p) => counts[p.Pos]++);
return counts.G === 1 && counts.D >= 3 && counts.M >= 2 && counts.F >= 1 && newStarters.length === 11;
};
const handleDragEnd = (event) => {
const { active, over } = event;
setActiveDragPlayer(null);
if (over && active.id !== over.id) {
const p1 = active.data.current.player; const p2 = over.data.current.player;
if (isValidSwap(p1, p2)) {
const newArr = [...teamData];
const idx1 = newArr.findIndex((p) => p.ID === p1.ID);
const idx2 = newArr.findIndex((p) => p.ID === p2.ID);
newArr[idx1] = p2; newArr[idx2] = p1;
const normalized = idx1 >= 11 || idx2 >= 11 ? normalizeBenchGkFirst(newArr, activeGW) : newArr;
const forcedZeros = manualOverrides[activeGW]?.forcedZeros || [];
if ((Number(p1[`${activeGW}_Pts`]) || 0) === 0 && idx2 < 11) forcedZeros.push(p1.ID);
if ((Number(p2[`${activeGW}_Pts`]) || 0) === 0 && idx1 < 11) forcedZeros.push(p2.ID);
let newCap = captainId; let newVice = viceId;
const getEV = (p) => p.isBlank ? -1000 : (Number(p[`${activeGW}_Pts`]) || 0);
const starters = normalized.slice(0, 11);
const starterIds = starters.map((p) => p.ID);
if (!starterIds.includes(newCap)) {
const sorted = [...starters].sort((a, b) => getEV(b) - getEV(a));
newCap = sorted[0]?.ID;
if (newCap === newVice) newVice = sorted[1]?.ID;
}
if (!starterIds.includes(newVice)) {
const sorted = [...starters].sort((a, b) => getEV(b) - getEV(a));
newVice = sorted.find((p) => p.ID !== newCap)?.ID;
}
setManualOverrides((prev) => clearFuture({ ...prev, [activeGW]: { ...prev[activeGW], ids: normalized.map((p) => p.ID), cap: newCap, vice: newVice, forcedZeros } }));
setTeamData(normalized);
setTransfersByGw(clearFuture);
setHighlightTransferIds(clearFuture);
setSolverTransferPairs(clearFuture);
setChipsByGw(clearFuture);
setAppliedPlanSummary(null);
}
}
};
const handleCapChange = (id, type) => {
let newCap = captainId; let newVice = viceId;
if (type === "C") { newCap = id; if (viceId === id) newVice = captainId; } else { newVice = id; if (captainId === id) newCap = viceId; }
setManualOverrides((prev) => clearFuture({ ...prev, [activeGW]: { ...prev[activeGW], ids: teamData.map((p) => p.ID), cap: newCap, vice: newVice, forcedZeros: prev[activeGW]?.forcedZeros } }));
setCaptainId(newCap); setViceId(newVice);
setTransfersByGw(clearFuture);
setHighlightTransferIds(clearFuture);
setSolverTransferPairs(clearFuture);
setChipsByGw(clearFuture);
setAppliedPlanSummary(null);
};
const handleResetGW = () => {
const opt = getValidLayoutWithPenalty(teamData, activeGW);
if (!opt) return;
setManualOverrides((prev) => clearFuture({ ...prev, [activeGW]: { ...prev[activeGW], ids: opt.optimalArray.map((p) => p.ID), cap: opt.cap, vice: opt.vice, forcedZeros: prev[activeGW]?.forcedZeros || [] } }));
setTeamData(opt.optimalArray); setCaptainId(opt.cap); setViceId(opt.vice);
setTransfersByGw(clearFuture);
setHighlightTransferIds(clearFuture);
setSolverTransferPairs(clearFuture);
setChipsByGw(clearFuture);
setAppliedPlanSummary(null);
};
const handleChipSelect = (gw, chipType) => {
setChipsByGw((prev) => {
const next = { ...prev };
if (!chipType) { delete next[gw]; } else { Object.keys(next).forEach((g) => { if (next[g] === chipType) delete next[g]; }); next[gw] = chipType; }
return clearFuture(next);
});
setManualOverrides(clearFuture);
setTransfersByGw(clearFuture);
setHighlightTransferIds(clearFuture);
setSolverTransferPairs(clearFuture);
setAppliedPlanSummary(null);
};
const handleTransferOut = (playerToDrop) => {
const sellPrice = getPlayerPrice(playerToDrop);
const blankId = `blank_${Date.now()}`;
const newSquad = teamData.map((p) => String(p.ID) === String(playerToDrop.ID) ? { ID: blankId, isBlank: true, Pos: p.Pos, Name: "", Team: "", Price: 0, replacedPlayer: playerToDrop } : p);
const opt = getValidLayoutWithPenalty(newSquad, activeGW);
const finalSquad = opt ? opt.optimalArray : newSquad;
let nextTransfers = { ...transfersByGw };
nextTransfers[activeGW] = { ...(nextTransfers[activeGW] || { count: 0, netDelta: 0 }), netDelta: (nextTransfers[activeGW]?.netDelta || 0) + sellPrice };
let nextOverrides = { ...manualOverrides };
nextOverrides[activeGW] = {
...(nextOverrides[activeGW] || {}), ids: finalSquad.map(p => p.ID),
cap: opt ? opt.cap : captainId, vice: opt ? opt.vice : viceId,
manualTransfers: { ...(nextOverrides[activeGW]?.manualTransfers || {}), [blankId]: playerToDrop }
};
const mapping = { [playerToDrop.ID]: blankId };
const { nextOverrides: cascadedO, nextTransfers: cascadedT, nextPairs: cascadedP } = updateFutureTimelines(teamData, finalSquad, nextOverrides, nextTransfers, solverTransferPairs, mapping);
setTransfersByGw(cascadedT); setManualOverrides(cascadedO); setTeamData(finalSquad); setSolverTransferPairs(cascadedP);
setHighlightTransferIds(clearFuture); setChipsByGw(clearFuture); setAppliedPlanSummary(null); setSelectedPlayer(null);
};
const handleAddPlayer = (newPlayer) => {
const cost = getPlayerPrice(newPlayer);
if (itb < cost) return alert("Insufficient funds!");
const newSquad = teamData.map((p) => String(p.ID) === String(selectedPlayer.ID) ? { ...newPlayer, Price: cost, purchase_price: newPlayer.now_cost, selling_price: newPlayer.now_cost, replacedPlayer: selectedPlayer.replacedPlayer } : p);
const opt = getValidLayoutWithPenalty(newSquad, activeGW);
const finalSquad = opt ? opt.optimalArray : newSquad;
let nextTransfers = { ...transfersByGw };
nextTransfers[activeGW] = { ...(nextTransfers[activeGW] || { count: 0, netDelta: 0 }), count: (nextTransfers[activeGW]?.count || 0) + 1, netDelta: (nextTransfers[activeGW]?.netDelta || 0) - cost };
const newManualTransfers = { ...(manualOverrides[activeGW]?.manualTransfers || {}) };
delete newManualTransfers[selectedPlayer.ID];
if (selectedPlayer.replacedPlayer) newManualTransfers[newPlayer.ID] = selectedPlayer.replacedPlayer;
let nextOverrides = { ...manualOverrides };
nextOverrides[activeGW] = {
...nextOverrides[activeGW], ids: finalSquad.map(p => p.ID),
cap: opt ? opt.cap : captainId, vice: opt ? opt.vice : viceId,
forcedZeros: nextOverrides[activeGW]?.forcedZeros || [], manualTransfers: newManualTransfers
};
const mapping = { [selectedPlayer.ID]: newPlayer.ID };
if (selectedPlayer.replacedPlayer) mapping[selectedPlayer.replacedPlayer.ID] = newPlayer.ID;
const { nextOverrides: cascadedO, nextTransfers: cascadedT, nextPairs: cascadedP } = updateFutureTimelines(teamData, finalSquad, nextOverrides, nextTransfers, solverTransferPairs, mapping);
setTransfersByGw(cascadedT); setManualOverrides(cascadedO); setTeamData(finalSquad); setSolverTransferPairs(cascadedP);
if (opt) { setCaptainId(opt.cap); setViceId(opt.vice); }
setHighlightTransferIds((prev) => clearFuture({ ...prev, [activeGW]: [...(prev[activeGW] || []), newPlayer.ID] }));
setChipsByGw(clearFuture); setAppliedPlanSummary(null); setSelectedPlayer(null); setSearchQuery("");
};
const handleUndoTransfer = (e, currentId, replacedPlayer) => {
e.stopPropagation();
const buyPlayer = teamData.find((p) => String(p.ID) === String(currentId)) || globalPlayers.find((p) => String(p.ID) === String(currentId));
const buy = (!String(currentId).startsWith("blank_") && buyPlayer) ? getPlayerPrice(buyPlayer) : 0;
const sell = getPlayerPrice(replacedPlayer);
// FRESHEN REPLACED PLAYER: Ensure EV is up to date before optimizing the lineup
// const freshReplacedPlayer = { ...(globalPlayers.find(p => String(p.ID) === String(replacedPlayer.ID)) || replacedPlayer), Price: getPlayerPrice(replacedPlayer) };
const freshReplacedPlayer = hydratePlayer(replacedPlayer.ID, replacedPlayer) || replacedPlayer;
const newSquad = teamData.map((p) => (String(p.ID) === String(currentId) ? freshReplacedPlayer : p));
const opt = getValidLayoutWithPenalty(newSquad, activeGW);
const finalSquad = opt ? opt.optimalArray : newSquad;
let nextTransfers = { ...transfersByGw };
const row = nextTransfers[activeGW] || { count: 0, netDelta: 0 };
nextTransfers[activeGW] = { ...row, count: Math.max(0, row.count - (!String(currentId).startsWith("blank_") ? 1 : 0)), netDelta: row.netDelta - (sell - buy) };
let nextOverrides = { ...manualOverrides };
const newManualTransfers = { ...(nextOverrides[activeGW]?.manualTransfers || {}) };
delete newManualTransfers[currentId];
nextOverrides[activeGW] = {
...nextOverrides[activeGW], ids: finalSquad.map(p => p.ID),
cap: opt ? opt.cap : captainId, vice: opt ? opt.vice : viceId,
forcedZeros: nextOverrides[activeGW]?.forcedZeros || [], manualTransfers: newManualTransfers
};
const mapping = { [currentId]: replacedPlayer.ID };
const { nextOverrides: cascadedO, nextTransfers: cascadedT, nextPairs: cascadedP } = updateFutureTimelines(teamData, finalSquad, nextOverrides, nextTransfers, solverTransferPairs, mapping);
setTransfersByGw(cascadedT); setManualOverrides(cascadedO); setTeamData(finalSquad); setSolverTransferPairs(cascadedP);
if (opt) { setCaptainId(opt.cap); setViceId(opt.vice); }
setHighlightTransferIds((prev) => clearFuture({ ...prev, [activeGW]: Array.from(prev[activeGW] || []).filter((id) => String(id) !== String(currentId)) }));
setChipsByGw(clearFuture); setAppliedPlanSummary(null);
};
const resetHighlightedTransfer = (player) => {
const pair = (solverTransferPairs[activeGW] || {})[player.ID];
if (pair?.outPlayer) {
const idx = teamData.findIndex((p) => p.ID === player.ID);
if (idx < 0) return;
// FRESHEN REPLACED PLAYER: Ensure EV is up to date before optimizing the lineup
// const freshOutPlayer = { ...(globalPlayers.find(p => String(p.ID) === String(pair.outPlayer.ID)) || pair.outPlayer), Price: getPlayerPrice(pair.outPlayer) };
const freshOutPlayer = hydratePlayer(pair.outPlayer.ID, pair.outPlayer) || pair.outPlayer;
const newSquad = [...teamData]; newSquad[idx] = freshOutPlayer;
const buyPrice = getPlayerPrice(player); const sellPrice = getPlayerPrice(pair.outPlayer);
let nextTransfers = { ...transfersByGw };
const row = nextTransfers[activeGW] || { count: 0, netDelta: 0 };
nextTransfers[activeGW] = {
...row, count: Math.max(0, row.count - 1), netDelta: row.netDelta - (sellPrice - buyPrice),
inIds: Array.from(row.inIds || []).filter(id => String(id) !== String(player.ID)), outIds: Array.from(row.outIds || []).filter(id => String(id) !== String(pair.outPlayer.ID))
};
const opt = getValidLayoutWithPenalty(newSquad, activeGW);
const finalSquad = opt ? opt.optimalArray : newSquad;
let nextOverrides = { ...manualOverrides };
nextOverrides[activeGW] = { ...nextOverrides[activeGW], ids: finalSquad.map(p => p.ID), cap: opt ? opt.cap : captainId, vice: opt ? opt.vice : viceId, forcedZeros: nextOverrides[activeGW]?.forcedZeros || [] };
const mapping = { [player.ID]: pair.outPlayer.ID };
const { nextOverrides: cascadedO, nextTransfers: cascadedT, nextPairs: cascadedP } = updateFutureTimelines(teamData, finalSquad, nextOverrides, nextTransfers, solverTransferPairs, mapping);
setTransfersByGw(cascadedT); setManualOverrides(cascadedO); setTeamData(finalSquad);
if (opt) { setCaptainId(opt.cap); setViceId(opt.vice); }
const nP = { ...cascadedP };
if (nP[activeGW]) { delete nP[activeGW][player.ID]; }
setSolverTransferPairs(nP);
setHighlightTransferIds((prev) => { const n = { ...prev }; if (n[activeGW]) { const gwSet = new Set(n[activeGW]); gwSet.delete(player.ID); n[activeGW] = Array.from(gwSet); } return clearFuture(n); });
setChipsByGw(clearFuture); setAppliedPlanSummary(null); setSelectedPlayer(null); return;
}
if (player.replacedPlayer) { handleUndoTransfer({ stopPropagation: () => { } }, player.ID, player.replacedPlayer); return; }
handleTransferOut(player);
};
const handleResetGWTransfers = () => {
let previousSquadIds = [];
const currentIndex = availableGWs.indexOf(activeGW);
if (currentIndex > 0) {
const prevGw = availableGWs[currentIndex - 1];
previousSquadIds = manualOverrides[prevGw]?.ids || initialSquadIds;
} else {
previousSquadIds = initialSquadIds;
}
//const restoredSquadUnsorted = previousSquadIds.map(id => {
// const p = globalPlayers.find(x => String(x.ID) === String(id));
// const existing = teamData.find(t => String(t.ID) === String(id));
// return existing ? { ...p, Price: existing.Price } : { ...p, Price: getPlayerPrice(p) };
// }).filter(Boolean);
const restoredSquadUnsorted = previousSquadIds.map(id => hydratePlayer(id)).filter(Boolean);
const opt = getValidLayoutWithPenalty(restoredSquadUnsorted, activeGW);
const finalSquad = opt ? opt.optimalArray : restoredSquadUnsorted;
let nextTransfers = { ...transfersByGw };
delete nextTransfers[activeGW];
let nextOverrides = { ...manualOverrides };
nextOverrides[activeGW] = {
ids: finalSquad.map(p => p.ID), cap: opt ? opt.cap : captainId, vice: opt ? opt.vice : viceId, forcedZeros: [], manualTransfers: {}
};
const { nextOverrides: cascadedO, nextTransfers: cascadedT, nextPairs: cascadedP } = updateFutureTimelines(teamData, finalSquad, nextOverrides, nextTransfers, solverTransferPairs);
setTransfersByGw(cascadedT); setManualOverrides(cascadedO); setTeamData(finalSquad);
if (opt) { setCaptainId(opt.cap); setViceId(opt.vice); }
const nP = { ...cascadedP };
delete nP[activeGW];
setSolverTransferPairs(nP);
setHighlightTransferIds(prev => { const next = { ...prev }; delete next[activeGW]; return clearFuture(next); });
setChipsByGw(prev => { const next = { ...prev }; delete next[activeGW]; return clearFuture(next); });
setSolverApplySnapshot(null); setAppliedPlanSummary(null);
};
// --- UI FIREWALL ---
// Forces the Pitch to instantly drop stale undo buttons during GW tab switches
const renderTeamData = useMemo(() => {
const lock = manualOverrides[activeGW];
return teamData.map(p => {
if (p.isBlank && String(p.ID).startsWith("blank_")) return p;
const cleanP = { ...p };
if (lock?.manualTransfers && lock.manualTransfers[p.ID]) {
cleanP.replacedPlayer = lock.manualTransfers[p.ID];
} else {
delete cleanP.replacedPlayer;
}
return cleanP;
});
}, [teamData, activeGW, manualOverrides]);
return (
<div className="flex flex-col w-full h-full pb-10">
{/* Minimal Top Bar for Load */}
<div className="w-full flex justify-end mb-4 z-dropdown">
<form onSubmit={fetchTeam} className="flex gap-2 items-center bg-slate-900/40 px-4 py-2 rounded-xl border border-slate-800 backdrop-blur-sm shadow-xl">
<div className="relative w-48">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 text-slate-500" size={14} />
<input type="text" placeholder="FPL Team ID..." value={teamId} onChange={(e) => { setTeamId(e.target.value); setLastLoadedId(null); }} className="w-full bg-slate-950 border border-slate-700 rounded-lg py-1.5 pl-8 pr-3 text-xs text-slate-200 focus:outline-none focus:border-luigi-400 shadow-inner" />
</div>
<button type="submit" disabled={isLoading || (teamData.length > 0 && teamId === lastLoadedId)} className="bg-luigi-500 hover:bg-luigi-400 text-slate-950 font-bold px-3 py-1.5 rounded-lg text-xs flex items-center gap-1.5 shadow-lg transition-colors disabled:opacity-50 disabled:cursor-not-allowed">
{isLoading ? <Loader2 size={14} className="animate-spin" /> : "Load"}
</button>
</form>
</div>
<div className="flex flex-col xl:flex-row gap-8 w-full">
<div className="w-full xl:w-[72%] flex flex-col gap-4 xl:-mt-12 relative z-base">
{teamData.length > 0 && isFixturesLoaded ? (
<>
{/* Pitch Rendering wrapper */}
<DndContext sensors={sensors} collisionDetection={closestCenter} onDragStart={handleDragStart} onDragEnd={handleDragEnd}>
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-2 bg-slate-900/30 p-3 rounded-xl border border-slate-800/50 mb-2">
{/* STATS ROW - Clean spacing, non-wrapping to prevent jitter */}
<div className="flex items-center gap-4 w-full overflow-x-auto hide-scrollbar">
<div className="flex flex-col">
<span className="text-[10px] font-black text-slate-500 uppercase">ITB</span>
<span className="text-sm font-mono font-bold text-emerald-400 tabular-nums">£{Math.abs(itb) < 0.05 ? "0.0" : itb.toFixed(1)}m</span>
</div>
<div className="flex flex-col">
<span className="text-[10px] font-black text-slate-500 uppercase">FT</span>
<span className="text-sm font-mono font-bold text-cyan-400 leading-tight whitespace-nowrap">
{(() => {
const chip = chipsByGw[activeGW]; const T = transfersByGw[activeGW]?.count ?? 0;
if (chip === "wc") return <><span className="text-yellow-400 text-xs font-black">⚡ WC</span> <span className="text-slate-500 text-[10px]">({T}/∞)</span></>;
if (chip === "fh") return <><span className="text-orange-400 text-xs font-black">↩ FH</span> <span className="text-slate-500 text-[10px]">({T}/∞)</span></>;
return `${T} / ${ftAtStartOfGw(activeGW, availableGWs, baselineFt, transfersByGw, chipsByGw)}${hitsThisGw > 0 ? ` (-${hitsThisGw * 4} pts)` : ""}`;
})()}
</span>
</div>
<div className="flex flex-col">
<span className="text-[10px] font-black text-slate-500 uppercase">Horizon</span>
<select value={horizon} onChange={(e) => setHorizon(Number(e.target.value))} className="bg-transparent text-sm font-mono font-bold text-luigi-400 outline-none cursor-pointer">
{Array.from({ length: maxAvailableHorizon }, (_, i) => i + 1).map((h) => (<option key={h} value={h} className="bg-slate-900">{h} {h === 1 ? "GW" : "GWs"}</option>))}
</select>
</div>
<div className="h-8 w-px bg-slate-700 hidden sm:block"></div>
<div className="flex flex-col">
<span className="text-[10px] font-black text-slate-500 uppercase whitespace-nowrap">GW {activeGW} EV</span>
<span className="text-sm font-mono font-bold text-cyan-400 tabular-nums">{activeGwEV.toFixed(2)}</span>
</div>
{horizonGWs.length > 1 && (
<div className="flex flex-col">
<span className="text-[10px] font-black text-slate-500 uppercase whitespace-nowrap">Horizon EV</span>
<span className="text-sm font-mono font-bold text-emerald-400 tabular-nums">{horizonEV.toFixed(2)}</span>
</div>
)}
</div>
{/* BUTTON ROW - Pushed to the right, strictly locked nowrap */}
<div className="flex flex-nowrap items-center justify-between gap-2 w-full">
{/* 1. RESET TRANSFERS/CHIPS BUTTON (Always Rendered, Disabled if Not Needed) */}
{(() => {
const canReset = (transfersByGw[activeGW]?.count > 0) || chipsByGw[activeGW] || (manualOverrides[activeGW]?.manualTransfers && Object.keys(manualOverrides[activeGW].manualTransfers).length > 0);
return (
<button
type="button"
onClick={handleResetGWTransfers}
disabled={!canReset}
className="flex items-center gap-1.5 px-3 py-1.5 rounded-lg bg-red-500/10 border border-red-500/20 text-red-400 text-[10px] font-black uppercase tracking-wider transition-all shadow-lg shrink-0 whitespace-nowrap hover:bg-red-500/20 hover:border-red-500/40 active:scale-95 disabled:opacity-30 disabled:cursor-not-allowed disabled:hover:bg-red-500/10 disabled:hover:border-red-500/20 disabled:active:scale-100 disabled:shadow-none"
>
<RotateCcw size={12} /> Reset
</button>
);
})()}
{/* 2. RESET LINEUP BUTTON (Always Rendered, Disabled if Not Needed) */}
{(() => {
let canResetLineup = false;
const gwLock = manualOverrides[activeGW];
if (gwLock?.ids && teamData.length === 15 && !teamData.some((p) => p.isBlank && !String(p.ID).startsWith("blank_"))) {
const opt = getValidLayoutWithPenalty(teamData, activeGW);
if (opt) {
const lockStarterSet = new Set(gwLock.ids.slice(0, 11));
const optStarterSet = new Set(opt.optimalArray.slice(0, 11).map((p) => p.ID));
const differentStarters = lockStarterSet.size !== optStarterSet.size || [...lockStarterSet].some((id) => !optStarterSet.has(id));
const meaningfulDiff = differentStarters || gwLock.cap !== opt.cap || gwLock.vice !== opt.vice;
if (meaningfulDiff) {
const getPts = (p) => Number(p[`${activeGW}_Pts`]) || 0;
const currentEV = teamData.slice(0, 11).reduce((sum, p) => sum + getPts(p) * (p.ID === gwLock.cap ? 2 : 1), 0);
const optEV = opt.optimalArray.slice(0, 11).reduce((sum, p) => sum + getPts(p) * (p.ID === opt.cap ? 2 : 1), 0);
if (optEV > currentEV + 0.01) {
canResetLineup = true;
}
}
}
}
return (
<button
onClick={handleResetGW}
disabled={!canResetLineup}
className="flex items-center gap-1.5 px-3 py-1.5 rounded-lg bg-luigi-500/10 border border-luigi-500/20 text-luigi-400 text-[10px] font-black uppercase tracking-wider transition-all shadow-lg shrink-0 whitespace-nowrap hover:bg-luigi-500/20 hover:border-luigi-500/40 active:scale-95 disabled:opacity-30 disabled:cursor-not-allowed disabled:hover:bg-luigi-500/10 disabled:hover:border-luigi-500/20 disabled:active:scale-100 disabled:shadow-none"
title="Reset Lineup to Optimal"
>
<RotateCcw size={12} /> Reset Lineup
</button>
);
})()}
{/* 3. CHIP DROPDOWN */}
<div className="flex items-center gap-1.5 shrink-0">
<span className="text-[10px] font-black text-slate-500 uppercase tracking-wider">Chip:</span>
<select value={chipsByGw[activeGW] || ""} onChange={(e) => handleChipSelect(activeGW, e.target.value || null)} className="bg-slate-900 border border-slate-700 rounded px-2 py-1 text-xs font-bold text-slate-300 focus:outline-none cursor-pointer focus:border-luigi-500">
<option value="">None</option><option value="wc">⚡ WC</option><option value="fh">↩ FH</option><option value="bb">⬆ BB</option><option value="tc">✕3 TC</option>
</select>
</div>
</div>
</div>
<DraftsComparisonTable
drafts={drafts}
horizonGWs={horizonGWs}
activeDraftId={activeDraftId}
globalPlayers={globalPlayers}
setActiveDraftId={setActiveDraftId}
getValidLayout={getValidLayoutWithPenalty}
availableGWs={availableGWs}
setDrafts={setDrafts}
baselineFt={baselineFt}
baselineItb={baselineItb}
ftAtStartOfGw={ftAtStartOfGw}
advancedSettings={advancedSettings}
/>
{/* MULTIVERSE TIMELINE CONTROL BAR */}
<div className="flex flex-col md:flex-row items-center justify-between gap-2 bg-slate-900/80 p-2 rounded-xl border border-[#2a2d5c] backdrop-blur-md shadow-lg mb-3 relative z-dropdown">
{/* LEFT: Custom Editable Dropdown Box */}
<div className="relative w-full md:w-56 shrink-0 z-popover">
<div className="flex items-center bg-[#0a0f1c] border border-[#2a2d5c] rounded-lg overflow-hidden shadow-[inset_0_2px_10px_rgba(0,0,0,0.5)] focus-within:border-indigo-500 transition-colors h-8">
<input
type="text"
value={drafts.find(d => d.id === activeDraftId)?.name || ""}
onChange={(e) => setDrafts(prev => prev.map(d => d.id === activeDraftId ? { ...d, name: e.target.value } : d))}
className="w-full bg-transparent text-indigo-100 font-bold text-[11px] py-1 px-3 outline-none placeholder:text-slate-600"
placeholder="Draft Name..."
/>
<button
onClick={() => setShowDraftMenu(!showDraftMenu)}
className="px-2.5 h-full flex items-center justify-center bg-[#151833] border-l border-[#2a2d5c] hover:bg-[#1e2247] transition-colors"
>
<span className="text-indigo-400 text-[8px]"></span>
</button>
</div>
{showDraftMenu && (
<>
<div className="fixed inset-0 z-modal-backdrop" onClick={() => setShowDraftMenu(false)} />
<div className="absolute top-full left-0 mt-1.5 w-full bg-[#0a0f1c] border border-[#2a2d5c] rounded-lg shadow-[0_10px_40px_rgba(0,0,0,0.8)] overflow-hidden py-1 z-popover">
{drafts.map(d => (
<button
key={d.id}
onClick={() => { setActiveDraftId(d.id); setShowDraftMenu(false); }}
className={`w-full text-left px-3 py-2 text-[11px] font-bold transition-colors ${d.id === activeDraftId ? "bg-indigo-500/20 text-indigo-300" : "text-slate-400 hover:bg-[#151833] hover:text-slate-200"}`}
>
{d.name}
</button>
))}
</div>
</>
)}
</div>
{/* CENTER: Gameweek Circles */}
<div className="flex gap-1.5 flex-wrap justify-center flex-1">
{horizonGWs.map((gw) => (
<button key={gw} type="button" onClick={() => setActiveGW(gw)} className={`small-touch-target relative w-7 h-7 rounded-full flex items-center justify-center text-[11px] font-bold transition-all ${activeGW === gw ? "bg-luigi-500 text-slate-950 scale-110 shadow-[0_0_10px_rgba(16,185,129,0.5)]" : "bg-slate-800 text-slate-400 hover:bg-slate-700 border border-slate-700"}`}>
{gw}
{chipsByGw[gw] && (<span className={`absolute -top-1 -right-1 w-3 h-3 rounded-full ${CHIP_CONFIG[chipsByGw[gw]].dot} flex items-center justify-center text-[6px] font-black text-slate-950 border border-slate-950 leading-none`} title={CHIP_CONFIG[chipsByGw[gw]].label}>{CHIP_CONFIG[chipsByGw[gw]].short[0]}</span>)}
</button>
))}
</div>
{/* RIGHT: Clone / New Draft / Delete */}
{/* RIGHT: Clone / New Draft */}
<div className="flex items-center justify-end gap-1.5 w-full md:w-auto shrink-0">
<button onClick={handleCloneDraft} disabled={drafts.length >= 5} className="small-touch-target flex items-center gap-1.5 px-3 py-1.5 bg-indigo-500/10 hover:bg-indigo-500/20 text-indigo-400 border border-indigo-500/20 rounded-lg text-[10px] font-black uppercase tracking-wider transition-all shadow-md active:scale-95 disabled:opacity-50 disabled:cursor-not-allowed" title="Clone reality">
<Copy size={12} /> Clone
</button>
<button onClick={handleNewDraft} disabled={drafts.length >= 5} className="small-touch-target flex items-center gap-1.5 px-3 py-1.5 bg-emerald-500/10 hover:bg-emerald-500/20 text-emerald-400 border border-emerald-500/20 rounded-lg text-[10px] font-black uppercase tracking-wider transition-all shadow-md active:scale-95 disabled:opacity-50 disabled:cursor-not-allowed" title="New timeline">
<Plus size={12} /> New
</button>
</div>
</div>
<PitchView
teamData={renderTeamData}
activeDragPlayer={activeDragPlayer}
isValidSwap={isValidSwap}
captainId={captainId}
viceId={viceId}
handleCapChange={handleCapChange}
playerCardGWs={playerCardGWs}
fixtures={fixtures}
activeGW={activeGW}
setSelectedPlayer={setSelectedPlayer}
handleUndoTransfer={handleUndoTransfer}
highlightTransferIds={highlightTransferIds}
solverTransferPairs={solverTransferPairs}
resetHighlightedTransfer={resetHighlightedTransfer}
chipsByGw={chipsByGw}
/>
<DragOverlay dropAnimation={null}>
{activeDragPlayer && !activeDragPlayer.isBlank ? (
<PlayerCardVisual player={activeDragPlayer} isBench={false} captainId={captainId} viceId={viceId} playerCardGWs={playerCardGWs} fixtures={fixtures} activeGW={activeGW} />
) : null}
</DragOverlay>
</DndContext>
</>
) : (
<div className="w-full min-h-[500px] border-2 border-dashed border-slate-800 rounded-2xl flex items-center justify-center text-slate-500 bg-[#0a3a2a]/30 flex-col gap-4 relative overflow-hidden">
{isLoadingDB || isLoading ? (
<>
<div className="absolute inset-0 pointer-events-none flex flex-col items-center justify-evenly py-12 px-8">
{[1, 4, 4, 2].map((count, rowIdx) => (
<div key={rowIdx} className="flex justify-center gap-6 sm:gap-10">
{Array.from({ length: count }).map((_, i) => (
<div key={i} className="w-[52px] sm:w-[68px] h-[72px] sm:h-[92px] rounded-xl bg-slate-800/50 skeleton-pulse" style={{ animationDelay: `${(rowIdx * count + i) * 0.12}s` }} />
))}
</div>
))}
</div>
<Loader2 size={32} className="animate-spin text-emerald-500 z-base" />
<span className="z-base text-sm font-bold text-slate-400">{isLoading ? "Loading squad..." : "Booting Global Engine..."}</span>
</>
) : (
"Enter your FPL ID above to load your squad."
)}
</div>
)}
</div>
{/* Right Column */}
<div className="w-full xl:w-[28%] flex flex-col gap-4">
{/* NEW HOME FOR TABS PANEL */}
<div className="rounded-2xl border border-slate-700/50 bg-slate-950/80 backdrop-blur-md shadow-xl overflow-hidden relative shrink-0">
<TabsPanel
solverTab={solverTab}
setSolverTab={setSolverTab}
isSolving={isSolving}
isRunningSens={isRunningSens}
isChipSolving={isChipSolving}
runMainSolver={runMainSolver}
runSensAnalysis={runSensAnalysis}
runChipSolve={runChipSolve}
setShowAdvancedSettings={setShowAdvancedSettings}
quickSettings={quickSettings}
setQuickSettings={setQuickSettings}
banSearch={banSearch}
setBanSearch={setBanSearch}
lockSearch={lockSearch}
setLockSearch={setLockSearch}
globalPlayers={globalPlayers}
teamData={renderTeamData}
solveGWLabel={solveGWLabel}
numSims={numSims}
setNumSims={setNumSims}
sensResults={sensResults}
setSensResults={setSensResults}
sensViewGw={sensViewGw}
setSensViewGw={setSensViewGw}
chipSolveOptions={chipSolveOptions}
setChipSolveOptions={setChipSolveOptions}
chipSolveSolutions={chipSolveSolutions}
setChipSolveSolutions={setChipSolveSolutions}
horizonGWs={horizonGWs}
baselineEv={horizonEV}
/>
</div>
<ActiveMovesPanel
activeGW={activeGW}
manualOverrides={manualOverrides}
globalPlayers={globalPlayers}
chipsByGw={chipsByGw}
transfersByGw={transfersByGw}
/>
<SolverOutputPanel
pendingSolutions={pendingSolutions}
setPendingSolutions={setPendingSolutions}
isSolving={isSolving}
globalPlayers={globalPlayers}
applySolution={applySolution}
appliedPlanSummary={appliedPlanSummary}
setAppliedPlanSummary={setAppliedPlanSummary}
baselineEv={horizonEV}
comprehensiveSettings={comprehensiveSettings}
/>
</div>
</div>
{/* MODALS */}
{selectedPlayer && !selectedPlayer.isBlank && (
<PlayerEditModal
selectedPlayer={selectedPlayer} setSelectedPlayer={setSelectedPlayer} activeGW={activeGW} horizonGWs={horizonGWs} updatePlayerStat={updatePlayerStat} handleTransferOut={handleTransferOut} fixtures={fixtures} fixtureOverrides={fixtureOverrides} sessionEdits={sessionEdits} globalPlayers={globalPlayers}
/>
)}
{selectedPlayer && selectedPlayer.isBlank && (
<PlayerSearchModal
selectedPlayer={selectedPlayer} setSelectedPlayer={setSelectedPlayer} searchQuery={searchQuery} setSearchQuery={setSearchQuery} sortConfig={sortConfig} setSortConfig={setSortConfig} globalPlayers={globalPlayers} ownedPlayerIds={ownedPlayerIds} activeGW={activeGW} itb={itb} handleAddPlayer={handleAddPlayer}
/>
)}
{showAdvancedSettings && (
<AdvancedSettingsModal
setShowAdvancedSettings={setShowAdvancedSettings} comprehensiveSettings={comprehensiveSettings} setComprehensiveSettings={setComprehensiveSettings} advancedSettings={advancedSettings} setAdvancedSettings={setAdvancedSettings}
/>
)}
{/* LOADING PORTALS */}
{isSolving && createPortal(
<div className="fixed inset-0 z-modal flex flex-col items-center justify-center bg-slate-950/80 backdrop-blur-md p-6">
<div className="pointer-events-none absolute inset-0 bg-[radial-gradient(circle_at_50%_35%,rgba(16,185,129,0.14),transparent_50%)]" />
<div className="relative flex max-w-md flex-col items-center gap-8 rounded-2xl border border-luigi-500/20 bg-slate-950/90 px-10 py-12 shadow-[0_0_60px_rgba(16,185,129,0.15)]">
<div className="relative flex h-32 w-32 items-center justify-center">
<div className="absolute inset-0 rounded-full border-4 border-slate-800" />
<div className="absolute inset-0 animate-spin rounded-full border-4 border-luigi-500 border-t-transparent" />
{/* BRANDED LOGO */}
<img src="/l-logo.png" alt="Solving" className="relative w-12 h-12 object-contain animate-pulse drop-shadow-[0_0_15px_rgba(16,185,129,0.6)]" />
</div>
<div className="text-center">
<p className="mb-2 text-lg font-black uppercase tracking-[0.2em] text-luigi-400">Solving</p>
<p className="font-mono text-xs text-slate-400">Elapsed {solveElapsedSec}s · up to {quickSettings.iterations} iteration(s)</p>
</div>
<button type="button" onClick={() => abortControllerRef.current?.abort()} className="mt-4 px-6 py-2 rounded-xl bg-slate-900 border border-slate-700 text-slate-400 hover:text-white hover:border-slate-500 font-bold transition-all text-sm">Cancel Solve</button>
</div>
</div>, document.body
)}
{isChipSolving && createPortal(
<div className="fixed inset-0 z-modal flex flex-col items-center justify-center bg-slate-950/80 backdrop-blur-md p-6">
<div className="pointer-events-none absolute inset-0 bg-[radial-gradient(circle_at_50%_35%,rgba(168,85,247,0.14),transparent_50%)]" />
<div className="relative flex max-w-md flex-col items-center gap-8 rounded-2xl border border-purple-500/20 bg-slate-950/90 px-10 py-12 shadow-[0_0_60px_rgba(168,85,247,0.15)]">
<div className="relative flex h-32 w-32 items-center justify-center">
<div className="absolute inset-0 rounded-full border-4 border-slate-800" />
<div className="absolute inset-0 animate-spin rounded-full border-4 border-purple-500 border-t-transparent" />
{/* BRANDED LOGO */}
<img src="l-logo.png" alt="Solving" className="relative w-12 h-12 object-contain animate-pulse drop-shadow-[0_0_15px_rgba(168,85,247,0.6)]" />
</div>
<div className="text-center">
<p className="mb-2 text-lg font-black uppercase tracking-[0.2em] text-purple-400">Chip Solving</p>
<p className="font-mono text-xs text-slate-400">Elapsed {chipSolveTimer}s</p>
</div>
<button type="button" onClick={() => abortControllerRef.current?.abort()} className="mt-4 px-6 py-2 rounded-xl bg-slate-900 border border-slate-700 text-slate-400 hover:text-white hover:border-slate-500 font-bold transition-all text-sm">Cancel Solve</button>
</div>
</div>, document.body
)}
{isRunningSens && createPortal(
<div className="fixed inset-0 z-modal flex flex-col items-center justify-center bg-slate-950/80 backdrop-blur-md p-6">
<div className="pointer-events-none absolute inset-0 bg-[radial-gradient(circle_at_50%_35%,rgba(6,182,212,0.14),transparent_50%)]" />
<div className="relative flex max-w-md flex-col items-center gap-8 rounded-2xl border border-cyan-500/20 bg-slate-950/90 px-10 py-12 shadow-[0_0_60px_rgba(6,182,212,0.15)]">
<div className="relative flex h-32 w-32 items-center justify-center">
<div className="absolute inset-0 rounded-full border-4 border-slate-800" />
<div className="absolute inset-0 animate-spin rounded-full border-4 border-cyan-500 border-t-transparent" />
{/* BRANDED LOGO */}
<img src="l-logo.png" alt="Solving" className="relative w-12 h-12 object-contain animate-pulse drop-shadow-[0_0_15px_rgba(6,182,212,0.6)]" />
</div>
<div className="text-center">
<p className="mb-2 text-lg font-black uppercase tracking-[0.2em] text-cyan-400">Sensitivity Analysis</p>
<p className="font-mono text-xs text-slate-400">Elapsed {sensTimer}s · {numSims} sims running…</p>
</div>
<button type="button" onClick={() => abortControllerRef.current?.abort()} className="mt-4 px-6 py-2 rounded-xl bg-slate-900 border border-slate-700 text-slate-400 hover:text-white hover:border-slate-500 font-bold transition-all text-sm">Cancel Solve</button>
</div>
</div>, document.body
)}
{showIdPrompt && (
<div className="fixed inset-0 z-modal flex items-center justify-center bg-black/80 backdrop-blur-sm p-4">
<div className="bg-slate-950 border border-slate-800 w-full max-w-sm rounded-2xl p-6 flex flex-col items-center">
<Shield size={24} className="text-luigi-400 mb-4" />
<h3 className="text-xl font-black text-slate-100 mb-2">Save as Default ID?</h3>
<div className="flex gap-3 w-full mt-4">
<button onClick={() => setShowIdPrompt(false)} className="flex-1 bg-slate-900 text-slate-300 py-2.5 rounded-xl border border-slate-700">Not Now</button>
<button onClick={() => { setUserProfile((prev) => ({ ...prev, defaultTeamId: pendingTeamId })); setShowIdPrompt(false); }} className="flex-1 bg-luigi-500 text-slate-950 py-2.5 rounded-xl font-bold">Save ID</button>
</div>
</div>
</div>
)}
{/* INITIAL LOGIN ID PROMPT */}
{showInitialIdPrompt && (
<div className="fixed inset-0 z-critical flex items-center justify-center bg-black/80 backdrop-blur-sm p-4">
<div className="bg-slate-950 border border-slate-800 w-full max-w-sm rounded-2xl p-6 flex flex-col items-center shadow-[0_0_40px_rgba(16,185,129,0.1)]">
<Shield size={32} className="text-emerald-500 mb-4" />
<h3 className="text-xl font-black text-slate-100 mb-2">Welcome!</h3>
<p className="text-xs text-slate-400 text-center mb-6">Enter your FPL Team ID to set it as your default for future logins.</p>
<input
type="number"
value={initialIdInput}
onChange={(e) => setInitialIdInput(e.target.value)}
placeholder="e.g. 123456"
className="w-full bg-slate-900 border border-slate-700 rounded-lg py-2.5 px-4 text-sm font-bold text-slate-200 focus:outline-none focus:border-emerald-500 text-center mb-4"
/>
<div className="flex gap-3 w-full">
<button onClick={() => setShowInitialIdPrompt(false)} className="flex-1 bg-slate-900 text-slate-400 py-2.5 rounded-xl border border-slate-700 hover:text-slate-300 transition-colors text-sm font-bold">Skip</button>
<button onClick={handleSaveInitialId} className="flex-1 bg-emerald-500 text-slate-950 py-2.5 rounded-xl font-black hover:bg-emerald-400 transition-colors shadow-lg text-sm">Save Default ID</button>
</div>
</div>
</div>
)}
</div>
);
}