From 4c643547b6bb613eecae5b67fe883175e25080ca Mon Sep 17 00:00:00 2001 From: shancheas Date: Tue, 1 Sep 2026 19:25:29 +0700 Subject: [PATCH] feat: enhance plan entity and transformer for customer handling - Added optional `customer` field to `PlanDestinationEntity` to improve customer association. - Updated `PlansRemoteDataTransformer` to map nested customer data from destinations, ensuring accurate customer representation in transformed entities. - Enhanced `FormDocumentsPreview` to display customer information alongside documents, improving clarity in document management. - Introduced new tests for customer mapping in transformers, ensuring reliability and correctness in data handling. These changes significantly improve the handling of customer data within plans, enhancing user experience and data integrity. --- .../plans/domain/entities/plan.entity.ts | 1 + .../plan.remote.transformer.test.ts | 47 ++++++++++++++ .../transformers/plan.remote.transformer.ts | 62 ++++++++++++++++--- .../detail-component/detail-general.tsx | 25 +++----- .../form-component/form-documents-preview.tsx | 9 ++- .../form-component/use-hydrated-records.ts | 10 ++- 6 files changed, 126 insertions(+), 28 deletions(-) diff --git a/apps/web/src/apps/main/modules/field/plans/domain/entities/plan.entity.ts b/apps/web/src/apps/main/modules/field/plans/domain/entities/plan.entity.ts index 3b83016..510610c 100644 --- a/apps/web/src/apps/main/modules/field/plans/domain/entities/plan.entity.ts +++ b/apps/web/src/apps/main/modules/field/plans/domain/entities/plan.entity.ts @@ -7,6 +7,7 @@ import type { RouteGeometry } from '../../../cycles/domain/entities'; export interface PlanDestinationEntity { id?: string; customerId: string; + customer?: RelationRef | null; sortOrder: number; } diff --git a/apps/web/src/apps/main/modules/field/plans/domain/transformers/plan.remote.transformer.test.ts b/apps/web/src/apps/main/modules/field/plans/domain/transformers/plan.remote.transformer.test.ts index 04d9456..866ffcd 100644 --- a/apps/web/src/apps/main/modules/field/plans/domain/transformers/plan.remote.transformer.test.ts +++ b/apps/web/src/apps/main/modules/field/plans/domain/transformers/plan.remote.transformer.test.ts @@ -29,6 +29,39 @@ describe('PlansRemoteDataTransformer', () => { expect(entity.customers).toEqual([{ id: 'cus-1' }]); }); + it('maps nested destination.customer onto customers for the edit form', () => { + const entity = salesTransformer.transformToEntity({ + id: 'plan-1', + employee: { id: 'emp-1', code: 'E1', name: 'Ada' }, + purpose: 'sales', + date: Date.UTC(2026, 0, 12), + startBranch: { id: 'br-1', code: 'BDG', name: 'Bandung' }, + endBranch: { id: 'br-2', code: 'BDG', name: 'Bandung' }, + destinations: [ + { + id: 'd-1', + customer: { id: 'cus-1', code: 'C1', name: 'Acme Corp' }, + sortOrder: 0, + }, + ], + invoices: [{ id: 'inv-1', code: 'SI-001' }], + packingSlips: [], + status: 'active', + } as any); + + expect(entity.customers).toEqual([{ id: 'cus-1', code: 'C1', name: 'Acme Corp' }]); + expect(entity.destinations).toEqual([ + { + id: 'd-1', + customerId: 'cus-1', + customer: { id: 'cus-1', code: 'C1', name: 'Acme Corp' }, + sortOrder: 0, + }, + ]); + expect(entity.invoices).toEqual([{ id: 'inv-1', code: 'SI-001' }]); + expect(entity.invoiceIds).toEqual(['inv-1']); + }); + it('injects purpose and sales invoice attachments on create', () => { const payload = salesTransformer.transformCreatePayload({ employee: { id: 'emp-1' }, @@ -63,4 +96,18 @@ describe('PlansRemoteDataTransformer', () => { expect(payload.packingSlipIds).toEqual(['ps-1']); expect(payload).not.toHaveProperty('invoiceIds'); }); + + it('does not resurrect invoices when the form list is emptied', () => { + const payload = salesTransformer.transformEditPayload({ + employee: { id: 'emp-1' }, + date: '2026-01-12', + startBranch: { id: 'br-1' }, + endBranch: { id: 'br-2' }, + customers: [{ id: 'cus-1' }], + invoices: [], + invoiceIds: ['inv-1'], + } as any); + + expect(payload.invoiceIds).toEqual([]); + }); }); diff --git a/apps/web/src/apps/main/modules/field/plans/domain/transformers/plan.remote.transformer.ts b/apps/web/src/apps/main/modules/field/plans/domain/transformers/plan.remote.transformer.ts index a432b1d..03c4c14 100644 --- a/apps/web/src/apps/main/modules/field/plans/domain/transformers/plan.remote.transformer.ts +++ b/apps/web/src/apps/main/modules/field/plans/domain/transformers/plan.remote.transformer.ts @@ -2,7 +2,8 @@ import { BaseDataTransformer } from '@repo/core-api/data-services'; import { formatDateValue, parseDateValue } from '@repo/ui/form'; import { omitEmptyFields } from '../../../../../../../core/domain/configuration-field-validators'; import type { FieldPurpose } from '../../../../../../../core/domain/field-purpose'; -import type { PlanEntity } from '../entities'; +import type { RelationRef } from '../../../../../../../core/domain/relation-ref'; +import type { PlanDestinationEntity, PlanEntity } from '../entities'; function relationId(value: unknown): string | undefined { if (value && typeof value === 'object' && 'id' in value) { @@ -17,12 +18,55 @@ function relationIds(value: unknown): string[] { return value.map((item) => relationId(item)).filter((id): id is string => Boolean(id)); } +function relationRef(value: unknown): RelationRef | undefined { + const id = relationId(value); + if (!id || !value || typeof value !== 'object') return undefined; + const row = value as { code?: unknown; name?: unknown }; + return { + id, + ...(typeof row.code === 'string' ? { code: row.code } : {}), + ...(typeof row.name === 'string' ? { name: row.name } : {}), + }; +} + +function customerFromDestination(destination: unknown): RelationRef | undefined { + if (!destination || typeof destination !== 'object') return undefined; + const row = destination as { customer?: unknown; customerId?: unknown }; + if (row.customerId != null && row.customerId !== '') { + return relationRef(row.customer) ?? { id: String(row.customerId) }; + } + return relationRef(row.customer); +} + +function mapDestinations(destinations: unknown): PlanDestinationEntity[] { + if (!Array.isArray(destinations)) return []; + return destinations.map((destination, index) => { + const row = destination && typeof destination === 'object' ? (destination as Record) : {}; + const customer = customerFromDestination(destination); + return { + id: typeof row.id === 'string' ? row.id : undefined, + customerId: customer?.id ?? '', + customer: customer ?? null, + sortOrder: typeof row.sortOrder === 'number' ? row.sortOrder : index, + }; + }); +} + export class PlansRemoteDataTransformer extends BaseDataTransformer { constructor(private readonly purpose: FieldPurpose) { super(); } transformToEntity(dto: PlanEntity): PlanEntity { + const destinations = mapDestinations(dto.destinations); + const customers = + Array.isArray(dto.customers) && dto.customers.length > 0 + ? dto.customers + : destinations + .map((destination) => destination.customer) + .filter((customer): customer is RelationRef => Boolean(customer?.id)); + const invoices = dto.invoices ?? (dto.invoiceIds ?? []).map((id) => ({ id })); + const packingSlips = dto.packingSlips ?? (dto.packingSlipIds ?? []).map((id) => ({ id })); return { id: dto.id, employeeId: dto.employeeId, @@ -34,12 +78,12 @@ export class PlansRemoteDataTransformer extends BaseDataTransformer endBranchId: dto.endBranchId, endBranch: dto.endBranch ?? (dto.endBranchId ? { id: dto.endBranchId } : null), routeGeometry: dto.routeGeometry ?? null, - destinations: dto.destinations ?? [], - customers: dto.customers ?? (dto.destinations ?? []).map((destination) => ({ id: destination.customerId })), - invoiceIds: dto.invoiceIds ?? [], - invoices: dto.invoices ?? (dto.invoiceIds ?? []).map((id) => ({ id })), - packingSlipIds: dto.packingSlipIds ?? [], - packingSlips: dto.packingSlips ?? (dto.packingSlipIds ?? []).map((id) => ({ id })), + destinations, + customers, + invoiceIds: dto.invoiceIds ?? relationIds(invoices), + invoices, + packingSlipIds: dto.packingSlipIds ?? relationIds(packingSlips), + packingSlips, status: dto.status, createdAt: dto.createdAt, updatedAt: dto.updatedAt, @@ -63,9 +107,9 @@ export class PlansRemoteDataTransformer extends BaseDataTransformer customerIds, }; if (this.purpose === 'sales') { - payload.invoiceIds = relationIds(entity.invoices).length ? relationIds(entity.invoices) : entity.invoiceIds; + payload.invoiceIds = Array.isArray(entity.invoices) ? relationIds(entity.invoices) : entity.invoiceIds; } else { - payload.packingSlipIds = relationIds(entity.packingSlips).length + payload.packingSlipIds = Array.isArray(entity.packingSlips) ? relationIds(entity.packingSlips) : entity.packingSlipIds; } diff --git a/apps/web/src/apps/main/modules/field/plans/presentation/components/detail-component/detail-general.tsx b/apps/web/src/apps/main/modules/field/plans/presentation/components/detail-component/detail-general.tsx index 4ff0eb5..a6a49f5 100644 --- a/apps/web/src/apps/main/modules/field/plans/presentation/components/detail-component/detail-general.tsx +++ b/apps/web/src/apps/main/modules/field/plans/presentation/components/detail-component/detail-general.tsx @@ -26,13 +26,13 @@ import { loadCustomerOptions } from '../../../../shared/load-customer-options'; import type { PlansRemoteDataServices } from '../../../data/plan.remote.service'; import type { PlanEntity } from '../../../domain/entities'; import type { CustomerEntity } from '../../../../../configuration/customers/domain/entities'; +import { FormDocumentsPreview } from '../form-component/form-documents-preview'; export function DetailGeneral() { const { detailData, reload } = useDetailPageContext(); const { t } = useEnterpriseModuleTranslationContext(); const { dataServices } = useEnterpriseModuleDataServiceContext(); const data = detailData; - const attachments = data?.purpose === 'sales' ? data?.invoices : data?.packingSlips; const destinationForm = useForm<{ customer: CustomerEntity | null }>({ defaultValues: { customer: null } }); const handleAdd = destinationForm.handleSubmit(async (values) => { @@ -92,9 +92,9 @@ export function DetailGeneral() { {(data?.destinations ?? []).map((destination, index) => ( - + - {index + 1}. {destination.customerId} + {index + 1}. {relationLabel(destination.customer) || destination.customerId} - {attachments?.length ? ( - - - {t('section_attachments')} - - - {attachments.map((item) => ( - - {relationLabel(item) || item.id} - - ))} - - - ) : null} + {data?.purpose === 'sales' ? ( + + ) : ( + + )} ); } diff --git a/apps/web/src/apps/main/modules/field/plans/presentation/components/form-component/form-documents-preview.tsx b/apps/web/src/apps/main/modules/field/plans/presentation/components/form-component/form-documents-preview.tsx index 84b921d..018be92 100644 --- a/apps/web/src/apps/main/modules/field/plans/presentation/components/form-component/form-documents-preview.tsx +++ b/apps/web/src/apps/main/modules/field/plans/presentation/components/form-component/form-documents-preview.tsx @@ -35,13 +35,20 @@ async function fetchPackingSlip(id: string): Promise { return unwrapEntity(await packingSlipsModuleDataService.getOne(id)); } +type PlanDocumentPreviewItem = { + id?: string | number; + code?: string | null; + name?: string; + customer?: { id?: string | number; code?: string | null; name?: string } | null; +}; + export function FormDocumentsPreview({ kind, items, customers = [], }: { kind: 'invoice' | 'packingSlip'; - items: Array; + items: PlanDocumentPreviewItem[]; customers?: Array<{ id?: string | number; code?: string | null; name?: string }>; }) { const { t } = useEnterpriseModuleTranslationContext(); diff --git a/apps/web/src/apps/main/modules/field/plans/presentation/components/form-component/use-hydrated-records.ts b/apps/web/src/apps/main/modules/field/plans/presentation/components/form-component/use-hydrated-records.ts index 4bc0732..d9a0d9e 100644 --- a/apps/web/src/apps/main/modules/field/plans/presentation/components/form-component/use-hydrated-records.ts +++ b/apps/web/src/apps/main/modules/field/plans/presentation/components/form-component/use-hydrated-records.ts @@ -10,6 +10,7 @@ export function useHydratedRecords( const ids = selectionIds(items); const idsKey = ids.join(','); const [details, setDetails] = useState>({}); + const [settled, setSettled] = useState>({}); const itemsRef = useRef(items); const fetchOneRef = useRef(fetchOne); const shouldFetchRef = useRef(shouldFetch); @@ -43,6 +44,13 @@ export function useHydratedRecords( } return next; }); + setSettled((prev) => { + const next = { ...prev }; + for (const row of rows) { + next[row.id] = true; + } + return next; + }); }); return () => { @@ -52,7 +60,7 @@ export function useHydratedRecords( const pending = items.some((item) => { const id = item.id == null ? '' : String(item.id); - return Boolean(id) && shouldFetch(item) && !details[id]; + return Boolean(id) && shouldFetch(item) && !details[id] && !settled[id]; }); return { records: mergeHydrated(items, details), pending };