// src/components/PlayerModals.jsx
import React,{ useState, useEffect, useContext } from "react";
import { Search, Plus } from "lucide-react";
import { getPlayerPrice } from "../utils/fplLogic";
import { getShortName } from "../utils/teams";
import { PlayerContext } from "../PlayerContext";
const SafeMinsInput = ({ initialValue, onSave, isChild = false, disabled = false }) => {
const [val, setVal] = useState(initialValue);
useEffect(() => setVal(initialValue), [initialValue]);
return (
{
setVal(e.target.value);
onSave(e.target.value);
}}
className={isChild
? "w-12 bg-slate-950 text-center font-mono text-xs font-bold text-indigo-400 rounded py-1 outline-none focus:ring-1 ring-indigo-500 border border-slate-800"
: `w-16 text-center font-mono text-sm font-bold rounded py-1 outline-none ${disabled ? 'bg-transparent text-slate-500' : 'bg-slate-900 text-emerald-400 focus:bg-slate-800 focus:ring-1 ring-emerald-500'}`
}
/>
);
};
export const PlayerEditModal = ({
selectedPlayer,
setSelectedPlayer,
activeGW,
horizonGWs,
updatePlayerStat,
handleTransferOut,
fixtures,
fixtureOverrides,
sessionEdits,
globalPlayers
}) => {
const { effectiveFixtures, globalXmins } = useContext(PlayerContext);
// THE FIX: Grab the live updating player, not the frozen snapshot!
const livePlayer = globalPlayers?.find(p => p.ID === selectedPlayer.ID) || selectedPlayer;
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"
};
return (
{livePlayer.Name}
{livePlayer.Team}
|
£{getPlayerPrice(livePlayer).toFixed(1)}m
{[
{ label: `GW${activeGW} xG`, val: livePlayer[`${activeGW}_xG`] ?? livePlayer.xG ?? "-" },
{ label: `GW${activeGW} xA`, val: livePlayer[`${activeGW}_xA`] ?? livePlayer.xA ?? "-" },
{ label: `GW${activeGW} CS%`, val: livePlayer[`${activeGW}_CS_Pct`] ?? livePlayer.CS_Pct ?? "-" },
]
.filter(stat => !(stat.label.includes('CS%') && livePlayer.Pos === 'F'))
.map((stat) => (
{stat.label}
{typeof stat.val === "number" ? stat.val.toFixed(2) : stat.val}
))}
| GW |
Fixture |
xMins |
Proj. EV |
{horizonGWs.map((gw) => {
const matches = [];
if (livePlayer.match_projections) {
Object.entries(livePlayer.match_projections).forEach(([mId, mData]) => {
// THE FIX 2: Look at the merged globals instead of the empty prop!
const override = effectiveFixtures?.[mId];
// THE FIX 3: Force Number() to prevent API float bugs
if (override && Number(override[gw]) > 0) matches.push({ ...mData, id: mId, prob: Number(override[gw]) });
else if (!override && String(mData.default_gw) === String(gw)) matches.push({ ...mData, id: mId, prob: 1.0 });
});
}
const hasMultiple = matches.length > 1;
const isBlank = matches.length === 0;
return (
| GW{gw} |
{isBlank ? (
"BLANK"
) : hasMultiple ? (
"MULTIPLE"
) : (
{matches[0]?.is_home ? `${TEAM_SHORTS[matches[0].opponent_team_id]} (H)` : `${TEAM_SHORTS[matches[0]?.opponent_team_id]} (A)`}
{matches[0]?.prob < 1.0 && (
{Math.round(matches[0].prob * 100)}%
)}
)}
|
{
if (hasMultiple) {
matches.forEach(m => updatePlayerStat(livePlayer.ID, m.id, "xMins", newVal));
} else {
updatePlayerStat(livePlayer.ID, gw, "xMins", newVal);
}
}}
/>
|
{Number(livePlayer[`${gw}_Pts`] || 0).toFixed(2)}
|
{hasMultiple && matches.map(m => {
const oppName = TEAM_SHORTS[m.opponent_team_id] || m.opponent_team_id;
const fixLabel = m.is_home ? `${oppName} (H)` : `${oppName} (A)`;
const globalMatchMins = globalXmins?.[livePlayer.ID]?.[m.id];
const sessionVal = sessionEdits?.[livePlayer.ID]?.[`${m.id}_xMins`];
const currentMins = Math.round(sessionVal !== undefined ? Number(sessionVal) : (globalMatchMins !== undefined ? Number(globalMatchMins) : m.xMins));
const scaledEV = (currentMins > 0 && m.xMins > 0) ? (m.Pts / m.xMins) * currentMins : 0;
return (
| ↳ |
{fixLabel} ({Math.round(m.prob * 100)}%)
|
updatePlayerStat(livePlayer.ID, m.id, "xMins", newVal)}
/>
|
{(scaledEV * m.prob).toFixed(2)}
|
);
})}
);
})}
);
};
export const PlayerSearchModal = ({
selectedPlayer,
setSelectedPlayer,
searchQuery,
setSearchQuery,
sortConfig,
setSortConfig,
globalPlayers,
ownedPlayerIds,
activeGW,
itb,
handleAddPlayer,
}) => {
const squadTeamCounts = {};
const currentSquad = globalPlayers.filter(p => ownedPlayerIds.has(p.ID) || ownedPlayerIds.has(String(p.ID)) || ownedPlayerIds.has(Number(p.ID)));
currentSquad.forEach(p => {
squadTeamCounts[p.Team] = (squadTeamCounts[p.Team] || 0) + 1;
});
// Free up a slot if we are actively transferring out a player from a team!
if (selectedPlayer && selectedPlayer.Team) {
squadTeamCounts[selectedPlayer.Team] = Math.max(0, (squadTeamCounts[selectedPlayer.Team] || 0) - 1);
}
const cleanString = (str) => str ? str.normalize("NFD").replace(/[\u0300-\u036f]/g, "").toLowerCase() : "";
const cleanSearch = cleanString(searchQuery);
return (
setSearchQuery(e.target.value)}
className="flex-1 bg-transparent border-none outline-none text-slate-200 font-bold"
autoFocus
/>
SORT BY:
{/* THE FIX: Changed max-h to 50vh on mobile so it doesn't span the whole screen */}
{globalPlayers
// THE FIX: Apply cleanString to both the player name and the search query
.filter((p) => !ownedPlayerIds.has(p.ID) && !ownedPlayerIds.has(String(p.ID)) && !ownedPlayerIds.has(Number(p.ID)) && String(p.ID) !== String(selectedPlayer.replacedPlayer?.ID) && p.Pos === selectedPlayer.Pos && cleanString(p.Name).includes(cleanSearch))
.sort((a, b) => {
let valA = sortConfig.key === "ev" ? Number(a[`${activeGW}_Pts`] || 0) : getPlayerPrice(a);
let valB = sortConfig.key === "ev" ? Number(b[`${activeGW}_Pts`] || 0) : getPlayerPrice(b);
if (valA < valB) return sortConfig.direction === "desc" ? 1 : -1;
if (valA > valB) return sortConfig.direction === "desc" ? -1 : 1;
return 0;
})
.slice(0, 50)
.map((p) => {
const sellingPrice = getPlayerPrice(selectedPlayer) || 0;
const maxBudget = itb + sellingPrice;
const cost = getPlayerPrice(p);
const isAffordable = cost <= maxBudget;
const isAtTeamLimit = (squadTeamCounts[p.Team] || 0) >= 3;
const isSelectable = isAffordable && !isAtTeamLimit;
return (
);
})}
);
};