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,