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

@@ -17,6 +17,8 @@ export interface CustomerRecord {
id: string;
organizationId: string;
branchId: string | null;
branchName: string | null;
branchCode: string | null;
code: string;
name: string;
abbr: string | null;
@@ -47,7 +49,9 @@ export interface CustomerRecord {
updatedAt: string;
deletedAt: string | null;
createdBy: string;
createdByName: string | null;
updatedBy: string;
updatedByName: string | null;
}
export interface CustomerListItem extends CustomerRecord {
@@ -230,4 +234,3 @@ export interface MutationSuccessResponse {
success: boolean;
message: string;
}

View File

@@ -301,8 +301,8 @@ export function CustomerDetail({
<CardDescription></CardDescription>
</CardHeader>
<CardContent className='space-y-4'>
<FieldItem label='Created By' value={customer.createdBy} />
<FieldItem label='Updated By' value={customer.updatedBy} />
<FieldItem label='Created By' value={customer.createdByName ?? customer.createdBy} />
<FieldItem label='Updated By' value={customer.updatedByName ?? customer.updatedBy} />
<FieldItem label='Created At' value={formatDateTime(customer.createdAt)} />
<FieldItem label='Updated At' value={formatDateTime(customer.updatedAt)} />
</CardContent>

View File

@@ -34,6 +34,10 @@ import {
getUserBranches,
} from "@/features/foundation/branch-scope/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 {
canAccessContact,
@@ -64,6 +68,13 @@ type CustomerRecordSource = Omit<
ownerAssignedByName?: string | null;
};
type CustomerDisplayOptions = {
branchName?: string | null;
branchCode?: string | null;
createdByName?: string | null;
updatedByName?: string | null;
};
let customerSubGroupColumnAvailable: boolean | undefined;
export type CustomerAccessContext = CrmSecurityContext;
@@ -154,11 +165,16 @@ function toOptionalIsoDateTime(value: Date | string | null | undefined) {
return value ? toIsoDateTime(value) : null;
}
function mapCustomerRecord(row: CustomerRecordSource): CustomerRecord {
function mapCustomerRecord(
row: CustomerRecordSource,
options?: CustomerDisplayOptions,
): CustomerRecord {
return {
id: row.id,
organizationId: row.organizationId,
branchId: row.branchId,
branchName: options?.branchName ?? null,
branchCode: options?.branchCode ?? null,
code: row.code,
name: row.name,
abbr: row.abbr,
@@ -189,7 +205,9 @@ function mapCustomerRecord(row: CustomerRecordSource): CustomerRecord {
updatedAt: toIsoDateTime(row.updatedAt),
deletedAt: toOptionalIsoDateTime(row.deletedAt),
createdBy: row.createdBy,
createdByName: options?.createdByName ?? null,
updatedBy: row.updatedBy,
updatedByName: options?.updatedByName ?? null,
};
}
@@ -956,26 +974,35 @@ export async function getCustomerDetail(
organizationId,
accessContext,
);
const relatedUserIds = [
customer.ownerUserId,
customer.ownerAssignedBy,
].filter(Boolean) as string[];
const userRows = relatedUserIds.length
? await db
.select({ id: users.id, name: users.name })
.from(users)
.where(inArray(users.id, relatedUserIds))
: [];
const userMap = new Map(userRows.map((row) => [row.id, row.name]));
const [userDisplayMap, branchDisplayMap] = await Promise.all([
resolveUserDisplays([
customer.ownerUserId,
customer.ownerAssignedBy,
customer.createdBy,
customer.updatedBy,
]),
resolveBranchDisplays({
organizationId,
branchIds: [customer.branchId],
}),
]);
const branchDisplay = customer.branchId
? (branchDisplayMap.get(customer.branchId) ?? null)
: null;
return mapCustomerRecord({
...customer,
ownerName: customer.ownerUserId
? (userMap.get(customer.ownerUserId) ?? null)
? (userDisplayMap.get(customer.ownerUserId)?.displayName ?? null)
: null,
ownerAssignedByName: customer.ownerAssignedBy
? (userMap.get(customer.ownerAssignedBy) ?? null)
? (userDisplayMap.get(customer.ownerAssignedBy)?.displayName ?? null)
: null,
}, {
branchName: branchDisplay?.name ?? null,
branchCode: branchDisplay?.code ?? null,
createdByName: userDisplayMap.get(customer.createdBy)?.displayName ?? null,
updatedByName: userDisplayMap.get(customer.updatedBy)?.displayName ?? null,
});
}
@@ -1615,4 +1642,3 @@ export async function resolveCustomerOptionLabel(
return row?.label ?? null;
}

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
})
});
}