From 532e30672e1b8927769feb5371638cb1e69495a4 Mon Sep 17 00:00:00 2001 From: shancheas Date: Mon, 31 Aug 2026 16:02:40 +0700 Subject: [PATCH] feat: enhance packing slips module with new action and language support - Added "Process" action to the packing slips module, allowing users to transition packing slips from draft to processed status. - Updated English and Indonesian language files to include the new "Process" action, improving user experience and consistency across languages. - Refactored sales status handling to accommodate the new action, ensuring proper workflow integration. These changes enhance the packing slips functionality by providing additional actions and improving language support, streamlining user interactions. --- .../languages/en/packing-slips.json | 1 + .../languages/id/packing-slips.json | 1 + .../pages/sales-order.page.detail.tsx | 13 +-- .../sales/shared/change-status-modal.tsx | 50 ----------- .../modules/sales/shared/sales-status.test.ts | 30 ++++++- .../main/modules/sales/shared/sales-status.ts | 20 ++++- .../shared/use-sales-document-actions.tsx | 83 +++---------------- 7 files changed, 64 insertions(+), 134 deletions(-) delete mode 100644 apps/web/src/apps/main/modules/sales/shared/change-status-modal.tsx 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 index 0dc64ac..7a9de65 100644 --- 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 @@ -23,6 +23,7 @@ "status_updated": "Status updated.", "action_complete": "Complete", "action_cancel": "Cancel", + "action_process": "Process", "complete_packing": "Complete packing", "complete_packing_help": "Enter delivered quantity for each line. Remaining quantity opens a new packing slip.", "delivered_quantity": "Delivered quantity", 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 index 33eb84b..777aa0a 100644 --- 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 @@ -23,6 +23,7 @@ "status_updated": "Status diperbarui.", "action_complete": "Selesaikan", "action_cancel": "Batalkan", + "action_process": "Proses", "complete_packing": "Selesaikan packing", "complete_packing_help": "Masukkan kuantitas terkirim untuk setiap baris. Sisa kuantitas akan membuka surat jalan baru.", "delivered_quantity": "Kuantitas terkirim", 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 56fa619..0b2851a 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 @@ -1,4 +1,3 @@ -import { useNavigate } from 'react-router-dom'; import { Stack } from '@repo/ui/components'; import { EnterpriseDetailPageProvider, useEnterpriseModuleTranslationContext } from '@repo/ui/foundations'; import { salesOrdersModuleConfig } from '../../domain/constants'; @@ -9,12 +8,10 @@ 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'; import type { SalesOrderEntity } from '../../domain/entities'; export default function SalesOrderPageDetail() { const { t } = useEnterpriseModuleTranslationContext(); - const navigate = useNavigate(); const actions = useSalesDocumentActions('order'); return ( @@ -28,13 +25,9 @@ export default function SalesOrderPageDetail() { { label: t('nav:sales-orders'), type: 'link', href: `${salesOrdersModuleConfig.webUrl}/index` }, ], }} - customPageActions={(data, pageActions) => { - const createInvoice = actions.createInvoiceAction(data as SalesOrderEntity, () => { - navigate(`${salesInvoicesModuleConfig.webUrl}/create?salesOrderId=${data.id}`); - }); - const withStatus = actions.detailStatusActions(data as SalesOrderEntity, pageActions ?? []); - return createInvoice ? [createInvoice, ...withStatus] : withStatus; - }} + customPageActions={(data, pageActions) => + actions.detailStatusActions(data as SalesOrderEntity, pageActions ?? []) + } > `${salesRequestsModuleConfig.webUrl}/detail/${id}`} /> diff --git a/apps/web/src/apps/main/modules/sales/shared/change-status-modal.tsx b/apps/web/src/apps/main/modules/sales/shared/change-status-modal.tsx deleted file mode 100644 index 0fb5c0a..0000000 --- a/apps/web/src/apps/main/modules/sales/shared/change-status-modal.tsx +++ /dev/null @@ -1,50 +0,0 @@ -import { Button, FieldSelect, Group, Modal, Stack } from '@repo/ui/components'; -import { useForm } from 'react-hook-form'; -import { useEnterpriseModuleTranslationContext } from '@repo/ui/foundations'; -import { allowedTransitions, type SalesDocumentType } from './sales-status'; - -export function ChangeStatusModal({ - opened, - onClose, - documentType, - currentStatus, - onSubmit, -}: { - opened: boolean; - onClose: () => 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); - form.reset(); - onClose(); - }); - - return ( - -
- - ({ value, label: t(`status_${value}`) }))} - /> - - - - - -
-
- ); -} diff --git a/apps/web/src/apps/main/modules/sales/shared/sales-status.test.ts b/apps/web/src/apps/main/modules/sales/shared/sales-status.test.ts index ff7dc31..e3e403f 100644 --- a/apps/web/src/apps/main/modules/sales/shared/sales-status.test.ts +++ b/apps/web/src/apps/main/modules/sales/shared/sales-status.test.ts @@ -1,5 +1,6 @@ import { describe, expect, it } from 'vitest'; -import { allowedTransitions, commonTransitions, namedActionsFor } from './sales-status'; +import { ModuleAction } from '@repo/ui/foundations'; +import { allowedTransitions, commonTransitions, keepSalesChromeActions, namedActionsFor } from './sales-status'; describe('sales status transitions', () => { it('allows request draft to pending or rejected', () => { @@ -30,8 +31,10 @@ describe('sales status transitions', () => { expect(allowedTransitions('invoice', 'draft')).toEqual(['processed', 'cancelled']); expect(allowedTransitions('invoice', 'processed')).toEqual(['cancelled']); expect(allowedTransitions('invoice', 'partial')).toEqual(['cancelled']); + expect(allowedTransitions('invoice', 'completed')).toEqual(['cancelled']); expect(namedActionsFor('invoice', 'draft').map((action) => action.key)).toEqual(['process', 'cancel']); expect(namedActionsFor('invoice', 'processed').map((action) => action.key)).toEqual(['cancel']); + expect(namedActionsFor('invoice', 'completed').map((action) => action.key)).toEqual(['cancel']); }); it('adds payment rollback from pending to draft', () => { @@ -43,8 +46,33 @@ describe('sales status transitions', () => { ]); }); + it('allows packing draft to processed or cancelled', () => { + expect(allowedTransitions('packing', 'draft')).toEqual(['processed', 'cancelled']); + expect(namedActionsFor('packing', 'draft').map((action) => action.key)).toEqual(['process', 'cancel']); + }); + it('allows packing processed to completed or cancelled', () => { expect(allowedTransitions('packing', 'processed')).toEqual(['completed', 'cancelled']); expect(namedActionsFor('packing', 'processed').map((action) => action.key)).toEqual(['complete', 'cancel']); }); + + it('strips generic transaction status chrome from default actions', () => { + const defaultActions = [ + { key: 'VIEW', label: 'View' }, + { key: ModuleAction.EDIT, label: 'Edit' }, + { key: ModuleAction.HOLD, label: 'Hold' }, + { key: ModuleAction.ROLLBACK, label: 'Rollback' }, + { key: ModuleAction.CANCEL, label: 'Cancel' }, + { key: ModuleAction.CONFIRM, label: 'Confirm' }, + { key: 'change-status', label: 'Change status' }, + { key: 'bulk-change-status', label: 'Change status' }, + { key: ModuleAction.DELETE, label: 'Delete' }, + ]; + + expect(keepSalesChromeActions(defaultActions).map((action) => action.key)).toEqual([ + 'VIEW', + ModuleAction.EDIT, + ModuleAction.DELETE, + ]); + }); }); diff --git a/apps/web/src/apps/main/modules/sales/shared/sales-status.ts b/apps/web/src/apps/main/modules/sales/shared/sales-status.ts index e7f57c8..ff8e503 100644 --- a/apps/web/src/apps/main/modules/sales/shared/sales-status.ts +++ b/apps/web/src/apps/main/modules/sales/shared/sales-status.ts @@ -56,13 +56,18 @@ export const SALES_ORDER_NAMED_ACTIONS = [ ] as const; export const PACKING_SLIP_NAMED_ACTIONS = [ + { key: 'process', target: 'processed' as const, from: ['draft'] as const }, { key: 'complete', target: 'completed' as const, from: ['processed'] as const }, { key: 'cancel', target: 'cancelled' as const, from: ['draft', 'processed'] as const }, ] as const; export const SALES_INVOICE_NAMED_ACTIONS = [ { key: 'process', target: 'processed' as const, from: ['draft'] as const }, - { key: 'cancel', target: 'cancelled' as const, from: ['draft', 'processed', 'partial'] as const }, + { + key: 'cancel', + target: 'cancelled' as const, + from: ['draft', 'processed', 'partial', 'completed'] as const, + }, ] as const; export const SALES_PAYMENT_NAMED_ACTIONS = [ @@ -120,3 +125,16 @@ export function documentStatusFilterOptions(t: (key: string) => string, type: Sa label: t(`status_${value}`), })); } + +export const SALES_GENERIC_STATUS_ACTION_KEYS = new Set([ + 'HOLD', + 'CONFIRM', + 'CANCEL', + 'ROLLBACK', + 'change-status', + 'bulk-change-status', +]); + +export function keepSalesChromeActions(actions: T[]): T[] { + return actions.filter((action) => !action.key || !SALES_GENERIC_STATUS_ACTION_KEYS.has(action.key)); +} diff --git a/apps/web/src/apps/main/modules/sales/shared/use-sales-document-actions.tsx b/apps/web/src/apps/main/modules/sales/shared/use-sales-document-actions.tsx index bbac68b..884fcc9 100644 --- a/apps/web/src/apps/main/modules/sales/shared/use-sales-document-actions.tsx +++ b/apps/web/src/apps/main/modules/sales/shared/use-sales-document-actions.tsx @@ -1,5 +1,5 @@ import { useMemo, useState } from 'react'; -import { Check, FileUp, ShoppingCart, XCircle, Play, Ban, Receipt, CreditCard, Undo2 } from 'lucide-react'; +import { Check, FileUp, ShoppingCart, XCircle, Play, Ban, CreditCard, Undo2 } from 'lucide-react'; import { notifications } from '@repo/ui/components'; import { useDetailPageContext, @@ -7,12 +7,11 @@ import { useEnterpriseModuleDataServiceContext, useEnterpriseModuleTranslationContext, } from '@repo/ui/foundations'; -import { namedActionsFor, type SalesDocumentType } from './sales-status'; +import { keepSalesChromeActions, namedActionsFor, type SalesDocumentType } from './sales-status'; import type { SalesDocumentRemoteDataServices } from './sales-document.remote.service'; import type { SalesDocumentEntity, SalesLineEntity } from './sales-document.entity'; type SalesActionEntity = { id?: string | number; status?: string; products?: SalesLineEntity[] }; -import { ChangeStatusModal } from './change-status-modal'; import { ImportCsvModal } from './import-csv-modal'; import { ProcessOrderModal } from './process-order-modal'; import { CompletePackingModal } from './complete-packing-modal'; @@ -34,12 +33,10 @@ export function useSalesDocumentActions(documentType: SalesDocumentType) { SalesDocumentEntity, SalesDocumentRemoteDataServices >(); - const [statusOpened, setStatusOpened] = useState(false); const [importOpened, setImportOpened] = useState(false); const [processOpened, setProcessOpened] = useState(false); const [completeOpened, setCompleteOpened] = useState(false); const [pendingIds, setPendingIds] = useState([]); - const [pendingStatus, setPendingStatus] = useState(); const [completeProducts, setCompleteProducts] = useState([]); const canEdit = privileges.ALLOW_EDIT; const canImport = privileges.ALLOW_IMPORT; @@ -72,7 +69,8 @@ export function useSalesDocumentActions(documentType: SalesDocumentType) { }; const namedRowActions = (data: SalesActionEntity, defaultActions: any[]) => { - if (!canEdit) return defaultActions; + const chromeActions = keepSalesChromeActions(defaultActions); + if (!canEdit) return chromeActions; const extras: any[] = namedActionsFor(documentType, data.status).map((action) => { const Icon = ACTION_ICONS[action.key] ?? Check; return { @@ -84,23 +82,12 @@ export function useSalesDocumentActions(documentType: SalesDocumentType) { }, }; }); - if (documentType !== 'invoice') { - extras.push({ - key: 'change-status', - label: t('change_status'), - icon: , - onClick: () => { - setPendingIds([String(data.id)]); - setPendingStatus(data.status); - setStatusOpened(true); - }, - }); - } - return [...extras, ...defaultActions]; + return [...extras, ...chromeActions]; }; const namedBulkActions = (selectedRows: SalesActionEntity[], defaultActions: any[]) => { - if (!canEdit || selectedRows.length === 0) return defaultActions; + const chromeActions = keepSalesChromeActions(defaultActions); + if (!canEdit || selectedRows.length === 0) return chromeActions; const statuses = selectedRows.map((row) => row.status); const extras: any[] = namedActionsFor(documentType, statuses[0]) .filter((action) => statuses.every((status) => (action.from as readonly string[]).includes(status ?? ''))) @@ -121,20 +108,7 @@ export function useSalesDocumentActions(documentType: SalesDocumentType) { }, }; }); - if (documentType !== 'invoice') { - extras.push({ - key: 'bulk-change-status', - label: t('change_status'), - icon: , - variant: 'light' as const, - onClick: () => { - setPendingIds(selectedRows.map((row) => String(row.id))); - setPendingStatus(selectedRows[0]?.status); - setStatusOpened(true); - }, - }); - } - return [...extras, ...defaultActions]; + return [...extras, ...chromeActions]; }; const importPageAction = canImport @@ -161,18 +135,6 @@ export function useSalesDocumentActions(documentType: SalesDocumentType) { } : null; - const createInvoiceAction = (_data: SalesActionEntity, onClick: () => void) => - canEdit - ? { - key: 'create-invoice', - label: t('create_sales_invoice'), - icon: , - intent: 'primary' as const, - variant: 'light' as const, - onClick, - } - : null; - const createPaymentAction = (_data: SalesActionEntity, onClick: () => void) => canEdit ? { @@ -186,7 +148,8 @@ export function useSalesDocumentActions(documentType: SalesDocumentType) { : null; const detailStatusActions = (data: SalesActionEntity, defaultActions: any[]) => { - if (!canEdit) return defaultActions; + const chromeActions = keepSalesChromeActions(defaultActions); + if (!canEdit) return chromeActions; const extras: any[] = namedActionsFor(documentType, data.status).map((action) => { const Icon = ACTION_ICONS[action.key] ?? Check; return { @@ -200,34 +163,11 @@ export function useSalesDocumentActions(documentType: SalesDocumentType) { }, }; }); - if (documentType !== 'invoice') { - extras.push({ - key: 'change-status', - label: t('change_status'), - icon: , - intent: 'primary' as const, - variant: 'light' as const, - onClick: () => { - setPendingIds([String(data.id)]); - setPendingStatus(data.status); - setStatusOpened(true); - }, - }); - } - return [...extras, ...defaultActions]; + return [...extras, ...chromeActions]; }; const modals = ( <> - setStatusOpened(false)} - documentType={documentType} - currentStatus={pendingStatus} - onSubmit={async (status) => { - await applyStatus(pendingIds, status); - }} - /> setProcessOpened(false)} @@ -260,7 +200,6 @@ export function useSalesDocumentActions(documentType: SalesDocumentType) { namedBulkActions, importPageAction, createOrderAction, - createInvoiceAction, createPaymentAction, detailStatusActions, applyStatus,