107 lines
3.4 KiB
TypeScript
107 lines
3.4 KiB
TypeScript
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<keyof ParsedScopeRow, string[]> = {
|
|
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<keyof ParsedScopeRow, string | null> {
|
|
const mapping: Record<keyof ParsedScopeRow, string | null> = {
|
|
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<keyof ParsedScopeRow, string | null>;
|
|
}
|
|
|
|
export async function parseScopeImport(file: File): Promise<ScopeImportResult> {
|
|
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 };
|
|
}
|