import React, { useContext, useState } from "react"; import { Settings } from "lucide-react"; import { CHIP_CONFIG } from "../utils/fplLogic"; import { PlayerContext } from "../PlayerContext"; import { FixtureMatrixPanel } from "./FixtureMatrixPanel"; export const TabsPanel = ({ solverTab, setSolverTab, isSolving, isRunningSens, isChipSolving, runMainSolver, runSensAnalysis, runChipSolve, setShowAdvancedSettings, quickSettings, setQuickSettings, banSearch, setBanSearch, lockSearch, setLockSearch, globalPlayers, teamData, solveGWLabel, numSims, setNumSims, sensResults, setSensResults, sensViewGw, setSensViewGw, chipSolveOptions, setChipSolveOptions, chipSolveSolutions, setChipSolveSolutions, horizonGWs, baselineEv = 0 }) => { const [lockGwSearch, setLockGwSearch] = useState(""); const [banGwSearch, setBanGwSearch] = useState(""); const { fixtureOverrides, setFixtureOverrides, availableGWs } = useContext(PlayerContext); const cleanString = (str) => str ? str.normalize("NFD").replace(/[\u0300-\u036f]/g, "").toLowerCase() : ""; const getRelativeEv = (sol) => { if (baselineEv === undefined || !sol) return "+0.00"; // Prevent 0.0 bug: Use locked baseline if it's the "Last Applied" summary const base = sol.lockedBaselineEv !== undefined ? sol.lockedBaselineEv : baselineEv; // Prevent NaN bug: Safely handle if sol is just a raw number by accident if (typeof sol === "number") { const diff = sol - base; return diff >= 0 ? `+${diff.toFixed(2)}` : diff.toFixed(2); } // Prevent NaN bug: If the API didn't return a full detailed plan (like in Chip Solve) if (!sol.plan || !Array.isArray(sol.plan) || sol.plan.length === 0) { const fallbackEv = sol.ev !== undefined ? sol.ev : 0; const diff = fallbackEv - base; return diff >= 0 ? `+${diff.toFixed(2)}` : diff.toFixed(2); } let pathEV = 0; let hasValidGw = false; sol.plan.forEach(gwPlan => { const gw = gwPlan.gw; if (gw === undefined) return; // Prevents checking p['undefined_Pts'] hasValidGw = true; const gwChip = gwPlan.chip; const gwCapMult = gwChip === "tc" ? 3 : 2; let gwPts = 0; const getPlayer = (id) => globalPlayers.find(p => String(p.ID) === String(id)); (gwPlan.lineup || []).forEach(id => { const p = getPlayer(id); if (p && !p.isBlank) { const pts = Number(p[`${gw}_Pts`]) || 0; gwPts += pts * (String(p.ID) === String(gwPlan.captain) ? gwCapMult : 1); } }); let ofIdx = 0; (gwPlan.bench || []).forEach(id => { const p = getPlayer(id); if (p && !p.isBlank) { const pts = Number(p[`${gw}_Pts`]) || 0; if (gwChip === "bb") { gwPts += pts; } else if (p.Pos === "G") { gwPts += pts * 0.04; } else { gwPts += pts * ([0.17, 0.05, 0.02][ofIdx] || 0.02); ofIdx++; } } }); pathEV += gwPts - (gwPlan.hits || 0) * 4; }); // Failsafe in case the plan existed but couldn't be parsed properly if (!hasValidGw || Number.isNaN(pathEV)) { const fallbackEv = sol.ev !== undefined ? sol.ev : 0; const diff = fallbackEv - base; return diff >= 0 ? `+${diff.toFixed(2)}` : diff.toFixed(2); } const diff = pathEV - base; return diff >= 0 ? `+${diff.toFixed(2)}` : diff.toFixed(2); }; return ( <>
{["solver", "sensitivity", "chips", "fixtures"].map((tab) => ( ))}
{solverTab === "solver" && (

Quick Settings

{/* TOOLBAR LAYOUT: Readable sizes, allowed to wrap if screen is small */}
{/* Decay */}
setQuickSettings({ ...quickSettings, decay: e.target.value })} className="w-full bg-slate-900 border border-slate-700 rounded px-2 py-1.5 text-slate-200 font-mono text-xs outline-none focus:border-luigi-500" />
{/* FT Val */}
setQuickSettings({ ...quickSettings, ft_value: Number(e.target.value) })} className="w-full bg-slate-900 border border-slate-700 rounded px-2 py-1.5 text-slate-200 font-mono text-xs outline-none focus:border-luigi-500" />
{/* Iters */}
{/* Lock */}
setLockSearch(e.target.value)} className="w-full bg-slate-900 border border-slate-700 rounded px-2 py-1.5 text-xs text-emerald-400 focus:outline-none focus:border-emerald-500" /> {lockSearch && (
{/* THE FIX: Added (quickSettings.locked || []) */} {globalPlayers.filter((p) => cleanString(p.Name).includes(cleanString(lockSearch)) && !(quickSettings.locked || []).some((l) => l.ID === p.ID)).slice(0, 10).map((p) => (
{ setQuickSettings((prev) => ({ ...prev, locked: [...(prev.locked || []), p] })); setLockSearch(""); }} className="px-2 py-1.5 text-xs text-slate-300 hover:bg-slate-700 cursor-pointer border-b border-slate-700/50 truncate"> {p.Name} ({p.Team})
))}
)}
{/* THE FIX: Added (quickSettings.locked || []) */} {(quickSettings.locked || []).map((p) => ( {p.Name} ))}
{/* Ban */}
setBanSearch(e.target.value)} className="w-full bg-slate-900 border border-slate-700 rounded px-2 py-1.5 text-xs text-red-400 focus:outline-none focus:border-red-500" /> {banSearch && (
{/* THE FIX: Added (quickSettings.banned || []) */} {globalPlayers.filter((p) => cleanString(p.Name).includes(cleanString(banSearch)) && !(quickSettings.banned || []).some((b) => b.ID === p.ID)).slice(0, 10).map((p) => (
{ setQuickSettings((prev) => ({ ...prev, banned: [...(prev.banned || []), p] })); setBanSearch(""); }} className="px-2 py-1.5 text-xs text-slate-300 hover:bg-slate-700 cursor-pointer border-b border-slate-700/50 truncate"> {p.Name} ({p.Team})
))}
)}
{/* THE FIX: Added (quickSettings.banned || []) */} {(quickSettings.banned || []).map((p) => ( {p.Name} ))}
{/* Lock (This GW) */}
setLockGwSearch(e.target.value)} className="w-full bg-slate-900 border border-slate-700 rounded px-2 py-1.5 text-xs text-emerald-400 focus:outline-none focus:border-emerald-500" /> {lockGwSearch && (
{globalPlayers.filter((p) => cleanString(p.Name).includes(cleanString(lockGwSearch)) && !(quickSettings.lock_this_gw || []).some((l) => l.ID === p.ID)).slice(0, 10).map((p) => (
{ setQuickSettings((prev) => ({ ...prev, lock_this_gw: [...(prev.lock_this_gw || []), p] })); setLockGwSearch(""); }} className="px-2 py-1.5 text-xs text-slate-300 hover:bg-slate-700 cursor-pointer border-b border-slate-700/50 truncate"> {p.Name} ({p.Team})
))}
)}
{(quickSettings.lock_this_gw || []).map((p) => ( {p.Name} ))}
{/* Ban (This GW) */}
setBanGwSearch(e.target.value)} className="w-full bg-slate-900 border border-slate-700 rounded px-2 py-1.5 text-xs text-red-400 focus:outline-none focus:border-red-500" /> {banGwSearch && (
{globalPlayers.filter((p) => cleanString(p.Name).includes(cleanString(banGwSearch)) && !(quickSettings.ban_this_gw || []).some((b) => b.ID === p.ID)).slice(0, 10).map((p) => (
{ setQuickSettings((prev) => ({ ...prev, ban_this_gw: [...(prev.ban_this_gw || []), p] })); setBanGwSearch(""); }} className="px-2 py-1.5 text-xs text-slate-300 hover:bg-slate-700 cursor-pointer border-b border-slate-700/50 truncate"> {p.Name} ({p.Team})
))}
)}
{(quickSettings.ban_this_gw || []).map((p) => ( {p.Name} ))}
)} {solverTab === "sensitivity" && (

Runs N randomised solves with per-player EV noise.

setNumSims(Math.max(2, Math.min(300, Number(e.target.value))))} className="w-20 bg-slate-900 border border-slate-700 rounded p-2 text-slate-200 font-mono text-sm outline-none focus:border-luigi-500" />
{sensResults && (

{sensResults.valid_runs}/{sensResults.num_sims} valid

{(sensResults.horizon_gws || []).map((gw) => { const isChipFree = sensResults.gw_results?.[String(gw)]?.is_chip_free; const isActive = sensViewGw === gw; return ( ); })}
{sensViewGw && sensResults.gw_results?.[String(sensViewGw)] && (() => { const gd = sensResults.gw_results[String(sensViewGw)]; if (gd.is_chip_free) { const POS_NAMES = { G: "Goalkeepers", D: "Defenders", M: "Midfielders", F: "Forwards" }; return (
⚡ Wildcard / Free Hit — Squad Selection
{["G", "D", "M", "F"].map((pos) => { const rows = gd.players?.[pos] || []; if (!rows.length) return null; return (
{POS_NAMES[pos]}
PlayerSquadLineup
{rows.map((r, i) => (
{r.name}
{r.squad_pct}%{r.lineup_pct}%
))}
); })}
); } return (
{gd.no_transfer_pct > 0 &&
Hold (no transfer): {gd.no_transfer_pct}% of sims
} {["moves", "buys", "sells"].map((key) => { const rows = gd[key] || []; if (!rows.length) return null; const titles = { moves: "Moves (Out → In)", buys: "Buys", sells: "Sells" }; return (
{titles[key]}
{rows.slice(0, 10).map((r, i) => (
{r.name}
{r.pct}%
))}
); })}
); })()}
)}
)} {solverTab === "chips" && (

Select which GWs each chip can be played in, then hit Chip Solve.

{["wc", "fh", "bb", "tc"].map((key) => { const cfg = CHIP_CONFIG[key]; const sel = chipSolveOptions[key] || []; return (
{cfg.label} {sel.length > 0 && {sel.length} GW{sel.length > 1 ? "s" : ""}}
{horizonGWs.map((gw) => { const active = sel.includes(gw); return ( ); })}
); })}
{chipSolveSolutions.length > 0 && (

Best Chip Combos

{chipSolveSolutions.map((sol, i) => { const combo = sol.chip_combo || {}; const active = Object.entries(combo) .filter(([, gws]) => Array.isArray(gws) && gws.length > 0) .map(([k, gws]) => ({ text: `${k.replace("use_", "").toUpperCase()}${gws[0]}`, gw: gws[0] })) .sort((a, b) => a.gw - b.gw) .map(x => x.text); return (
{/* 1. Added Rank Number (1., 2., 3...) */} {i + 1}. {active.length ? active.join(" + ") : "No chips"}
{sol.objective_score != null ? sol.objective_score.toFixed(2) : sol.ev.toFixed(2)} {/* 2. EV is now bigger (text-[10px]) and colored purple to match! */} ({getRelativeEv(sol)} ev)
); })}
)}
)} {solverTab === "fixtures" && ( )}
); };