fpl-solver / frontend /src /PlayerContext.jsx
AnayShukla's picture
updates
be5cc5e
// 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>
);
};