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 (
Override schedules & EV splits. Only customized fixtures are displayed below.
{Object.keys(fixtureOverrides).length > 0 && ( )}