task-4.3.1

This commit is contained in:
phaichayon
2026-06-29 11:57:37 +07:00
parent 0c0450b152
commit 94fe19c658
17 changed files with 836 additions and 174 deletions

View File

@@ -5,6 +5,7 @@ import type { ResolvedDocumentTemplate } from '@/features/foundation/document-te
import type { QuotationDocumentData } from '../types';
import { buildQuotationPdfRuntime } from './quotation-pdf-runtime';
import { resolveTemplatePages } from './page-resolver';
import { assembleTemplate } from './template-assembler';
import type { ResolvedTemplate } from './runtime-contracts';
import { adaptTemplateForSectionRuntime } from './template-compatibility-adapter';
@@ -238,3 +239,54 @@ test('runtime keeps signature block on the last generated topic page', async ()
assert.equal(fieldNames.has('app1'), false);
}
});
test('runtime restores legacy static labels into final template input', async () => {
const schema = createLegacyTemplate();
const template = createResolvedDocumentTemplate(schema);
const runtime = await buildQuotationPdfRuntime({
documentData: createDocumentData(1),
template,
mappings: [],
templateInput: {
customer_name: 'Customer',
},
});
assert.equal(runtime.assembled.templateInput.tel_label, 'Tel');
assert.equal(runtime.assembled.templateInput.email_label, 'Email');
assert.equal(runtime.assembled.templateInput.att_label, 'Att');
assert.equal(runtime.assembled.templateInput.project_label, 'Project');
assert.equal(runtime.assembled.templateInput.site_label, 'Location');
});
test('template assembler does not overwrite populated values with nullish or blank patches', () => {
const assembled = assembleTemplate({
template: createLegacyTemplate(),
templateInput: {
tel_label: 'Tel',
customer_name: 'Customer',
},
staticInputPatch: {
tel_label: '',
email_label: 'Email',
},
sections: [
{
role: 'topics',
enabled: true,
rendered: false,
anchorPageIndex: 1,
pages: [],
templateInputPatch: {
tel_label: undefined,
project_label: 'Project',
},
issues: [],
},
],
});
assert.equal(assembled.templateInput.tel_label, 'Tel');
assert.equal(assembled.templateInput.email_label, 'Email');
assert.equal(assembled.templateInput.project_label, 'Project');
});

View File

@@ -7,6 +7,7 @@ import type { QuotationDocumentData } from '../types';
import { buildPdfTopicPages } from './pdf-topic-engine';
import { resolveTemplatePages } from './page-resolver';
import { createRenderContext, resolveRenderPolicies } from './render-context';
import { resolveQuotationStaticTemplateInputs } from './static-input-resolver';
import type {
BuiltSection,
QuotationPdfRuntimeResult,
@@ -129,6 +130,7 @@ export async function buildQuotationPdfRuntime(args: {
const compatibleTemplate = adaptTemplateForSectionRuntime(resolvedTemplate);
const pages = resolveTemplatePages(compatibleTemplate);
const policies = resolveRenderPolicies();
const staticInputPatch = resolveQuotationStaticTemplateInputs(args.documentData);
const context = createRenderContext({
documentData: args.documentData,
template: resolvedTemplate,
@@ -141,6 +143,7 @@ export async function buildQuotationPdfRuntime(args: {
const assembled = assembleTemplate({
template: resolvedTemplate.schema,
templateInput: args.templateInput,
staticInputPatch,
sections: composed.sections,
});
const issues = [...pages.issues, ...composed.issues, ...assembled.issues];

View File

@@ -0,0 +1,50 @@
import type { QuotationDocumentData } from '../types';
const LEGACY_STATIC_LABEL_DEFAULTS = {
tel_label: 'Tel',
email_label: 'Email',
att_label: 'Att',
project_label: 'Project',
site_label: 'Location',
} as const;
function normalizeStringValue(
value: string | null | undefined,
fallback: string,
): string {
if (typeof value !== 'string') {
return fallback;
}
const trimmed = value.trim();
return trimmed.length > 0 ? trimmed : fallback;
}
export function resolveQuotationStaticTemplateInputs(
documentData: QuotationDocumentData,
): Record<string, unknown> {
const labels = documentData.pdfme.labels;
return {
tel_label: normalizeStringValue(
labels.tel,
LEGACY_STATIC_LABEL_DEFAULTS.tel_label,
),
email_label: normalizeStringValue(
labels.email,
LEGACY_STATIC_LABEL_DEFAULTS.email_label,
),
att_label: normalizeStringValue(
labels.att,
LEGACY_STATIC_LABEL_DEFAULTS.att_label,
),
project_label: normalizeStringValue(
labels.project,
LEGACY_STATIC_LABEL_DEFAULTS.project_label,
),
site_label: normalizeStringValue(
labels.location,
LEGACY_STATIC_LABEL_DEFAULTS.site_label,
),
};
}

View File

@@ -9,9 +9,29 @@ function cloneTemplate<T>(value: T): T {
return JSON.parse(JSON.stringify(value)) as T;
}
function mergeTemplateInputPatch(
target: Record<string, unknown>,
patch: Record<string, unknown>,
): Record<string, unknown> {
for (const [key, value] of Object.entries(patch)) {
if (value === null || value === undefined) {
continue;
}
if (typeof value === 'string' && value.trim().length === 0) {
continue;
}
target[key] = value;
}
return target;
}
export function assembleTemplate(args: {
template: Template;
templateInput: Record<string, unknown>;
staticInputPatch?: Record<string, unknown>;
sections: BuiltSection[];
}): AssembledTemplate {
const baseTemplate = cloneTemplate(args.template);
@@ -62,13 +82,15 @@ export function assembleTemplate(args: {
baseTemplate.schemas = nextPages;
const templateInput = args.sections.reduce<Record<string, unknown>>(
(accumulator, section) => ({
...accumulator,
...section.templateInputPatch,
}),
{ ...args.templateInput },
);
const templateInput = mergeTemplateInputPatch({}, args.templateInput);
if (args.staticInputPatch) {
mergeTemplateInputPatch(templateInput, args.staticInputPatch);
}
for (const section of args.sections) {
mergeTemplateInputPatch(templateInput, section.templateInputPatch);
}
return {
template: baseTemplate,