From 2f084174df522ddb6e1124003d97c56c6302d2e7 Mon Sep 17 00:00:00 2001 From: BOHA Date: Tue, 16 Jun 2026 20:07:35 +0200 Subject: [PATCH] feat(documents): selected-custom-fields encode/decode helpers Co-Authored-By: Claude Opus 4.8 (1M context) --- src/__tests__/selected-custom-fields.test.ts | 38 ++++++++++++++++++++ src/utils/custom-fields.ts | 33 +++++++++++++++++ 2 files changed, 71 insertions(+) create mode 100644 src/__tests__/selected-custom-fields.test.ts diff --git a/src/__tests__/selected-custom-fields.test.ts b/src/__tests__/selected-custom-fields.test.ts new file mode 100644 index 0000000..74801a9 --- /dev/null +++ b/src/__tests__/selected-custom-fields.test.ts @@ -0,0 +1,38 @@ +import { describe, it, expect } from "vitest"; +import { + encodeSelectedCustomFields, + parseSelectedCustomFields, +} from "../utils/custom-fields"; + +describe("selected custom fields encode/decode", () => { + it("encodes a non-empty index array to a JSON string", () => { + expect(encodeSelectedCustomFields([0, 2])).toBe("[0,2]"); + }); + + it("encodes empty / non-array to null", () => { + expect(encodeSelectedCustomFields([])).toBeNull(); + expect(encodeSelectedCustomFields(undefined)).toBeNull(); + expect(encodeSelectedCustomFields("nope")).toBeNull(); + }); + + it("dedupes, sorts, and drops invalid entries before encoding", () => { + expect(encodeSelectedCustomFields([2, 0, 2, -1, 1.5, 3])).toBe("[0,2,3]"); + }); + + it("parses a stored string back to a number array", () => { + expect(parseSelectedCustomFields("[0,2]")).toEqual([0, 2]); + }); + + it("parses null / malformed to an empty array", () => { + expect(parseSelectedCustomFields(null)).toEqual([]); + expect(parseSelectedCustomFields("")).toEqual([]); + expect(parseSelectedCustomFields("{garbage")).toEqual([]); + expect(parseSelectedCustomFields('"x"')).toEqual([]); + }); + + it("parses already-array input (defensive) and filters invalid", () => { + expect(parseSelectedCustomFields([0, "1", 2, -3] as unknown)).toEqual([ + 0, 2, + ]); + }); +}); diff --git a/src/utils/custom-fields.ts b/src/utils/custom-fields.ts index 3c27e5a..2883ba6 100644 --- a/src/utils/custom-fields.ts +++ b/src/utils/custom-fields.ts @@ -46,3 +46,36 @@ export function decodeCustomFields(raw: string | null): { return { custom_fields: [], field_order: [] }; } } + +/** + * Per-document selection of which COMPANY custom fields print on a PDF. + * Stored positionally (matching the `custom_` keys the PDF builder emits) + * as a JSON array string, e.g. "[0,2]". Null/empty means "none selected". + */ +export function encodeSelectedCustomFields(indices: unknown): string | null { + const clean = normalizeIndices(indices); + return clean.length > 0 ? JSON.stringify(clean) : null; +} + +/** Decode the stored selection (string OR defensive array) into a clean number[]. */ +export function parseSelectedCustomFields(raw: unknown): number[] { + if (raw == null) return []; + if (Array.isArray(raw)) return normalizeIndices(raw); + if (typeof raw !== "string" || raw.trim() === "") return []; + try { + return normalizeIndices(JSON.parse(raw)); + } catch { + // Malformed JSON in a selection column degrades to "none" (expected + // condition — a hand-edited/legacy row should never 500 a PDF render). + return []; + } +} + +function normalizeIndices(input: unknown): number[] { + if (!Array.isArray(input)) return []; + const set = new Set(); + for (const v of input) { + if (typeof v === "number" && Number.isInteger(v) && v >= 0) set.add(v); + } + return [...set].sort((a, b) => a - b); +}