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