commit
This commit is contained in:
@@ -49,6 +49,7 @@
|
|||||||
"audit:pdf:visual": "node --experimental-strip-types scripts/audit-pdf-visual-regression.ts",
|
"audit:pdf:visual": "node --experimental-strip-types scripts/audit-pdf-visual-regression.ts",
|
||||||
"audit:pdf:visual:baseline": "node --experimental-strip-types scripts/audit-pdf-visual-regression.ts --refresh-baseline",
|
"audit:pdf:visual:baseline": "node --experimental-strip-types scripts/audit-pdf-visual-regression.ts --refresh-baseline",
|
||||||
"audit:pdf": "npm run audit:pdf:inventory && npm run audit:pdf:coverage && npm run audit:pdf:runtime && npm run audit:pdf:versions && npm run audit:pdf:placeholders && npm run audit:pdf:parity && npm run audit:pdf:snapshot && npm run audit:pdf:report && npm run audit:pdf:integrity",
|
"audit:pdf": "npm run audit:pdf:inventory && npm run audit:pdf:coverage && npm run audit:pdf:runtime && npm run audit:pdf:versions && npm run audit:pdf:placeholders && npm run audit:pdf:parity && npm run audit:pdf:snapshot && npm run audit:pdf:report && npm run audit:pdf:integrity",
|
||||||
|
"pdf:seed:product": "node --disable-warning=ExperimentalWarning --disable-warning=MODULE_TYPELESS_PACKAGE_JSON --experimental-strip-types scripts/reseed-pdf-template-version.ts --template-variant=product-v1",
|
||||||
"pdf:activate:product": "node --experimental-strip-types scripts/reseed-pdf-template-version.ts --template-variant=product-v1 --activate"
|
"pdf:activate:product": "node --experimental-strip-types scripts/reseed-pdf-template-version.ts --template-variant=product-v1 --activate"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
|||||||
@@ -311,6 +311,7 @@ async function main() {
|
|||||||
matchingVersion,
|
matchingVersion,
|
||||||
});
|
});
|
||||||
const targetVersionId = matchingVersion?.id ?? randomUUID();
|
const targetVersionId = matchingVersion?.id ?? randomUUID();
|
||||||
|
const isProductVariant = options.variant === 'product-v1';
|
||||||
|
|
||||||
await sql.begin(async (tx) => {
|
await sql.begin(async (tx) => {
|
||||||
if (!matchingVersion) {
|
if (!matchingVersion) {
|
||||||
@@ -322,6 +323,7 @@ async function main() {
|
|||||||
version,
|
version,
|
||||||
file_path,
|
file_path,
|
||||||
schema_json,
|
schema_json,
|
||||||
|
metadata_json,
|
||||||
preview_image_url,
|
preview_image_url,
|
||||||
is_active,
|
is_active,
|
||||||
deleted_at,
|
deleted_at,
|
||||||
@@ -333,6 +335,10 @@ async function main() {
|
|||||||
${targetVersionLabel},
|
${targetVersionLabel},
|
||||||
${templateSource.relativePath},
|
${templateSource.relativePath},
|
||||||
${JSON.stringify(targetSchema)},
|
${JSON.stringify(targetSchema)},
|
||||||
|
case
|
||||||
|
when ${isProductVariant} then jsonb_build_object('templateVariant', 'product-v1')
|
||||||
|
else null
|
||||||
|
end,
|
||||||
${null},
|
${null},
|
||||||
${options.activate},
|
${options.activate},
|
||||||
${null},
|
${null},
|
||||||
@@ -344,6 +350,11 @@ async function main() {
|
|||||||
update crm_document_template_versions
|
update crm_document_template_versions
|
||||||
set
|
set
|
||||||
file_path = ${templateSource.relativePath},
|
file_path = ${templateSource.relativePath},
|
||||||
|
metadata_json = case
|
||||||
|
when ${isProductVariant}
|
||||||
|
then coalesce(metadata_json, '{}'::jsonb) || jsonb_build_object('templateVariant', 'product-v1')
|
||||||
|
else metadata_json
|
||||||
|
end,
|
||||||
is_active = ${options.activate ? true : matchingVersion.isActive},
|
is_active = ${options.activate ? true : matchingVersion.isActive},
|
||||||
updated_at = now()
|
updated_at = now()
|
||||||
where id = ${targetVersionId}
|
where id = ${targetVersionId}
|
||||||
|
|||||||
@@ -12,9 +12,15 @@ type Params = {
|
|||||||
params: Promise<{ id: string }>;
|
params: Promise<{ id: string }>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export async function GET(_request: NextRequest, { params }: Params) {
|
export async function GET(request: NextRequest, { params }: Params) {
|
||||||
try {
|
try {
|
||||||
const { id } = await params;
|
const { id } = await params;
|
||||||
|
const templateVariant = request.nextUrl.searchParams.get('templateVariant');
|
||||||
|
|
||||||
|
if (templateVariant && templateVariant !== 'product-v1') {
|
||||||
|
return NextResponse.json({ message: 'Unsupported template variant' }, { status: 400 });
|
||||||
|
}
|
||||||
|
|
||||||
const { organization, session, membership } = await requireOrganizationAccess({
|
const { organization, session, membership } = await requireOrganizationAccess({
|
||||||
permission: PERMISSIONS.crmQuotationPdfPreview
|
permission: PERMISSIONS.crmQuotationPdfPreview
|
||||||
});
|
});
|
||||||
@@ -36,7 +42,9 @@ export async function GET(_request: NextRequest, { params }: Params) {
|
|||||||
return NextResponse.json({ message: 'Forbidden' }, { status: 403 });
|
return NextResponse.json({ message: 'Forbidden' }, { status: 403 });
|
||||||
}
|
}
|
||||||
|
|
||||||
const pdf = await generateQuotationPreviewPdf(id, organization.id);
|
const pdf = await generateQuotationPreviewPdf(id, organization.id, {
|
||||||
|
templateVariant
|
||||||
|
});
|
||||||
|
|
||||||
return new NextResponse(pdf.buffer, {
|
return new NextResponse(pdf.buffer, {
|
||||||
headers: {
|
headers: {
|
||||||
|
|||||||
@@ -33,6 +33,8 @@ import type { OpportunityMutationPayload, OpportunityRecord, OpportunityReferenc
|
|||||||
import { opportunitySchema, type OpportunityFormValues } from '../schemas/opportunity.schema';
|
import { opportunitySchema, type OpportunityFormValues } from '../schemas/opportunity.schema';
|
||||||
import { isHotProjectSuggested } from '@/features/crm/shared/hot-project';
|
import { isHotProjectSuggested } from '@/features/crm/shared/hot-project';
|
||||||
|
|
||||||
|
const CHANCE_PERCENT_OPTIONS = [10, 25, 50, 75, 85] as const;
|
||||||
|
|
||||||
function toDefaultValues(
|
function toDefaultValues(
|
||||||
opportunity: OpportunityRecord | undefined,
|
opportunity: OpportunityRecord | undefined,
|
||||||
referenceData: OpportunityReferenceData
|
referenceData: OpportunityReferenceData
|
||||||
@@ -93,8 +95,7 @@ export function OpportunityFormSheet({
|
|||||||
FormSelectField,
|
FormSelectField,
|
||||||
FormSwitchField,
|
FormSwitchField,
|
||||||
FormDatePickerField,
|
FormDatePickerField,
|
||||||
FormCurrencyField,
|
FormCurrencyField
|
||||||
FormPercentageField
|
|
||||||
} = useFormFields<OpportunityFormValues>();
|
} = useFormFields<OpportunityFormValues>();
|
||||||
const projectPartiesQuery = useQuery({
|
const projectPartiesQuery = useQuery({
|
||||||
...opportunityProjectPartiesOptions(opportunity?.id ?? ''),
|
...opportunityProjectPartiesOptions(opportunity?.id ?? ''),
|
||||||
@@ -395,10 +396,38 @@ form.setFieldValue('isHotProject', suggested);
|
|||||||
currencyLabel='THB'
|
currencyLabel='THB'
|
||||||
placeholder='2500000'
|
placeholder='2500000'
|
||||||
/>
|
/>
|
||||||
<FormPercentageField
|
<form.AppField
|
||||||
name='chancePercent'
|
name='chancePercent'
|
||||||
label='โอกาสปิดการขาย %'
|
children={(field) => (
|
||||||
placeholder='60'
|
<field.FieldSet>
|
||||||
|
<field.Field>
|
||||||
|
<field.FieldLabel>โอกาสปิดการขาย %</field.FieldLabel>
|
||||||
|
<Select
|
||||||
|
value={
|
||||||
|
typeof field.state.value === 'number'
|
||||||
|
? String(field.state.value)
|
||||||
|
: ''
|
||||||
|
}
|
||||||
|
onValueChange={(value) => {
|
||||||
|
field.handleChange(value ? Number(value) : undefined);
|
||||||
|
field.handleBlur();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<SelectTrigger aria-label='Chance percentage'>
|
||||||
|
<SelectValue placeholder='เลือก Chance %' />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
{CHANCE_PERCENT_OPTIONS.map((option) => (
|
||||||
|
<SelectItem key={option} value={String(option)}>
|
||||||
|
{option}%
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</field.Field>
|
||||||
|
<field.FieldError />
|
||||||
|
</field.FieldSet>
|
||||||
|
)}
|
||||||
/>
|
/>
|
||||||
<FormDatePickerField
|
<FormDatePickerField
|
||||||
name='expectedCloseDate'
|
name='expectedCloseDate'
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1,72 +1,64 @@
|
|||||||
import { and, eq, inArray, isNull } from "drizzle-orm";
|
import { and, eq, inArray, isNull } from 'drizzle-orm';
|
||||||
import {
|
import { crmCustomerContacts, crmCustomers, crmQuotations, organizations } from '@/db/schema';
|
||||||
crmCustomerContacts,
|
import { db } from '@/lib/db';
|
||||||
crmCustomers,
|
import { AuthError } from '@/lib/auth/session';
|
||||||
crmQuotations,
|
|
||||||
organizations,
|
|
||||||
} from "@/db/schema";
|
|
||||||
import { db } from "@/lib/db";
|
|
||||||
import { AuthError } from "@/lib/auth/session";
|
|
||||||
import {
|
import {
|
||||||
listApprovalRequests,
|
listApprovalRequests,
|
||||||
getApprovalRequest,
|
getApprovalRequest
|
||||||
} from "@/features/foundation/approval/server/service";
|
} from '@/features/foundation/approval/server/service';
|
||||||
import {
|
import {
|
||||||
mapDocumentDataToTemplateInput,
|
mapDocumentDataToTemplateInput,
|
||||||
resolveTemplateForDocument,
|
resolveTemplateForDocument,
|
||||||
resolveTemplateMappings,
|
resolveTemplateMappings
|
||||||
} from "@/features/foundation/document-template/server/service";
|
} from '@/features/foundation/document-template/server/service';
|
||||||
import { getActiveOptionsByCategory } from "@/features/foundation/master-options/service";
|
import { getActiveOptionsByCategory } from '@/features/foundation/master-options/service';
|
||||||
import {
|
import {
|
||||||
getQuotationDetail,
|
getQuotationDetail,
|
||||||
listQuotationCustomers,
|
listQuotationCustomers,
|
||||||
listQuotationItems,
|
listQuotationItems,
|
||||||
listQuotationTopics,
|
listQuotationTopics
|
||||||
} from "@/features/crm/quotations/server/service";
|
} from '@/features/crm/quotations/server/service';
|
||||||
import type {
|
import type {
|
||||||
ApprovedQuotationSnapshot,
|
ApprovedQuotationSnapshot,
|
||||||
QuotationDocumentApprovalStep,
|
QuotationDocumentApprovalStep,
|
||||||
QuotationDocumentData,
|
QuotationDocumentData
|
||||||
} from "../types";
|
} from '../types';
|
||||||
import type { PdfTopic } from "./pdf-topic.type";
|
import type { PdfTopic } from './pdf-topic.type';
|
||||||
import {
|
import {
|
||||||
buildPdfPriceTableData,
|
buildPdfPriceTableData,
|
||||||
formatPdfCurrency,
|
formatPdfCurrency,
|
||||||
formatPdfDate,
|
formatPdfDate,
|
||||||
formatTopicItems,
|
formatTopicItems
|
||||||
} from "./pdfme-transforms";
|
} from './pdfme-transforms';
|
||||||
import { buildQuotationPdfRuntime } from "./quotation-pdf-runtime";
|
import { buildQuotationPdfRuntime } from './quotation-pdf-runtime';
|
||||||
import { resolveQuotationSignatures } from "./signature-resolver";
|
import { resolveQuotationSignatures } from './signature-resolver';
|
||||||
import { resolveQuotationTopicTypeMapping } from "./topic-mapping";
|
import { resolveQuotationTopicTypeMapping } from './topic-mapping';
|
||||||
|
|
||||||
const DOCUMENT_OPTION_CATEGORIES = {
|
const DOCUMENT_OPTION_CATEGORIES = {
|
||||||
branch: "crm_branch",
|
branch: 'crm_branch',
|
||||||
status: "crm_quotation_status",
|
status: 'crm_quotation_status',
|
||||||
quotationType: "crm_quotation_type",
|
quotationType: 'crm_quotation_type',
|
||||||
currency: "crm_currency",
|
currency: 'crm_currency',
|
||||||
discountType: "crm_discount_type",
|
discountType: 'crm_discount_type',
|
||||||
unit: "crm_unit",
|
unit: 'crm_unit',
|
||||||
customerRole: "crm_project_party_role",
|
customerRole: 'crm_project_party_role',
|
||||||
productType: "crm_product_type",
|
productType: 'crm_product_type',
|
||||||
topicType: "crm_quotation_topic_type",
|
topicType: 'crm_quotation_topic_type'
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
const PDFME_DEFAULT_LABELS = {
|
const PDFME_DEFAULT_LABELS = {
|
||||||
tel: "Tel",
|
tel: 'Tel',
|
||||||
email: "Email",
|
email: 'Email',
|
||||||
att: "Att",
|
att: 'Att',
|
||||||
project: "Project",
|
project: 'Project',
|
||||||
location: "Location",
|
location: 'Location'
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
function toRevisionLabel(revision: number) {
|
function toRevisionLabel(revision: number) {
|
||||||
return `R${String(revision).padStart(2, "0")}`;
|
return `R${String(revision).padStart(2, '0')}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function assertQuotationDocumentAccess(
|
async function assertQuotationDocumentAccess(quotationId: string, organizationId: string) {
|
||||||
quotationId: string,
|
|
||||||
organizationId: string,
|
|
||||||
) {
|
|
||||||
const [quotation] = await db
|
const [quotation] = await db
|
||||||
.select()
|
.select()
|
||||||
.from(crmQuotations)
|
.from(crmQuotations)
|
||||||
@@ -74,13 +66,13 @@ async function assertQuotationDocumentAccess(
|
|||||||
and(
|
and(
|
||||||
eq(crmQuotations.id, quotationId),
|
eq(crmQuotations.id, quotationId),
|
||||||
eq(crmQuotations.organizationId, organizationId),
|
eq(crmQuotations.organizationId, organizationId),
|
||||||
isNull(crmQuotations.deletedAt),
|
isNull(crmQuotations.deletedAt)
|
||||||
),
|
)
|
||||||
)
|
)
|
||||||
.limit(1);
|
.limit(1);
|
||||||
|
|
||||||
if (!quotation) {
|
if (!quotation) {
|
||||||
throw new AuthError("Quotation not found", 404);
|
throw new AuthError('Quotation not found', 404);
|
||||||
}
|
}
|
||||||
|
|
||||||
return quotation;
|
return quotation;
|
||||||
@@ -88,16 +80,14 @@ async function assertQuotationDocumentAccess(
|
|||||||
|
|
||||||
function findOptionLabel(
|
function findOptionLabel(
|
||||||
options: Array<{ id: string; code: string; label: string }>,
|
options: Array<{ id: string; code: string; label: string }>,
|
||||||
id: string | null | undefined,
|
id: string | null | undefined
|
||||||
) {
|
) {
|
||||||
return id
|
return id ? (options.find((option) => option.id === id)?.label ?? null) : null;
|
||||||
? (options.find((option) => option.id === id)?.label ?? null)
|
|
||||||
: null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function findOptionCode(
|
function findOptionCode(
|
||||||
options: Array<{ id: string; code: string; label: string }>,
|
options: Array<{ id: string; code: string; label: string }>,
|
||||||
id: string | null | undefined,
|
id: string | null | undefined
|
||||||
) {
|
) {
|
||||||
return id ? (options.find((option) => option.id === id)?.code ?? null) : null;
|
return id ? (options.find((option) => option.id === id)?.code ?? null) : null;
|
||||||
}
|
}
|
||||||
@@ -111,10 +101,10 @@ function extractSinglePlaceholder(value: string) {
|
|||||||
|
|
||||||
function getFieldName(field: unknown) {
|
function getFieldName(field: unknown) {
|
||||||
return field &&
|
return field &&
|
||||||
typeof field === "object" &&
|
typeof field === 'object' &&
|
||||||
typeof (field as { name?: unknown }).name === "string"
|
typeof (field as { name?: unknown }).name === 'string'
|
||||||
? ((field as { name: string }).name ?? "")
|
? ((field as { name: string }).name ?? '')
|
||||||
: "";
|
: '';
|
||||||
}
|
}
|
||||||
|
|
||||||
function normalizeTopicKey(value: string | null | undefined) {
|
function normalizeTopicKey(value: string | null | undefined) {
|
||||||
@@ -122,14 +112,11 @@ function normalizeTopicKey(value: string | null | undefined) {
|
|||||||
value
|
value
|
||||||
?.trim()
|
?.trim()
|
||||||
.toLowerCase()
|
.toLowerCase()
|
||||||
.replaceAll(/[\s_-]+/g, "") ?? ""
|
.replaceAll(/[\s_-]+/g, '') ?? ''
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function matchesTopicAlias(
|
function matchesTopicAlias(actualCode: string | null | undefined, expectedCode: string) {
|
||||||
actualCode: string | null | undefined,
|
|
||||||
expectedCode: string,
|
|
||||||
) {
|
|
||||||
const actual = normalizeTopicKey(actualCode);
|
const actual = normalizeTopicKey(actualCode);
|
||||||
const expected = normalizeTopicKey(expectedCode);
|
const expected = normalizeTopicKey(expectedCode);
|
||||||
|
|
||||||
@@ -142,33 +129,30 @@ function matchesTopicAlias(
|
|||||||
}
|
}
|
||||||
|
|
||||||
const aliasGroups = [
|
const aliasGroups = [
|
||||||
["scope", "scopeofwork", "dockscopeofwork", "solarcellscopeofwork"],
|
['scope', 'scopeofwork', 'dockscopeofwork', 'solarcellscopeofwork'],
|
||||||
[
|
[
|
||||||
"exclusion",
|
'exclusion',
|
||||||
"exclusionfromscopeofsupply",
|
'exclusionfromscopeofsupply',
|
||||||
"exclusionofsupplyinstallation",
|
'exclusionofsupplyinstallation',
|
||||||
"solarcellexclusionofsupplyinstallation",
|
'solarcellexclusionofsupplyinstallation'
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
"payment",
|
'payment',
|
||||||
"paymentconditions",
|
'paymentconditions',
|
||||||
"termsofpaymentuponprogressofeachitem",
|
'termsofpaymentuponprogressofeachitem',
|
||||||
"solarcellpaymentconditions",
|
'solarcellpaymentconditions'
|
||||||
],
|
],
|
||||||
["delivery", "deliverydate", "solarcelldelivery"],
|
['delivery', 'deliverydate', 'solarcelldelivery'],
|
||||||
["warranty", "solarcellwarranty"],
|
['warranty', 'solarcellwarranty']
|
||||||
];
|
];
|
||||||
|
|
||||||
return aliasGroups.some(
|
return aliasGroups.some((group) => group.includes(actual) && group.includes(expected));
|
||||||
(group) => group.includes(actual) && group.includes(expected),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function sortBySortOrder<T extends { sortOrder?: number | null }>(items: T[]) {
|
function sortBySortOrder<T extends { sortOrder?: number | null }>(items: T[]) {
|
||||||
return [...items].sort((left, right) => {
|
return [...items].sort((left, right) => {
|
||||||
const delta =
|
const delta =
|
||||||
(left.sortOrder ?? Number.MAX_SAFE_INTEGER) -
|
(left.sortOrder ?? Number.MAX_SAFE_INTEGER) - (right.sortOrder ?? Number.MAX_SAFE_INTEGER);
|
||||||
(right.sortOrder ?? Number.MAX_SAFE_INTEGER);
|
|
||||||
|
|
||||||
if (delta !== 0) {
|
if (delta !== 0) {
|
||||||
return delta;
|
return delta;
|
||||||
@@ -178,12 +162,9 @@ function sortBySortOrder<T extends { sortOrder?: number | null }>(items: T[]) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function normalizePdfmeTemplateInput(
|
function normalizePdfmeTemplateInput(schemaJson: unknown, templateInput: Record<string, unknown>) {
|
||||||
schemaJson: unknown,
|
|
||||||
templateInput: Record<string, unknown>,
|
|
||||||
) {
|
|
||||||
const pages =
|
const pages =
|
||||||
schemaJson && typeof schemaJson === "object" && "schemas" in schemaJson
|
schemaJson && typeof schemaJson === 'object' && 'schemas' in schemaJson
|
||||||
? (schemaJson as { schemas?: unknown }).schemas
|
? (schemaJson as { schemas?: unknown }).schemas
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
@@ -199,37 +180,28 @@ function normalizePdfmeTemplateInput(
|
|||||||
}
|
}
|
||||||
|
|
||||||
for (const field of page) {
|
for (const field of page) {
|
||||||
if (!field || typeof field !== "object") {
|
if (!field || typeof field !== 'object') {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
const tableField = field as { type?: unknown; content?: unknown };
|
const tableField = field as { type?: unknown; content?: unknown };
|
||||||
const fieldName = getFieldName(field);
|
const fieldName = getFieldName(field);
|
||||||
const placeholderKey =
|
const placeholderKey =
|
||||||
typeof tableField.content === "string"
|
typeof tableField.content === 'string'
|
||||||
? extractSinglePlaceholder(tableField.content)
|
? extractSinglePlaceholder(tableField.content)
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
if (fieldName && placeholderKey && fieldName !== placeholderKey) {
|
if (fieldName && placeholderKey && fieldName !== placeholderKey) {
|
||||||
if (
|
if (normalized[fieldName] !== undefined && normalized[placeholderKey] === undefined) {
|
||||||
normalized[fieldName] !== undefined &&
|
|
||||||
normalized[placeholderKey] === undefined
|
|
||||||
) {
|
|
||||||
normalized[placeholderKey] = normalized[fieldName];
|
normalized[placeholderKey] = normalized[fieldName];
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (normalized[placeholderKey] !== undefined && normalized[fieldName] === undefined) {
|
||||||
normalized[placeholderKey] !== undefined &&
|
|
||||||
normalized[fieldName] === undefined
|
|
||||||
) {
|
|
||||||
normalized[fieldName] = normalized[placeholderKey];
|
normalized[fieldName] = normalized[placeholderKey];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (tableField.type !== 'table' || typeof tableField.content !== 'string') {
|
||||||
tableField.type !== "table" ||
|
|
||||||
typeof tableField.content !== "string"
|
|
||||||
) {
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -239,12 +211,12 @@ function normalizePdfmeTemplateInput(
|
|||||||
|
|
||||||
const value = normalized[placeholderKey];
|
const value = normalized[placeholderKey];
|
||||||
|
|
||||||
if (typeof value !== "string") {
|
if (typeof value !== 'string') {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
const lines = value
|
const lines = value
|
||||||
.split("\n")
|
.split('\n')
|
||||||
.map((line) => line.trim())
|
.map((line) => line.trim())
|
||||||
.filter(Boolean);
|
.filter(Boolean);
|
||||||
|
|
||||||
@@ -264,20 +236,14 @@ function logQuotationPdfRuntimeWarnings(issues: Array<{ message: string }>) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
console.warn(
|
console.warn('[quotation-pdf-runtime]', issues.map((issue) => issue.message).join(' | '));
|
||||||
"[quotation-pdf-runtime]",
|
|
||||||
issues.map((issue) => issue.message).join(" | "),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function buildQuotationDocumentData(
|
export async function buildQuotationDocumentData(
|
||||||
quotationId: string,
|
quotationId: string,
|
||||||
organizationId: string,
|
organizationId: string
|
||||||
): Promise<QuotationDocumentData> {
|
): Promise<QuotationDocumentData> {
|
||||||
const quotation = await assertQuotationDocumentAccess(
|
const quotation = await assertQuotationDocumentAccess(quotationId, organizationId);
|
||||||
quotationId,
|
|
||||||
organizationId,
|
|
||||||
);
|
|
||||||
const [
|
const [
|
||||||
company,
|
company,
|
||||||
quotationDetail,
|
quotationDetail,
|
||||||
@@ -293,7 +259,7 @@ export async function buildQuotationDocumentData(
|
|||||||
customerRoleOptions,
|
customerRoleOptions,
|
||||||
productTypeOptions,
|
productTypeOptions,
|
||||||
topicTypeOptions,
|
topicTypeOptions,
|
||||||
approvalRequests,
|
approvalRequests
|
||||||
] = await Promise.all([
|
] = await Promise.all([
|
||||||
db
|
db
|
||||||
.select()
|
.select()
|
||||||
@@ -305,41 +271,41 @@ export async function buildQuotationDocumentData(
|
|||||||
listQuotationCustomers(quotationId, organizationId),
|
listQuotationCustomers(quotationId, organizationId),
|
||||||
listQuotationTopics(quotationId, organizationId),
|
listQuotationTopics(quotationId, organizationId),
|
||||||
getActiveOptionsByCategory(DOCUMENT_OPTION_CATEGORIES.branch, {
|
getActiveOptionsByCategory(DOCUMENT_OPTION_CATEGORIES.branch, {
|
||||||
organizationId,
|
organizationId
|
||||||
}),
|
}),
|
||||||
getActiveOptionsByCategory(DOCUMENT_OPTION_CATEGORIES.status, {
|
getActiveOptionsByCategory(DOCUMENT_OPTION_CATEGORIES.status, {
|
||||||
organizationId,
|
organizationId
|
||||||
}),
|
}),
|
||||||
getActiveOptionsByCategory(DOCUMENT_OPTION_CATEGORIES.quotationType, {
|
getActiveOptionsByCategory(DOCUMENT_OPTION_CATEGORIES.quotationType, {
|
||||||
organizationId,
|
organizationId
|
||||||
}),
|
}),
|
||||||
getActiveOptionsByCategory(DOCUMENT_OPTION_CATEGORIES.currency, {
|
getActiveOptionsByCategory(DOCUMENT_OPTION_CATEGORIES.currency, {
|
||||||
organizationId,
|
organizationId
|
||||||
}),
|
}),
|
||||||
getActiveOptionsByCategory(DOCUMENT_OPTION_CATEGORIES.discountType, {
|
getActiveOptionsByCategory(DOCUMENT_OPTION_CATEGORIES.discountType, {
|
||||||
organizationId,
|
organizationId
|
||||||
}),
|
}),
|
||||||
getActiveOptionsByCategory(DOCUMENT_OPTION_CATEGORIES.unit, {
|
getActiveOptionsByCategory(DOCUMENT_OPTION_CATEGORIES.unit, {
|
||||||
organizationId,
|
organizationId
|
||||||
}),
|
}),
|
||||||
getActiveOptionsByCategory(DOCUMENT_OPTION_CATEGORIES.customerRole, {
|
getActiveOptionsByCategory(DOCUMENT_OPTION_CATEGORIES.customerRole, {
|
||||||
organizationId,
|
organizationId
|
||||||
}),
|
}),
|
||||||
getActiveOptionsByCategory(DOCUMENT_OPTION_CATEGORIES.productType, {
|
getActiveOptionsByCategory(DOCUMENT_OPTION_CATEGORIES.productType, {
|
||||||
organizationId,
|
organizationId
|
||||||
}),
|
}),
|
||||||
getActiveOptionsByCategory(DOCUMENT_OPTION_CATEGORIES.topicType, {
|
getActiveOptionsByCategory(DOCUMENT_OPTION_CATEGORIES.topicType, {
|
||||||
organizationId,
|
organizationId
|
||||||
}),
|
}),
|
||||||
listApprovalRequests(organizationId, {
|
listApprovalRequests(organizationId, {
|
||||||
entityType: "quotation",
|
entityType: 'quotation',
|
||||||
entityId: quotationId,
|
entityId: quotationId,
|
||||||
limit: 20,
|
limit: 20
|
||||||
}),
|
})
|
||||||
]);
|
]);
|
||||||
|
|
||||||
if (!company) {
|
if (!company) {
|
||||||
throw new AuthError("Organization not found", 404);
|
throw new AuthError('Organization not found', 404);
|
||||||
}
|
}
|
||||||
|
|
||||||
const [customer, contact, relatedCustomers, signatures] = await Promise.all([
|
const [customer, contact, relatedCustomers, signatures] = await Promise.all([
|
||||||
@@ -350,8 +316,8 @@ export async function buildQuotationDocumentData(
|
|||||||
and(
|
and(
|
||||||
eq(crmCustomers.id, quotation.customerId),
|
eq(crmCustomers.id, quotation.customerId),
|
||||||
eq(crmCustomers.organizationId, organizationId),
|
eq(crmCustomers.organizationId, organizationId),
|
||||||
isNull(crmCustomers.deletedAt),
|
isNull(crmCustomers.deletedAt)
|
||||||
),
|
)
|
||||||
)
|
)
|
||||||
.then((rows) => rows[0] ?? null),
|
.then((rows) => rows[0] ?? null),
|
||||||
quotation.contactId
|
quotation.contactId
|
||||||
@@ -362,8 +328,8 @@ export async function buildQuotationDocumentData(
|
|||||||
and(
|
and(
|
||||||
eq(crmCustomerContacts.id, quotation.contactId),
|
eq(crmCustomerContacts.id, quotation.contactId),
|
||||||
eq(crmCustomerContacts.organizationId, organizationId),
|
eq(crmCustomerContacts.organizationId, organizationId),
|
||||||
isNull(crmCustomerContacts.deletedAt),
|
isNull(crmCustomerContacts.deletedAt)
|
||||||
),
|
)
|
||||||
)
|
)
|
||||||
.then((rows) => rows[0] ?? null)
|
.then((rows) => rows[0] ?? null)
|
||||||
: Promise.resolve(null),
|
: Promise.resolve(null),
|
||||||
@@ -372,7 +338,7 @@ export async function buildQuotationDocumentData(
|
|||||||
.select({
|
.select({
|
||||||
id: crmCustomers.id,
|
id: crmCustomers.id,
|
||||||
code: crmCustomers.code,
|
code: crmCustomers.code,
|
||||||
name: crmCustomers.name,
|
name: crmCustomers.name
|
||||||
})
|
})
|
||||||
.from(crmCustomers)
|
.from(crmCustomers)
|
||||||
.where(
|
.where(
|
||||||
@@ -380,39 +346,33 @@ export async function buildQuotationDocumentData(
|
|||||||
eq(crmCustomers.organizationId, organizationId),
|
eq(crmCustomers.organizationId, organizationId),
|
||||||
inArray(
|
inArray(
|
||||||
crmCustomers.id,
|
crmCustomers.id,
|
||||||
quotationCustomers.map((item) => item.customerId),
|
quotationCustomers.map((item) => item.customerId)
|
||||||
),
|
),
|
||||||
isNull(crmCustomers.deletedAt),
|
isNull(crmCustomers.deletedAt)
|
||||||
),
|
)
|
||||||
)
|
)
|
||||||
: Promise.resolve([]),
|
: Promise.resolve([]),
|
||||||
resolveQuotationSignatures(quotationId, organizationId),
|
resolveQuotationSignatures(quotationId, organizationId)
|
||||||
]);
|
]);
|
||||||
|
|
||||||
if (!customer) {
|
if (!customer) {
|
||||||
throw new AuthError("Customer not found", 404);
|
throw new AuthError('Customer not found', 404);
|
||||||
}
|
}
|
||||||
|
|
||||||
const approvalRequest =
|
const approvalRequest =
|
||||||
approvalRequests.items.find((item) => item.status === "approved") ??
|
approvalRequests.items.find((item) => item.status === 'approved') ??
|
||||||
approvalRequests.items[0] ??
|
approvalRequests.items[0] ??
|
||||||
null;
|
null;
|
||||||
const approvalDetail = approvalRequest
|
const approvalDetail = approvalRequest
|
||||||
? await getApprovalRequest(approvalRequest.id, organizationId)
|
? await getApprovalRequest(approvalRequest.id, organizationId)
|
||||||
: null;
|
: null;
|
||||||
const branch =
|
const branch = branchOptions.find((option) => option.id === quotation.branchId) ?? null;
|
||||||
branchOptions.find((option) => option.id === quotation.branchId) ?? null;
|
const relatedCustomerMap = new Map(relatedCustomers.map((item) => [item.id, item]));
|
||||||
const relatedCustomerMap = new Map(
|
|
||||||
relatedCustomers.map((item) => [item.id, item]),
|
|
||||||
);
|
|
||||||
const statusCode = findOptionCode(statusOptions, quotation.status);
|
const statusCode = findOptionCode(statusOptions, quotation.status);
|
||||||
const statusLabel = findOptionLabel(statusOptions, quotation.status);
|
const statusLabel = findOptionLabel(statusOptions, quotation.status);
|
||||||
const topicCodeById = new Map(
|
const topicCodeById = new Map(topicTypeOptions.map((option) => [option.id, option.code]));
|
||||||
topicTypeOptions.map((option) => [option.id, option.code]),
|
|
||||||
);
|
|
||||||
const primaryProductType =
|
const primaryProductType =
|
||||||
items.find((item) => findOptionCode(productTypeOptions, item.productType))
|
items.find((item) => findOptionCode(productTypeOptions, item.productType))?.productType ?? null;
|
||||||
?.productType ?? null;
|
|
||||||
const productTypeCode =
|
const productTypeCode =
|
||||||
findOptionCode(productTypeOptions, primaryProductType) ??
|
findOptionCode(productTypeOptions, primaryProductType) ??
|
||||||
findOptionCode(quotationTypeOptions, quotationDetail.quotationType) ??
|
findOptionCode(quotationTypeOptions, quotationDetail.quotationType) ??
|
||||||
@@ -424,7 +384,7 @@ export async function buildQuotationDocumentData(
|
|||||||
findOptionLabel(topicTypeOptions, item.topicType) ??
|
findOptionLabel(topicTypeOptions, item.topicType) ??
|
||||||
item.title?.trim() ??
|
item.title?.trim() ??
|
||||||
topicCodeById.get(item.topicType) ??
|
topicCodeById.get(item.topicType) ??
|
||||||
"Topic";
|
'Topic';
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: item.id,
|
id: item.id,
|
||||||
@@ -435,8 +395,8 @@ export async function buildQuotationDocumentData(
|
|||||||
items: sortBySortOrder(item.items).map((child) => ({
|
items: sortBySortOrder(item.items).map((child) => ({
|
||||||
id: child.id,
|
id: child.id,
|
||||||
content: child.content,
|
content: child.content,
|
||||||
sortOrder: child.sortOrder,
|
sortOrder: child.sortOrder
|
||||||
})),
|
}))
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
const pdfTopics: PdfTopic[] = resolvedTopics.map((item) => ({
|
const pdfTopics: PdfTopic[] = resolvedTopics.map((item) => ({
|
||||||
@@ -445,24 +405,24 @@ export async function buildQuotationDocumentData(
|
|||||||
sortOrder: item.sortOrder,
|
sortOrder: item.sortOrder,
|
||||||
items: item.items.map((child) => ({
|
items: item.items.map((child) => ({
|
||||||
id: child.id,
|
id: child.id,
|
||||||
content: child.content?.trim() || "-",
|
content: child.content?.trim() || '-',
|
||||||
sortOrder: child.sortOrder,
|
sortOrder: child.sortOrder
|
||||||
})),
|
}))
|
||||||
}));
|
}));
|
||||||
const scopeTopics = resolvedTopics.filter((item) =>
|
const scopeTopics = resolvedTopics.filter((item) =>
|
||||||
matchesTopicAlias(item.topicCode, topicTypeMapping.scope),
|
matchesTopicAlias(item.topicCode, topicTypeMapping.scope)
|
||||||
);
|
);
|
||||||
const exclusionTopics = resolvedTopics.filter((item) =>
|
const exclusionTopics = resolvedTopics.filter((item) =>
|
||||||
matchesTopicAlias(item.topicCode, topicTypeMapping.exclusion),
|
matchesTopicAlias(item.topicCode, topicTypeMapping.exclusion)
|
||||||
);
|
);
|
||||||
const paymentTopics = resolvedTopics.filter((item) =>
|
const paymentTopics = resolvedTopics.filter((item) =>
|
||||||
matchesTopicAlias(item.topicCode, topicTypeMapping.payment),
|
matchesTopicAlias(item.topicCode, topicTypeMapping.payment)
|
||||||
);
|
);
|
||||||
const warrantyTopics = resolvedTopics.filter((item) =>
|
const warrantyTopics = resolvedTopics.filter((item) =>
|
||||||
matchesTopicAlias(item.topicCode, topicTypeMapping.warranty),
|
matchesTopicAlias(item.topicCode, topicTypeMapping.warranty)
|
||||||
);
|
);
|
||||||
const deliveryTopics = resolvedTopics.filter((item) =>
|
const deliveryTopics = resolvedTopics.filter((item) =>
|
||||||
matchesTopicAlias(item.topicCode, topicTypeMapping.delivery),
|
matchesTopicAlias(item.topicCode, topicTypeMapping.delivery)
|
||||||
);
|
);
|
||||||
const otherTopics = resolvedTopics.filter((item) => {
|
const otherTopics = resolvedTopics.filter((item) => {
|
||||||
return ![
|
return ![
|
||||||
@@ -470,7 +430,7 @@ export async function buildQuotationDocumentData(
|
|||||||
topicTypeMapping.exclusion,
|
topicTypeMapping.exclusion,
|
||||||
topicTypeMapping.payment,
|
topicTypeMapping.payment,
|
||||||
topicTypeMapping.warranty,
|
topicTypeMapping.warranty,
|
||||||
topicTypeMapping.delivery,
|
topicTypeMapping.delivery
|
||||||
].some((expectedCode) => matchesTopicAlias(item.topicCode, expectedCode));
|
].some((expectedCode) => matchesTopicAlias(item.topicCode, expectedCode));
|
||||||
});
|
});
|
||||||
const exclusionTopic = exclusionTopics[0];
|
const exclusionTopic = exclusionTopics[0];
|
||||||
@@ -479,23 +439,22 @@ export async function buildQuotationDocumentData(
|
|||||||
const deliveryTopic = deliveryTopics[0];
|
const deliveryTopic = deliveryTopics[0];
|
||||||
const approvalApprovers: QuotationDocumentApprovalStep[] =
|
const approvalApprovers: QuotationDocumentApprovalStep[] =
|
||||||
approvalDetail?.timeline
|
approvalDetail?.timeline
|
||||||
.filter((item) => ["approve", "reject", "return"].includes(item.action))
|
.filter((item) => ['approve', 'reject', 'return'].includes(item.action))
|
||||||
.map((item) => {
|
.map((item) => {
|
||||||
const step = approvalDetail.steps.find(
|
const step = approvalDetail.steps.find(
|
||||||
(candidate) => candidate.stepNumber === item.stepNumber,
|
(candidate) => candidate.stepNumber === item.stepNumber
|
||||||
);
|
);
|
||||||
return {
|
return {
|
||||||
stepNumber: item.stepNumber,
|
stepNumber: item.stepNumber,
|
||||||
roleCode: step?.roleCode ?? "",
|
roleCode: step?.roleCode ?? '',
|
||||||
roleName: step?.roleName ?? `Step ${item.stepNumber}`,
|
roleName: step?.roleName ?? `Step ${item.stepNumber}`,
|
||||||
action: item.action,
|
action: item.action,
|
||||||
actorName: item.actorName,
|
actorName: item.actorName,
|
||||||
actedAt: item.actedAt,
|
actedAt: item.actedAt,
|
||||||
remark: item.remark,
|
remark: item.remark
|
||||||
};
|
};
|
||||||
}) ?? [];
|
}) ?? [];
|
||||||
const currencyCode =
|
const currencyCode = findOptionCode(currencyOptions, quotationDetail.currency) ?? 'THB';
|
||||||
findOptionCode(currencyOptions, quotationDetail.currency) ?? "THB";
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
company: {
|
company: {
|
||||||
@@ -503,14 +462,14 @@ export async function buildQuotationDocumentData(
|
|||||||
name: company.name,
|
name: company.name,
|
||||||
slug: company.slug,
|
slug: company.slug,
|
||||||
imageUrl: company.imageUrl,
|
imageUrl: company.imageUrl,
|
||||||
plan: company.plan,
|
plan: company.plan
|
||||||
},
|
},
|
||||||
branch: branch
|
branch: branch
|
||||||
? {
|
? {
|
||||||
id: branch.id,
|
id: branch.id,
|
||||||
code: branch.code,
|
code: branch.code,
|
||||||
label: branch.label,
|
label: branch.label,
|
||||||
value: branch.value,
|
value: branch.value
|
||||||
}
|
}
|
||||||
: null,
|
: null,
|
||||||
customer: {
|
customer: {
|
||||||
@@ -520,7 +479,7 @@ export async function buildQuotationDocumentData(
|
|||||||
address: customer.address,
|
address: customer.address,
|
||||||
phone: customer.phone,
|
phone: customer.phone,
|
||||||
email: customer.email,
|
email: customer.email,
|
||||||
taxId: customer.taxId,
|
taxId: customer.taxId
|
||||||
},
|
},
|
||||||
contact: contact
|
contact: contact
|
||||||
? {
|
? {
|
||||||
@@ -528,7 +487,7 @@ export async function buildQuotationDocumentData(
|
|||||||
name: contact.name,
|
name: contact.name,
|
||||||
email: contact.email,
|
email: contact.email,
|
||||||
mobile: contact.mobile,
|
mobile: contact.mobile,
|
||||||
position: contact.position,
|
position: contact.position
|
||||||
}
|
}
|
||||||
: null,
|
: null,
|
||||||
quotation: {
|
quotation: {
|
||||||
@@ -544,32 +503,20 @@ export async function buildQuotationDocumentData(
|
|||||||
revisionLabel: toRevisionLabel(quotationDetail.revision),
|
revisionLabel: toRevisionLabel(quotationDetail.revision),
|
||||||
statusCode,
|
statusCode,
|
||||||
statusLabel,
|
statusLabel,
|
||||||
quotationTypeCode: findOptionCode(
|
quotationTypeCode: findOptionCode(quotationTypeOptions, quotationDetail.quotationType),
|
||||||
quotationTypeOptions,
|
quotationTypeLabel: findOptionLabel(quotationTypeOptions, quotationDetail.quotationType),
|
||||||
quotationDetail.quotationType,
|
|
||||||
),
|
|
||||||
quotationTypeLabel: findOptionLabel(
|
|
||||||
quotationTypeOptions,
|
|
||||||
quotationDetail.quotationType,
|
|
||||||
),
|
|
||||||
currency: currencyCode,
|
currency: currencyCode,
|
||||||
currencyCode,
|
currencyCode,
|
||||||
currencyLabel: findOptionLabel(currencyOptions, quotationDetail.currency),
|
currencyLabel: findOptionLabel(currencyOptions, quotationDetail.currency),
|
||||||
exchangeRate: quotationDetail.exchangeRate,
|
exchangeRate: quotationDetail.exchangeRate,
|
||||||
subtotal: quotationDetail.subtotal,
|
subtotal: quotationDetail.subtotal,
|
||||||
discount: quotationDetail.discount,
|
discount: quotationDetail.discount,
|
||||||
discountTypeCode: findOptionCode(
|
discountTypeCode: findOptionCode(discountTypeOptions, quotationDetail.discountType),
|
||||||
discountTypeOptions,
|
discountTypeLabel: findOptionLabel(discountTypeOptions, quotationDetail.discountType),
|
||||||
quotationDetail.discountType,
|
|
||||||
),
|
|
||||||
discountTypeLabel: findOptionLabel(
|
|
||||||
discountTypeOptions,
|
|
||||||
quotationDetail.discountType,
|
|
||||||
),
|
|
||||||
taxRate: quotationDetail.taxRate,
|
taxRate: quotationDetail.taxRate,
|
||||||
taxAmount: quotationDetail.taxAmount,
|
taxAmount: quotationDetail.taxAmount,
|
||||||
totalAmount: quotationDetail.totalAmount,
|
totalAmount: quotationDetail.totalAmount,
|
||||||
notes: quotationDetail.notes,
|
notes: quotationDetail.notes
|
||||||
},
|
},
|
||||||
items: items.map((item) => ({
|
items: items.map((item) => ({
|
||||||
id: item.id,
|
id: item.id,
|
||||||
@@ -585,130 +532,107 @@ export async function buildQuotationDocumentData(
|
|||||||
discountTypeCode: findOptionCode(discountTypeOptions, item.discountType),
|
discountTypeCode: findOptionCode(discountTypeOptions, item.discountType),
|
||||||
taxRate: item.taxRate,
|
taxRate: item.taxRate,
|
||||||
totalPrice: item.totalPrice,
|
totalPrice: item.totalPrice,
|
||||||
notes: item.notes,
|
notes: item.notes
|
||||||
})),
|
})),
|
||||||
quotationCustomers: quotationCustomers.map((item) => ({
|
quotationCustomers: quotationCustomers.map((item) => ({
|
||||||
id: item.id,
|
id: item.id,
|
||||||
customerId: item.customerId,
|
customerId: item.customerId,
|
||||||
customerName:
|
customerName: relatedCustomerMap.get(item.customerId)?.name ?? item.customerName,
|
||||||
relatedCustomerMap.get(item.customerId)?.name ?? item.customerName,
|
customerCode: relatedCustomerMap.get(item.customerId)?.code ?? item.customerCode,
|
||||||
customerCode:
|
|
||||||
relatedCustomerMap.get(item.customerId)?.code ?? item.customerCode,
|
|
||||||
roleCode: item.roleCode,
|
roleCode: item.roleCode,
|
||||||
roleLabel:
|
roleLabel: item.roleLabel ?? findOptionLabel(customerRoleOptions, item.role),
|
||||||
item.roleLabel ?? findOptionLabel(customerRoleOptions, item.role),
|
isPrimary: item.isPrimary
|
||||||
isPrimary: item.isPrimary,
|
|
||||||
})),
|
})),
|
||||||
topics: {
|
topics: {
|
||||||
scope: scopeTopics.map((item) => ({
|
scope: scopeTopics.map((item) => ({
|
||||||
title: item.title,
|
title: item.title,
|
||||||
items: item.items.map((child) => child.content ?? "-"),
|
items: item.items.map((child) => child.content ?? '-')
|
||||||
})),
|
})),
|
||||||
exclusion: exclusionTopics.map((item) => ({
|
exclusion: exclusionTopics.map((item) => ({
|
||||||
title: item.title,
|
title: item.title,
|
||||||
items: item.items.map((child) => child.content ?? "-"),
|
items: item.items.map((child) => child.content ?? '-')
|
||||||
})),
|
})),
|
||||||
payment: paymentTopics.map((item) => ({
|
payment: paymentTopics.map((item) => ({
|
||||||
title: item.title,
|
title: item.title,
|
||||||
items: item.items.map((child) => child.content ?? "-"),
|
items: item.items.map((child) => child.content ?? '-')
|
||||||
})),
|
})),
|
||||||
warranty: warrantyTopics.map((item) => ({
|
warranty: warrantyTopics.map((item) => ({
|
||||||
title: item.title,
|
title: item.title,
|
||||||
items: item.items.map((child) => child.content ?? "-"),
|
items: item.items.map((child) => child.content ?? '-')
|
||||||
})),
|
})),
|
||||||
delivery: deliveryTopics.map((item) => ({
|
delivery: deliveryTopics.map((item) => ({
|
||||||
title: item.title,
|
title: item.title,
|
||||||
items: item.items.map((child) => child.content ?? "-"),
|
items: item.items.map((child) => child.content ?? '-')
|
||||||
})),
|
})),
|
||||||
other: otherTopics.map((item) => ({
|
other: otherTopics.map((item) => ({
|
||||||
id: item.id,
|
id: item.id,
|
||||||
title: item.title,
|
title: item.title,
|
||||||
sortOrder: item.sortOrder,
|
sortOrder: item.sortOrder,
|
||||||
items: item.items.map((child) => child.content ?? "-"),
|
items: item.items.map((child) => child.content ?? '-')
|
||||||
})),
|
})),
|
||||||
all: pdfTopics,
|
all: pdfTopics
|
||||||
},
|
},
|
||||||
pdfme: {
|
pdfme: {
|
||||||
labels: PDFME_DEFAULT_LABELS,
|
labels: PDFME_DEFAULT_LABELS,
|
||||||
quotation_date: formatPdfDate(quotationDetail.quotationDate, "en"),
|
quotation_date: formatPdfDate(quotationDetail.quotationDate, 'en'),
|
||||||
quotation_price: formatPdfCurrency(
|
quotation_price: formatPdfCurrency(quotationDetail.subtotal, currencyCode),
|
||||||
quotationDetail.subtotal,
|
quotation_price_data: buildPdfPriceTableData(quotationDetail.subtotal, currencyCode),
|
||||||
currencyCode,
|
|
||||||
),
|
|
||||||
quotation_price_data: buildPdfPriceTableData(
|
|
||||||
quotationDetail.subtotal,
|
|
||||||
currencyCode,
|
|
||||||
),
|
|
||||||
exclusion_data: formatTopicItems(exclusionTopic),
|
exclusion_data: formatTopicItems(exclusionTopic),
|
||||||
payment_data: formatTopicItems(paymentTopic),
|
payment_data: formatTopicItems(paymentTopic),
|
||||||
warranty_data: formatTopicItems(warrantyTopic),
|
warranty_data: formatTopicItems(warrantyTopic),
|
||||||
delivery_data: formatTopicItems(deliveryTopic),
|
delivery_data: formatTopicItems(deliveryTopic),
|
||||||
topics_data: formatTopicItems(
|
topics_data: formatTopicItems(scopeTopics[0] ?? exclusionTopic ?? paymentTopic),
|
||||||
scopeTopics[0] ?? exclusionTopic ?? paymentTopic,
|
topic_inputs: {}
|
||||||
),
|
|
||||||
topic_inputs: {},
|
|
||||||
},
|
},
|
||||||
approval: {
|
approval: {
|
||||||
requestId: approvalDetail?.request.id ?? null,
|
requestId: approvalDetail?.request.id ?? null,
|
||||||
workflowName: approvalDetail?.workflow.name ?? null,
|
workflowName: approvalDetail?.workflow.name ?? null,
|
||||||
status: approvalDetail?.request.status ?? null,
|
status: approvalDetail?.request.status ?? null,
|
||||||
approvedAt:
|
approvedAt:
|
||||||
approvalDetail?.request.status === "approved"
|
approvalDetail?.request.status === 'approved' ? approvalDetail.request.completedAt : null,
|
||||||
? approvalDetail.request.completedAt
|
|
||||||
: null,
|
|
||||||
currentStep: approvalDetail?.request.currentStep ?? null,
|
currentStep: approvalDetail?.request.currentStep ?? null,
|
||||||
currentStepRoleName: approvalDetail?.currentStep?.roleName ?? null,
|
currentStepRoleName: approvalDetail?.currentStep?.roleName ?? null,
|
||||||
approvers: approvalApprovers,
|
approvers: approvalApprovers
|
||||||
},
|
},
|
||||||
signatures,
|
signatures,
|
||||||
watermarkStatus:
|
watermarkStatus: statusCode && statusCode !== 'approved' ? (statusLabel ?? statusCode) : null
|
||||||
statusCode && statusCode !== "approved"
|
|
||||||
? (statusLabel ?? statusCode)
|
|
||||||
: null,
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getQuotationDocumentPreviewData(
|
export async function getQuotationDocumentPreviewData(
|
||||||
quotationId: string,
|
quotationId: string,
|
||||||
organizationId: string,
|
organizationId: string,
|
||||||
|
options?: {
|
||||||
|
templateVariant?: string | null;
|
||||||
|
}
|
||||||
) {
|
) {
|
||||||
const documentData = await buildQuotationDocumentData(
|
const documentData = await buildQuotationDocumentData(quotationId, organizationId);
|
||||||
quotationId,
|
|
||||||
organizationId,
|
|
||||||
);
|
|
||||||
const template = await resolveTemplateForDocument(organizationId, {
|
const template = await resolveTemplateForDocument(organizationId, {
|
||||||
documentType: "quotation",
|
documentType: 'quotation',
|
||||||
productType: "default",
|
productType: 'default',
|
||||||
fileType: "pdfme",
|
fileType: 'pdfme',
|
||||||
|
templateVariant: options?.templateVariant ?? null
|
||||||
});
|
});
|
||||||
const mappings = await resolveTemplateMappings(
|
const mappings = await resolveTemplateMappings(template.version.id, organizationId);
|
||||||
template.version.id,
|
|
||||||
organizationId,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!mappings.length) {
|
if (!mappings.length) {
|
||||||
throw new AuthError(
|
throw new AuthError(
|
||||||
"Document template mappings are not configured for this template version",
|
'Document template mappings are not configured for this template version',
|
||||||
409,
|
409
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const templateInput = normalizePdfmeTemplateInput(
|
const templateInput = normalizePdfmeTemplateInput(
|
||||||
template.version.schemaJson,
|
template.version.schemaJson,
|
||||||
mapDocumentDataToTemplateInput(
|
mapDocumentDataToTemplateInput(documentData as unknown as Record<string, unknown>, mappings)
|
||||||
documentData as unknown as Record<string, unknown>,
|
|
||||||
mappings,
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
const runtime = await buildQuotationPdfRuntime({
|
const runtime = await buildQuotationPdfRuntime({
|
||||||
documentData,
|
documentData,
|
||||||
template,
|
template,
|
||||||
mappings,
|
mappings,
|
||||||
templateInput,
|
templateInput
|
||||||
});
|
});
|
||||||
const runtimeWarnings = runtime.issues.filter(
|
const runtimeWarnings = runtime.issues.filter((issue) => issue.severity === 'warning');
|
||||||
(issue) => issue.severity === "warning",
|
|
||||||
);
|
|
||||||
|
|
||||||
logQuotationPdfRuntimeWarnings(runtimeWarnings);
|
logQuotationPdfRuntimeWarnings(runtimeWarnings);
|
||||||
|
|
||||||
@@ -720,23 +644,20 @@ export async function getQuotationDocumentPreviewData(
|
|||||||
...template,
|
...template,
|
||||||
version: {
|
version: {
|
||||||
...template.version,
|
...template.version,
|
||||||
schemaJson: runtime.assembled.template,
|
schemaJson: runtime.assembled.template
|
||||||
},
|
}
|
||||||
},
|
},
|
||||||
mappings,
|
mappings,
|
||||||
templateInput: runtime.assembled.templateInput,
|
templateInput: runtime.assembled.templateInput
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function prepareApprovedQuotationSnapshot(
|
export async function prepareApprovedQuotationSnapshot(
|
||||||
quotationId: string,
|
quotationId: string,
|
||||||
organizationId: string,
|
organizationId: string,
|
||||||
userId: string,
|
userId: string
|
||||||
): Promise<ApprovedQuotationSnapshot> {
|
): Promise<ApprovedQuotationSnapshot> {
|
||||||
const preview = await getQuotationDocumentPreviewData(
|
const preview = await getQuotationDocumentPreviewData(quotationId, organizationId);
|
||||||
quotationId,
|
|
||||||
organizationId,
|
|
||||||
);
|
|
||||||
const generatedAt = new Date().toISOString();
|
const generatedAt = new Date().toISOString();
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -746,6 +667,6 @@ export async function prepareApprovedQuotationSnapshot(
|
|||||||
templateVersionId: preview.template.version.id,
|
templateVersionId: preview.template.version.id,
|
||||||
templateInput: preview.templateInput,
|
templateInput: preview.templateInput,
|
||||||
generatedAt,
|
generatedAt,
|
||||||
generatedBy: userId,
|
generatedBy: userId
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ import {
|
|||||||
CURRENT_TEMPLATE_RUNTIME_VERSION,
|
CURRENT_TEMPLATE_RUNTIME_VERSION,
|
||||||
getManagementMetadata,
|
getManagementMetadata,
|
||||||
mergeManagementMetadata,
|
mergeManagementMetadata,
|
||||||
sanitizeTemplateSchemaJson,
|
sanitizeTemplateSchemaJson
|
||||||
} from './metadata';
|
} from './metadata';
|
||||||
import type {
|
import type {
|
||||||
DocumentTemplateDetail,
|
DocumentTemplateDetail,
|
||||||
@@ -71,7 +71,7 @@ function mapVersionRecord(
|
|||||||
): DocumentTemplateVersionRecord {
|
): DocumentTemplateVersionRecord {
|
||||||
const metadata = getManagementMetadata({
|
const metadata = getManagementMetadata({
|
||||||
metadataJson: row.metadataJson,
|
metadataJson: row.metadataJson,
|
||||||
schemaJson: row.schemaJson,
|
schemaJson: row.schemaJson
|
||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -218,7 +218,10 @@ export async function resolveTemplateMappings(
|
|||||||
isNull(crmDocumentTemplateMappings.deletedAt)
|
isNull(crmDocumentTemplateMappings.deletedAt)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
.orderBy(asc(crmDocumentTemplateMappings.sortOrder), asc(crmDocumentTemplateMappings.createdAt));
|
.orderBy(
|
||||||
|
asc(crmDocumentTemplateMappings.sortOrder),
|
||||||
|
asc(crmDocumentTemplateMappings.createdAt)
|
||||||
|
);
|
||||||
const columns = await db
|
const columns = await db
|
||||||
.select()
|
.select()
|
||||||
.from(crmDocumentTemplateTableColumns)
|
.from(crmDocumentTemplateTableColumns)
|
||||||
@@ -257,7 +260,9 @@ export async function listDocumentTemplates(
|
|||||||
and(
|
and(
|
||||||
eq(crmDocumentTemplates.organizationId, organizationId),
|
eq(crmDocumentTemplates.organizationId, organizationId),
|
||||||
isNull(crmDocumentTemplates.deletedAt),
|
isNull(crmDocumentTemplates.deletedAt),
|
||||||
...(filters.documentType ? [eq(crmDocumentTemplates.documentType, filters.documentType)] : []),
|
...(filters.documentType
|
||||||
|
? [eq(crmDocumentTemplates.documentType, filters.documentType)]
|
||||||
|
: []),
|
||||||
...(filters.fileType ? [eq(crmDocumentTemplates.fileType, filters.fileType)] : []),
|
...(filters.fileType ? [eq(crmDocumentTemplates.fileType, filters.fileType)] : []),
|
||||||
...(filters.isActive === 'true' ? [eq(crmDocumentTemplates.isActive, true)] : []),
|
...(filters.isActive === 'true' ? [eq(crmDocumentTemplates.isActive, true)] : []),
|
||||||
...(filters.isActive === 'false' ? [eq(crmDocumentTemplates.isActive, false)] : [])
|
...(filters.isActive === 'false' ? [eq(crmDocumentTemplates.isActive, false)] : [])
|
||||||
@@ -288,7 +293,9 @@ export async function listDocumentTemplates(
|
|||||||
const templateVersions = versions.filter((item) => item.templateId === template.id);
|
const templateVersions = versions.filter((item) => item.templateId === template.id);
|
||||||
const latestVersion = templateVersions.at(-1) ?? null;
|
const latestVersion = templateVersions.at(-1) ?? null;
|
||||||
const templateVersionIds = new Set(templateVersions.map((item) => item.id));
|
const templateVersionIds = new Set(templateVersions.map((item) => item.id));
|
||||||
const mappingCount = mappings.filter((item) => templateVersionIds.has(item.templateVersionId)).length;
|
const mappingCount = mappings.filter((item) =>
|
||||||
|
templateVersionIds.has(item.templateVersionId)
|
||||||
|
).length;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...mapTemplateRecord(template),
|
...mapTemplateRecord(template),
|
||||||
@@ -379,7 +386,12 @@ export async function createDocumentTemplate(
|
|||||||
.returning();
|
.returning();
|
||||||
|
|
||||||
if (created.isDefault) {
|
if (created.isDefault) {
|
||||||
await normalizeDefaultTemplate(created.id, organizationId, created.documentType, created.fileType);
|
await normalizeDefaultTemplate(
|
||||||
|
created.id,
|
||||||
|
organizationId,
|
||||||
|
created.documentType,
|
||||||
|
created.fileType
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return created;
|
return created;
|
||||||
@@ -399,7 +411,10 @@ export async function updateDocumentTemplate(
|
|||||||
productType: payload.productType ?? template.productType,
|
productType: payload.productType ?? template.productType,
|
||||||
fileType: payload.fileType ?? template.fileType,
|
fileType: payload.fileType ?? template.fileType,
|
||||||
templateName: payload.templateName?.trim() ?? template.templateName,
|
templateName: payload.templateName?.trim() ?? template.templateName,
|
||||||
description: payload.description === undefined ? template.description : payload.description?.trim() || null,
|
description:
|
||||||
|
payload.description === undefined
|
||||||
|
? template.description
|
||||||
|
: payload.description?.trim() || null,
|
||||||
isDefault: payload.isDefault ?? template.isDefault,
|
isDefault: payload.isDefault ?? template.isDefault,
|
||||||
isActive: payload.isActive ?? template.isActive,
|
isActive: payload.isActive ?? template.isActive,
|
||||||
updatedAt: new Date(),
|
updatedAt: new Date(),
|
||||||
@@ -409,7 +424,12 @@ export async function updateDocumentTemplate(
|
|||||||
.returning();
|
.returning();
|
||||||
|
|
||||||
if (updated.isDefault) {
|
if (updated.isDefault) {
|
||||||
await normalizeDefaultTemplate(updated.id, organizationId, updated.documentType, updated.fileType);
|
await normalizeDefaultTemplate(
|
||||||
|
updated.id,
|
||||||
|
organizationId,
|
||||||
|
updated.documentType,
|
||||||
|
updated.fileType
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return updated;
|
return updated;
|
||||||
@@ -445,7 +465,11 @@ async function normalizeDefaultTemplate(
|
|||||||
.where(eq(crmDocumentTemplates.id, templateId));
|
.where(eq(crmDocumentTemplates.id, templateId));
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function softDeleteDocumentTemplate(id: string, organizationId: string, userId: string) {
|
export async function softDeleteDocumentTemplate(
|
||||||
|
id: string,
|
||||||
|
organizationId: string,
|
||||||
|
userId: string
|
||||||
|
) {
|
||||||
await assertTemplate(id, organizationId);
|
await assertTemplate(id, organizationId);
|
||||||
const [updated] = await db
|
const [updated] = await db
|
||||||
.update(crmDocumentTemplates)
|
.update(crmDocumentTemplates)
|
||||||
@@ -473,8 +497,8 @@ export async function createDocumentTemplateVersion(
|
|||||||
lifecycleStatus: payload.isActive ? 'active' : 'draft',
|
lifecycleStatus: payload.isActive ? 'active' : 'draft',
|
||||||
runtimeVersion: payload.runtimeVersion ?? CURRENT_TEMPLATE_RUNTIME_VERSION,
|
runtimeVersion: payload.runtimeVersion ?? CURRENT_TEMPLATE_RUNTIME_VERSION,
|
||||||
templateVariant: payload.templateVariant ?? null,
|
templateVariant: payload.templateVariant ?? null,
|
||||||
brand: payload.brand ?? null,
|
brand: payload.brand ?? null
|
||||||
},
|
}
|
||||||
});
|
});
|
||||||
const [created] = await db
|
const [created] = await db
|
||||||
.insert(crmDocumentTemplateVersions)
|
.insert(crmDocumentTemplateVersions)
|
||||||
@@ -503,13 +527,14 @@ export async function updateDocumentTemplateVersion(
|
|||||||
const current = await assertTemplateVersion(versionId, organizationId);
|
const current = await assertTemplateVersion(versionId, organizationId);
|
||||||
const currentMetadata = getManagementMetadata({
|
const currentMetadata = getManagementMetadata({
|
||||||
metadataJson: current.metadataJson,
|
metadataJson: current.metadataJson,
|
||||||
schemaJson: current.schemaJson,
|
schemaJson: current.schemaJson
|
||||||
});
|
});
|
||||||
const [updated] = await db
|
const [updated] = await db
|
||||||
.update(crmDocumentTemplateVersions)
|
.update(crmDocumentTemplateVersions)
|
||||||
.set({
|
.set({
|
||||||
version: payload.version?.trim() ?? current.version,
|
version: payload.version?.trim() ?? current.version,
|
||||||
filePath: payload.filePath === undefined ? current.filePath : payload.filePath?.trim() || null,
|
filePath:
|
||||||
|
payload.filePath === undefined ? current.filePath : payload.filePath?.trim() || null,
|
||||||
schemaJson:
|
schemaJson:
|
||||||
payload.schemaJson === undefined
|
payload.schemaJson === undefined
|
||||||
? sanitizeTemplateSchemaJson(current.schemaJson)
|
? sanitizeTemplateSchemaJson(current.schemaJson)
|
||||||
@@ -520,8 +545,8 @@ export async function updateDocumentTemplateVersion(
|
|||||||
patch: {
|
patch: {
|
||||||
runtimeVersion: payload.runtimeVersion ?? currentMetadata.runtimeVersion ?? null,
|
runtimeVersion: payload.runtimeVersion ?? currentMetadata.runtimeVersion ?? null,
|
||||||
templateVariant: payload.templateVariant ?? currentMetadata.templateVariant ?? null,
|
templateVariant: payload.templateVariant ?? currentMetadata.templateVariant ?? null,
|
||||||
brand: payload.brand ?? currentMetadata.brand ?? null,
|
brand: payload.brand ?? currentMetadata.brand ?? null
|
||||||
},
|
}
|
||||||
}),
|
}),
|
||||||
previewImageUrl:
|
previewImageUrl:
|
||||||
payload.previewImageUrl === undefined
|
payload.previewImageUrl === undefined
|
||||||
@@ -555,7 +580,7 @@ export function createDuplicateVersionLabel(existingVersions: string[], baseVers
|
|||||||
export async function duplicateDocumentTemplateVersion(
|
export async function duplicateDocumentTemplateVersion(
|
||||||
versionId: string,
|
versionId: string,
|
||||||
organizationId: string,
|
organizationId: string,
|
||||||
userId: string,
|
userId: string
|
||||||
) {
|
) {
|
||||||
const sourceVersion = await assertTemplateVersion(versionId, organizationId);
|
const sourceVersion = await assertTemplateVersion(versionId, organizationId);
|
||||||
const siblingVersions = await db
|
const siblingVersions = await db
|
||||||
@@ -565,18 +590,18 @@ export async function duplicateDocumentTemplateVersion(
|
|||||||
and(
|
and(
|
||||||
eq(crmDocumentTemplateVersions.templateId, sourceVersion.templateId),
|
eq(crmDocumentTemplateVersions.templateId, sourceVersion.templateId),
|
||||||
eq(crmDocumentTemplateVersions.organizationId, organizationId),
|
eq(crmDocumentTemplateVersions.organizationId, organizationId),
|
||||||
isNull(crmDocumentTemplateVersions.deletedAt),
|
isNull(crmDocumentTemplateVersions.deletedAt)
|
||||||
),
|
)
|
||||||
)
|
)
|
||||||
.orderBy(asc(crmDocumentTemplateVersions.createdAt));
|
.orderBy(asc(crmDocumentTemplateVersions.createdAt));
|
||||||
const metadata = getManagementMetadata({
|
const metadata = getManagementMetadata({
|
||||||
metadataJson: sourceVersion.metadataJson,
|
metadataJson: sourceVersion.metadataJson,
|
||||||
schemaJson: sourceVersion.schemaJson,
|
schemaJson: sourceVersion.schemaJson
|
||||||
});
|
});
|
||||||
const duplicateId = crypto.randomUUID();
|
const duplicateId = crypto.randomUUID();
|
||||||
const duplicateVersionLabel = createDuplicateVersionLabel(
|
const duplicateVersionLabel = createDuplicateVersionLabel(
|
||||||
siblingVersions.map((item) => item.version),
|
siblingVersions.map((item) => item.version),
|
||||||
sourceVersion.version,
|
sourceVersion.version
|
||||||
);
|
);
|
||||||
|
|
||||||
const [createdVersion] = await db
|
const [createdVersion] = await db
|
||||||
@@ -601,11 +626,11 @@ export async function duplicateDocumentTemplateVersion(
|
|||||||
nextVersionId: null,
|
nextVersionId: null,
|
||||||
validationSummary: null,
|
validationSummary: null,
|
||||||
auditSummary: null,
|
auditSummary: null,
|
||||||
visualSummary: null,
|
visualSummary: null
|
||||||
},
|
},
|
||||||
previewImageUrl: sourceVersion.previewImageUrl,
|
previewImageUrl: sourceVersion.previewImageUrl,
|
||||||
isActive: false,
|
isActive: false,
|
||||||
createdBy: userId,
|
createdBy: userId
|
||||||
})
|
})
|
||||||
.returning();
|
.returning();
|
||||||
|
|
||||||
@@ -622,7 +647,7 @@ export async function duplicateDocumentTemplateVersion(
|
|||||||
sheetName: mapping.sheetName,
|
sheetName: mapping.sheetName,
|
||||||
defaultValue: mapping.defaultValue,
|
defaultValue: mapping.defaultValue,
|
||||||
formatMask: mapping.formatMask,
|
formatMask: mapping.formatMask,
|
||||||
sortOrder: mapping.sortOrder,
|
sortOrder: mapping.sortOrder
|
||||||
});
|
});
|
||||||
|
|
||||||
if (mapping.columns.length) {
|
if (mapping.columns.length) {
|
||||||
@@ -635,8 +660,8 @@ export async function duplicateDocumentTemplateVersion(
|
|||||||
sourceField: column.sourceField,
|
sourceField: column.sourceField,
|
||||||
columnLetter: column.columnLetter,
|
columnLetter: column.columnLetter,
|
||||||
sortOrder: column.sortOrder,
|
sortOrder: column.sortOrder,
|
||||||
formatMask: column.formatMask,
|
formatMask: column.formatMask
|
||||||
})),
|
}))
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -721,9 +746,12 @@ export async function updateDocumentTemplateMapping(
|
|||||||
placeholderKey: payload.placeholderKey?.trim() ?? current.placeholderKey,
|
placeholderKey: payload.placeholderKey?.trim() ?? current.placeholderKey,
|
||||||
sourcePath: payload.sourcePath?.trim() ?? current.sourcePath,
|
sourcePath: payload.sourcePath?.trim() ?? current.sourcePath,
|
||||||
dataType: payload.dataType ?? current.dataType,
|
dataType: payload.dataType ?? current.dataType,
|
||||||
sheetName: payload.sheetName === undefined ? current.sheetName : payload.sheetName?.trim() || null,
|
sheetName:
|
||||||
|
payload.sheetName === undefined ? current.sheetName : payload.sheetName?.trim() || null,
|
||||||
defaultValue:
|
defaultValue:
|
||||||
payload.defaultValue === undefined ? current.defaultValue : payload.defaultValue?.trim() || null,
|
payload.defaultValue === undefined
|
||||||
|
? current.defaultValue
|
||||||
|
: payload.defaultValue?.trim() || null,
|
||||||
formatMask:
|
formatMask:
|
||||||
payload.formatMask === undefined ? current.formatMask : payload.formatMask?.trim() || null,
|
payload.formatMask === undefined ? current.formatMask : payload.formatMask?.trim() || null,
|
||||||
sortOrder: payload.sortOrder ?? current.sortOrder,
|
sortOrder: payload.sortOrder ?? current.sortOrder,
|
||||||
@@ -753,8 +781,8 @@ export async function updateDocumentTemplateMapping(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const [mapping] = await resolveTemplateMappings(updated.templateVersionId, organizationId).then((items) =>
|
const [mapping] = await resolveTemplateMappings(updated.templateVersionId, organizationId).then(
|
||||||
items.filter((item) => item.id === mappingId)
|
(items) => items.filter((item) => item.id === mappingId)
|
||||||
);
|
);
|
||||||
|
|
||||||
return mapping ?? { ...mapMappingRecord(updated), columns: [] };
|
return mapping ?? { ...mapMappingRecord(updated), columns: [] };
|
||||||
@@ -820,15 +848,30 @@ export async function resolveTemplateForDocument(
|
|||||||
and(
|
and(
|
||||||
eq(crmDocumentTemplateVersions.organizationId, organizationId),
|
eq(crmDocumentTemplateVersions.organizationId, organizationId),
|
||||||
eq(crmDocumentTemplateVersions.templateId, exactTemplate.id),
|
eq(crmDocumentTemplateVersions.templateId, exactTemplate.id),
|
||||||
eq(crmDocumentTemplateVersions.isActive, true),
|
|
||||||
isNull(crmDocumentTemplateVersions.deletedAt)
|
isNull(crmDocumentTemplateVersions.deletedAt)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
.orderBy(asc(crmDocumentTemplateVersions.createdAt));
|
.orderBy(asc(crmDocumentTemplateVersions.createdAt));
|
||||||
const version = versions.at(-1);
|
const templateVariant = params.templateVariant?.trim() || null;
|
||||||
|
const eligibleVersions = templateVariant
|
||||||
|
? versions.filter((item) => {
|
||||||
|
const metadata = getManagementMetadata({
|
||||||
|
metadataJson: item.metadataJson,
|
||||||
|
schemaJson: item.schemaJson
|
||||||
|
});
|
||||||
|
|
||||||
|
return metadata.templateVariant === templateVariant;
|
||||||
|
})
|
||||||
|
: versions.filter((item) => item.isActive);
|
||||||
|
const version = eligibleVersions.at(-1);
|
||||||
|
|
||||||
if (!version) {
|
if (!version) {
|
||||||
throw new AuthError('No active template version found', 404);
|
throw new AuthError(
|
||||||
|
templateVariant === 'product-v1'
|
||||||
|
? 'product-v1 template version is not seeded'
|
||||||
|
: 'No active template version found',
|
||||||
|
404
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -861,8 +904,7 @@ function applyFormatMask(value: unknown, formatMask?: string | null) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (formatMask === 'currency' || formatMask?.startsWith('currency_')) {
|
if (formatMask === 'currency' || formatMask?.startsWith('currency_')) {
|
||||||
const currencyCode =
|
const currencyCode = formatMask === 'currency' ? 'THB' : formatMask.replace('currency_', '');
|
||||||
formatMask === 'currency' ? 'THB' : formatMask.replace('currency_', '');
|
|
||||||
return formatPdfCurrency(value as number | string, currencyCode);
|
return formatPdfCurrency(value as number | string, currencyCode);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -943,7 +985,10 @@ export function mapDocumentDataToTemplateInput(
|
|||||||
const rowRecord = row as Record<string, unknown>;
|
const rowRecord = row as Record<string, unknown>;
|
||||||
|
|
||||||
return mapping.columns.map((column) => {
|
return mapping.columns.map((column) => {
|
||||||
const formattedValue = applyFormatMask(rowRecord[column.sourceField], column.formatMask);
|
const formattedValue = applyFormatMask(
|
||||||
|
rowRecord[column.sourceField],
|
||||||
|
column.formatMask
|
||||||
|
);
|
||||||
return String(formattedValue ?? '-').trim() || '-';
|
return String(formattedValue ?? '-').trim() || '-';
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -143,13 +143,11 @@ export interface DocumentTemplateMappingMutationPayload {
|
|||||||
columns?: DocumentTemplateMappingColumnMutationPayload[];
|
columns?: DocumentTemplateMappingColumnMutationPayload[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface DocumentTemplateMappingWithColumns
|
export interface DocumentTemplateMappingWithColumns extends DocumentTemplateMappingRecord {
|
||||||
extends DocumentTemplateMappingRecord {
|
|
||||||
columns: DocumentTemplateTableColumnRecord[];
|
columns: DocumentTemplateTableColumnRecord[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface DocumentTemplateVersionDetail
|
export interface DocumentTemplateVersionDetail extends DocumentTemplateVersionRecord {
|
||||||
extends DocumentTemplateVersionRecord {
|
|
||||||
mappings: DocumentTemplateMappingWithColumns[];
|
mappings: DocumentTemplateMappingWithColumns[];
|
||||||
validationSummary?: DocumentTemplateVersionValidationSummary | null;
|
validationSummary?: DocumentTemplateVersionValidationSummary | null;
|
||||||
auditSummary?: DocumentTemplateVersionAuditSummary | null;
|
auditSummary?: DocumentTemplateVersionAuditSummary | null;
|
||||||
@@ -200,6 +198,7 @@ export interface ResolveTemplateParams {
|
|||||||
documentType: string;
|
documentType: string;
|
||||||
productType: string;
|
productType: string;
|
||||||
fileType: string;
|
fileType: string;
|
||||||
|
templateVariant?: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ResolvedDocumentTemplate {
|
export interface ResolvedDocumentTemplate {
|
||||||
@@ -227,18 +226,15 @@ export interface DocumentTemplateVersionsResponse extends MutationSuccessRespons
|
|||||||
items: DocumentTemplateVersionDetail[];
|
items: DocumentTemplateVersionDetail[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface DocumentTemplateVersionActionResponse
|
export interface DocumentTemplateVersionActionResponse extends MutationSuccessResponse {
|
||||||
extends MutationSuccessResponse {
|
|
||||||
version: DocumentTemplateVersionDetail;
|
version: DocumentTemplateVersionDetail;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface DocumentTemplateVersionValidateResponse
|
export interface DocumentTemplateVersionValidateResponse extends MutationSuccessResponse {
|
||||||
extends MutationSuccessResponse {
|
|
||||||
validation: DocumentTemplateVersionValidationSummary;
|
validation: DocumentTemplateVersionValidationSummary;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface DocumentTemplateVersionCompareResponse
|
export interface DocumentTemplateVersionCompareResponse extends MutationSuccessResponse {
|
||||||
extends MutationSuccessResponse {
|
|
||||||
comparison: {
|
comparison: {
|
||||||
leftVersionId: string;
|
leftVersionId: string;
|
||||||
rightVersionId: string;
|
rightVersionId: string;
|
||||||
@@ -277,8 +273,7 @@ export interface DocumentTemplateVersionCompareResponse
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface DocumentTemplateVersionPreviewResponse
|
export interface DocumentTemplateVersionPreviewResponse extends MutationSuccessResponse {
|
||||||
extends MutationSuccessResponse {
|
|
||||||
preview: {
|
preview: {
|
||||||
fileName: string;
|
fileName: string;
|
||||||
contentType: 'application/pdf';
|
contentType: 'application/pdf';
|
||||||
|
|||||||
@@ -299,9 +299,14 @@ async function loadLegacyApprovedQuotationPdf(
|
|||||||
|
|
||||||
export async function generateQuotationPdf(
|
export async function generateQuotationPdf(
|
||||||
quotationId: string,
|
quotationId: string,
|
||||||
organizationId: string
|
organizationId: string,
|
||||||
|
options?: {
|
||||||
|
templateVariant?: string | null;
|
||||||
|
}
|
||||||
): Promise<GeneratedQuotationPdf> {
|
): Promise<GeneratedQuotationPdf> {
|
||||||
const preview = await getQuotationDocumentPreviewData(quotationId, organizationId);
|
const preview = await getQuotationDocumentPreviewData(quotationId, organizationId, {
|
||||||
|
templateVariant: options?.templateVariant ?? null
|
||||||
|
});
|
||||||
const buffer = await generatePdfFromTemplate({
|
const buffer = await generatePdfFromTemplate({
|
||||||
template: preview.template.version.schemaJson as Template,
|
template: preview.template.version.schemaJson as Template,
|
||||||
inputs: [preview.templateInput]
|
inputs: [preview.templateInput]
|
||||||
@@ -314,8 +319,14 @@ export async function generateQuotationPdf(
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function generateQuotationPreviewPdf(quotationId: string, organizationId: string) {
|
export async function generateQuotationPreviewPdf(
|
||||||
return generateQuotationPdf(quotationId, organizationId);
|
quotationId: string,
|
||||||
|
organizationId: string,
|
||||||
|
options?: {
|
||||||
|
templateVariant?: string | null;
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
return generateQuotationPdf(quotationId, organizationId, options);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function generateApprovedQuotationPdf(
|
export async function generateApprovedQuotationPdf(
|
||||||
|
|||||||
@@ -380,17 +380,9 @@
|
|||||||
"content": "[[\"Price (Exclude VAT)\",\"{quotation_price}\",\" \",\"{currency}\"]]",
|
"content": "[[\"Price (Exclude VAT)\",\"{quotation_price}\",\" \",\"{currency}\"]]",
|
||||||
"showHead": false,
|
"showHead": false,
|
||||||
"repeatHead": false,
|
"repeatHead": false,
|
||||||
"head": [
|
"head": ["Name", "City", "Head 3", "Head 4"],
|
||||||
"Name",
|
|
||||||
"City",
|
|
||||||
"Head 3",
|
|
||||||
"Head 4"
|
|
||||||
],
|
|
||||||
"headWidthPercentages": [
|
"headWidthPercentages": [
|
||||||
28.125,
|
28.125, 58.34767550533545, 2.4513428524798244, 11.075981642184729
|
||||||
58.34767550533545,
|
|
||||||
2.4513428524798244,
|
|
||||||
11.075981642184729
|
|
||||||
],
|
],
|
||||||
"tableStyles": {
|
"tableStyles": {
|
||||||
"borderWidth": 0,
|
"borderWidth": 0,
|
||||||
@@ -1023,7 +1015,7 @@
|
|||||||
{
|
{
|
||||||
"name": "product_items_title",
|
"name": "product_items_title",
|
||||||
"type": "text",
|
"type": "text",
|
||||||
"content": "Product Item Details",
|
"content": "",
|
||||||
"position": {
|
"position": {
|
||||||
"x": 12,
|
"x": 12,
|
||||||
"y": 22
|
"y": 22
|
||||||
@@ -1050,7 +1042,7 @@
|
|||||||
{
|
{
|
||||||
"name": "product_items_subtitle",
|
"name": "product_items_subtitle",
|
||||||
"type": "text",
|
"type": "text",
|
||||||
"content": "Detailed quotation item list",
|
"content": "",
|
||||||
"position": {
|
"position": {
|
||||||
"x": 12,
|
"x": 12,
|
||||||
"y": 29
|
"y": 29
|
||||||
@@ -1111,15 +1103,7 @@
|
|||||||
"Discount",
|
"Discount",
|
||||||
"Total"
|
"Total"
|
||||||
],
|
],
|
||||||
"headWidthPercentages": [
|
"headWidthPercentages": [8, 34, 8, 10, 14, 12, 14],
|
||||||
8,
|
|
||||||
34,
|
|
||||||
8,
|
|
||||||
10,
|
|
||||||
14,
|
|
||||||
12,
|
|
||||||
14
|
|
||||||
],
|
|
||||||
"tableStyles": {
|
"tableStyles": {
|
||||||
"borderWidth": 0.2,
|
"borderWidth": 0.2,
|
||||||
"borderColor": "#000000"
|
"borderColor": "#000000"
|
||||||
@@ -1376,12 +1360,8 @@
|
|||||||
"content": "[[\"Topic:\"]]",
|
"content": "[[\"Topic:\"]]",
|
||||||
"showHead": false,
|
"showHead": false,
|
||||||
"repeatHead": false,
|
"repeatHead": false,
|
||||||
"head": [
|
"head": ["Terms of payment upon progress of each item:"],
|
||||||
"Terms of payment upon progress of each item:"
|
"headWidthPercentages": [100],
|
||||||
],
|
|
||||||
"headWidthPercentages": [
|
|
||||||
100
|
|
||||||
],
|
|
||||||
"tableStyles": {
|
"tableStyles": {
|
||||||
"borderWidth": 0,
|
"borderWidth": 0,
|
||||||
"borderColor": "#000000"
|
"borderColor": "#000000"
|
||||||
@@ -1449,12 +1429,8 @@
|
|||||||
"content": "[[\"{item_topic}\"]]",
|
"content": "[[\"{item_topic}\"]]",
|
||||||
"showHead": false,
|
"showHead": false,
|
||||||
"repeatHead": false,
|
"repeatHead": false,
|
||||||
"head": [
|
"head": ["Head 2"],
|
||||||
"Head 2"
|
"headWidthPercentages": [100],
|
||||||
],
|
|
||||||
"headWidthPercentages": [
|
|
||||||
100
|
|
||||||
],
|
|
||||||
"tableStyles": {
|
"tableStyles": {
|
||||||
"borderWidth": 0,
|
"borderWidth": 0,
|
||||||
"borderColor": "#000000"
|
"borderColor": "#000000"
|
||||||
@@ -1731,12 +1707,7 @@
|
|||||||
"basePdf": {
|
"basePdf": {
|
||||||
"width": 210,
|
"width": 210,
|
||||||
"height": 297,
|
"height": 297,
|
||||||
"padding": [
|
"padding": [31, 10, 25, 10],
|
||||||
31,
|
|
||||||
10,
|
|
||||||
25,
|
|
||||||
10
|
|
||||||
],
|
|
||||||
"staticSchema": [
|
"staticSchema": [
|
||||||
{
|
{
|
||||||
"name": "field1",
|
"name": "field1",
|
||||||
|
|||||||
Reference in New Issue
Block a user