/assets gets Export CSV (GET /api/assets/export) and Import CSV (POST /api/assets/import) buttons. Export emits human-readable manufacturer/model columns instead of raw modelId; import resolves those back to an AssetModel (case-insensitive match, must already exist -- this does not auto-create models) so export -> edit in Excel -> re-import is a real round trip. Per-row import errors (unmatched model, missing required field, bad date) are collected and returned alongside the created count rather than aborting the whole file on the first bad row -- a typo in row 200 should not cost rows 1-199. warrantyEndDate still auto-fills from the models warrantyMonths when the CSV omits it, same as manual creation. Extracted src/lib/csv.ts (splitCsvLine/splitCsvLines/normalizeHeader/ csvField) out of expected-assets.ts so both CSV parsers share one implementation instead of duplicating it. Manual smoke test only (tsx script: export shape, valid row + unmatched- model row + missing-date row all handled correctly), no Vitest coverage, not through the ba->pm->feature-implementer->test-runner->code-reviewer pipeline -- same deviation as the rest of this branch, see CLAUDE.md. tsc/lint clean.
This commit is contained in:
@@ -0,0 +1,60 @@
|
||||
// Shared minimal CSV parsing helpers -- extracted from
|
||||
// src/lib/expected-assets.ts (2026-08-21) when src/lib/assets.ts needed
|
||||
// the same line-splitting logic for AssetItem import (2026-08-23). Handles
|
||||
// quoted fields (incl. embedded commas/escaped double-quotes) without a
|
||||
// dependency, which is enough for this app's actual import files (simple
|
||||
// flat tables, not arbitrary user-supplied CSV).
|
||||
|
||||
/**
|
||||
* Splits one CSV line into fields. A quote only opens a quoted field when
|
||||
* it's the very first character of that field -- a stray/legitimate quote
|
||||
* mid-value (e.g. a 24" monitor size) is kept as a literal character
|
||||
* instead of wrongly starting quoted-content mode.
|
||||
*/
|
||||
export function splitCsvLine(line: string): string[] {
|
||||
const fields: string[] = [];
|
||||
let current = "";
|
||||
let inQuotes = false;
|
||||
for (let i = 0; i < line.length; i++) {
|
||||
const char = line[i];
|
||||
if (inQuotes) {
|
||||
if (char === '"') {
|
||||
if (line[i + 1] === '"') {
|
||||
current += '"';
|
||||
i++;
|
||||
} else {
|
||||
inQuotes = false;
|
||||
}
|
||||
} else {
|
||||
current += char;
|
||||
}
|
||||
} else if (char === '"' && current === "") {
|
||||
inQuotes = true;
|
||||
} else if (char === ",") {
|
||||
fields.push(current);
|
||||
current = "";
|
||||
} else {
|
||||
current += char;
|
||||
}
|
||||
}
|
||||
fields.push(current);
|
||||
return fields.map((f) => f.trim());
|
||||
}
|
||||
|
||||
/** Splits raw CSV text into non-blank lines (handles \r\n, \r, \n). */
|
||||
export function splitCsvLines(text: string): string[] {
|
||||
return text.split(/\r\n|\r|\n/).filter((line) => line.trim() !== "");
|
||||
}
|
||||
|
||||
export function normalizeHeader(header: string): string {
|
||||
return header.trim().toLowerCase().replace(/[_-]+/g, " ").replace(/\s+/g, " ");
|
||||
}
|
||||
|
||||
/** Quotes a single CSV field for output if it contains a comma, quote, or
|
||||
* newline -- doubling any embedded quotes, per RFC4180. */
|
||||
export function csvField(value: string): string {
|
||||
if (/[",\n]/.test(value)) {
|
||||
return `"${value.replace(/"/g, '""')}"`;
|
||||
}
|
||||
return value;
|
||||
}
|
||||
Reference in New Issue
Block a user