This commit is contained in:
2026-07-15 16:42:28 -05:00
commit 201f5c1b95
11 changed files with 592 additions and 0 deletions

45
src/data.ts Normal file
View File

@@ -0,0 +1,45 @@
import type { BuyerDocument, PersonGroup } from "./types.ts";
const text = (value: unknown) => typeof value === "string" ? value.trim() : "";
const normalize = (value: unknown) => text(value).toLocaleLowerCase();
export function validateDocuments(value: unknown): BuyerDocument[] {
if (!Array.isArray(value)) throw new Error("JSON root must be an array.");
return value.map((item, index) => {
if (!item || typeof item !== "object") throw new Error(`Record ${index + 1} is not an object.`);
const doc = item as Partial<BuyerDocument>;
if (!text(doc.file_name) || !text(doc.name_from_filename) || !text(doc._letter)) {
throw new Error(`Record ${index + 1} is missing file_name, name_from_filename, or _letter.`);
}
return doc as BuyerDocument;
});
}
export function groupDocuments(documents: BuyerDocument[]): PersonGroup[] {
const groups = new Map<string, BuyerDocument[]>();
for (const doc of documents) {
const key = doc.name_from_filename.trim();
const bucket = groups.get(key) ?? [];
bucket.push(doc);
groups.set(key, bucket);
}
return [...groups.entries()]
.map(([key, docs]) => {
docs.sort((a, b) => a.file_name.localeCompare(b.file_name));
const preferred = docs.find((d) => text(d.prospective_buyer))?.prospective_buyer;
const searchText = normalize([
key,
preferred,
...docs.flatMap((d) => [d.types_of_business_raw, d.notes_business_raw, d.address]),
].filter(Boolean).join("\n"));
return { key, displayName: text(preferred) || key, documents: docs, searchText };
})
.sort((a, b) => a.key.localeCompare(b.key));
}
export function filterGroups(groups: PersonGroup[], query: string): PersonGroup[] {
const terms = normalize(query).split(/\s+/).filter(Boolean);
if (!terms.length) return groups;
return groups.filter((group) => terms.every((term) => group.searchText.includes(term)));
}

13
src/paths.ts Normal file
View File

@@ -0,0 +1,13 @@
import { isAbsolute, join, normalize, relative } from "jsr:@std/path";
import type { BuyerDocument } from "./types.ts";
export function resolvePdfPath(baseDirectory: string, doc: BuyerDocument): string {
if (!baseDirectory.trim()) throw new Error("PDF base directory is not configured.");
if (!isAbsolute(baseDirectory)) throw new Error("PDF base directory must be absolute.");
const base = normalize(baseDirectory);
const full = normalize(join(base, doc._letter, doc.file_name));
const rel = relative(base, full);
if (rel.startsWith("..") || isAbsolute(rel)) throw new Error("Resolved PDF path escapes the base directory.");
return full;
}

35
src/types.ts Normal file
View File

@@ -0,0 +1,35 @@
export interface BuyerDocument {
file_name: string;
name_from_filename: string;
_letter: string;
prospective_buyer?: string | null;
name_company?: string | null;
company?: string | null;
phone?: string | null;
cell?: string | null;
email?: string | null;
address?: string | null;
state?: string | null;
how_did_you_hear?: string | null;
interested_in_updates?: string | null;
types_of_business_raw?: string | null;
notes_business_raw?: string | null;
background_experience?: string | null;
total_purchase_price?: string | null;
down_payment?: string | null;
down_payment_raw?: string | null;
date_of_introduction?: string | null;
_doc_type?: string | null;
_pages_total?: number | null;
_notes_page?: number | null;
_info_page?: number | null;
_ca_page?: number | null;
_vision_error?: string | null;
}
export interface PersonGroup {
key: string;
displayName: string;
documents: BuyerDocument[];
searchText: string;
}