task-p.7.1.1

This commit is contained in:
phaichayon
2026-06-30 20:42:28 +07:00
parent e5766ea311
commit 8feaee61cb
29 changed files with 2673 additions and 875 deletions

View File

@@ -5,6 +5,7 @@ import {
createQuotation,
createQuotationAttachment,
createQuotationCustomer,
generateQuotationCustomerPackage,
createQuotationFollowup,
createQuotationItem,
createQuotationRevision,
@@ -374,3 +375,13 @@ export const createQuotationRevisionMutation = mutationOptions({
}
}
});
export const generateQuotationCustomerPackageMutation = mutationOptions({
mutationFn: ({ quotationId }: { quotationId: string }) =>
generateQuotationCustomerPackage(quotationId),
onSettled: async (_data, error, variables) => {
if (!error) {
await invalidateQuotationHeaderQueries(variables.quotationId);
}
}
});

View File

@@ -3,6 +3,7 @@ import type {
MutationSuccessResponse,
QuotationAttachmentMutationPayload,
QuotationAttachmentsResponse,
QuotationCustomerPackageGenerateResponse,
QuotationCustomerMutationPayload,
QuotationCustomersResponse,
QuotationDetailResponse,
@@ -41,6 +42,37 @@ export async function getQuotationById(id: string): Promise<QuotationDetailRespo
return apiClient<QuotationDetailResponse>(`/crm/quotations/${id}`);
}
export async function generateQuotationCustomerPackage(
quotationId: string
): Promise<QuotationCustomerPackageGenerateResponse> {
return apiClient<QuotationCustomerPackageGenerateResponse>(
`/crm/quotations/${quotationId}/assembled-pdf`,
{
method: 'POST'
}
);
}
export async function downloadQuotationCustomerPackage(quotationId: string): Promise<{
blob: Blob;
fileName: string;
}> {
const response = await fetch(`/api/crm/quotations/${quotationId}/assembled-pdf/download`);
if (!response.ok) {
const payload = (await response.json().catch(() => null)) as { message?: string } | null;
throw new Error(payload?.message ?? 'Failed to download customer package');
}
const contentDisposition = response.headers.get('content-disposition') ?? '';
const fileNameMatch = contentDisposition.match(/filename="?([^"]+)"?/i);
return {
blob: await response.blob(),
fileName: fileNameMatch?.[1] ?? `${quotationId}-customer-package.pdf`
};
}
export async function createQuotation(data: QuotationMutationPayload) {
return apiClient<MutationSuccessResponse>('/crm/quotations', {
method: 'POST',
@@ -212,4 +244,3 @@ export async function createQuotationRevision(id: string, revisionRemark?: strin
body: JSON.stringify({ revisionRemark })
});
}

View File

@@ -59,6 +59,37 @@ export interface QuotationApprovedArtifactSummary {
lockedByName: string | null;
}
export interface QuotationCustomerPackageSource {
role: string;
sourceType: 'generated' | 'document_library';
fileName: string;
checksum: string | null;
fileSize: number | null;
pageCount: number | null;
metadata: Record<string, unknown> | null;
}
export interface QuotationCustomerPackageWarning {
code: string;
role: string;
message: string;
}
export interface QuotationCustomerPackageSummary {
artifactId: string;
fileName: string;
contentType: string;
fileSize: number | null;
checksum: string | null;
status: string;
pageCount: number | null;
generatedAt: string;
generatedBy: string;
generatedByName: string | null;
includedDocuments: QuotationCustomerPackageSource[];
warnings: QuotationCustomerPackageWarning[];
}
export interface QuotationRecord {
id: string;
organizationId: string;
@@ -103,6 +134,7 @@ export interface QuotationRecord {
approvedSnapshot: unknown | null;
approvedTemplateVersionId: string | null;
approvedArtifact: QuotationApprovedArtifactSummary | null;
customerPackage: QuotationCustomerPackageSummary | null;
hasLegacyApprovedPdf: boolean;
acceptedAt: string | null;
rejectedAt: string | null;
@@ -432,3 +464,20 @@ export interface MutationSuccessResponse {
success: boolean;
message: string;
}
export interface QuotationCustomerPackageGenerateResponse {
success: boolean;
fileName: string;
checksum: string;
fileSize: number;
pageCount: number;
warnings: QuotationCustomerPackageWarning[];
artifact: {
artifactId: string;
storageKey: string;
fileName: string;
checksum: string;
fileSize: number;
pageCount: number;
} | null;
}

View File

@@ -40,6 +40,7 @@ import {
createQuotationAttachmentMutation,
createQuotationCustomerMutation,
createQuotationFollowupMutation,
generateQuotationCustomerPackageMutation,
createQuotationItemMutation,
createQuotationRevisionMutation,
createQuotationTopicMutation,
@@ -64,10 +65,12 @@ import {
quotationRevisionsOptions,
quotationTopicsOptions
} from '../api/queries';
import { downloadQuotationCustomerPackage } from '../api/service';
import type {
QuotationAttachmentMutationPayload,
QuotationAttachmentRecord,
QuotationCustomerListItem,
QuotationCustomerPackageSource,
QuotationCustomerMutationPayload,
QuotationFollowupMutationPayload,
QuotationFollowupRecord,
@@ -79,6 +82,11 @@ import type {
} from '../api/types';
import { submitQuotationApprovalMutation } from '@/features/foundation/approval/mutations';
import { quotationDocumentKeys } from '@/features/crm/quotations/document/queries';
import {
getCustomerPackageRoleLabel,
getCustomerPackageUiStatus,
mapCustomerPackageErrorMessage
} from '../customer-package';
import { QuotationDocumentPreview } from './quotation-document-preview';
import { QuotationFormSheet } from './quotation-form-sheet';
import { QuotationApprovalTab } from './quotation-approval-tab';
@@ -102,6 +110,27 @@ function EmptyState({ message }: { message: string }) {
);
}
function CustomerPackageSourceItem({
item
}: {
item: QuotationCustomerPackageSource;
}) {
return (
<div className='rounded-lg border p-3'>
<div className='flex flex-wrap items-center gap-2'>
<span className='font-medium'>{getCustomerPackageRoleLabel(item.role)}</span>
<Badge variant='outline'>{item.sourceType === 'generated' ? 'Generated' : 'Library'}</Badge>
</div>
<div className='text-muted-foreground mt-2 text-sm'>{item.fileName}</div>
<div className='text-muted-foreground mt-1 text-xs'>
{item.pageCount ? `${formatNumber(item.pageCount)} page(s)` : 'Page count unavailable'}
{item.fileSize ? `${formatNumber(item.fileSize)} bytes` : ''}
{item.checksum ? `${item.checksum.slice(0, 16)}` : ''}
</div>
</div>
);
}
function ItemDialog({
open,
onOpenChange,
@@ -673,7 +702,10 @@ export function QuotationDetail({
currentUserId,
canPreviewDocument,
canPreviewPdf,
canPreviewCustomerPackage,
canDownloadPdf,
canGenerateCustomerPackage,
canDownloadCustomerPackage,
canGenerateApprovedPdf
}: {
quotationId: string;
@@ -694,7 +726,10 @@ export function QuotationDetail({
currentUserId: string;
canPreviewDocument: boolean;
canPreviewPdf: boolean;
canPreviewCustomerPackage: boolean;
canDownloadPdf: boolean;
canGenerateCustomerPackage: boolean;
canDownloadCustomerPackage: boolean;
canGenerateApprovedPdf: boolean;
}) {
const { data } = useSuspenseQuery(quotationByIdOptions(quotationId));
@@ -705,6 +740,12 @@ export function QuotationDetail({
const { data: attachmentsData } = useSuspenseQuery(quotationAttachmentsOptions(quotationId));
const { data: revisionsData } = useSuspenseQuery(quotationRevisionsOptions(quotationId));
const quotation = data.quotation;
const [customerPackageError, setCustomerPackageError] = useState<string | null>(null);
const [confirmRegenerateOpen, setConfirmRegenerateOpen] = useState(false);
const customerPackageStatus = getCustomerPackageUiStatus({
customerPackage: quotation.customerPackage,
lastErrorMessage: customerPackageError
});
const [editOpen, setEditOpen] = useState(false);
const [itemOpen, setItemOpen] = useState(false);
@@ -844,6 +885,44 @@ export function QuotationDetail({
onError: (error) =>
toast.error(error instanceof Error ? error.message : 'Failed to submit for approval')
});
const generateCustomerPackage = useMutation({
...generateQuotationCustomerPackageMutation,
onSuccess: () => {
setCustomerPackageError(null);
setConfirmRegenerateOpen(false);
toast.success('Customer package generated successfully');
},
onError: (error) => {
const message = mapCustomerPackageErrorMessage(
error instanceof Error ? error.message : null
);
setCustomerPackageError(message);
setConfirmRegenerateOpen(false);
toast.error(message);
}
});
const downloadCustomerPackage = useMutation({
mutationFn: async () => downloadQuotationCustomerPackage(quotationId),
onSuccess: async ({ blob, fileName }) => {
setCustomerPackageError(null);
const url = URL.createObjectURL(blob);
const anchor = document.createElement('a');
anchor.href = url;
anchor.download = fileName;
document.body.append(anchor);
anchor.click();
anchor.remove();
URL.revokeObjectURL(url);
toast.success('Customer package downloaded successfully');
},
onError: (error) => {
const message = mapCustomerPackageErrorMessage(
error instanceof Error ? error.message : null
);
setCustomerPackageError(message);
toast.error(message);
}
});
const generateApprovedPdf = useMutation({
mutationFn: async () => {
const response = await fetch(`/api/crm/quotations/${quotationId}/approved-pdf`, {
@@ -918,16 +997,30 @@ export function QuotationDetail({
() => new Map(referenceData.topicTypes.map((item) => [item.id, item])),
[referenceData.topicTypes]
);
const followupTypeMap = useMemo(
() => new Map(referenceData.followupTypes.map((item) => [item.id, item])),
[referenceData.followupTypes]
);
const customer = customerMap.get(quotation.customerId);
const contact = quotation.contactId ? contactMap.get(quotation.contactId) : null;
const status = statusMap.get(quotation.status);
const canReviseByStatus = ['accepted', 'rejected', 'sent', 'approved'].includes(
status?.code ?? ''
);
const followupTypeMap = useMemo(
() => new Map(referenceData.followupTypes.map((item) => [item.id, item])),
[referenceData.followupTypes]
);
const customer = customerMap.get(quotation.customerId);
const contact = quotation.contactId ? contactMap.get(quotation.contactId) : null;
const status = statusMap.get(quotation.status);
const customerPackage = quotation.customerPackage;
const customerPackageActionLabel = customerPackage ? 'Regenerate Package' : 'Generate Customer Package';
const customerPackageStatusLabel =
customerPackageStatus === 'generated'
? 'Generated'
: customerPackageStatus === 'failed'
? 'Failed'
: 'Not Generated';
const customerPackageStatusDescription =
customerPackageStatus === 'generated'
? 'Customer-facing assembled PDF is ready.'
: customerPackageStatus === 'failed'
? customerPackageError
: 'Generate the package before downloading or previewing it.';
const canReviseByStatus = ['accepted', 'rejected', 'sent', 'approved'].includes(
status?.code ?? ''
);
const canSubmitCurrentQuotation =
canSubmitApproval && ['draft', 'revised'].includes(status?.code ?? '');
const canGenerateApprovedNow = canGenerateApprovedPdf && (status?.code ?? '') === 'approved';
@@ -1075,6 +1168,31 @@ export function QuotationDetail({
onOpenChange={setEditOpen}
referenceData={referenceData}
/>
<Dialog open={confirmRegenerateOpen} onOpenChange={setConfirmRegenerateOpen}>
<DialogContent>
<DialogHeader>
<DialogTitle>Regenerate Customer Package?</DialogTitle>
<DialogDescription>
A new assembled package artifact will replace the current active package for this quotation.
</DialogDescription>
</DialogHeader>
<DialogFooter>
<Button
variant='outline'
onClick={() => setConfirmRegenerateOpen(false)}
disabled={generateCustomerPackage.isPending}
>
Cancel
</Button>
<Button
isLoading={generateCustomerPackage.isPending}
onClick={() => generateCustomerPackage.mutate({ quotationId })}
>
Continue
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
<div className='flex flex-col gap-4 rounded-xl border p-5 lg:flex-row lg:items-start lg:justify-between'>
<div className='space-y-3'>
@@ -1162,6 +1280,131 @@ export function QuotationDetail({
</div>
</div>
<Card>
<CardHeader className='flex flex-col gap-4 lg:flex-row lg:items-start lg:justify-between'>
<div>
<CardTitle>Customer Package</CardTitle>
<CardDescription>
Generate and download the assembled customer-facing quotation package.
</CardDescription>
</div>
<div className='flex flex-wrap gap-2'>
{canGenerateCustomerPackage ? (
<Button
variant='outline'
isLoading={generateCustomerPackage.isPending}
onClick={() =>
customerPackage
? setConfirmRegenerateOpen(true)
: generateCustomerPackage.mutate({ quotationId })
}
>
<Icons.badgeCheck className='mr-2 h-4 w-4' />
{customerPackageActionLabel}
</Button>
) : null}
{canPreviewCustomerPackage ? (
<Button asChild variant='outline' disabled={!customerPackage}>
<Link
href={`/api/crm/quotations/${quotationId}/assembled-pdf`}
target='_blank'
aria-disabled={!customerPackage}
onClick={(event) => {
if (!customerPackage) {
event.preventDefault();
}
}}
>
<Icons.eyeOff className='mr-2 h-4 w-4' /> View Customer Package
</Link>
</Button>
) : null}
{canDownloadCustomerPackage ? (
<Button
variant='outline'
disabled={!customerPackage}
isLoading={downloadCustomerPackage.isPending}
onClick={() => downloadCustomerPackage.mutate()}
>
<Icons.download className='mr-2 h-4 w-4' /> Download Customer Package
</Button>
) : null}
</div>
</CardHeader>
<CardContent className='space-y-4'>
{!canGenerateCustomerPackage && !canDownloadCustomerPackage && !canPreviewCustomerPackage ? (
<EmptyState message='You do not have permission to generate or download customer package.' />
) : (
<>
<div className='grid gap-4 md:grid-cols-2 xl:grid-cols-4'>
<FieldItem label='Status' value={customerPackageStatusLabel} />
<FieldItem
label='Generated At'
value={customerPackage?.generatedAt ? formatDateTime(customerPackage.generatedAt) : null}
/>
<FieldItem
label='Generated By'
value={customerPackage?.generatedByName ?? customerPackage?.generatedBy ?? null}
/>
<FieldItem
label='Page Count'
value={
customerPackage?.pageCount !== null && customerPackage?.pageCount !== undefined
? String(customerPackage.pageCount)
: null
}
/>
<FieldItem
label='File Size'
value={customerPackage?.fileSize ? `${formatNumber(customerPackage.fileSize)} bytes` : null}
/>
<FieldItem
label='Checksum'
value={customerPackage?.checksum ? customerPackage.checksum.slice(0, 16) : null}
/>
<FieldItem label='File Name' value={customerPackage?.fileName ?? null} />
<FieldItem
label='Included Documents'
value={
customerPackage?.includedDocuments.length
? String(customerPackage.includedDocuments.length)
: null
}
/>
</div>
<div className='rounded-lg border border-dashed p-4 text-sm'>
{customerPackageStatusDescription}
</div>
{customerPackage?.warnings.length ? (
<div className='space-y-2'>
<div className='text-sm font-medium'>Warnings</div>
{customerPackage.warnings.map((warning) => (
<div key={`${warning.code}-${warning.role}`} className='rounded-lg border p-3 text-sm'>
{warning.message}
</div>
))}
</div>
) : null}
<div className='space-y-3'>
<div className='text-sm font-medium'>Included Documents</div>
{!customerPackage?.includedDocuments.length ? (
<EmptyState message='No assembled package has been generated yet.' />
) : (
<div className='grid gap-3 md:grid-cols-2'>
{customerPackage.includedDocuments.map((item) => (
<CustomerPackageSourceItem
key={`${item.role}-${item.fileName}-${item.checksum ?? 'no-checksum'}`}
item={item}
/>
))}
</div>
)}
</div>
</>
)}
</CardContent>
</Card>
<div className='grid gap-6 lg:grid-cols-3'>
<div className='space-y-6 lg:col-span-2'>
<Card>

View File

@@ -0,0 +1,72 @@
import assert from 'node:assert/strict';
import test from 'node:test';
import {
getCustomerPackageRoleLabel,
getCustomerPackageUiStatus,
mapCustomerPackageErrorMessage
} from './customer-package';
test('mapCustomerPackageErrorMessage rewrites not-generated error', () => {
assert.equal(
mapCustomerPackageErrorMessage('Assembled quotation PDF has not been generated yet.'),
'Customer package has not been generated yet. Please generate it first.'
);
});
test('mapCustomerPackageErrorMessage rewrites missing document error', () => {
assert.equal(
mapCustomerPackageErrorMessage(
'Missing SLA document (SLA, brand generic, language th, product type crane).'
),
'Cannot generate package because required SLA document is missing.'
);
});
test('mapCustomerPackageErrorMessage rewrites forbidden error', () => {
assert.equal(
mapCustomerPackageErrorMessage('Forbidden'),
'You do not have permission to generate or download customer package.'
);
});
test('getCustomerPackageRoleLabel falls back to humanized labels', () => {
assert.equal(getCustomerPackageRoleLabel('main'), 'Quotation');
assert.equal(getCustomerPackageRoleLabel('custom_appendix'), 'Custom Appendix');
});
test('getCustomerPackageUiStatus reflects generated, failed, and not-generated states', () => {
assert.equal(
getCustomerPackageUiStatus({
customerPackage: {
artifactId: 'artifact-1',
fileName: 'pkg.pdf',
contentType: 'application/pdf',
fileSize: 100,
checksum: 'abc',
status: 'active',
pageCount: 2,
generatedAt: '2026-06-30T00:00:00.000Z',
generatedBy: 'user-1',
generatedByName: 'User',
includedDocuments: [],
warnings: []
},
lastErrorMessage: 'Something failed before'
}),
'generated'
);
assert.equal(
getCustomerPackageUiStatus({
customerPackage: null,
lastErrorMessage: 'Customer package generation failed.'
}),
'failed'
);
assert.equal(
getCustomerPackageUiStatus({
customerPackage: null,
lastErrorMessage: null
}),
'not_generated'
);
});

View File

@@ -0,0 +1,65 @@
import type { QuotationCustomerPackageSummary } from './api/types';
const CUSTOMER_PACKAGE_ROLE_LABELS: Record<string, string> = {
main: 'Quotation',
sla: 'SLA',
warranty: 'Warranty',
datasheet: 'Datasheet',
drawing: 'Drawing'
};
export type CustomerPackageUiStatus = 'not_generated' | 'generated' | 'failed';
export function getCustomerPackageRoleLabel(role: string): string {
return (
CUSTOMER_PACKAGE_ROLE_LABELS[role] ??
role
.split(/[_\s-]+/)
.filter(Boolean)
.map((part) => part.charAt(0).toUpperCase() + part.slice(1))
.join(' ')
);
}
export function mapCustomerPackageErrorMessage(message?: string | null): string {
const normalized = message?.trim();
if (!normalized) {
return 'Customer package generation failed. Please check document library configuration.';
}
if (
normalized === 'Forbidden' ||
normalized.toLowerCase().includes('permission') ||
normalized.toLowerCase().includes('forbidden')
) {
return 'You do not have permission to generate or download customer package.';
}
if (normalized === 'Assembled quotation PDF has not been generated yet.') {
return 'Customer package has not been generated yet. Please generate it first.';
}
const missingRequiredDocument = normalized.match(/^Missing ([A-Z_ ]+) document/i);
if (missingRequiredDocument) {
const role = getCustomerPackageRoleLabel(missingRequiredDocument[1].toLowerCase());
return `Cannot generate package because required ${role} document is missing.`;
}
return normalized;
}
export function getCustomerPackageUiStatus(input: {
customerPackage: QuotationCustomerPackageSummary | null;
lastErrorMessage?: string | null;
}): CustomerPackageUiStatus {
if (input.customerPackage) {
return 'generated';
}
if (input.lastErrorMessage) {
return 'failed';
}
return 'not_generated';
}

View File

@@ -19,6 +19,10 @@ import { AuthError } from '@/lib/auth/session';
import { hasBranchScopeAccess, hasProductScopeAccess } from '@/lib/auth/crm-access';
import { listAuditLogs } from '@/features/foundation/audit-log/service';
import { getUserBranches, validateBranchAccess } from '@/features/foundation/branch-scope/service';
import {
getActiveArtifactForEntity,
type DocumentArtifactRecord
} from '@/features/foundation/document-artifact/server/service';
import {
type CrmSecurityContext,
canAccessScopedCrmRecord
@@ -47,6 +51,9 @@ import type {
QuotationApprovedArtifactSummary,
QuotationBranchOption,
QuotationContactLookup,
QuotationCustomerPackageSource,
QuotationCustomerPackageSummary,
QuotationCustomerPackageWarning,
QuotationCustomerListItem,
QuotationCustomerLookup,
QuotationCustomerMutationPayload,
@@ -133,10 +140,99 @@ function mapApprovedArtifactSummary(
};
}
function toCustomerPackageSource(
value: unknown
): QuotationCustomerPackageSource | null {
if (!value || typeof value !== 'object' || Array.isArray(value)) {
return null;
}
const source = value as Record<string, unknown>;
if (typeof source.role !== 'string' || typeof source.fileName !== 'string') {
return null;
}
if (source.sourceType !== 'generated' && source.sourceType !== 'document_library') {
return null;
}
return {
role: source.role,
sourceType: source.sourceType,
fileName: source.fileName,
checksum: typeof source.checksum === 'string' ? source.checksum : null,
fileSize: typeof source.fileSize === 'number' ? source.fileSize : null,
pageCount: typeof source.pageCount === 'number' ? source.pageCount : null,
metadata:
source.metadata && typeof source.metadata === 'object' && !Array.isArray(source.metadata)
? (source.metadata as Record<string, unknown>)
: null
};
}
function toCustomerPackageWarning(
value: unknown
): QuotationCustomerPackageWarning | null {
if (!value || typeof value !== 'object' || Array.isArray(value)) {
return null;
}
const warning = value as Record<string, unknown>;
if (
typeof warning.code !== 'string' ||
typeof warning.role !== 'string' ||
typeof warning.message !== 'string'
) {
return null;
}
return {
code: warning.code,
role: warning.role,
message: warning.message
};
}
function mapCustomerPackageSummary(
artifact: DocumentArtifactRecord
): QuotationCustomerPackageSummary {
const metadata =
artifact.metadata && typeof artifact.metadata === 'object' && !Array.isArray(artifact.metadata)
? (artifact.metadata as Record<string, unknown>)
: null;
const includedDocuments = Array.isArray(metadata?.sourceArtifacts)
? metadata.sourceArtifacts
.map((item) => toCustomerPackageSource(item))
.filter((item): item is QuotationCustomerPackageSource => item !== null)
: [];
const warnings = Array.isArray(metadata?.warnings)
? metadata.warnings
.map((item) => toCustomerPackageWarning(item))
.filter((item): item is QuotationCustomerPackageWarning => item !== null)
: [];
return {
artifactId: artifact.id,
fileName: artifact.fileName,
contentType: artifact.contentType,
fileSize: artifact.fileSize,
checksum: artifact.checksum,
status: artifact.status,
pageCount: typeof metadata?.pageCount === 'number' ? metadata.pageCount : null,
generatedAt: artifact.generatedAt,
generatedBy: artifact.generatedBy,
generatedByName: artifact.generatedByName,
includedDocuments,
warnings
};
}
function mapQuotationRecord(
row: typeof crmQuotations.$inferSelect,
options?: {
approvedArtifact?: QuotationApprovedArtifactSummary | null;
customerPackage?: QuotationCustomerPackageSummary | null;
branchName?: string | null;
branchCode?: string | null;
salesmanName?: string | null;
@@ -188,6 +284,7 @@ function mapQuotationRecord(
approvedSnapshot: row.approvedSnapshot ?? null,
approvedTemplateVersionId: row.approvedTemplateVersionId,
approvedArtifact: options?.approvedArtifact ?? null,
customerPackage: options?.customerPackage ?? null,
hasLegacyApprovedPdf:
!row.approvedArtifactId &&
typeof row.approvedPdfUrl === 'string' &&
@@ -1240,6 +1337,12 @@ export async function getQuotationDetail(
})
]);
const branchDisplay = quotation.branchId ? (branchDisplayMap.get(quotation.branchId) ?? null) : null;
const customerPackageArtifactPromise = getActiveArtifactForEntity({
organizationId,
entityType: 'crm_quotation',
entityId: id,
artifactType: 'assembled_pdf'
});
const baseDisplayOptions = {
branchName: branchDisplay?.name ?? null,
branchCode: branchDisplay?.code ?? null,
@@ -1256,7 +1359,13 @@ export async function getQuotationDetail(
: null);
if (!approvedArtifactId) {
return mapQuotationRecord(quotation, baseDisplayOptions);
const customerPackageArtifact = await customerPackageArtifactPromise;
return mapQuotationRecord(quotation, {
...baseDisplayOptions,
customerPackage: customerPackageArtifact
? mapCustomerPackageSummary(customerPackageArtifact)
: null
});
}
const [artifact] = await db
@@ -1272,10 +1381,19 @@ export async function getQuotationDetail(
.limit(1);
if (!artifact) {
return mapQuotationRecord(quotation);
const customerPackageArtifact = await customerPackageArtifactPromise;
return mapQuotationRecord(quotation, {
...baseDisplayOptions,
customerPackage: customerPackageArtifact
? mapCustomerPackageSummary(customerPackageArtifact)
: null
});
}
const artifactActorMap = await resolveUserDisplays([artifact.generatedBy, artifact.lockedBy]);
const [artifactActorMap, customerPackageArtifact] = await Promise.all([
resolveUserDisplays([artifact.generatedBy, artifact.lockedBy]),
customerPackageArtifactPromise
]);
return mapQuotationRecord(quotation, {
...baseDisplayOptions,
@@ -1284,7 +1402,10 @@ export async function getQuotationDetail(
lockedByName: artifact.lockedBy
? (artifactActorMap.get(artifact.lockedBy)?.displayName ?? null)
: null
})
}),
customerPackage: customerPackageArtifact
? mapCustomerPackageSummary(customerPackageArtifact)
: null
});
}