Spaces:
Running
Running
| // src/PlayerContext.jsx | |
| import React, { createContext, useState, useEffect, useRef, useCallback, useMemo } from 'react'; | |
| export const PlayerContext = createContext(); | |
| export const FixturesContext = createContext({}); | |
| export const PlayerProvider = ({ children }) => { | |
| const globalFixturesRef = useRef({}); | |
| const [originalPlayers, setOriginalPlayers] = useState([]); | |
| const [globalPlayers, setGlobalPlayers] = useState([]); | |
| const [globalFixtures, setGlobalFixtures] = useState({}); | |
| const [isLoadingDB, setIsLoadingDB] = useState(true); | |
| const [teamId, setTeamId] = useState(''); | |
| const [availableGWs, setAvailableGWs] = useState([]); | |
| const [itb, setItb] = useState(0); | |
| const [availableFts, setAvailableFts] = useState(1); | |
| const [initialSquadIds, setInitialSquadIds] = useState([]); | |
| const [solverResult, setSolverResult] = useState(null); | |
| const [activeChip, setActiveChip] = useState(null); | |
| const [solveElapsedSec, setSolveElapsedSec] = useState(0); | |
| const [numSims, setNumSims] = useState(100); | |
| const HIT_COST = 4; | |
| const [baselineItb, setBaselineItb] = useState(0); | |
| const [baselineFt, setBaselineFt] = useState(1); | |
| const [comprehensiveSettings, setComprehensiveSettings] = useState({}); | |
| const [globalXmins, setGlobalXmins] = useState({}); | |
| const [quickSettings, setQuickSettings] = useState({ | |
| decay: 0.85, | |
| ft_value: 1.5, | |
| iterations: 1, | |
| banned: [], | |
| locked: [], | |
| }); | |
| const [advancedSettings, setAdvancedSettings] = useState({ | |
| hit_cost: 4, | |
| itb_value: 0.08, | |
| max_per_team: 3, | |
| vice_weight: 0.05, | |
| time_limit_sec: 30, | |
| no_transfer_last_gws: 0 | |
| }); | |
| // FIXED: FPL 24/25 FT Rollover Logic | |
| const ftAtStartOfGw = (targetGw, gwsArray, baseFt, trByGw, chByGw) => { | |
| if (!gwsArray || !gwsArray.length) return baseFt; | |
| let ft = baseFt; | |
| for (let gw = gwsArray[0]; gw < targetGw; gw++) { | |
| const chip = chByGw[gw]; | |
| if (chip === 'wc' || chip === 'fh') { | |
| ft = Math.min(5, ft); | |
| } else { | |
| const used = trByGw[gw]?.count || 0; | |
| ft = Math.min(5, Math.max(0, ft - used) + 1); | |
| } | |
| } | |
| return Math.max(1, Math.min(5, ft)); | |
| }; | |
| const itbAtStartOfGw = (targetGw, gwsArray, baseItb, trByGw) => { | |
| let currentItb = baseItb; | |
| if (!gwsArray || !gwsArray.length) return currentItb; | |
| for (let gw = gwsArray[0]; gw < targetGw; gw++) { | |
| currentItb += (trByGw[gw]?.netDelta || 0); | |
| } | |
| return currentItb; | |
| }; | |
| // ========================================================= | |
| // --- MULTIVERSE DRAFTS ENGINE (Phase 1) --- | |
| // ========================================================= | |
| const [drafts, setDrafts] = useState([{ | |
| id: "main_1", | |
| name: "Main Timeline", | |
| teamData: [], | |
| horizon: 5, | |
| activeGW: null, | |
| captainId: null, | |
| viceId: null, | |
| solverTransferPairs: {}, | |
| solverApplySnapshot: null, | |
| appliedPlanSummary: null, | |
| hitsThisGw: 0, | |
| highlightTransferIds: {}, | |
| transfersByGw: {}, | |
| chipsByGw: {}, | |
| manualOverrides: {}, | |
| fixtureOverrides: {}, // <-- ADDED: Isolated Fixtures | |
| sessionEdits: {} // <-- ADDED: Isolated Minutes | |
| }]); | |
| const [activeDraftId, setActiveDraftId] = useState("main_1"); | |
| // 1. EXTRACT CURRENT REALITY | |
| const activeDraft = drafts.find(d => d.id === activeDraftId) || drafts[0]; | |
| const teamData = activeDraft.teamData; | |
| const horizon = activeDraft.horizon; | |
| const activeGW = activeDraft.activeGW; | |
| const captainId = activeDraft.captainId; | |
| const viceId = activeDraft.viceId; | |
| const solverTransferPairs = activeDraft.solverTransferPairs || {}; | |
| const solverApplySnapshot = activeDraft.solverApplySnapshot; | |
| const appliedPlanSummary = activeDraft.appliedPlanSummary; | |
| const hitsThisGw = activeDraft.hitsThisGw; | |
| const highlightTransferIds = activeDraft.highlightTransferIds || {}; | |
| const transfersByGw = activeDraft.transfersByGw || {}; | |
| const chipsByGw = activeDraft.chipsByGw || {}; | |
| // Safe extraction fallbacks for older local cache hits | |
| const manualOverrides = activeDraft.manualOverrides || {}; | |
| const fixtureOverrides = activeDraft.fixtureOverrides || {}; | |
| const sessionEdits = activeDraft.sessionEdits || {}; | |
| const effectiveFixtures = useMemo(() => { | |
| return { ...globalFixtures, ...(fixtureOverrides || {}) }; | |
| }, [globalFixtures, fixtureOverrides]); | |
| // 2. PROXY SETTERS (Intercepts state calls and routes them to the active draft) | |
| const updateDraftState = useCallback((key, newValue) => { | |
| setDrafts(prevDrafts => { | |
| const activeIndex = prevDrafts.findIndex(d => d.id === activeDraftId); | |
| if (activeIndex === -1) return prevDrafts; | |
| const draft = prevDrafts[activeIndex]; | |
| // Provide an empty object fallback for expected object states to prevent functional crashes | |
| const currentValue = draft[key] !== undefined ? draft[key] : (key === 'teamData' || key === 'availableGWs' ? [] : {}); | |
| const evaluatedValue = typeof newValue === 'function' ? newValue(currentValue) : newValue; | |
| const newDrafts = [...prevDrafts]; | |
| newDrafts[activeIndex] = { ...draft, [key]: evaluatedValue }; | |
| return newDrafts; | |
| }); | |
| }, [activeDraftId]); | |
| const setTeamData = useCallback((val) => updateDraftState("teamData", val), [updateDraftState]); | |
| const setHorizon = useCallback((val) => updateDraftState("horizon", val), [updateDraftState]); | |
| const setActiveGW = useCallback((val) => updateDraftState("activeGW", val), [updateDraftState]); | |
| const setCaptainId = useCallback((val) => updateDraftState("captainId", val), [updateDraftState]); | |
| const setViceId = useCallback((val) => updateDraftState("viceId", val), [updateDraftState]); | |
| const setSolverTransferPairs = useCallback((val) => updateDraftState("solverTransferPairs", val), [updateDraftState]); | |
| const setSolverApplySnapshot = useCallback((val) => updateDraftState("solverApplySnapshot", val), [updateDraftState]); | |
| const setAppliedPlanSummary = useCallback((val) => updateDraftState("appliedPlanSummary", val), [updateDraftState]); | |
| const setHitsThisGw = useCallback((val) => updateDraftState("hitsThisGw", val), [updateDraftState]); | |
| const setHighlightTransferIds = useCallback((val) => updateDraftState("highlightTransferIds", val), [updateDraftState]); | |
| const setTransfersByGw = useCallback((val) => updateDraftState("transfersByGw", val), [updateDraftState]); | |
| const setChipsByGw = useCallback((val) => updateDraftState("chipsByGw", val), [updateDraftState]); | |
| const setFixtureOverrides = useCallback((val) => updateDraftState("fixtureOverrides", val), [updateDraftState]); // <-- GHOST PATCH | |
| const setSessionEdits = useCallback((val) => updateDraftState("sessionEdits", val), [updateDraftState]); // <-- GHOST PATCH | |
| // ========================================================= | |
| const manualOverridesRef = useRef(manualOverrides); | |
| useEffect(() => { manualOverridesRef.current = manualOverrides; }, [manualOverrides]); | |
| const [projSearchTerm, setProjSearchTerm] = useState(''); | |
| const sessionEditsRef = useRef(sessionEdits); | |
| useEffect(() => { sessionEditsRef.current = sessionEdits; }, [sessionEdits]); | |
| const [isLoggedIn, setIsLoggedIn] = useState(false); | |
| const [isCheckingAuth, setIsCheckingAuth] = useState(true); | |
| const [userProfile, setUserProfile] = useState({ username: "Guest", defaultTeamId: null, isAdmin: false }); | |
| const [hasGuestMadeEdits, setHasGuestMadeEdits] = useState(false); | |
| const [pendingWorkspaceLoad, setPendingWorkspaceLoad] = useState(null); | |
| // Custom proxy setter for manualOverrides to retain your exact Auth saving logic | |
| const setManualOverrides = useCallback((updater) => { | |
| updateDraftState("manualOverrides", (prev) => { | |
| const next = typeof updater === 'function' ? updater(prev) : updater; | |
| 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({ saved_edits: { ...sessionEditsRef.current, _solver_overrides: next } }) | |
| }); | |
| } | |
| return next; | |
| }); | |
| }, [updateDraftState]); | |
| const saveSession = (overrides) => { | |
| 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({ | |
| saved_edits: { ...sessionEditsRef.current, _solver_overrides: overrides }, | |
| drafts: drafts | |
| }) | |
| }); | |
| } | |
| }; | |
| useEffect(() => { | |
| // YOUR EXACT PROJECTIONS API ENDPOINT RESTORED | |
| const cachedProjections = localStorage.getItem('fpl_projections_cache'); | |
| if (cachedProjections) { | |
| try { | |
| const parsed = JSON.parse(cachedProjections); | |
| setOriginalPlayers(JSON.parse(JSON.stringify(parsed))); | |
| setGlobalPlayers(parsed); | |
| setIsLoadingDB(false); // Instantly drop the skeleton loader! | |
| } catch (e) { | |
| console.error("Cache corrupted, waiting for server..."); | |
| } | |
| } | |
| // 2. Silently fetch the fresh data in the background | |
| fetch('https://anayshukla-fpl-solver.hf.space/api/projections') | |
| .then(res => { if (!res.ok) throw new Error("DB Error"); return res.json(); }) | |
| .then(data => { | |
| localStorage.setItem('fpl_projections_cache', JSON.stringify(data)); // Save for next time | |
| setOriginalPlayers(JSON.parse(JSON.stringify(data))); | |
| setGlobalPlayers(data); | |
| setIsLoadingDB(false); // Drop loader if cache was empty | |
| }) | |
| .catch(err => { if (!cachedProjections) setIsLoadingDB(false); }); | |
| // THE FIX: Store global fixtures in the Ref so the Auth wipe doesn't delete them! | |
| fetch('https://anayshukla-fpl-solver.hf.space/api/fixtures/overrides') | |
| .then(res => res.ok ? res.json() : {}) | |
| .then(data => { | |
| if (Object.keys(data).length > 0) setGlobalFixtures(data); | |
| }) | |
| .catch(err => console.error("Failed to load global fixtures:", err)); | |
| fetch('https://anayshukla-fpl-solver.hf.space/api/xmins/overrides') | |
| .then(res => res.ok ? res.json() : {}) | |
| .then(data => { if (Object.keys(data).length > 0) setGlobalXmins(data); }) | |
| .catch(err => console.error("Failed to load global xMins:", err)); | |
| }, []); | |
| useEffect(() => { | |
| const token = localStorage.getItem('fpl_token'); | |
| if (token) { | |
| fetch('https://anayshukla-fpl-solver.hf.space/api/auth/me', { headers: { 'Authorization': `Bearer ${token}` } }) | |
| .then(res => res.json()) | |
| .then(data => { | |
| if (data.email) { | |
| setUserProfile({ username: data.email.split('@')[0], defaultTeamId: data.default_team_id, isAdmin: data.is_admin }); | |
| // THE FIX: Inject the Multiverse realities from the database! | |
| if (data.drafts && data.drafts.length > 0) { | |
| setDrafts(data.drafts); | |
| const saved = data.saved_edits || {}; | |
| if (saved._active_draft_id && data.drafts.some(d => d.id === saved._active_draft_id)) { | |
| setActiveDraftId(saved._active_draft_id); | |
| } else { | |
| setActiveDraftId(data.drafts[0].id); | |
| } | |
| } else { | |
| // ONLY run these legacy injectors if the Multiverse doesn't exist yet! | |
| // Otherwise, they use a stale React closure and overwrite Draft A with Draft B's moves! | |
| const saved = data.saved_edits || {}; | |
| if (saved._solver_overrides) { | |
| setManualOverrides(saved._solver_overrides); | |
| delete saved._solver_overrides; | |
| } | |
| if (saved._workspace) { | |
| setPendingWorkspaceLoad(saved._workspace); | |
| delete saved._workspace; | |
| } | |
| setSessionEdits(saved); | |
| } | |
| setIsLoggedIn(true); | |
| if (data.default_team_id) setTeamId(String(data.default_team_id)); | |
| } else { | |
| localStorage.removeItem('fpl_token'); | |
| setSessionEdits({}); | |
| setManualOverrides({}); | |
| setTeamId(''); | |
| setTeamData([]); | |
| setDrafts([{ | |
| id: "main_1", | |
| name: "Main Timeline", | |
| teamData: [], horizon: 5, activeGW: null, captainId: null, viceId: null, | |
| solverTransferPairs: {}, solverApplySnapshot: null, appliedPlanSummary: null, | |
| hitsThisGw: 0, highlightTransferIds: {}, transfersByGw: {}, chipsByGw: {}, | |
| manualOverrides: {}, fixtureOverrides: {}, sessionEdits: {} | |
| }]); | |
| setActiveDraftId("main_1"); | |
| } | |
| }).catch(() => { | |
| localStorage.removeItem('fpl_token'); | |
| setSessionEdits({}); | |
| setManualOverrides({}); | |
| setTeamId(''); | |
| setTeamData([]); | |
| setDrafts([{ | |
| id: "main_1", | |
| name: "Main Timeline", | |
| teamData: [], horizon: 5, activeGW: null, captainId: null, viceId: null, | |
| solverTransferPairs: {}, solverApplySnapshot: null, appliedPlanSummary: null, | |
| hitsThisGw: 0, highlightTransferIds: {}, transfersByGw: {}, chipsByGw: {}, | |
| manualOverrides: {}, fixtureOverrides: {}, sessionEdits: {} | |
| }]); | |
| setActiveDraftId("main_1"); | |
| }).finally(() => { | |
| setIsCheckingAuth(false); | |
| }); | |
| } else { | |
| setSessionEdits({}); | |
| setManualOverrides({}); | |
| setTeamId(''); | |
| setTeamData([]); | |
| setIsCheckingAuth(false); | |
| } | |
| }, [isLoggedIn]); | |
| useEffect(() => { | |
| if (originalPlayers.length > 0) { | |
| setGlobalPlayers(prev => { | |
| const newPlayers = JSON.parse(JSON.stringify(originalPlayers)); | |
| // --- 1. THE STOCHASTIC FIXTURE ENGINE --- | |
| newPlayers.forEach(p => { | |
| if (p.match_projections) { | |
| // A. Zero out old stats + Track probability sum for averaging | |
| const gwKeys = Object.keys(p).filter(k => k.includes('_Pts')).map(k => k.split('_')[0]); | |
| gwKeys.forEach(gw => { | |
| p[`${gw}_Pts`] = 0; p[`${gw}_xMins`] = 0; p[`${gw}_xG`] = 0; p[`${gw}_xA`] = 0; p[`${gw}_CS`] = 0; | |
| p[`${gw}_probSum`] = 0; // NEW: Tracks total matches in this GW | |
| }); | |
| // B. Loop matches and apply Match-Level Minute Edits | |
| const manualBaseline = sessionEdits[p.ID]?.baseline_xMins; | |
| const origBaseline = p.baseline_xMins || 90; | |
| const baselineScale = (manualBaseline != null && origBaseline > 0) ? (Number(manualBaseline) / origBaseline) : 1.0; | |
| if (!p.match_projections || typeof p.match_projections !== 'object' || Array.isArray(p.match_projections)) return; | |
| Object.entries(p.match_projections).forEach(([matchId, mData]) => { | |
| const override = effectiveFixtures[matchId]; | |
| let manualMins = sessionEdits[p.ID]?.[`${matchId}_xMins`]; | |
| let globalMatchMins = globalXmins[p.ID]?.[matchId]; | |
| let isGwOverride = false; | |
| if (manualMins === undefined) { | |
| if (globalMatchMins !== undefined) { | |
| manualMins = globalMatchMins; | |
| } else { | |
| let activeGw = override ? Object.keys(override).find(g => override[g] > 0) : mData.default_gw; | |
| if (activeGw) { | |
| manualMins = sessionEdits[p.ID]?.[`${activeGw}_xMins`] ?? globalXmins?.[p.ID]?.[activeGw]; | |
| if (manualMins !== undefined) isGwOverride = true; | |
| } | |
| } | |
| } | |
| // THE FIX: Safely extract data so NaN never infects the app! | |
| const origMins = mData.xMins !== undefined ? mData.xMins : (mData.mins || 0); | |
| const origPts = mData.Pts !== undefined ? mData.Pts : (mData.points || 0); | |
| let activeMins = origMins; | |
| if (manualMins != null) { | |
| if (isGwOverride) { | |
| // DISTRIBUTE LEAK FIX: If an admin edited a whole GW, distribute it proportionally across the DGW splits! | |
| let totalGwMins = 0; | |
| Object.values(p.match_projections).forEach(m => { | |
| if (m.default_gw === mData.default_gw) totalGwMins += (m.xMins !== undefined ? m.xMins : (m.mins || 0)); | |
| }); | |
| const ratio = totalGwMins > 0 ? (origMins / totalGwMins) : 1; | |
| activeMins = Math.min(Number(manualMins) * ratio, 90); | |
| } else { | |
| activeMins = Number(manualMins); | |
| } | |
| } else { | |
| activeMins = Math.min((origMins * baselineScale), 90); | |
| } | |
| // Scale the match EV safely | |
| const scaling = (activeMins > 0 && origMins > 0) ? (activeMins / origMins) : 0; | |
| const aPts = origPts * scaling; | |
| const axG = (mData.xG || 0) * scaling; | |
| const axA = (mData.xA || 0) * scaling; | |
| const aCS = (mData.CS || 0) * scaling; | |
| // C. Distribute the scaled EV | |
| if (override) { | |
| Object.entries(override).forEach(([gwStr, prob]) => { | |
| const gw = gwStr; | |
| p[`${gw}_Pts`] += (aPts * prob); | |
| p[`${gw}_xMins`] += (activeMins * prob); | |
| p[`${gw}_probSum`] += prob; // Add probability to the GW sum | |
| p[`${gw}_xG`] += (axG * prob); p[`${gw}_xA`] += (axA * prob); p[`${gw}_CS`] += (aCS * prob); | |
| }); | |
| } else { | |
| const gw = mData.default_gw; | |
| p[`${gw}_Pts`] += aPts; | |
| p[`${gw}_xMins`] += activeMins; | |
| p[`${gw}_probSum`] += 1.0; | |
| p[`${gw}_xG`] += axG; p[`${gw}_xA`] += axA; p[`${gw}_CS`] += aCS; | |
| } | |
| }); | |
| // D. Calculate FPL Average xMins | |
| gwKeys.forEach(gw => { | |
| if (p[`${gw}_probSum`] > 0) { | |
| p[`${gw}_xMins`] = Math.round(p[`${gw}_xMins`] / p[`${gw}_probSum`]); | |
| } | |
| }); | |
| } | |
| }); | |
| // --- 2. APPLY MANUAL SESSION EDITS (Overwrites everything) --- | |
| if (Object.keys(sessionEdits).length > 0) { | |
| Object.keys(sessionEdits).forEach(playerId => { | |
| if (playerId === '_solver_overrides') return; | |
| const pid = parseInt(playerId); | |
| const pIdx = newPlayers.findIndex(p => p.ID === pid); | |
| if (pIdx > -1) { | |
| const edits = sessionEdits[playerId]; | |
| Object.keys(edits).forEach(editKey => { | |
| newPlayers[pIdx][editKey] = edits[editKey]; | |
| if (editKey.includes('_xMins') && !editKey.includes('_vs_')) { | |
| const gw = editKey.split('_')[0]; | |
| if (edits[`${gw}_Pts`] === undefined) { | |
| const baseMins = originalPlayers[pIdx][`${gw}_xMins`] || 90; | |
| const basePts = originalPlayers[pIdx][`${gw}_Pts`] || 0; | |
| newPlayers[pIdx][`${gw}_Pts`] = baseMins > 0 ? (basePts / baseMins) * edits[editKey] : 0; | |
| } | |
| } | |
| }); | |
| } | |
| }); | |
| } | |
| return newPlayers; | |
| }); | |
| } | |
| }, [originalPlayers, isLoggedIn, sessionEdits, effectiveFixtures]); | |
| useEffect(() => { | |
| if (!isLoggedIn || isLoadingDB || globalPlayers.length === 0) return; | |
| const timeout = setTimeout(() => { | |
| const workspace = { | |
| teamData: teamData.map(p => ({ ID: p.ID, Price: p.Price, isBlank: p.isBlank, replacedPlayer: p.replacedPlayer })), | |
| horizon, | |
| activeGW, | |
| baselineItb, | |
| baselineFt, | |
| transfersByGw, | |
| chipsByGw, | |
| quickSettings, | |
| advancedSettings, | |
| highlightTransferIds: Object.fromEntries(Object.entries(highlightTransferIds).map(([k, v]) => [k, Array.from(v)])), | |
| solverTransferPairs | |
| }; | |
| 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({ | |
| saved_edits: { | |
| ...sessionEditsRef.current, | |
| _solver_overrides: manualOverridesRef.current, | |
| _workspace: workspace, | |
| _active_draft_id: activeDraftId | |
| }, | |
| drafts: drafts // <-- THE FIX: Package and send the entire Multiverse! | |
| }) | |
| }); | |
| } | |
| }, 500); | |
| // THE FIX: Added 'drafts' to the end of this dependency array so edits trigger the save | |
| }, [teamData, horizon, activeGW, baselineItb, baselineFt, transfersByGw, chipsByGw, quickSettings, advancedSettings, highlightTransferIds, solverTransferPairs, isLoggedIn, isLoadingDB, globalPlayers, drafts]); | |
| useEffect(() => { | |
| if (globalPlayers.length === 0 || teamData.length === 0) return; | |
| setTeamData(prevTeam => { | |
| let needsUpdate = false; | |
| const syncedTeam = prevTeam.map(tp => { | |
| const gp = globalPlayers.find(p => p.ID === tp.ID); | |
| if (!gp) return tp; | |
| // If the squad's math is out of sync with the global math, flag it for an update! | |
| // (We check a few sample gameweeks to guarantee we catch the mismatch) | |
| if ( | |
| tp['1_Pts'] !== gp['1_Pts'] || | |
| tp['19_Pts'] !== gp['19_Pts'] || | |
| tp['38_Pts'] !== gp['38_Pts'] || | |
| tp.baseline_xMins !== gp.baseline_xMins | |
| ) { | |
| needsUpdate = true; | |
| return { ...tp, ...gp, Price: tp.Price }; // Merges the math, but protects your specific Selling Price! | |
| } | |
| return tp; | |
| }); | |
| // Only triggers a re-render if it actually found stale data | |
| return needsUpdate ? syncedTeam : prevTeam; | |
| }); | |
| }, [globalPlayers, teamData, setTeamData]); | |
| const updatePlayerStat = (playerId, gw, statKey, rawValue) => { | |
| let finalValue = statKey === 'xMins' ? Math.min(Math.max(Number(rawValue), 0), 90) : rawValue; | |
| if (!isLoggedIn) setHasGuestMadeEdits(true); | |
| const pristinePlayer = originalPlayers.find(p => p.ID === playerId); | |
| let calculatedPts = 0; | |
| // 1. Determine the exact original value to see if we are reverting | |
| let isRevertingToOriginal = false; | |
| const isMatchId = String(gw).includes('_vs_'); | |
| // FIX: Look specifically inside match_projections for DGW match edits! | |
| if (pristinePlayer && pristinePlayer.match_projections && isMatchId) { | |
| const mData = pristinePlayer.match_projections[gw]; | |
| if (mData) { | |
| const mOrig = statKey === 'xMins' ? mData.xMins : mData[statKey]; | |
| isRevertingToOriginal = (finalValue === mOrig); | |
| } | |
| } else { | |
| const originalValue = pristinePlayer ? pristinePlayer[`${gw}_${statKey}`] : undefined; | |
| if (originalValue !== undefined) { | |
| isRevertingToOriginal = (finalValue === originalValue); | |
| } else if (statKey === 'xMins' && finalValue === 90) { | |
| isRevertingToOriginal = true; | |
| } | |
| } | |
| // Calculate instant EV for UI feedback | |
| if (statKey === 'xMins') { | |
| if (pristinePlayer && pristinePlayer.match_projections && isMatchId) { | |
| const mData = pristinePlayer.match_projections[gw]; | |
| const baseMins = mData ? mData.xMins : 90; | |
| const basePts = mData ? mData.Pts : 0; | |
| calculatedPts = baseMins > 0 ? (basePts / baseMins) * finalValue : 0; | |
| } else { | |
| const baseMins = pristinePlayer ? pristinePlayer[`${gw}_xMins`] || 90 : 90; | |
| const basePts = pristinePlayer ? pristinePlayer[`${gw}_Pts`] || 0 : 0; | |
| calculatedPts = baseMins > 0 ? (basePts / baseMins) * finalValue : 0; | |
| } | |
| } | |
| setGlobalPlayers(prev => prev.map(p => { | |
| if (p.ID === playerId) { | |
| let updated = { ...p, [`${gw}_${statKey}`]: finalValue }; | |
| if (statKey === 'xMins') updated[`${gw}_Pts`] = calculatedPts; | |
| return updated; | |
| } | |
| return p; | |
| })); | |
| setTeamData(prev => prev.map(p => { | |
| if (p.ID === playerId) { | |
| let updated = { ...p, [`${gw}_${statKey}`]: finalValue }; | |
| if (statKey === 'xMins') updated[`${gw}_Pts`] = calculatedPts; | |
| return updated; | |
| } | |
| return p; | |
| })); | |
| // 2. The Smart Self-Cleaning Session Edits | |
| setSessionEdits(prev => { | |
| const newEdits = { ...prev }; | |
| if (isRevertingToOriginal) { | |
| if (newEdits[playerId]) { | |
| newEdits[playerId] = { ...newEdits[playerId] }; | |
| delete newEdits[playerId][`${gw}_${statKey}`]; | |
| if (statKey === 'xMins') delete newEdits[playerId][`${gw}_Pts`]; | |
| if (Object.keys(newEdits[playerId]).length === 0) delete newEdits[playerId]; | |
| } | |
| } else { | |
| if (!newEdits[playerId]) newEdits[playerId] = {}; | |
| newEdits[playerId] = { ...newEdits[playerId], [`${gw}_${statKey}`]: finalValue }; | |
| if (statKey === 'xMins') { | |
| // Do not hardcode match points so the stochastic engine can dynamically scale it! | |
| if (!isMatchId) newEdits[playerId][`${gw}_Pts`] = calculatedPts; | |
| } | |
| } | |
| if (isLoggedIn) { | |
| fetch('https://anayshukla-fpl-solver.hf.space/api/auth/save_session', { | |
| method: 'POST', | |
| headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${localStorage.getItem('fpl_token')}` }, | |
| body: JSON.stringify({ saved_edits: { ...newEdits, _solver_overrides: manualOverridesRef.current } }) | |
| }); | |
| } | |
| return newEdits; | |
| }); | |
| }; | |
| // --- STOCHASTIC ENGINE INVALIDATION --- | |
| // If the user changes any fixture odds, the current solver lineup is now mathematically invalid. | |
| // This instantly clears the stale lineup and forces the UI to reset. | |
| useEffect(() => { | |
| if (solverResult) { | |
| setSolverResult(null); | |
| } | |
| if (appliedPlanSummary) { | |
| setAppliedPlanSummary(null); | |
| } | |
| }, [fixtureOverrides]); | |
| return ( | |
| <PlayerContext.Provider value={{ | |
| globalPlayers, setGlobalPlayers, isLoadingDB, updatePlayerStat, teamId, setTeamId, teamData, setTeamData, | |
| availableGWs, setAvailableGWs, horizon, setHorizon, activeGW, setActiveGW, captainId, setCaptainId, | |
| viceId, setViceId, itb, setItb, availableFts, setAvailableFts, initialSquadIds, setInitialSquadIds, | |
| solverResult, setSolverResult, activeChip, setActiveChip, manualOverrides, setManualOverrides, isLoggedIn, | |
| setIsLoggedIn, userProfile, setUserProfile, hasGuestMadeEdits, setHasGuestMadeEdits, projSearchTerm, setProjSearchTerm, | |
| sessionEdits, setSessionEdits, highlightTransferIds, setHighlightTransferIds, transfersByGw, setTransfersByGw, | |
| chipsByGw, setChipsByGw, baselineItb, setBaselineItb, baselineFt, setBaselineFt, quickSettings, setQuickSettings, | |
| advancedSettings, setAdvancedSettings, solveElapsedSec, setSolveElapsedSec, solverTransferPairs, setSolverTransferPairs, | |
| solverApplySnapshot, setSolverApplySnapshot, appliedPlanSummary, setAppliedPlanSummary, hitsThisGw, setHitsThisGw, | |
| numSims, setNumSims, HIT_COST, comprehensiveSettings, setComprehensiveSettings, saveSession, ftAtStartOfGw, itbAtStartOfGw, | |
| isCheckingAuth, drafts, setDrafts, activeDraftId, setActiveDraftId, fixtureOverrides, setFixtureOverrides, originalPlayers, | |
| setOriginalPlayers, globalFixtures, setGlobalFixtures, effectiveFixtures, globalXmins | |
| }}> | |
| <FixturesContext.Provider value={effectiveFixtures}> | |
| {children} | |
| </FixturesContext.Provider> | |
| </PlayerContext.Provider> | |
| ); | |
| }; |