feat(berths): normalize mooring numbers to canonical form
Sweep CRM mooring numbers from the legacy hyphen+padded form ("A-01")
to the canonical bare form ("A1") used by NocoDB, the public website,
the per-berth PDFs, and the Documenso EOI templates. Drift was
introduced by the original load-berths-to-port-nimara.ts seed; this
gates the Phase 3 public-website cutover where /berths/A1 URLs would
404 against a CRM still storing "A-01".
- 0024 data migration: idempotent regexp_replace + post-update sanity
check that surfaces any non-conforming rows for manual triage.
- Invert normalizeLegacyMooring in dedup/migration-apply: it now
canonicalizes ("D-32" -> "D32") instead of legacy-izing.
- Update tiptap-to-pdfme example tokens, EOI fixture moorings, and
smoke-test seed moorings.
- Refresh seed-data/berths.json to canonical form; drop the now-
redundant legacyMooringNumber field.
- Delete scripts/load-berths-to-port-nimara.ts (superseded in 0c).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -48,10 +48,10 @@ interface SchemaField {
|
||||
|
||||
// ─── Constants ────────────────────────────────────────────────────────────────
|
||||
|
||||
const PAGE_WIDTH_MM = 170; // A4 content width (210 - 20mm margins each side)
|
||||
const PAGE_BREAK_THRESHOLD = 250; // y position (mm from page top) to start new logical page
|
||||
const MARGIN_X_MM = 20; // Left margin
|
||||
const MARGIN_TOP_MM = 20; // Top margin
|
||||
const PAGE_WIDTH_MM = 170; // A4 content width (210 - 20mm margins each side)
|
||||
const PAGE_BREAK_THRESHOLD = 250; // y position (mm from page top) to start new logical page
|
||||
const MARGIN_X_MM = 20; // Left margin
|
||||
const MARGIN_TOP_MM = 20; // Top margin
|
||||
|
||||
const UNSUPPORTED_NODES = new Set([
|
||||
'blockquote',
|
||||
@@ -85,8 +85,8 @@ export const TEMPLATE_VARIABLES: Array<{ key: string; label: string; example: st
|
||||
{ key: 'client.email', label: 'Client Email', example: 'john@smithholdings.com' },
|
||||
{ key: 'client.phone', label: 'Client Phone', example: '+61 400 000 000' },
|
||||
{ key: 'interest.stage', label: 'Pipeline Stage', example: 'Signed EOI/NDA' },
|
||||
{ key: 'interest.berthNumber', label: 'Berth Number (from interest)', example: 'A-23' },
|
||||
{ key: 'berth.mooring_number', label: 'Berth Number', example: 'A-23' },
|
||||
{ key: 'interest.berthNumber', label: 'Berth Number (from interest)', example: 'A23' },
|
||||
{ key: 'berth.mooring_number', label: 'Berth Number', example: 'A23' },
|
||||
{ key: 'berth.price', label: 'Berth Price', example: '$45,000' },
|
||||
{ key: 'berth.tenure_type', label: 'Tenure Type', example: 'Freehold' },
|
||||
{ key: 'port.name', label: 'Port Name', example: 'Port Nimara' },
|
||||
@@ -417,10 +417,7 @@ export function buildTemplateInputs(
|
||||
* Replaces all {{variable.key}} tokens in a string with values from the data map.
|
||||
* Unmatched tokens are left as-is.
|
||||
*/
|
||||
export function substituteVariables(
|
||||
text: string,
|
||||
data: Record<string, string>,
|
||||
): string {
|
||||
export function substituteVariables(text: string, data: Record<string, string>): string {
|
||||
return text.replace(/\{\{([^}]+)\}\}/g, (_match, key: string) => {
|
||||
return data[key.trim()] ?? _match;
|
||||
});
|
||||
@@ -468,10 +465,7 @@ export function tiptapDocumentToTemplateWithData(
|
||||
/**
|
||||
* Deeply substitutes variables in all text nodes of a TipTap document.
|
||||
*/
|
||||
function substituteInDoc(
|
||||
node: TipTapNode,
|
||||
data: Record<string, string>,
|
||||
): TipTapNode {
|
||||
function substituteInDoc(node: TipTapNode, data: Record<string, string>): TipTapNode {
|
||||
if (node.type === 'text' && node.text) {
|
||||
return { ...node, text: substituteVariables(node.text, data) };
|
||||
}
|
||||
@@ -488,10 +482,7 @@ function substituteInDoc(
|
||||
* Builds pdfme input records by extracting text content from the TipTap doc
|
||||
* and mapping it to generated field names (in schema order).
|
||||
*/
|
||||
function buildContentInputs(
|
||||
doc: TipTapNode,
|
||||
template: Template,
|
||||
): Record<string, string>[] {
|
||||
function buildContentInputs(doc: TipTapNode, template: Template): Record<string, string>[] {
|
||||
const textContents = extractAllTextContents(doc);
|
||||
const schemas = template.schemas;
|
||||
|
||||
@@ -501,9 +492,8 @@ function buildContentInputs(
|
||||
|
||||
fields.forEach((field, fieldIdx) => {
|
||||
const globalIdx =
|
||||
schemas
|
||||
.slice(0, pageIdx)
|
||||
.reduce((acc, ps) => acc + (ps as SchemaField[]).length, 0) + fieldIdx;
|
||||
schemas.slice(0, pageIdx).reduce((acc, ps) => acc + (ps as SchemaField[]).length, 0) +
|
||||
fieldIdx;
|
||||
record[field.name] = textContents[globalIdx] ?? '';
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user