task-fic-display

This commit is contained in:
phaichayon
2026-06-27 22:14:16 +07:00
parent 1c019d12f3
commit 071293b60f
30 changed files with 2329 additions and 107 deletions

View File

@@ -63,6 +63,8 @@ export interface QuotationRecord {
id: string;
organizationId: string;
branchId: string | null;
branchName: string | null;
branchCode: string | null;
code: string;
opportunityId: string | null;
customerId: string;
@@ -91,6 +93,7 @@ export interface QuotationRecord {
isHotProject: boolean;
competitor: string | null;
salesmanId: string | null;
salesmanName: string | null;
isSent: boolean;
sentAt: string | null;
sentVia: string | null;
@@ -109,7 +112,9 @@ export interface QuotationRecord {
updatedAt: string;
deletedAt: string | null;
createdBy: string;
createdByName: string | null;
updatedBy: string;
updatedByName: string | null;
}
export interface QuotationListItem extends QuotationRecord {

View File

@@ -1716,8 +1716,8 @@ export function QuotationDetail({
<CardDescription>Operational metadata for this quotation row.</CardDescription>
</CardHeader>
<CardContent className='space-y-4'>
<FieldItem label='Created By' value={quotation.createdBy} />
<FieldItem label='Updated By' value={quotation.updatedBy} />
<FieldItem label='Created By' value={quotation.createdByName ?? quotation.createdBy} />
<FieldItem label='Updated By' value={quotation.updatedByName ?? quotation.updatedBy} />
<FieldItem
label='Created At'
value={formatDateTime(quotation.createdAt)}

View File

@@ -24,6 +24,10 @@ import {
canAccessScopedCrmRecord
} from '@/features/crm/security/server/service';
import { generateNextDocumentCode } from '@/features/foundation/document-sequence/service';
import {
resolveBranchDisplays,
resolveUserDisplays
} from '@/features/foundation/display/server/display-resolver';
import { getActiveOptionsByCategory } from '@/features/foundation/master-options/service';
import { syncOpportunityPipelineStageFromQuotationStatus } from '@/features/crm/opportunities/server/service';
import {
@@ -130,12 +134,21 @@ function mapApprovedArtifactSummary(
function mapQuotationRecord(
row: typeof crmQuotations.$inferSelect,
options?: { approvedArtifact?: QuotationApprovedArtifactSummary | null }
options?: {
approvedArtifact?: QuotationApprovedArtifactSummary | null;
branchName?: string | null;
branchCode?: string | null;
salesmanName?: string | null;
createdByName?: string | null;
updatedByName?: string | null;
}
): QuotationRecord {
return {
id: row.id,
organizationId: row.organizationId,
branchId: row.branchId,
branchName: options?.branchName ?? null,
branchCode: options?.branchCode ?? null,
code: row.code,
opportunityId: row.opportunityId,
customerId: row.customerId,
@@ -164,6 +177,7 @@ function mapQuotationRecord(
isHotProject: row.isHotProject,
competitor: row.competitor,
salesmanId: row.salesmanId,
salesmanName: options?.salesmanName ?? null,
isSent: row.isSent,
sentAt: row.sentAt?.toISOString() ?? null,
sentVia: row.sentVia,
@@ -185,7 +199,9 @@ function mapQuotationRecord(
updatedAt: row.updatedAt.toISOString(),
deletedAt: row.deletedAt?.toISOString() ?? null,
createdBy: row.createdBy,
updatedBy: row.updatedBy
createdByName: options?.createdByName ?? null,
updatedBy: row.updatedBy,
updatedByName: options?.updatedByName ?? null
};
}
@@ -1211,6 +1227,27 @@ export async function getQuotationDetail(
accessContext?: QuotationAccessContext
): Promise<QuotationRecord> {
const quotation = await assertQuotationBelongsToOrganization(id, organizationId, accessContext);
const [userDisplayMap, branchDisplayMap] = await Promise.all([
resolveUserDisplays([
quotation.salesmanId,
quotation.createdBy,
quotation.updatedBy
]),
resolveBranchDisplays({
organizationId,
branchIds: [quotation.branchId]
})
]);
const branchDisplay = quotation.branchId ? (branchDisplayMap.get(quotation.branchId) ?? null) : null;
const baseDisplayOptions = {
branchName: branchDisplay?.name ?? null,
branchCode: branchDisplay?.code ?? null,
salesmanName: quotation.salesmanId
? (userDisplayMap.get(quotation.salesmanId)?.displayName ?? null)
: null,
createdByName: userDisplayMap.get(quotation.createdBy)?.displayName ?? null,
updatedByName: userDisplayMap.get(quotation.updatedBy)?.displayName ?? null
};
const approvedArtifactId =
quotation.approvedArtifactId ??
(quotation.approvedPdfUrl?.startsWith('artifact:')
@@ -1218,7 +1255,7 @@ export async function getQuotationDetail(
: null);
if (!approvedArtifactId) {
return mapQuotationRecord(quotation);
return mapQuotationRecord(quotation, baseDisplayOptions);
}
const [artifact] = await db
@@ -1237,19 +1274,15 @@ export async function getQuotationDetail(
return mapQuotationRecord(quotation);
}
const relatedUserIds = [artifact.generatedBy, artifact.lockedBy].filter(Boolean) as string[];
const actorRows = relatedUserIds.length
? await db
.select({ id: users.id, name: users.name })
.from(users)
.where(inArray(users.id, relatedUserIds))
: [];
const actorMap = new Map(actorRows.map((row) => [row.id, row.name]));
const artifactActorMap = await resolveUserDisplays([artifact.generatedBy, artifact.lockedBy]);
return mapQuotationRecord(quotation, {
...baseDisplayOptions,
approvedArtifact: mapApprovedArtifactSummary(artifact, {
generatedByName: actorMap.get(artifact.generatedBy) ?? null,
lockedByName: artifact.lockedBy ? (actorMap.get(artifact.lockedBy) ?? null) : null
generatedByName: artifactActorMap.get(artifact.generatedBy)?.displayName ?? null,
lockedByName: artifact.lockedBy
? (artifactActorMap.get(artifact.lockedBy)?.displayName ?? null)
: null
})
});
}