task p-4.5

This commit is contained in:
phaichayon
2026-06-29 16:07:03 +07:00
parent aa73b9aed2
commit a1cac84e2b
19 changed files with 4812 additions and 413 deletions

View File

@@ -35,6 +35,7 @@ export interface ActiveTemplateVersionRecord {
fileType: string;
versionId: string;
version: string;
filePath: string | null;
schemaJson: unknown;
}
@@ -152,6 +153,7 @@ export const PDFME_STATIC_LABEL_FIELD_KEYS = [
'project_label',
'site_label'
] as const;
export type TemplateSourceVariant = 'legacy' | 'product-v1';
export const SUPPORTED_TEMPLATE_MAPPING_SEEDS: TemplateMappingSeed[] = [
{
placeholderKey: 'customer_name',
@@ -327,11 +329,18 @@ export const SUPPORTED_TEMPLATE_MAPPING_SEEDS: TemplateMappingSeed[] = [
formatMask: 'currency'
},
{
columnName: 'Total',
sourceField: 'totalPrice',
columnName: 'Discount',
sourceField: 'discount',
columnLetter: 'F',
sortOrder: 6,
formatMask: 'currency'
},
{
columnName: 'Total',
sourceField: 'totalPrice',
columnLetter: 'G',
sortOrder: 7,
formatMask: 'currency'
}
]
}
@@ -665,12 +674,23 @@ export function normalizeTemplateInputAliases(
return normalized;
}
export function loadTemplateSourceByOrganizationName(organizationName: string) {
export function loadTemplateSourceByOrganizationName(
organizationName: string,
variant: TemplateSourceVariant = 'legacy',
) {
const normalized = organizationName.toUpperCase();
const fileName = normalized.includes('ONVALLA')
? 'ONVALLA_template_pdfme_fainal3.json'
? (
variant === 'product-v1'
? 'ONVALLA_template_pdfme_product_v1.json'
: 'ONVALLA_template_pdfme_fainal3.json'
)
: normalized.includes('ALLA')
? 'ALLA_template_pdfme_fainal3.json'
? (
variant === 'product-v1'
? 'ALLA_template_pdfme_product_v1.json'
: 'ALLA_template_pdfme_fainal3.json'
)
: null;
if (!fileName) {
@@ -686,6 +706,26 @@ export function loadTemplateSourceByOrganizationName(organizationName: string) {
};
}
export function loadTemplateSourceByRelativePath(relativePath: string | null | undefined) {
if (!relativePath) {
return null;
}
const normalizedRelativePath = relativePath.replaceAll('\\', '/');
const absolutePath = path.resolve(process.cwd(), normalizedRelativePath);
if (!fs.existsSync(absolutePath)) {
return null;
}
return {
fileName: path.basename(normalizedRelativePath),
relativePath: normalizedRelativePath,
absolutePath,
schemaJson: JSON.parse(fs.readFileSync(absolutePath, 'utf8')) as unknown
};
}
export function computeFieldDiff(
currentFields: TemplateFieldRecord[],
previousFields: TemplateFieldRecord[]
@@ -739,16 +779,17 @@ function normalizeSnapshotPayload(snapshot: unknown) {
export async function getActiveTemplateVersions(sql: SqlClient): Promise<ActiveTemplateVersionRecord[]> {
const rows = await sql`
select
t.id as "templateId",
t.template_name as "templateName",
t.organization_id as "organizationId",
t.document_type as "documentType",
t.product_type as "productType",
t.file_type as "fileType",
v.id as "versionId",
v.version,
v.schema_json as "schemaJson"
select
t.id as "templateId",
t.template_name as "templateName",
t.organization_id as "organizationId",
t.document_type as "documentType",
t.product_type as "productType",
t.file_type as "fileType",
v.id as "versionId",
v.version,
v.file_path as "filePath",
v.schema_json as "schemaJson"
from crm_document_templates t
inner join crm_document_template_versions v
on v.template_id = t.id
@@ -975,6 +1016,55 @@ function formatTopicItems(items: Array<{ content?: string | null; sortOrder?: nu
.map((item) => [item.content?.trim() || '-']);
}
function formatPdfDecimal(amount: number | string | null | undefined) {
if (amount === null || amount === undefined) {
return '-';
}
const normalized =
typeof amount === 'number' ? amount : Number(String(amount).replaceAll(',', ''));
if (!Number.isFinite(normalized)) {
return '-';
}
return new Intl.NumberFormat('en-US', {
minimumFractionDigits: 0,
maximumFractionDigits: 3
}).format(normalized);
}
function buildAuditItemsTable(
items: Array<{
itemNumber?: number | null;
description?: string | null;
quantity?: number | null;
unitLabel?: string | null;
unitPrice?: number | null;
discount?: number | null;
totalPrice?: number | null;
notes?: string | null;
}>,
currencyCode?: string | null
) {
return items.map((item, index) => {
const description = [item.description?.trim(), item.notes?.trim()]
.filter((value): value is string => Boolean(value))
.join('\n')
.trim();
return [
String(item.itemNumber ?? index + 1),
description || '-',
formatPdfDecimal(item.quantity ?? null),
item.unitLabel?.trim() || '-',
formatPdfCurrency(item.unitPrice ?? null, currencyCode?.trim() || 'THB'),
formatPdfCurrency(item.discount ?? 0, currencyCode?.trim() || 'THB'),
formatPdfCurrency(item.totalPrice ?? null, currencyCode?.trim() || 'THB')
];
});
}
function applyFormatMask(value: unknown, formatMask?: string | null) {
if (value === null || value === undefined) {
return value;
@@ -1436,10 +1526,22 @@ export async function buildAuditPayload(sql: SqlClient): Promise<AuditPayload> {
),
...topicInputs
};
const normalizedTemplateInput = normalizeTemplateInputAliases(
extractTemplateFields(templateVersion.schemaJson),
templateInput
);
const templateFields = extractTemplateFields(templateVersion.schemaJson);
const hasItemsTableField = templateFields.some((field) => field.name === 'items_table');
const normalizedTemplateInput = normalizeTemplateInputAliases(
templateFields,
{
...templateInput,
...(hasItemsTableField
? {
items_table: buildAuditItemsTable(
(documentData as { items: Parameters<typeof buildAuditItemsTable>[0] }).items,
(documentData as { quotation: { currencyCode?: string | null } }).quotation.currencyCode
)
}
: {}),
}
);
return {
organizationId: quotation.organizationId,