import { parseSpreadsheet } from "./excel.js"; export interface ParsedScopeRow { sequenceNo: number; scopeType: string; packageCode: string; name: string; description: string; } /** * Known column header aliases for each field. * Case-insensitive match; first match wins. */ const FIELD_ALIASES: Record = { sequenceNo: ["sequence", "seq", "no", "nr", "#", "number", "sequenceno", "sequence_no"], scopeType: ["type", "scope_type", "scopetype", "category", "asset_type", "assettype"], packageCode: ["package", "pkg", "package_code", "packagecode", "code"], name: ["name", "title", "shot", "asset", "scene", "item", "scope_item", "scopeitem"], description: ["description", "desc", "details", "notes", "comment"], }; function normalizeHeader(header: string): string { return header.trim().toLowerCase().replace(/[\s_-]+/g, "_"); } function resolveFieldMapping( headers: string[], ): Record { const mapping: Record = { sequenceNo: null, scopeType: null, packageCode: null, name: null, description: null, }; for (const header of headers) { const normalized = normalizeHeader(header); for (const [field, aliases] of Object.entries(FIELD_ALIASES)) { if (mapping[field as keyof ParsedScopeRow] !== null) continue; if (aliases.some((alias) => normalized === alias || normalized.includes(alias))) { mapping[field as keyof ParsedScopeRow] = header; } } } return mapping; } export interface ScopeImportResult { rows: ParsedScopeRow[]; warnings: string[]; mapping: Record; } export async function parseScopeImport(file: File): Promise { const rawRows = await parseSpreadsheet(file); const warnings: string[] = []; if (rawRows.length === 0) { return { rows: [], warnings: ["File contains no data rows."], mapping: { sequenceNo: null, scopeType: null, packageCode: null, name: null, description: null } }; } const headers = Object.keys(rawRows[0] ?? {}); const mapping = resolveFieldMapping(headers); if (!mapping.name) { warnings.push( `Could not identify a "Name" column. Available headers: ${headers.join(", ")}`, ); return { rows: [], warnings, mapping }; } const rows: ParsedScopeRow[] = []; for (let i = 0; i < rawRows.length; i++) { const raw = rawRows[i]!; const name = mapping.name ? (raw[mapping.name] ?? "").trim() : ""; if (!name) { continue; } const seqRaw = mapping.sequenceNo ? (raw[mapping.sequenceNo] ?? "").trim() : ""; const seqParsed = parseInt(seqRaw, 10); const sequenceNo = Number.isFinite(seqParsed) && seqParsed > 0 ? seqParsed : rows.length + 1; rows.push({ sequenceNo, scopeType: mapping.scopeType ? (raw[mapping.scopeType] ?? "").trim() || "SHOT" : "SHOT", packageCode: mapping.packageCode ? (raw[mapping.packageCode] ?? "").trim() : "", name, description: mapping.description ? (raw[mapping.description] ?? "").trim() : "", }); } if (rows.length === 0) { warnings.push("No rows with a non-empty name found."); } if (!mapping.scopeType) warnings.push("No scope type column detected — defaulting to SHOT."); if (!mapping.sequenceNo) warnings.push("No sequence number column detected — auto-numbering."); return { rows, warnings, mapping }; }