Skip to content

Commit b424ca7

Browse files
Todd GauglerTodd Gaugler
authored andcommitted
Improving upload
1 parent 65203fd commit b424ca7

File tree

1 file changed

+320
-23
lines changed

1 file changed

+320
-23
lines changed

ff-rankings-app/src/components/DraftTracker.jsx

Lines changed: 320 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -219,54 +219,348 @@ const DraftTrackerContent = () => {
219219
}, [players, currentDraftPick, numTeams, rosterSettings, autoDraftSettings, teamVariability,
220220
teamNames, draftStyle, isKeeperMode]);
221221

222-
// CSV parsing - now creates unified player objects with stable IDs
223-
const parseCSV = (csvText) => {
222+
// Team mapping utilities for CSVs without team information
223+
const normalizePlayerName = (name) => {
224+
return name
225+
.toLowerCase()
226+
.replace(/[^a-z0-9\s]/g, '') // Remove special characters
227+
.replace(/\s+/g, ' ') // Normalize whitespace
228+
.trim()
229+
.replace(/\s/g, ''); // Remove all spaces for exact matching
230+
};
231+
232+
const fuzzyMatchPlayerName = (targetName, candidateName, threshold = 0.8) => {
233+
const target = normalizePlayerName(targetName);
234+
const candidate = normalizePlayerName(candidateName);
235+
236+
// Exact match
237+
if (target === candidate) return 1.0;
238+
239+
// Check if one contains the other
240+
if (target.includes(candidate) || candidate.includes(target)) {
241+
return 0.9;
242+
}
243+
244+
// Simple similarity check
245+
const longer = target.length > candidate.length ? target : candidate;
246+
const shorter = target.length > candidate.length ? candidate : target;
247+
248+
if (longer.length === 0) return 1.0;
249+
250+
let matches = 0;
251+
for (let i = 0; i < shorter.length; i++) {
252+
if (longer.includes(shorter[i])) matches++;
253+
}
254+
255+
const similarity = matches / longer.length;
256+
return similarity >= threshold ? similarity : 0;
257+
};
258+
259+
const createTeamMappingFromPreloadedCSVs = async () => {
260+
const csvFiles = [
261+
'FantasyPros 2025 PPR.csv',
262+
'4for4 Underdog ADP.csv',
263+
'BB10s ADP.csv',
264+
'CBS ADP.csv',
265+
'ESPN ADP.csv',
266+
'FFPC ADP.csv',
267+
'Y! ADP.csv',
268+
'FantasyPros .5 PPR.csv',
269+
'FantasyPros 2025 Top 10 Accurate Overall PPR.csv',
270+
'FantasyNow+ PPR.csv',
271+
'The Fantasy Headliners PPR.csv'
272+
];
273+
274+
const teamMapping = new Map();
275+
let filesProcessed = 0;
276+
277+
console.log('🔍 Building team mapping from preloaded CSV files...');
278+
279+
// Column patterns for reference files
280+
const COLUMN_PATTERNS = {
281+
name: [
282+
'name', 'player', 'playername', 'player_name', 'player name', 'full_name', 'fullname',
283+
'full name', 'lastname', 'last_name', 'first_name', 'firstname'
284+
],
285+
team: [
286+
'team', 'tm', 'nfl_team', 'nfl team', 'franchise', 'club', 'organization',
287+
'team_abbr', 'team abbr', 'team_abbreviation', 'team abbreviation', 'teamname', 'team_name'
288+
]
289+
};
290+
291+
const findColumnIndex = (headers, patterns) => {
292+
const normalizedHeaders = headers.map(h => h.trim().toLowerCase());
293+
294+
for (const pattern of patterns) {
295+
const index = normalizedHeaders.findIndex(h =>
296+
h === pattern.toLowerCase() ||
297+
h.includes(pattern.toLowerCase()) ||
298+
pattern.toLowerCase().includes(h)
299+
);
300+
if (index !== -1) {
301+
return index;
302+
}
303+
}
304+
return -1;
305+
};
306+
307+
for (const filename of csvFiles) {
308+
try {
309+
const response = await fetch(`/${filename}`);
310+
if (!response.ok) continue;
311+
312+
const csvText = await response.text();
313+
const lines = csvText.trim().split('\n');
314+
const headers = lines[0].split(',').map(h => h.trim());
315+
316+
const nameIndex = findColumnIndex(headers, COLUMN_PATTERNS.name);
317+
const teamIndex = findColumnIndex(headers, COLUMN_PATTERNS.team);
318+
319+
if (nameIndex === -1 || teamIndex === -1) {
320+
console.log(`⚠️ Skipping ${filename}: missing name or team columns`);
321+
continue;
322+
}
323+
324+
// Process each player in this reference file
325+
lines.slice(1).forEach(line => {
326+
const values = line.split(',').map(v => v.trim());
327+
const playerName = values[nameIndex];
328+
const playerTeam = values[teamIndex];
329+
330+
if (playerName && playerTeam) {
331+
const normalizedName = normalizePlayerName(playerName);
332+
if (!teamMapping.has(normalizedName)) {
333+
teamMapping.set(normalizedName, playerTeam.toUpperCase());
334+
}
335+
}
336+
});
337+
338+
filesProcessed++;
339+
console.log(`✅ Processed ${filename} for team mapping`);
340+
341+
} catch (error) {
342+
console.log(`❌ Failed to process ${filename}:`, error);
343+
}
344+
}
345+
346+
console.log(`📊 Team mapping complete: ${teamMapping.size} players from ${filesProcessed} files`);
347+
return teamMapping;
348+
};
349+
350+
const applyTeamMapping = async (playersWithoutTeams, sourceFilename = '') => {
351+
if (playersWithoutTeams.length === 0) {
352+
console.log('✅ No players need team mapping');
353+
return { mappedCount: 0, unmappedPlayers: [] };
354+
}
355+
356+
console.log(`🔄 Attempting to map teams for ${playersWithoutTeams.length} players...`);
357+
358+
const teamMapping = await createTeamMappingFromPreloadedCSVs();
359+
360+
if (teamMapping.size === 0) {
361+
console.log('❌ No team mapping data available from preloaded CSVs');
362+
return { mappedCount: 0, unmappedPlayers: playersWithoutTeams };
363+
}
364+
365+
let mappedCount = 0;
366+
const unmappedPlayers = [];
367+
368+
for (const player of playersWithoutTeams) {
369+
let bestMatch = null;
370+
let bestScore = 0;
371+
372+
// Try to find the best match in our team mapping
373+
for (const [mappedName, team] of teamMapping.entries()) {
374+
const score = fuzzyMatchPlayerName(player.name, mappedName);
375+
if (score > bestScore && score >= 0.8) {
376+
bestScore = score;
377+
bestMatch = team;
378+
}
379+
}
380+
381+
if (bestMatch) {
382+
player.team = bestMatch;
383+
mappedCount++;
384+
console.log(`✅ Mapped ${player.name}${bestMatch} (confidence: ${(bestScore * 100).toFixed(1)}%)`);
385+
} else {
386+
unmappedPlayers.push(player);
387+
console.log(`❌ No team found for ${player.name}`);
388+
}
389+
}
390+
391+
console.log(`📊 Team mapping results: ${mappedCount} mapped, ${unmappedPlayers.length} unmapped`);
392+
393+
return { mappedCount, unmappedPlayers };
394+
};
395+
396+
// Enhanced CSV parsing with flexible column detection and team mapping
397+
const parseCSV = async (csvText, filename = '') => {
224398
const lines = csvText.trim().split('\n');
225-
const headers = lines[0].split(',').map(h => h.trim().toLowerCase());
399+
const headers = lines[0].split(',').map(h => h.trim());
400+
401+
console.log('🔍 Parsing CSV with enhanced detection...');
402+
console.log('📊 Headers found:', headers);
403+
404+
// Column patterns for flexible matching (case insensitive)
405+
const COLUMN_PATTERNS = {
406+
name: [
407+
'name', 'player', 'playername', 'player_name', 'player name', 'full_name', 'fullname',
408+
'full name', 'lastname', 'last_name', 'first_name', 'firstname'
409+
],
410+
position: [
411+
'position', 'pos', 'positions', 'player_position', 'player position', 'eligibility',
412+
'eligible_positions', 'eligible positions', 'fantasy_position', 'fantasy position'
413+
],
414+
team: [
415+
'team', 'tm', 'nfl_team', 'nfl team', 'franchise', 'club', 'organization',
416+
'team_abbr', 'team abbr', 'team_abbreviation', 'team abbreviation', 'teamname', 'team_name'
417+
],
418+
rank: [
419+
'rank', 'ranking', 'overall', 'overall_rank', 'overall rank', 'player_rank', 'player rank',
420+
'draft_rank', 'draft rank', 'fantasy_rank', 'fantasy rank', 'ecr', 'consensus_rank',
421+
'consensus rank', 'expert_consensus_rank', 'expert consensus rank', 'avg_rank', 'avg rank',
422+
'average_rank', 'average rank', 'rk', 'rnk', 'position_rank', 'positional_rank'
423+
],
424+
tier: [
425+
'tier', 'tiers', 'draft_tier', 'draft tier', 'fantasy_tier', 'fantasy tier',
426+
'tier_rank', 'tier rank', 'player_tier', 'player tier'
427+
]
428+
};
429+
430+
// Find column index using flexible patterns
431+
const findColumnIndex = (headers, patterns) => {
432+
const normalizedHeaders = headers.map(h => h.trim().toLowerCase());
433+
434+
for (const pattern of patterns) {
435+
const index = normalizedHeaders.findIndex(h =>
436+
h === pattern.toLowerCase() ||
437+
h.includes(pattern.toLowerCase()) ||
438+
pattern.toLowerCase().includes(h)
439+
);
440+
if (index !== -1) {
441+
console.log(`✅ Found ${pattern} column at index ${index}: "${headers[index]}"`);
442+
return index;
443+
}
444+
}
445+
return -1;
446+
};
226447

227-
const nameIndex = headers.findIndex(h => h.includes('name'));
228-
const positionIndex = headers.findIndex(h => h.includes('position') || h.includes('pos'));
229-
const teamIndex = headers.findIndex(h => h.includes('team'));
230-
const rankIndex = headers.findIndex(h => h.includes('rank'));
231-
const tierIndex = headers.findIndex(h => h.includes('tier'));
448+
const nameIndex = findColumnIndex(headers, COLUMN_PATTERNS.name);
449+
const positionIndex = findColumnIndex(headers, COLUMN_PATTERNS.position);
450+
const teamIndex = findColumnIndex(headers, COLUMN_PATTERNS.team);
451+
const rankIndex = findColumnIndex(headers, COLUMN_PATTERNS.rank);
452+
const tierIndex = findColumnIndex(headers, COLUMN_PATTERNS.tier);
453+
454+
console.log('📍 Column mapping results:', {
455+
name: nameIndex >= 0 ? `"${headers[nameIndex]}"` : 'NOT FOUND',
456+
position: positionIndex >= 0 ? `"${headers[positionIndex]}"` : 'NOT FOUND',
457+
team: teamIndex >= 0 ? `"${headers[teamIndex]}"` : 'NOT FOUND',
458+
rank: rankIndex >= 0 ? `"${headers[rankIndex]}"` : 'NOT FOUND',
459+
tier: tierIndex >= 0 ? `"${headers[tierIndex]}"` : 'NOT FOUND'
460+
});
232461

233-
if (nameIndex === -1 || positionIndex === -1 || rankIndex === -1) {
234-
throw new Error('CSV must contain name, position, and rank columns');
462+
if (nameIndex === -1) {
463+
throw new Error('CSV must contain a player name column. Expected headers like: name, player, playername, player_name, full_name, etc.');
464+
}
465+
if (positionIndex === -1) {
466+
throw new Error('CSV must contain a position column. Expected headers like: position, pos, eligibility, fantasy_position, etc.');
467+
}
468+
if (rankIndex === -1) {
469+
throw new Error('CSV must contain a rank column. Expected headers like: rank, overall, player_rank, draft_rank, ecr, etc.');
235470
}
236471

472+
const hasTeamInfo = teamIndex !== -1;
473+
console.log(hasTeamInfo ? '✅ Team information found in CSV' : '⚠️ No team information found - will attempt automatic mapping');
474+
237475
const playersObj = {};
476+
const playersWithoutTeams = [];
477+
238478
lines.slice(1).forEach((line, index) => {
239479
const values = line.split(',').map(v => v.trim());
240480
const playerName = values[nameIndex] || '';
241481
const playerPosition = values[positionIndex] || '';
242-
const playerTeam = teamIndex !== -1 ? values[teamIndex] || '' : '';
482+
const playerTeam = hasTeamInfo ? (values[teamIndex] || '') : '';
483+
const playerRank = parseInt(values[rankIndex]) || index + 1;
484+
const playerTier = tierIndex !== -1 ? (parseInt(values[tierIndex]) || null) : null;
243485

244-
// Create stable ID based on name, position, and team to avoid shifting
486+
if (!playerName || !playerPosition) {
487+
console.warn(`⚠️ Skipping row ${index + 2}: missing name or position`);
488+
return;
489+
}
490+
491+
// Create initial stable ID
245492
const playerId = `${playerName.toLowerCase().replace(/[^a-z0-9]/g, '')}_${playerPosition.toLowerCase()}_${playerTeam.toLowerCase()}`.substring(0, 50);
246493

247-
playersObj[playerId] = {
494+
const playerObj = {
248495
id: playerId,
249496
name: playerName,
250-
position: playerPosition,
251-
team: playerTeam,
252-
rank: parseInt(values[rankIndex]) || index + 1,
253-
tier: tierIndex !== -1 ? parseInt(values[tierIndex]) || null : null,
254-
status: 'available', // 'available', 'drafted', 'keeper'
255-
draftInfo: null, // { teamId, pickNumber, round, isKeeper }
256-
watchStatus: null // 'watched', 'avoided', null
497+
position: playerPosition.toUpperCase(),
498+
team: playerTeam.toUpperCase(),
499+
rank: playerRank,
500+
tier: playerTier,
501+
status: 'available',
502+
draftInfo: null,
503+
watchStatus: null
257504
};
505+
506+
playersObj[playerId] = playerObj;
507+
508+
// Track players without teams for mapping
509+
if (!hasTeamInfo || !playerTeam) {
510+
playersWithoutTeams.push(playerObj);
511+
}
258512
});
259513

514+
// If we have players without teams, try to map them
515+
if (playersWithoutTeams.length > 0) {
516+
console.log(`🔄 Attempting to map teams for ${playersWithoutTeams.length} players...`);
517+
518+
try {
519+
const mappingResult = await applyTeamMapping(playersWithoutTeams, filename);
520+
521+
if (mappingResult.mappedCount > 0) {
522+
console.log(`✅ Successfully mapped ${mappingResult.mappedCount} players to teams`);
523+
524+
// Update the players object with mapped teams and new IDs
525+
playersWithoutTeams.forEach(player => {
526+
// Remove old ID
527+
delete playersObj[player.id];
528+
529+
// Create new ID with team info
530+
const newId = `${player.name.toLowerCase().replace(/[^a-z0-9]/g, '')}_${player.position.toLowerCase()}_${player.team.toLowerCase()}`.substring(0, 50);
531+
player.id = newId;
532+
playersObj[newId] = player;
533+
});
534+
535+
// Show user-friendly notification about team mapping
536+
const mappedPercentage = Math.round((mappingResult.mappedCount / playersWithoutTeams.length) * 100);
537+
console.log(`🎯 Team mapping summary: ${mappingResult.mappedCount}/${playersWithoutTeams.length} players (${mappedPercentage}%) successfully mapped to teams`);
538+
}
539+
540+
if (mappingResult.unmappedPlayers.length > 0) {
541+
console.log(`⚠️ ${mappingResult.unmappedPlayers.length} players still without teams - they will show as blank teams`);
542+
}
543+
} catch (error) {
544+
console.error('❌ Team mapping failed:', error);
545+
console.log('📄 Continuing with original data (players without teams will have blank team fields)');
546+
}
547+
}
548+
549+
console.log(`📊 CSV parsing complete: ${Object.keys(playersObj).length} players loaded from "${filename}"`);
260550
return playersObj;
261551
};
262552

263-
// File upload handlers
553+
// File upload handlers - FIXED to properly handle async parseCSV
264554
const handleFileUpload = (file, isSwitch = false) => {
265555
if (file?.type === 'text/csv') {
266556
const reader = new FileReader();
267-
reader.onload = (e) => {
557+
reader.onload = async (e) => {
268558
try {
269-
const parsedPlayers = parseCSV(e.target.result);
559+
console.log('📁 Processing uploaded CSV file:', file.name);
560+
const parsedPlayers = await parseCSV(e.target.result, file.name);
561+
console.log('✅ CSV parsed successfully, players object:', parsedPlayers);
562+
console.log('🔢 Number of players parsed:', Object.keys(parsedPlayers).length);
563+
270564
setPlayers(parsedPlayers);
271565
setCurrentCSVSource(file.name);
272566

@@ -277,7 +571,10 @@ const DraftTrackerContent = () => {
277571
setIsKeeperMode(false);
278572
clearDraftState();
279573
}
574+
575+
console.log('🎯 Players state updated, UI should now show draft interface');
280576
} catch (error) {
577+
console.error('❌ CSV parsing error:', error);
281578
alert('Error parsing CSV: ' + error.message);
282579
}
283580
};

0 commit comments

Comments
 (0)