const DRIVER_COLUMNS = { id: ["id", "driver_id", "forar_id"], name: ["name", "namn", "driver", "driver_name", "forare", "forarnamn"], className: ["class", "klass", "class_name", "klassnamn", "class_id", "klass_id"], brand: ["brand", "marke", "team", "sponsor", "model", "modell"], transponder: ["transponder", "transponder_id", "transpondernummer", "ponder"], speechName: ["spoken_name", "speech_name", "talnamn", "upplasningsnamn"], club: ["club", "klubb"], country: ["country", "land", "nation"], profileId: ["profile_id", "training_profile_id", "profil_id", "traningsprofil_id"], }; const CAR_COLUMNS = { id: ["id", "car_id", "bil_id"], name: ["name", "namn", "car", "car_name", "bil", "bilnamn"], brand: ["brand", "marke", "model", "modell", "chassis", "chassi"], transponder: ["transponder", "transponder_id", "transpondernummer", "ponder"], }; function normalizeHeader(value) { return String(value || "") .replace(/^\uFEFF/, "") .normalize("NFD") .replace(/[\u0300-\u036f]/g, "") .trim() .toLowerCase() .replace(/[^a-z0-9]+/g, "_") .replace(/^_+|_+$/g, ""); } function normalizeMatch(value) { return String(value || "").trim().toLocaleLowerCase().replace(/\s+/g, " "); } function countDelimiter(line, delimiter) { let quoted = false; let count = 0; for (let index = 0; index < line.length; index += 1) { if (line[index] === '"') { if (quoted && line[index + 1] === '"') index += 1; else quoted = !quoted; } else if (!quoted && line[index] === delimiter) { count += 1; } } return count; } function detectDelimiter(text) { const firstLine = String(text || "").split(/\r?\n/).find((line) => line.trim()) || ""; return [",", ";", "\t"] .map((delimiter) => ({ delimiter, count: countDelimiter(firstLine, delimiter) })) .sort((left, right) => right.count - left.count)[0]?.delimiter || ","; } export function parseCsvRows(text) { const source = String(text || ""); if (!source.trim()) throw new Error("CSV file is empty"); const delimiter = detectDelimiter(source); const rows = []; let row = []; let field = ""; let quoted = false; for (let index = 0; index < source.length; index += 1) { const char = source[index]; if (quoted) { if (char === '"' && source[index + 1] === '"') { field += '"'; index += 1; } else if (char === '"') { quoted = false; } else { field += char; } } else if (char === '"') { quoted = true; } else if (char === delimiter) { row.push(field.trim()); field = ""; } else if (char === "\n" || char === "\r") { if (char === "\r" && source[index + 1] === "\n") index += 1; row.push(field.trim()); if (row.some((value) => value)) rows.push(row); row = []; field = ""; } else { field += char; } } if (quoted) throw new Error("CSV file has an unclosed quoted field"); row.push(field.trim()); if (row.some((value) => value)) rows.push(row); if (rows.length < 2) throw new Error("CSV file has no data rows"); const headers = rows[0].map(normalizeHeader); if (!headers.some(Boolean)) throw new Error("CSV file has no header row"); return rows.slice(1).map((values, rowIndex) => ({ rowNumber: rowIndex + 2, values: Object.fromEntries(headers.map((header, index) => [header, String(values[index] || "").trim()])), })); } function readColumn(record, aliases) { for (const alias of aliases) { const value = record[normalizeHeader(alias)]; if (value != null && String(value).trim()) return String(value).trim(); } return ""; } function readMappedRecord(record, columns) { return Object.fromEntries(Object.entries(columns).map(([key, aliases]) => [key, readColumn(record, aliases)])); } function mergeNonEmpty(current, incoming, fields) { const next = { ...current }; fields.forEach((field) => { if (String(incoming[field] ?? "").trim()) next[field] = incoming[field]; }); return next; } function resolveClass(classValue, classes, uid) { const value = String(classValue || "").trim(); if (!value) return ""; const existing = classes.find((item) => String(item.id || "") === value || normalizeMatch(item.name) === normalizeMatch(value)); if (existing) return existing.id; const created = { id: uid("class"), name: value }; classes.push(created); return created.id; } export function importDriversCsv(text, context) { const classes = (context.classes || []).map((item) => ({ ...item })); const drivers = (context.drivers || []).map((item) => ({ ...item })); const uid = context.uid; const parsedRows = parseCsvRows(text); let added = 0; let updated = 0; let skipped = 0; parsedRows.forEach(({ values }) => { const row = readMappedRecord(values, DRIVER_COLUMNS); if (!row.name) { skipped += 1; return; } const classId = resolveClass(row.className, classes, uid); const normalizedName = normalizeMatch(row.name); const byNameAndClass = drivers.filter((item) => ( normalizeMatch(item.name) === normalizedName && (!classId || String(item.classId || "") === classId) )); const existing = drivers.find((item) => row.id && String(item.id || "") === row.id) || drivers.find((item) => row.transponder && String(item.transponder || "") === row.transponder) || (byNameAndClass.length === 1 ? byNameAndClass[0] : null); const samePerson = drivers.find((item) => normalizeMatch(item.name) === normalizedName && item.profileId); const incoming = { id: row.id || uid("driver"), name: row.name, classId, brand: row.brand, transponder: row.transponder, speechName: row.speechName, club: row.club, country: row.country, profileId: row.profileId || samePerson?.profileId || uid("profile"), }; if (existing) { const index = drivers.indexOf(existing); drivers[index] = mergeNonEmpty(existing, incoming, ["name", "classId", "brand", "transponder", "speechName", "club", "country", "profileId"]); updated += 1; } else { drivers.push(incoming); added += 1; } }); return { classes, drivers, added, updated, skipped }; } export function importCarsCsv(text, context) { const cars = (context.cars || []).map((item) => ({ ...item })); const uid = context.uid; const parsedRows = parseCsvRows(text); let added = 0; let updated = 0; let skipped = 0; parsedRows.forEach(({ values }) => { const row = readMappedRecord(values, CAR_COLUMNS); if (!row.name) { skipped += 1; return; } const matchingNames = cars.filter((item) => normalizeMatch(item.name) === normalizeMatch(row.name)); const existing = cars.find((item) => row.id && String(item.id || "") === row.id) || cars.find((item) => row.transponder && String(item.transponder || "") === row.transponder) || (matchingNames.length === 1 ? matchingNames[0] : null); const incoming = { id: row.id || uid("car"), name: row.name, brand: row.brand, transponder: row.transponder, }; if (existing) { const index = cars.indexOf(existing); cars[index] = mergeNonEmpty(existing, incoming, ["name", "brand", "transponder"]); updated += 1; } else { cars.push(incoming); added += 1; } }); return { cars, added, updated, skipped }; }