diff --git a/apps/web/src/apps/main/layouts/data/menu.data.test.ts b/apps/web/src/apps/main/layouts/data/menu.data.test.ts
index 1f7aaef..e39e4c4 100644
--- a/apps/web/src/apps/main/layouts/data/menu.data.test.ts
+++ b/apps/web/src/apps/main/layouts/data/menu.data.test.ts
@@ -6,7 +6,8 @@ import type { MenuItemType } from '../types/menu.types';
const childKeys = (item: MenuItemType | undefined): string[] => (item?.children ?? []).map((child) => child.key);
-const findItem = (items: MenuItemType[], key: string): MenuItemType | undefined => items.find((item) => item.key === key);
+const findItem = (items: MenuItemType[], key: string): MenuItemType | undefined =>
+ items.find((item) => item.key === key);
const flatten = (items: MenuItemType[]): MenuItemType[] =>
items.flatMap((item) => [item, ...(item.children ? flatten(item.children) : [])]);
@@ -58,7 +59,10 @@ describe('MENU_ITEMS', () => {
'configuration-customers',
'configuration-products',
]);
- expect(childKeys(findItem(settings?.children ?? [], 'settings-user'))).toEqual(['system-users', 'system-privileges']);
+ expect(childKeys(findItem(settings?.children ?? [], 'settings-user'))).toEqual([
+ 'system-users',
+ 'system-privileges',
+ ]);
});
it('uses unique keys across the whole tree', () => {
diff --git a/apps/web/src/apps/main/layouts/data/menu.data.ts b/apps/web/src/apps/main/layouts/data/menu.data.ts
index c0e7dd0..57c0059 100644
--- a/apps/web/src/apps/main/layouts/data/menu.data.ts
+++ b/apps/web/src/apps/main/layouts/data/menu.data.ts
@@ -151,7 +151,7 @@ export const MENU_ITEMS: MenuItemType[] = [
key: 'logistics-packing-slips',
label: 'nav:logistics-packing-slips',
icon: Package,
- path: '/app/logistics/packing-slips',
+ path: '/app/logistics/packing-slips/index',
moduleKey: 'SALES.PACKING_SLIP',
},
{
diff --git a/apps/web/src/apps/main/modules/field/logistics/index.tsx b/apps/web/src/apps/main/modules/field/logistics/index.tsx
index 8846bcd..2a6f9be 100644
--- a/apps/web/src/apps/main/modules/field/logistics/index.tsx
+++ b/apps/web/src/apps/main/modules/field/logistics/index.tsx
@@ -5,6 +5,7 @@ import { EmbeddedComingSoonPage } from '../../../../../core/components/coming-so
const EmployeesModule = lazy(() => import('../../configuration/employees/presentation/factory'));
const CyclesModule = lazy(() => import('../cycles/presentation/factory'));
const PlansModule = lazy(() => import('../plans/presentation/factory'));
+const PackingSlipsModule = lazy(() => import('../packing-slips/presentation/factory'));
export default function LogisticsFieldModule() {
return (
@@ -12,7 +13,7 @@ export default function LogisticsFieldModule() {
} />
} />
} />
- } />
+ } />
} />
} />
diff --git a/apps/web/src/apps/main/modules/field/packing-slips/data/packing-slip.remote.service.ts b/apps/web/src/apps/main/modules/field/packing-slips/data/packing-slip.remote.service.ts
new file mode 100644
index 0000000..8b9db0f
--- /dev/null
+++ b/apps/web/src/apps/main/modules/field/packing-slips/data/packing-slip.remote.service.ts
@@ -0,0 +1,13 @@
+import type { AxiosInstance } from '@repo/core-api/http-client';
+import type { DataServicesConfig } from '@repo/core-api/data-services';
+import { SalesDocumentRemoteDataServices } from '../../../sales/shared/sales-document.remote.service';
+import type { PackingSlipEntity } from '../domain/entities';
+
+export class PackingSlipsRemoteDataServices extends SalesDocumentRemoteDataServices {
+ constructor(httpClient: AxiosInstance, config: DataServicesConfig) {
+ super(httpClient, {
+ ...config,
+ apiUrl: config.apiUrl ?? '/packing-slips',
+ });
+ }
+}
diff --git a/apps/web/src/apps/main/modules/field/packing-slips/domain/constants/index.ts b/apps/web/src/apps/main/modules/field/packing-slips/domain/constants/index.ts
new file mode 100644
index 0000000..c3e7456
--- /dev/null
+++ b/apps/web/src/apps/main/modules/field/packing-slips/domain/constants/index.ts
@@ -0,0 +1 @@
+export * from './packing-slip.constants';
diff --git a/apps/web/src/apps/main/modules/field/packing-slips/domain/constants/packing-slip.constants.ts b/apps/web/src/apps/main/modules/field/packing-slips/domain/constants/packing-slip.constants.ts
new file mode 100644
index 0000000..60a27aa
--- /dev/null
+++ b/apps/web/src/apps/main/modules/field/packing-slips/domain/constants/packing-slip.constants.ts
@@ -0,0 +1,11 @@
+import { ModuleConfigEntity } from '@repo/ui/foundations';
+import type { PackingSlipEntity } from '../entities';
+
+export const packingSlipsModuleConfig: ModuleConfigEntity = {
+ moduleKey: 'SALES.PACKING_SLIP',
+ translationNamespace: 'PACKING_SLIPS',
+ apiUrl: '/packing-slips',
+ webUrl: '/app/logistics/packing-slips',
+ moduleCategory: 'FULL_PAGE',
+ moduleType: 'TRANSACTION',
+} as const;
diff --git a/apps/web/src/apps/main/modules/field/packing-slips/domain/entities/index.ts b/apps/web/src/apps/main/modules/field/packing-slips/domain/entities/index.ts
new file mode 100644
index 0000000..c920273
--- /dev/null
+++ b/apps/web/src/apps/main/modules/field/packing-slips/domain/entities/index.ts
@@ -0,0 +1 @@
+export * from './packing-slip.entity';
diff --git a/apps/web/src/apps/main/modules/field/packing-slips/domain/entities/packing-slip.entity.ts b/apps/web/src/apps/main/modules/field/packing-slips/domain/entities/packing-slip.entity.ts
new file mode 100644
index 0000000..cd81c4e
--- /dev/null
+++ b/apps/web/src/apps/main/modules/field/packing-slips/domain/entities/packing-slip.entity.ts
@@ -0,0 +1,13 @@
+import type { SalesDocumentDto, SalesDocumentEntity } from '../../../sales/shared/sales-document.entity';
+
+export interface PackingSlipEntity extends SalesDocumentEntity {
+ salesOrderId?: string | null;
+ salesOrder?: { id: string; code?: string; name?: string } | null;
+ salesOrderCode?: string | null;
+}
+
+export interface PackingSlipDto extends SalesDocumentDto {
+ salesOrderId?: string | null;
+ salesOrder?: { id: string; code?: string; name?: string } | null;
+ salesOrderCode?: string | null;
+}
diff --git a/apps/web/src/apps/main/modules/field/packing-slips/domain/factories/index.ts b/apps/web/src/apps/main/modules/field/packing-slips/domain/factories/index.ts
new file mode 100644
index 0000000..86061e6
--- /dev/null
+++ b/apps/web/src/apps/main/modules/field/packing-slips/domain/factories/index.ts
@@ -0,0 +1,12 @@
+import { apiClient } from '../../../../../../../core/lib/api-client';
+import { PackingSlipsRemoteDataServices } from '../../data/packing-slip.remote.service';
+import { packingSlipsModuleConfig } from '../constants';
+import { PackingSlipsRemoteDataTransformer } from '../transformers/packing-slip.remote.transformer';
+
+export const packingSlipsDataTransformer = new PackingSlipsRemoteDataTransformer();
+
+export const packingSlipsModuleDataService = new PackingSlipsRemoteDataServices(apiClient, {
+ apiUrl: packingSlipsModuleConfig.apiUrl,
+ moduleKey: packingSlipsModuleConfig.moduleKey,
+ transformer: packingSlipsDataTransformer,
+});
diff --git a/apps/web/src/apps/main/modules/field/packing-slips/domain/transformers/packing-slip.remote.transformer.ts b/apps/web/src/apps/main/modules/field/packing-slips/domain/transformers/packing-slip.remote.transformer.ts
new file mode 100644
index 0000000..32bec78
--- /dev/null
+++ b/apps/web/src/apps/main/modules/field/packing-slips/domain/transformers/packing-slip.remote.transformer.ts
@@ -0,0 +1,49 @@
+import { BaseDataTransformer } from '@repo/core-api/data-services';
+import { omitEmptyFields } from '../../../../../../../core/domain/configuration-field-validators';
+import {
+ mapSalesDocumentFromDto,
+ relationId,
+ toLookup,
+ toSalesFilterPayload,
+ toSalesWritePayload,
+} from '../../../sales/shared/sales-document.mapper';
+import type { PackingSlipDto, PackingSlipEntity } from '../entities';
+
+export class PackingSlipsRemoteDataTransformer extends BaseDataTransformer {
+ transformToEntity(dto: PackingSlipDto | PackingSlipEntity): PackingSlipEntity {
+ const packingDto = dto as PackingSlipDto;
+ const base = mapSalesDocumentFromDto(packingDto);
+ const salesOrder = toLookup(packingDto.salesOrder, packingDto.salesOrderId);
+ return {
+ ...base,
+ salesOrderId: relationId(packingDto.salesOrder) ?? packingDto.salesOrderId ?? null,
+ salesOrder,
+ salesOrderCode: packingDto.salesOrderCode ?? salesOrder?.code ?? null,
+ };
+ }
+
+ transformToDTO(entity: PackingSlipEntity): PackingSlipEntity {
+ return { ...entity };
+ }
+
+ transformCreatePayload(entity: Partial): Partial {
+ return omitEmptyFields(
+ toSalesWritePayload(entity, {
+ includeSalesOrderId: true,
+ omitImages: true,
+ omitStaffFields: true,
+ }),
+ ) as Partial;
+ }
+
+ transformEditPayload(entity: Partial): Partial {
+ return toSalesWritePayload(entity, {
+ omitImages: true,
+ omitStaffFields: true,
+ }) as Partial;
+ }
+
+ transformPayloadFilter(filter: Record): Record {
+ return toSalesFilterPayload(filter);
+ }
+}
diff --git a/apps/web/src/apps/main/modules/field/packing-slips/presentation/components/form-component/form-packing-general.tsx b/apps/web/src/apps/main/modules/field/packing-slips/presentation/components/form-component/form-packing-general.tsx
new file mode 100644
index 0000000..19f9709
--- /dev/null
+++ b/apps/web/src/apps/main/modules/field/packing-slips/presentation/components/form-component/form-packing-general.tsx
@@ -0,0 +1,64 @@
+import { Box, FieldAsyncSelect, FieldDatePicker, FieldTextInput, Paper, SimpleGrid, Text } from '@repo/ui/components';
+import { useEnterpriseModuleTranslationContext, useFormPageContext } from '@repo/ui/foundations';
+import { loadCustomerOptions } from '../../../../shared/load-customer-options';
+import { loadSalesOrderOptions } from '../../../../sales/shared/load-sales-order-options';
+import { relationLabel } from '../../../../shared/relation-label';
+import type { CustomerEntity } from '../../../../../configuration/customers/domain/entities';
+import type { SalesOrderEntity } from '../../../../sales/orders/domain/entities';
+
+export function FormPackingGeneral() {
+ const { formControl } = useFormPageContext();
+ const { t } = useEnterpriseModuleTranslationContext();
+ const customer = formControl.watch('customer');
+ const salesOrder = formControl.watch('salesOrder');
+
+ return (
+
+
+ {t('section_general')}
+
+
+
+
+
+
+ control={formControl.control}
+ name="customer"
+ label={t('common:fields.customer')}
+ valueKey="id"
+ labelKey="name"
+ required
+ searchable
+ loadOptions={loadCustomerOptions}
+ defaultOptions={customer ? [customer] : []}
+ renderLabel={relationLabel}
+ />
+
+ control={formControl.control}
+ name="salesOrder"
+ label={t('common:fields.salesOrder')}
+ valueKey="id"
+ labelKey="code"
+ searchable
+ clearable
+ loadOptions={loadSalesOrderOptions}
+ defaultOptions={salesOrder ? [salesOrder] : []}
+ renderLabel={relationLabel}
+ />
+
+
+
+ );
+}
diff --git a/apps/web/src/apps/main/modules/field/packing-slips/presentation/factory/index.tsx b/apps/web/src/apps/main/modules/field/packing-slips/presentation/factory/index.tsx
new file mode 100644
index 0000000..123ab49
--- /dev/null
+++ b/apps/web/src/apps/main/modules/field/packing-slips/presentation/factory/index.tsx
@@ -0,0 +1,40 @@
+import { lazy } from 'react';
+import { Navigate, Route, Routes } from 'react-router-dom';
+import { EnterpriseModuleProvider } from '@repo/ui/foundations';
+import { registerModuleNamespace } from '@repo/core-i18n';
+import { packingSlipsModuleConfig } from '../../domain/constants';
+import { packingSlipsModuleDataService } from '../../domain/factories';
+import { PackingSlipEntity } from '../../domain/entities';
+import { packingSlipsStore } from '../store';
+
+import packingSlipsId from '../languages/id/packing-slips.json';
+import packingSlipsEn from '../languages/en/packing-slips.json';
+
+const IndexPage = lazy(() => import('../pages/packing-slip.page.index'));
+const FormPage = lazy(() => import('../pages/packing-slip.page.form'));
+const DetailPage = lazy(() => import('../pages/packing-slip.page.detail'));
+
+registerModuleNamespace(packingSlipsModuleConfig.translationNamespace, {
+ id: packingSlipsId,
+ en: packingSlipsEn,
+});
+
+export default function PackingSlipsModule() {
+ return (
+
+ config={packingSlipsModuleConfig}
+ dataServices={packingSlipsModuleDataService}
+ store={packingSlipsStore}
+ >
+
+ } />
+ } />
+ } />
+ } />
+ } />
+ } />
+ } />
+
+
+ );
+}
diff --git a/apps/web/src/apps/main/modules/field/packing-slips/presentation/languages/en/packing-slips.json b/apps/web/src/apps/main/modules/field/packing-slips/presentation/languages/en/packing-slips.json
new file mode 100644
index 0000000..0dc64ac
--- /dev/null
+++ b/apps/web/src/apps/main/modules/field/packing-slips/presentation/languages/en/packing-slips.json
@@ -0,0 +1,33 @@
+{
+ "title": "Packing Slips",
+ "detail_page_title": "Packing Slip Detail",
+ "create_page_title": "New Packing Slip",
+ "edit_page_title": "Edit Packing Slip",
+ "duplicate_page_title": "Duplicate Packing Slip",
+ "description": "Create <1>packing slips1> from a sales order or as a standalone document.",
+ "detail_page_description": "Review packing header, location, and product quantities.",
+ "create_page_description": "Create a packing slip, optionally sourced from a sales order.",
+ "edit_page_description": "Update packing header, location, products, and notes.",
+ "duplicate_page_description": "Copy an existing packing slip to create a new one.",
+ "section_general": "General",
+ "section_location": "Location",
+ "section_products": "Products",
+ "section_notes": "Notes",
+ "add_line": "Add line",
+ "remove_line": "Remove line",
+ "empty_products": "No product lines.",
+ "change_status": "Change status",
+ "import_csv": "Import CSV",
+ "csv_file": "CSV file",
+ "import_success": "CSV imported.",
+ "status_updated": "Status updated.",
+ "action_complete": "Complete",
+ "action_cancel": "Cancel",
+ "complete_packing": "Complete packing",
+ "complete_packing_help": "Enter delivered quantity for each line. Remaining quantity opens a new packing slip.",
+ "delivered_quantity": "Delivered quantity",
+ "status_draft": "Draft",
+ "status_processed": "Processed",
+ "status_completed": "Completed",
+ "status_cancelled": "Cancelled"
+}
diff --git a/apps/web/src/apps/main/modules/field/packing-slips/presentation/languages/id/packing-slips.json b/apps/web/src/apps/main/modules/field/packing-slips/presentation/languages/id/packing-slips.json
new file mode 100644
index 0000000..33eb84b
--- /dev/null
+++ b/apps/web/src/apps/main/modules/field/packing-slips/presentation/languages/id/packing-slips.json
@@ -0,0 +1,33 @@
+{
+ "title": "Surat Jalan",
+ "detail_page_title": "Detail Surat Jalan",
+ "create_page_title": "Surat Jalan Baru",
+ "edit_page_title": "Ubah Surat Jalan",
+ "duplicate_page_title": "Duplikat Surat Jalan",
+ "description": "Buat <1>surat jalan1> dari pesanan penjualan atau sebagai dokumen mandiri.",
+ "detail_page_description": "Tinjau header, lokasi, dan kuantitas produk surat jalan.",
+ "create_page_description": "Buat surat jalan, opsional dari pesanan penjualan.",
+ "edit_page_description": "Perbarui header, lokasi, produk, dan catatan surat jalan.",
+ "duplicate_page_description": "Salin surat jalan yang ada untuk membuat data baru.",
+ "section_general": "Umum",
+ "section_location": "Lokasi",
+ "section_products": "Produk",
+ "section_notes": "Catatan",
+ "add_line": "Tambah baris",
+ "remove_line": "Hapus baris",
+ "empty_products": "Tidak ada baris produk.",
+ "change_status": "Ubah status",
+ "import_csv": "Impor CSV",
+ "csv_file": "File CSV",
+ "import_success": "CSV berhasil diimpor.",
+ "status_updated": "Status diperbarui.",
+ "action_complete": "Selesaikan",
+ "action_cancel": "Batalkan",
+ "complete_packing": "Selesaikan packing",
+ "complete_packing_help": "Masukkan kuantitas terkirim untuk setiap baris. Sisa kuantitas akan membuka surat jalan baru.",
+ "delivered_quantity": "Kuantitas terkirim",
+ "status_draft": "Draft",
+ "status_processed": "Diproses",
+ "status_completed": "Selesai",
+ "status_cancelled": "Dibatalkan"
+}
diff --git a/apps/web/src/apps/main/modules/field/packing-slips/presentation/pages/packing-slip.page.detail.tsx b/apps/web/src/apps/main/modules/field/packing-slips/presentation/pages/packing-slip.page.detail.tsx
new file mode 100644
index 0000000..42ce0b8
--- /dev/null
+++ b/apps/web/src/apps/main/modules/field/packing-slips/presentation/pages/packing-slip.page.detail.tsx
@@ -0,0 +1,38 @@
+import { Stack } from '@repo/ui/components';
+import { EnterpriseDetailPageProvider, useEnterpriseModuleTranslationContext } from '@repo/ui/foundations';
+import { packingSlipsModuleConfig } from '../../domain/constants';
+import { DetailGeneral } from '../../../sales/shared/detail-general';
+import { DetailLocation } from '../../../sales/shared/detail-location';
+import { DetailProducts } from '../../../sales/shared/detail-products';
+import { useSalesDocumentActions } from '../../../sales/shared/use-sales-document-actions';
+import { salesOrdersModuleConfig } from '../../../sales/orders/domain/constants';
+import type { PackingSlipEntity } from '../../domain/entities';
+
+export default function PackingSlipPageDetail() {
+ const { t } = useEnterpriseModuleTranslationContext();
+ const actions = useSalesDocumentActions('packing');
+
+ return (
+
+ actions.detailStatusActions(data as PackingSlipEntity, pageActions ?? [])
+ }
+ >
+
+ `${salesOrdersModuleConfig.webUrl}/detail/${id}`} />
+
+
+
+ {actions.modals}
+
+ );
+}
diff --git a/apps/web/src/apps/main/modules/field/packing-slips/presentation/pages/packing-slip.page.form.tsx b/apps/web/src/apps/main/modules/field/packing-slips/presentation/pages/packing-slip.page.form.tsx
new file mode 100644
index 0000000..555938c
--- /dev/null
+++ b/apps/web/src/apps/main/modules/field/packing-slips/presentation/pages/packing-slip.page.form.tsx
@@ -0,0 +1,61 @@
+import { useMemo } from 'react';
+import { useEnterpriseModuleTranslationContext, EnterpriseFormPageProvider, FormPageType } from '@repo/ui/foundations';
+import { Stack } from '@repo/ui/components';
+import { useForm } from 'react-hook-form';
+import { zodResolver } from '@hookform/resolvers/zod';
+import { packingSlipsModuleConfig } from '../../domain/constants';
+import { createSalesPackingSlipSchema } from '../../../sales/shared/sales-document.validator';
+import { FormPackingGeneral } from '../components/form-component/form-packing-general';
+import { FormLocation } from '../../../sales/shared/form-location';
+import { FormProducts } from '../../../sales/shared/form-products';
+import { FormNotes } from '../../../sales/shared/form-notes';
+
+export default function PackingSlipPageForm({ formPageType }: { formPageType: FormPageType }) {
+ const { t } = useEnterpriseModuleTranslationContext();
+
+ const title = useMemo(() => {
+ if (formPageType === 'CREATE') {
+ return { title: t('create_page_title'), description: t('create_page_description') };
+ }
+ if (formPageType === 'EDIT') {
+ return { title: t('edit_page_title'), description: t('edit_page_description') };
+ }
+ if (formPageType === 'DUPLICATE') {
+ return { title: t('duplicate_page_title'), description: t('duplicate_page_description') };
+ }
+ return { title: '', description: '' };
+ }, [formPageType, t]);
+
+ const validator = useMemo(() => createSalesPackingSlipSchema(t), [t]);
+ const formControl = useForm({
+ resolver: zodResolver(validator),
+ defaultValues: {
+ products: [{ quantity: '1', price: '' }],
+ },
+ });
+
+ return (
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/apps/web/src/apps/main/modules/field/packing-slips/presentation/pages/packing-slip.page.index.tsx b/apps/web/src/apps/main/modules/field/packing-slips/presentation/pages/packing-slip.page.index.tsx
new file mode 100644
index 0000000..eed2826
--- /dev/null
+++ b/apps/web/src/apps/main/modules/field/packing-slips/presentation/pages/packing-slip.page.index.tsx
@@ -0,0 +1,72 @@
+import { useMemo } from 'react';
+import {
+ EnterpriseIndexPageProvider,
+ useEnterpriseModuleTranslationContext,
+ EnterpriseDataTable,
+} from '@repo/ui/foundations';
+import { ColDef, Text } from '@repo/ui/components';
+import { Trans } from '@repo/core-i18n';
+import { Package } from 'lucide-react';
+import { SalesFilterFormContent } from '../../../sales/shared/filter-content';
+import { useSalesDocumentActions } from '../../../sales/shared/use-sales-document-actions';
+import { relationLabel } from '../../shared/relation-label';
+import type { PackingSlipEntity } from '../../domain/entities';
+
+export default function PackingSlipPageIndex() {
+ const { t } = useEnterpriseModuleTranslationContext();
+ const actions = useSalesDocumentActions('packing');
+
+ const columnDefs: ColDef[] = useMemo(
+ () => [
+ { field: 'code', headerName: t('common:fields.code'), minWidth: 160 },
+ { field: 'date', headerName: t('common:fields.date'), minWidth: 140 },
+ {
+ field: 'customerId',
+ headerName: t('common:fields.customer'),
+ minWidth: 180,
+ valueGetter: ({ data }) => relationLabel(data?.customer) || data?.customerId,
+ },
+ {
+ field: 'salesOrderId',
+ headerName: t('common:fields.salesOrder'),
+ minWidth: 160,
+ valueGetter: ({ data }) => relationLabel(data?.salesOrder) || data?.salesOrderCode || data?.salesOrderId,
+ },
+ ],
+ [t],
+ );
+
+ const filterConfig = useMemo(
+ () => ({
+ renderBody: (form: any) => (form ? : null),
+ }),
+ [t],
+ );
+
+ return (
+ }} />
+ ),
+ icon: Package,
+ breadcrumbs: [
+ { label: t('nav:logistics'), type: 'text' },
+ { label: t('nav:logistics-packing-slips'), type: 'text' },
+ ],
+ }}
+ customPageActions={(pageActions) =>
+ actions.importPageAction ? [actions.importPageAction, ...(pageActions ?? [])] : pageActions
+ }
+ >
+ actions.namedRowActions(data, defaultActions)}
+ customBulkActions={(rows, defaultActions) => actions.namedBulkActions(rows, defaultActions)}
+ />
+ {actions.modals}
+
+ );
+}
diff --git a/apps/web/src/apps/main/modules/field/packing-slips/presentation/store/index.ts b/apps/web/src/apps/main/modules/field/packing-slips/presentation/store/index.ts
new file mode 100644
index 0000000..25a4c7b
--- /dev/null
+++ b/apps/web/src/apps/main/modules/field/packing-slips/presentation/store/index.ts
@@ -0,0 +1,18 @@
+import { create } from 'zustand';
+import { EnterpriseModuleState } from '@repo/ui/foundations';
+import { PackingSlipEntity } from '../../domain/entities';
+
+export interface PackingSlipsStoreState extends EnterpriseModuleState {}
+
+export const packingSlipsStore = create((set) => ({
+ metaData: { limit: 15 },
+ setMetaData: (data) => set({ metaData: data }),
+ filterData: {},
+ setFilterData: (data) => set({ filterData: data }),
+ selectedRows: [],
+ setSelectedRows: (rows) => set({ selectedRows: rows }),
+ privileges: [],
+ setPrivileges: (privileges) => set({ privileges }),
+ tableConfig: null,
+ setTableConfig: (config) => set({ tableConfig: config }),
+}));
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 12b9b0c..ab19fc9 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
@@ -125,6 +125,21 @@ export function DetailGeneral() {
+
+ {(data?.purpose === 'sales' ? data?.invoices : data?.packingSlips)?.length ? (
+
+
+ {t('section_attachments')}
+
+
+ {(data.purpose === 'sales' ? data.invoices : data?.packingSlips)?.map((item) => (
+
+ {relationLabel(item) || item.id}
+
+ ))}
+
+
+ ) : null}
);
}
diff --git a/apps/web/src/apps/main/modules/field/plans/presentation/languages/en/plans.json b/apps/web/src/apps/main/modules/field/plans/presentation/languages/en/plans.json
index 067f995..d18fe54 100644
--- a/apps/web/src/apps/main/modules/field/plans/presentation/languages/en/plans.json
+++ b/apps/web/src/apps/main/modules/field/plans/presentation/languages/en/plans.json
@@ -19,6 +19,7 @@
"add_destination": "Add destination",
"remove_destination": "Remove destination",
"empty_route": "No route geometry",
+ "section_attachments": "Attachments",
"purpose_sales": "Sales",
"purpose_logistics": "Logistics",
"status_draft": "Draft",
diff --git a/apps/web/src/apps/main/modules/field/plans/presentation/languages/id/plans.json b/apps/web/src/apps/main/modules/field/plans/presentation/languages/id/plans.json
index dc6c689..54d8c92 100644
--- a/apps/web/src/apps/main/modules/field/plans/presentation/languages/id/plans.json
+++ b/apps/web/src/apps/main/modules/field/plans/presentation/languages/id/plans.json
@@ -19,6 +19,7 @@
"add_destination": "Tambah destinasi",
"remove_destination": "Hapus destinasi",
"empty_route": "Tidak ada geometri rute",
+ "section_attachments": "Lampiran",
"purpose_sales": "Penjualan",
"purpose_logistics": "Logistik",
"status_draft": "Draft",
diff --git a/apps/web/src/apps/main/modules/sales/invoices/presentation/languages/en/sales-invoices.json b/apps/web/src/apps/main/modules/sales/invoices/presentation/languages/en/sales-invoices.json
index a1ffafd..91c1c67 100644
--- a/apps/web/src/apps/main/modules/sales/invoices/presentation/languages/en/sales-invoices.json
+++ b/apps/web/src/apps/main/modules/sales/invoices/presentation/languages/en/sales-invoices.json
@@ -23,8 +23,6 @@
"import_success": "CSV imported.",
"status_updated": "Status updated.",
"create_sales_payment": "Create Sales Payment",
- "action_process": "Process",
- "action_complete": "Complete",
"action_cancel": "Cancel",
"status_draft": "Draft",
"status_processed": "Processed",
diff --git a/apps/web/src/apps/main/modules/sales/invoices/presentation/languages/id/sales-invoices.json b/apps/web/src/apps/main/modules/sales/invoices/presentation/languages/id/sales-invoices.json
index 7a835bc..65a84b4 100644
--- a/apps/web/src/apps/main/modules/sales/invoices/presentation/languages/id/sales-invoices.json
+++ b/apps/web/src/apps/main/modules/sales/invoices/presentation/languages/id/sales-invoices.json
@@ -23,8 +23,6 @@
"import_success": "CSV berhasil diimpor.",
"status_updated": "Status diperbarui.",
"create_sales_payment": "Buat Pembayaran Penjualan",
- "action_process": "Proses",
- "action_complete": "Selesaikan",
"action_cancel": "Batalkan",
"status_draft": "Draft",
"status_processed": "Diproses",
diff --git a/apps/web/src/apps/main/modules/sales/invoices/presentation/pages/sales-invoice.page.detail.tsx b/apps/web/src/apps/main/modules/sales/invoices/presentation/pages/sales-invoice.page.detail.tsx
index e3258ed..76b5ee8 100644
--- a/apps/web/src/apps/main/modules/sales/invoices/presentation/pages/sales-invoice.page.detail.tsx
+++ b/apps/web/src/apps/main/modules/sales/invoices/presentation/pages/sales-invoice.page.detail.tsx
@@ -35,10 +35,7 @@ export default function SalesInvoicePageDetail() {
}}
>
- `${salesOrdersModuleConfig.webUrl}/detail/${id}`}
- showBalance
- />
+ `${salesOrdersModuleConfig.webUrl}/detail/${id}`} showBalance />
diff --git a/apps/web/src/apps/main/modules/sales/orders/domain/entities/sales-order.entity.ts b/apps/web/src/apps/main/modules/sales/orders/domain/entities/sales-order.entity.ts
index b39ab3e..1e3d43e 100644
--- a/apps/web/src/apps/main/modules/sales/orders/domain/entities/sales-order.entity.ts
+++ b/apps/web/src/apps/main/modules/sales/orders/domain/entities/sales-order.entity.ts
@@ -3,9 +3,13 @@ import type { SalesDocumentDto, SalesDocumentEntity } from '../../../shared/sale
export interface SalesOrderEntity extends SalesDocumentEntity {
salesRequestId?: string | null;
salesRequest?: { id: string; code?: string; name?: string } | null;
+ packingSlipIds?: string[];
+ invoiceIds?: string[];
}
export interface SalesOrderDto extends SalesDocumentDto {
salesRequestId?: string | null;
salesRequest?: { id: string; code?: string; name?: string } | null;
+ packingSlipIds?: string[];
+ invoiceIds?: string[];
}
diff --git a/apps/web/src/apps/main/modules/sales/orders/domain/transformers/sales-order.remote.transformer.ts b/apps/web/src/apps/main/modules/sales/orders/domain/transformers/sales-order.remote.transformer.ts
index a1e3f50..05e98ef 100644
--- a/apps/web/src/apps/main/modules/sales/orders/domain/transformers/sales-order.remote.transformer.ts
+++ b/apps/web/src/apps/main/modules/sales/orders/domain/transformers/sales-order.remote.transformer.ts
@@ -18,6 +18,8 @@ export class SalesOrdersRemoteDataTransformer extends BaseDataTransformer();
+ const { t } = useEnterpriseModuleTranslationContext();
+ const packingSlipIds = detailData?.packingSlipIds ?? [];
+ const invoiceIds = detailData?.invoiceIds ?? [];
+
+ return (
+
+
+ {t('section_related')}
+
+
+
+
+ {t('common:fields.packingSlip')}
+
+ {packingSlipIds.length === 0 ? (
+ -
+ ) : (
+ packingSlipIds.map((id) => (
+
+ {id}
+
+ ))
+ )}
+
+
+
+ {t('common:fields.salesInvoice')}
+
+ {invoiceIds.length === 0 ? (
+ -
+ ) : (
+ invoiceIds.map((id) => (
+
+ {id}
+
+ ))
+ )}
+
+
+
+ );
+}
diff --git a/apps/web/src/apps/main/modules/sales/orders/presentation/languages/en/sales-orders.json b/apps/web/src/apps/main/modules/sales/orders/presentation/languages/en/sales-orders.json
index e4e1816..b8e3abb 100644
--- a/apps/web/src/apps/main/modules/sales/orders/presentation/languages/en/sales-orders.json
+++ b/apps/web/src/apps/main/modules/sales/orders/presentation/languages/en/sales-orders.json
@@ -28,8 +28,10 @@
"status_updated": "Status updated.",
"create_sales_invoice": "Create Sales Invoice",
"action_process": "Process",
- "action_complete": "Complete",
"action_cancel": "Cancel",
+ "section_related": "Related documents",
+ "generate_packing_slip": "Generate packing slip",
+ "process_order_help": "Processing creates a sales invoice and, unless skipped, a packing slip.",
"status_draft": "Draft",
"status_processed": "Processed",
"status_completed": "Completed",
diff --git a/apps/web/src/apps/main/modules/sales/orders/presentation/languages/id/sales-orders.json b/apps/web/src/apps/main/modules/sales/orders/presentation/languages/id/sales-orders.json
index b1555e4..812f068 100644
--- a/apps/web/src/apps/main/modules/sales/orders/presentation/languages/id/sales-orders.json
+++ b/apps/web/src/apps/main/modules/sales/orders/presentation/languages/id/sales-orders.json
@@ -28,8 +28,10 @@
"status_updated": "Status diperbarui.",
"create_sales_invoice": "Buat Faktur Penjualan",
"action_process": "Proses",
- "action_complete": "Selesaikan",
"action_cancel": "Batalkan",
+ "section_related": "Dokumen terkait",
+ "generate_packing_slip": "Buat surat jalan",
+ "process_order_help": "Memproses akan membuat faktur penjualan dan, kecuali dilewati, surat jalan.",
"status_draft": "Draft",
"status_processed": "Diproses",
"status_completed": "Selesai",
diff --git a/apps/web/src/apps/main/modules/sales/orders/presentation/pages/sales-order.page.detail.tsx b/apps/web/src/apps/main/modules/sales/orders/presentation/pages/sales-order.page.detail.tsx
index e7b628d..56fa619 100644
--- a/apps/web/src/apps/main/modules/sales/orders/presentation/pages/sales-order.page.detail.tsx
+++ b/apps/web/src/apps/main/modules/sales/orders/presentation/pages/sales-order.page.detail.tsx
@@ -6,6 +6,7 @@ import { DetailGeneral } from '../../../shared/detail-general';
import { DetailLocation } from '../../../shared/detail-location';
import { DetailProducts } from '../../../shared/detail-products';
import { DetailImages } from '../../../shared/detail-images';
+import { DetailRelated } from '../components/detail-component/detail-related';
import { useSalesDocumentActions } from '../../../shared/use-sales-document-actions';
import { salesRequestsModuleConfig } from '../../../requests/domain/constants';
import { salesInvoicesModuleConfig } from '../../../invoices/domain/constants';
@@ -39,6 +40,7 @@ export default function SalesOrderPageDetail() {
`${salesRequestsModuleConfig.webUrl}/detail/${id}`} />
+
{actions.modals}
diff --git a/apps/web/src/apps/main/modules/sales/payments/presentation/components/form-component/form-allocations.tsx b/apps/web/src/apps/main/modules/sales/payments/presentation/components/form-component/form-allocations.tsx
index 7066825..a273138 100644
--- a/apps/web/src/apps/main/modules/sales/payments/presentation/components/form-component/form-allocations.tsx
+++ b/apps/web/src/apps/main/modules/sales/payments/presentation/components/form-component/form-allocations.tsx
@@ -1,4 +1,14 @@
-import { ActionIcon, Box, Button, FieldAsyncSelect, FieldTextInput, Group, Paper, Table, Text } from '@repo/ui/components';
+import {
+ ActionIcon,
+ Box,
+ Button,
+ FieldAsyncSelect,
+ FieldTextInput,
+ Group,
+ Paper,
+ Table,
+ Text,
+} from '@repo/ui/components';
import { useEnterpriseModuleTranslationContext, useFormPageContext } from '@repo/ui/foundations';
import { useFieldArray, useWatch } from '@repo/ui/form';
import { Plus, Trash2 } from 'lucide-react';
@@ -66,12 +76,7 @@ export function FormAllocations() {
- }
- onClick={() => append({ amount: '' })}
- >
+ } onClick={() => append({ amount: '' })}>
{t('add_allocation')}
{fields.length === 0 ? (
diff --git a/apps/web/src/apps/main/modules/sales/payments/presentation/components/form-component/form-payment-general.tsx b/apps/web/src/apps/main/modules/sales/payments/presentation/components/form-component/form-payment-general.tsx
index 734e746..4434ed9 100644
--- a/apps/web/src/apps/main/modules/sales/payments/presentation/components/form-component/form-payment-general.tsx
+++ b/apps/web/src/apps/main/modules/sales/payments/presentation/components/form-component/form-payment-general.tsx
@@ -12,12 +12,7 @@ export function FormPaymentGeneral() {
-
+
void;
documentType: SalesDocumentType;
+ currentStatus?: string | null;
onSubmit: (status: string) => Promise | void;
}) {
const { t } = useEnterpriseModuleTranslationContext();
const form = useForm<{ status: string }>();
+ const options = allowedTransitions(documentType, currentStatus);
const handleSubmit = form.handleSubmit(async (values) => {
await onSubmit(values.status);
@@ -32,7 +35,7 @@ export function ChangeStatusModal({
name="status"
label={t('common:fields.status')}
required
- data={documentStatuses(documentType).map((value) => ({ value, label: t(`status_${value}`) }))}
+ data={options.map((value) => ({ value, label: t(`status_${value}`) }))}
/>