task-h.1 complate
This commit is contained in:
@@ -30,9 +30,6 @@ export async function GET(_request: NextRequest, { params }: Params) {
|
||||
if (error instanceof AuthError) {
|
||||
return NextResponse.json({ message: error.message }, { status: error.status });
|
||||
}
|
||||
if (error instanceof Error) {
|
||||
return NextResponse.json({ message: error.message }, { status: 400 });
|
||||
}
|
||||
return NextResponse.json({ message: 'Unable to load approved PDF' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
@@ -70,9 +67,6 @@ export async function POST(_request: NextRequest, { params }: Params) {
|
||||
if (error instanceof AuthError) {
|
||||
return NextResponse.json({ message: error.message }, { status: error.status });
|
||||
}
|
||||
if (error instanceof Error) {
|
||||
return NextResponse.json({ message: error.message }, { status: 400 });
|
||||
}
|
||||
return NextResponse.json({ message: 'Unable to generate approved PDF' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,9 +25,6 @@ export async function GET(_request: NextRequest, { params }: Params) {
|
||||
if (error instanceof AuthError) {
|
||||
return NextResponse.json({ message: error.message }, { status: error.status });
|
||||
}
|
||||
if (error instanceof Error) {
|
||||
return NextResponse.json({ message: error.message }, { status: 400 });
|
||||
}
|
||||
return NextResponse.json({ message: 'Unable to generate quotation PDF' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,9 +25,6 @@ export async function GET(_request: NextRequest, { params }: Params) {
|
||||
if (error instanceof AuthError) {
|
||||
return NextResponse.json({ message: error.message }, { status: error.status });
|
||||
}
|
||||
if (error instanceof Error) {
|
||||
return NextResponse.json({ message: error.message }, { status: 400 });
|
||||
}
|
||||
return NextResponse.json({ message: 'Unable to generate preview PDF' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,7 +35,8 @@ const DOCUMENT_OPTION_CATEGORIES = {
|
||||
discountType: 'crm_discount_type',
|
||||
unit: 'crm_unit',
|
||||
customerRole: 'crm_quotation_customer_role',
|
||||
productType: 'crm_product_type'
|
||||
productType: 'crm_product_type',
|
||||
topicType: 'crm_quotation_topic_type'
|
||||
} as const;
|
||||
|
||||
function toRevisionLabel(revision: number) {
|
||||
@@ -76,6 +77,72 @@ function findOptionCode(
|
||||
return id ? options.find((option) => option.id === id)?.code ?? null : null;
|
||||
}
|
||||
|
||||
function extractSinglePlaceholder(value: string) {
|
||||
const matches = [...value.matchAll(/\{([^}]+)\}/g)].map((match) => match[1]);
|
||||
const uniqueMatches = [...new Set(matches)];
|
||||
|
||||
return uniqueMatches.length === 1 ? uniqueMatches[0] : null;
|
||||
}
|
||||
|
||||
function normalizePdfmeTemplateInput(
|
||||
schemaJson: unknown,
|
||||
templateInput: Record<string, unknown>
|
||||
) {
|
||||
const pages =
|
||||
schemaJson && typeof schemaJson === 'object' && 'schemas' in schemaJson
|
||||
? (schemaJson as { schemas?: unknown }).schemas
|
||||
: null;
|
||||
|
||||
if (!Array.isArray(pages)) {
|
||||
return templateInput;
|
||||
}
|
||||
|
||||
const normalized = { ...templateInput };
|
||||
|
||||
for (const page of pages) {
|
||||
if (!Array.isArray(page)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
for (const field of page) {
|
||||
if (!field || typeof field !== 'object') {
|
||||
continue;
|
||||
}
|
||||
|
||||
const tableField = field as { type?: unknown; content?: unknown };
|
||||
|
||||
if (tableField.type !== 'table' || typeof tableField.content !== 'string') {
|
||||
continue;
|
||||
}
|
||||
|
||||
const placeholderKey = extractSinglePlaceholder(tableField.content);
|
||||
|
||||
if (!placeholderKey) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const value = normalized[placeholderKey];
|
||||
|
||||
if (typeof value !== 'string') {
|
||||
continue;
|
||||
}
|
||||
|
||||
const lines = value
|
||||
.split('\n')
|
||||
.map((line) => line.trim())
|
||||
.filter(Boolean);
|
||||
|
||||
if (!lines.length) {
|
||||
continue;
|
||||
}
|
||||
|
||||
normalized[placeholderKey] = JSON.stringify(lines.map((line) => [line]));
|
||||
}
|
||||
}
|
||||
|
||||
return normalized;
|
||||
}
|
||||
|
||||
export async function buildQuotationDocumentData(
|
||||
quotationId: string,
|
||||
organizationId: string
|
||||
@@ -95,6 +162,7 @@ export async function buildQuotationDocumentData(
|
||||
unitOptions,
|
||||
customerRoleOptions,
|
||||
productTypeOptions,
|
||||
topicTypeOptions,
|
||||
approvalRequests
|
||||
] = await Promise.all([
|
||||
db
|
||||
@@ -114,6 +182,7 @@ export async function buildQuotationDocumentData(
|
||||
getActiveOptionsByCategory(DOCUMENT_OPTION_CATEGORIES.unit, { organizationId }),
|
||||
getActiveOptionsByCategory(DOCUMENT_OPTION_CATEGORIES.customerRole, { organizationId }),
|
||||
getActiveOptionsByCategory(DOCUMENT_OPTION_CATEGORIES.productType, { organizationId }),
|
||||
getActiveOptionsByCategory(DOCUMENT_OPTION_CATEGORIES.topicType, { organizationId }),
|
||||
listApprovalRequests(organizationId, { entityType: 'quotation', entityId: quotationId, limit: 20 })
|
||||
]);
|
||||
|
||||
@@ -180,6 +249,7 @@ export async function buildQuotationDocumentData(
|
||||
const relatedCustomerMap = new Map(relatedCustomers.map((item) => [item.id, item]));
|
||||
const statusCode = findOptionCode(statusOptions, quotation.status);
|
||||
const statusLabel = findOptionLabel(statusOptions, quotation.status);
|
||||
const topicCodeById = new Map(topicTypeOptions.map((option) => [option.id, option.code]));
|
||||
const approvalApprovers: QuotationDocumentApprovalStep[] =
|
||||
approvalDetail?.timeline
|
||||
.filter((item) => ['approve', 'reject', 'return'].includes(item.action))
|
||||
@@ -284,16 +354,19 @@ export async function buildQuotationDocumentData(
|
||||
})),
|
||||
topics: {
|
||||
scope: topics
|
||||
.filter((item) => item.topicType === 'scope')
|
||||
.filter((item) => topicCodeById.get(item.topicType) === 'scope')
|
||||
.map((item) => ({ title: item.title, items: item.items.map((child) => child.content) })),
|
||||
exclusion: topics
|
||||
.filter((item) => item.topicType === 'exclusion')
|
||||
.filter((item) => topicCodeById.get(item.topicType) === 'exclusion')
|
||||
.map((item) => ({ title: item.title, items: item.items.map((child) => child.content) })),
|
||||
payment: topics
|
||||
.filter((item) => item.topicType === 'payment')
|
||||
.filter((item) => topicCodeById.get(item.topicType) === 'payment')
|
||||
.map((item) => ({ title: item.title, items: item.items.map((child) => child.content) })),
|
||||
other: topics
|
||||
.filter((item) => !['scope', 'exclusion', 'payment'].includes(item.topicType))
|
||||
.filter((item) => {
|
||||
const topicCode = topicCodeById.get(item.topicType);
|
||||
return !['scope', 'exclusion', 'payment'].includes(topicCode ?? '');
|
||||
})
|
||||
.map((item) => ({ title: item.title, items: item.items.map((child) => child.content) }))
|
||||
},
|
||||
approval: {
|
||||
@@ -330,9 +403,14 @@ export async function getQuotationDocumentPreviewData(
|
||||
fileType: 'pdfme'
|
||||
});
|
||||
const mappings = await resolveTemplateMappings(template.version.id, organizationId);
|
||||
const templateInput = mapDocumentDataToTemplateInput(
|
||||
documentData as unknown as Record<string, unknown>,
|
||||
mappings
|
||||
|
||||
if (!mappings.length) {
|
||||
throw new AuthError('Document template mappings are not configured for this template version', 409);
|
||||
}
|
||||
|
||||
const templateInput = normalizePdfmeTemplateInput(
|
||||
template.version.schemaJson,
|
||||
mapDocumentDataToTemplateInput(documentData as unknown as Record<string, unknown>, mappings)
|
||||
);
|
||||
|
||||
return {
|
||||
|
||||
@@ -350,6 +350,7 @@ async function syncEntityStatusFromApproval(
|
||||
}
|
||||
|
||||
if (approvalStatus === APPROVAL_REQUEST_STATUSES.approved) {
|
||||
// TODO(task-h.1): add an async hook/job to auto-generate approved PDFs after final approval.
|
||||
await syncQuotationStatusFromApproval(entityId, organizationId, userId, 'approved');
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -107,13 +107,17 @@ export async function generatePdfFromTemplate(input: GeneratePdfFromTemplateInpu
|
||||
error instanceof Error &&
|
||||
(error.message.includes('Font "') || error.message.includes('fontKitFont.layout'))
|
||||
) {
|
||||
pdf = await generate({
|
||||
...generatorInput,
|
||||
template: normalizeTemplateFonts(input.template),
|
||||
options: undefined
|
||||
});
|
||||
try {
|
||||
pdf = await generate({
|
||||
...generatorInput,
|
||||
template: normalizeTemplateFonts(input.template),
|
||||
options: undefined
|
||||
});
|
||||
} catch {
|
||||
throw new AuthError('Unable to generate PDF from the current template', 500);
|
||||
}
|
||||
} else {
|
||||
throw error;
|
||||
throw new AuthError('Unable to generate PDF from the current template', 500);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -155,8 +159,15 @@ async function persistApprovedQuotationArtifact(args: {
|
||||
const absolutePath = path.join(directory, safeFileName);
|
||||
const publicPath = `/generated/quotations/${args.organizationId}/${safeFileName}`;
|
||||
|
||||
await writeFile(absolutePath, args.buffer);
|
||||
const fileStats = await stat(absolutePath);
|
||||
try {
|
||||
await writeFile(absolutePath, args.buffer);
|
||||
} catch {
|
||||
throw new AuthError('Unable to persist approved PDF to local storage', 500);
|
||||
}
|
||||
|
||||
const fileStats = await stat(absolutePath).catch(() => {
|
||||
throw new AuthError('Approved PDF was written but could not be verified on disk', 500);
|
||||
});
|
||||
|
||||
await db
|
||||
.update(crmQuotations)
|
||||
@@ -289,7 +300,9 @@ export async function getStoredApprovedQuotationPdf(
|
||||
|
||||
const relativePath = quotation.approvedPdfUrl.replace(/^\//, '');
|
||||
const absolutePath = path.join(process.cwd(), 'public', relativePath);
|
||||
const buffer = await readFile(absolutePath);
|
||||
const buffer = await readFile(absolutePath).catch(() => {
|
||||
throw new AuthError('Approved PDF file is missing from local storage', 404);
|
||||
});
|
||||
|
||||
return {
|
||||
buffer,
|
||||
|
||||
Reference in New Issue
Block a user