import React, { useMemo, useState, useRef, useContext } from "react"; import { Plus, Trash2, Zap, Search, X, Database } from "lucide-react"; import { PlayerContext } from '../PlayerContext'; export const FixtureMatrixPanel = ({ globalPlayers, fixtureOverrides, setFixtureOverrides, availableGWs }) => { const { globalFixtures = {}, effectiveFixtures = {} } = useContext(PlayerContext); const [search, setSearch] = useState(""); // --- ADMIN BACKDOOR STATE --- const [isAdmin, setIsAdmin] = useState(false); const [adminPassword, setAdminPassword] = useState(''); const [showAdminLogin, setShowAdminLogin] = useState(false); const [clickCount, setClickCount] = useState(0); const clickTimeoutRef = useRef(null); const handleSecretClick = () => { setClickCount((prev) => { const newCount = prev + 1; if (newCount === 5) { setShowAdminLogin(!showAdminLogin); return 0; } return newCount; }); if (clickTimeoutRef.current) clearTimeout(clickTimeoutRef.current); clickTimeoutRef.current = setTimeout(() => setClickCount(0), 1000); }; const handlePublishGlobal = async () => { if (!window.confirm("WARNING: This will overwrite the live FPL database for ALL users. Proceed?")) return; try { const res = await fetch('https://anayshukla-fpl-solver.hf.space/api/fixtures/update', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ is_admin: isAdmin, admin_password: adminPassword, overrides: { ...globalFixtures, ...fixtureOverrides } // Merges admin edits with existing globals }) }); if (!res.ok) { if (res.status === 401) { alert("Invalid Admin Password!"); setIsAdmin(false); } throw new Error('Backend publish failed'); } alert("Success! Global fixtures updated. All users will see these on refresh."); } catch (err) { console.error("Publish error:", err); } }; // 1. Extract all matches const allMatches = useMemo(() => { const TEAM_SHORTS = { 1: "ARS", 2: "AVL", 3: "BUR", 4: "BOU", 5: "BRE", 6: "BHA", 7: "CHE", 8: "CRY", 9: "EVE", 10: "FUL", 11: "LEE", 12: "LIV", 13: "MCI", 14: "MUN", 15: "NEW", 16: "NFO", 17: "SUN", 18: "TOT", 19: "WHU", 20: "WOL" }; const matchMap = new Map(); globalPlayers.forEach(p => { if (p.match_projections) { Object.entries(p.match_projections).forEach(([matchId, data]) => { if (!matchMap.has(matchId)) { const [homeId, awayId] = matchId.split("_vs_"); const hName = TEAM_SHORTS[homeId] || homeId; const aName = TEAM_SHORTS[awayId] || awayId; matchMap.set(matchId, { id: matchId, homeTeam: hName, awayTeam: aName, defaultGw: data.default_gw, searchString: `${hName} ${aName}`.toLowerCase() }); } }); } }); return Array.from(matchMap.values()).sort((a, b) => a.defaultGw - b.defaultGw); }, [globalPlayers]); const activeSplits = allMatches.filter(m => effectiveFixtures[m.id]); const searchResults = search ? allMatches.filter(m => !effectiveFixtures[m.id] && m.searchString.includes(search.toLowerCase())).slice(0, 10) : []; // --- HANDLERS --- const handleAddOverride = (match) => { // THE FIX 4b: If they search a hidden global fixture, load its true splits! Otherwise default to 100%. const initialSplit = globalFixtures[match.id] ? { ...globalFixtures[match.id] } : { [match.defaultGw]: 1.0 }; setFixtureOverrides(prev => ({ ...prev, [match.id]: initialSplit })); setSearch(""); }; // 1. Create a "Blank" Split Row const handleAddSplitGw = (matchId) => { setFixtureOverrides(prev => { const next = { ...prev }; const tempId = `unselected_${Date.now()}`; next[matchId] = { ...next[matchId], [tempId]: 0.0 }; return next; }); }; // Convert the "Blank" row into a real GW when user selects from dropdown const handleChangeSplitGw = (matchId, oldGw, newGw) => { setFixtureOverrides(prev => { const next = { ...prev }; const matchOverrides = { ...next[matchId] }; const prob = matchOverrides[oldGw]; delete matchOverrides[oldGw]; matchOverrides[newGw] = prob; next[matchId] = matchOverrides; return next; }); }; // 2. The Auto-Balancer Engine (Always enforces 100% sum) const handleUpdateSplit = (matchId, gw, newProbRaw) => { setFixtureOverrides(prev => { const next = { ...prev }; const matchOverrides = { ...next[matchId] }; let newProb = Math.min(Math.max(parseFloat(newProbRaw), 0), 1); const oldProb = matchOverrides[gw] || 0; let diff = newProb - oldProb; // Find all OTHER gameweeks that are already fully configured (ignoring blanks) const otherGws = Object.keys(matchOverrides).filter(k => k !== String(gw) && !k.startsWith('unselected')); if (otherGws.length > 0 && diff !== 0) { if (otherGws.length === 1) { // If 2 total GWs, modifying one perfectly scales the other let otherProb = matchOverrides[otherGws[0]] - diff; otherProb = Math.min(Math.max(otherProb, 0), 1); matchOverrides[otherGws[0]] = otherProb; newProb = 1 - otherProb; } else { // If 3+ GWs, distribute the remainder proportionally let sumOthers = otherGws.reduce((acc, key) => acc + matchOverrides[key], 0); if (sumOthers === 0) { matchOverrides[otherGws[0]] = 1 - newProb; } else { const targetOthersSum = 1 - newProb; otherGws.forEach(k => { matchOverrides[k] = (matchOverrides[k] / sumOthers) * targetOthersSum; }); } } } matchOverrides[gw] = newProb; next[matchId] = matchOverrides; return next; }); }; // Deleting a row gives its probability to the remaining rows const handleRemoveSplit = (matchId, gw) => { setFixtureOverrides(prev => { const next = { ...prev }; const matchOverrides = { ...next[matchId] }; const deletedProb = matchOverrides[gw] || 0; delete matchOverrides[gw]; const remaining = Object.keys(matchOverrides).filter(k => !k.startsWith('unselected')); if (remaining.length > 0 && deletedProb > 0) { if (remaining.length === 1) { matchOverrides[remaining[0]] += deletedProb; } else { let sumRem = remaining.reduce((a, k) => a + matchOverrides[k], 0); if(sumRem === 0) { matchOverrides[remaining[0]] = 1.0; } else { remaining.forEach(k => { matchOverrides[k] += (matchOverrides[k] / sumRem) * deletedProb; }); } } } if (Object.keys(matchOverrides).length === 0) delete next[matchId]; else next[matchId] = matchOverrides; return next; }); }; const handleRemoveEntireOverride = (matchId) => { setFixtureOverrides(prev => { const next = { ...prev }; delete next[matchId]; return next; }); }; return (
{/* Header */}

Override schedules & EV splits. Only customized fixtures are displayed below.

{Object.keys(fixtureOverrides).length > 0 && ( )}
{/* Fixture Search Bar & Admin Tools */}
{/* Secret Click Zone */}
setSearch(e.target.value)} className="w-full bg-transparent py-2 pl-6 text-xs font-bold text-slate-200 outline-none" /> {search && }
{/* Admin Login UI */} {showAdminLogin && !isAdmin && (
setAdminPassword(e.target.value)} onKeyDown={(e) => e.key === 'Enter' && setIsAdmin(true)} className="bg-slate-950 border border-slate-700 rounded py-1.5 px-3 text-xs w-28 outline-none focus:border-orange-500 text-slate-200" />
)} {isAdmin && ( )} {/* Search Results Dropdown */} {search && (
{searchResults.length === 0 ? (
No matches found...
) : ( searchResults.map(match => ( )) )}
)}
{/* Active Overrides List */}
{activeSplits.length === 0 ? (
No Active Overrides
) : ( activeSplits.map(match => { const overrides = effectiveFixtures[match.id]; return (
{/* Match Header */}
{match.homeTeam} vs {match.awayTeam}
{/* Sliders / Override UI */}
{Object.entries(overrides).map(([gw, prob]) => { // Check if this row is an empty placeholder waiting for a selection const isUnselected = gw.startsWith('unselected'); return (
{/* 1% Step Sliders, locked if no GW is selected */} handleUpdateSplit(match.id, gw, e.target.value)} className={`flex-1 accent-indigo-500 ${isUnselected ? 'opacity-30 cursor-not-allowed' : 'cursor-ew-resize'}`} /> {/* Editable Number Input with rounding and boundaries */}
{ let val = Math.round(Number(e.target.value)); if (isNaN(val)) val = 0; if (val > 100) val = 100; if (val < 0) val = 0; handleUpdateSplit(match.id, gw, val / 100); }} className={`w-14 bg-slate-900 border border-indigo-500/30 text-indigo-300 text-xs font-bold rounded px-1 py-1 outline-none text-right pr-4 transition-colors ${isUnselected ? 'opacity-30 cursor-not-allowed' : 'hover:border-indigo-500/80 focus:border-indigo-400'}`} /> %
); })} {/* Footer: Add Row & EV Sum Validator */}
{(() => { const totalProb = Object.values(overrides).reduce((a, b) => a + b, 0); const isBalanced = Math.abs(totalProb - 1.0) < 0.01; return ( {isBalanced ? : null} Total: {Math.round(totalProb * 100)}% ); })()}
); }) )}
); };