diff --git a/.gitignore b/.gitignore
index 82965e2d..d86875dd 100644
--- a/.gitignore
+++ b/.gitignore
@@ -40,8 +40,5 @@ yarn-error.log*
*.tsbuildinfo
next-env.d.ts
-# prettier
-.prettierrc
-
# idea
.idea
diff --git a/package-lock.json b/package-lock.json
index b5a70787..ec1316ae 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -14,7 +14,6 @@
"axios": "^1.12.2",
"clsx": "^2.1.1",
"formik": "^2.4.6",
- "inputmask": "^5.0.9",
"moment": "^2.30.1",
"next": "15.5.3",
"react": "19.1.0",
@@ -34,7 +33,6 @@
"@eslint/eslintrc": "^3",
"@iconify/react": "^6.0.2",
"@tailwindcss/postcss": "^4",
- "@types/inputmask": "^5.0.7",
"@types/node": "^20",
"@types/react": "^19",
"@types/react-dom": "^19",
@@ -1822,13 +1820,6 @@
"@types/react": "*"
}
},
- "node_modules/@types/inputmask": {
- "version": "5.0.7",
- "resolved": "https://registry.npmjs.org/@types/inputmask/-/inputmask-5.0.7.tgz",
- "integrity": "sha512-uojbVPWzBQ/n/0jc/d16fLqmGasFIptbrLD2WrCPWArlk+5PgblOqH4EDkI3AoobXLAlOK5yF01V8jMmvMG5qg==",
- "dev": true,
- "license": "MIT"
- },
"node_modules/@types/json-schema": {
"version": "7.0.15",
"resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz",
@@ -4564,12 +4555,6 @@
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
"license": "ISC"
},
- "node_modules/inputmask": {
- "version": "5.0.9",
- "resolved": "https://registry.npmjs.org/inputmask/-/inputmask-5.0.9.tgz",
- "integrity": "sha512-s0lUfqcEbel+EQXtehXqwCJGShutgieOaIImFKC/r4reYNvX3foyrChl6LOEvaEgxEbesePIrw1Zi2jhZaDZbQ==",
- "license": "MIT"
- },
"node_modules/internal-slot": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.1.0.tgz",
@@ -5056,9 +5041,9 @@
"license": "MIT"
},
"node_modules/js-yaml": {
- "version": "4.1.0",
- "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz",
- "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==",
+ "version": "4.1.1",
+ "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz",
+ "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==",
"dev": true,
"license": "MIT",
"dependencies": {
diff --git a/package.json b/package.json
index 6d3edacd..7396d49d 100644
--- a/package.json
+++ b/package.json
@@ -17,7 +17,6 @@
"axios": "^1.12.2",
"clsx": "^2.1.1",
"formik": "^2.4.6",
- "inputmask": "^5.0.9",
"moment": "^2.30.1",
"next": "15.5.3",
"react": "19.1.0",
@@ -37,7 +36,6 @@
"@eslint/eslintrc": "^3",
"@iconify/react": "^6.0.2",
"@tailwindcss/postcss": "^4",
- "@types/inputmask": "^5.0.7",
"@types/node": "^20",
"@types/react": "^19",
"@types/react-dom": "^19",
diff --git a/src/app/production/recording/add/layout.tsx b/src/app/production/recording/add/layout.tsx
new file mode 100644
index 00000000..7220dfa1
--- /dev/null
+++ b/src/app/production/recording/add/layout.tsx
@@ -0,0 +1,11 @@
+import SuspenseHelper from '@/components/helper/SuspenseHelper';
+
+const Layout = ({
+ children,
+}: Readonly<{
+ children: React.ReactNode;
+}>) => {
+ return {children};
+};
+
+export default Layout;
diff --git a/src/app/production/recording/grading/add/page.tsx b/src/app/production/recording/grading/add/page.tsx
new file mode 100644
index 00000000..9b918d98
--- /dev/null
+++ b/src/app/production/recording/grading/add/page.tsx
@@ -0,0 +1,49 @@
+'use client';
+
+import { useRouter, useSearchParams } from 'next/navigation';
+import useSWR from 'swr';
+import GradingForm from '@/components/pages/production/recording/grading/form/GradingForm';
+import { RecordingApi } from '@/services/api/production';
+import { isResponseSuccess } from '@/lib/api-helper';
+
+const AddGrading = () => {
+ const router = useRouter();
+ const searchParams = useSearchParams();
+
+ const recordingId = searchParams.get('recording_id');
+
+ const { data: recording, isLoading: isLoadingRecording } = useSWR(
+ recordingId && recordingId !== 'new' ? [recordingId] : null,
+ ([id]) => RecordingApi.getSingle(parseInt(id))
+ );
+
+ if (
+ recordingId &&
+ recordingId !== 'new' &&
+ !isLoadingRecording &&
+ (!recording || !isResponseSuccess(recording))
+ ) {
+ router.replace('/404');
+ return;
+ }
+
+ return (
+
+ {recordingId && recordingId !== 'new' && isLoadingRecording && (
+
+ )}
+ {(!recordingId ||
+ recordingId === 'new' ||
+ (!isLoadingRecording && recording && isResponseSuccess(recording))) && (
+
+ )}
+
+ );
+};
+
+export default AddGrading;
diff --git a/src/app/production/recording/grading/detail/edit/page.tsx b/src/app/production/recording/grading/detail/edit/page.tsx
new file mode 100644
index 00000000..0a65f528
--- /dev/null
+++ b/src/app/production/recording/grading/detail/edit/page.tsx
@@ -0,0 +1,53 @@
+'use client';
+
+import { useRouter, useSearchParams } from 'next/navigation';
+import useSWR from 'swr';
+import GradingForm from '@/components/pages/production/recording/grading/form/GradingForm';
+import { RecordingApi } from '@/services/api/production';
+import { isResponseSuccess } from '@/lib/api-helper';
+
+const EditGrading = () => {
+ const router = useRouter();
+ const searchParams = useSearchParams();
+
+ const recordingId = searchParams.get('recordingId');
+ const gradingId = searchParams.get('gradingId');
+
+ const { data: recording, isLoading: isLoadingRecording } = useSWR(
+ recordingId ? [recordingId] : null,
+ ([id]) => RecordingApi.getSingle(parseInt(id))
+ );
+
+ if (!recordingId) {
+ router.back();
+
+ return (
+
+
+
+ );
+ }
+
+ if (!isLoadingRecording && (!recording || !isResponseSuccess(recording))) {
+ router.replace('/404');
+ return;
+ }
+
+ return (
+
+ {isLoadingRecording && (
+
+ )}
+ {!isLoadingRecording && recording && isResponseSuccess(recording) && (
+ egg.id === parseInt(gradingId || '0')
+ )}
+ />
+ )}
+
+ );
+};
+
+export default EditGrading;
diff --git a/src/app/production/recording/grading/detail/page.tsx b/src/app/production/recording/grading/detail/page.tsx
new file mode 100644
index 00000000..6a5fbcba
--- /dev/null
+++ b/src/app/production/recording/grading/detail/page.tsx
@@ -0,0 +1,52 @@
+'use client';
+
+import { useRouter, useSearchParams } from 'next/navigation';
+import useSWR from 'swr';
+import GradingForm from '@/components/pages/production/recording/grading/form/GradingForm';
+import { RecordingApi } from '@/services/api/production';
+import { isResponseSuccess } from '@/lib/api-helper';
+
+const DetailGrading = () => {
+ const router = useRouter();
+ const searchParams = useSearchParams();
+
+ const gradingId = searchParams.get('gradingId');
+
+ const { data: grading, isLoading: isLoadingGrading } = useSWR(
+ gradingId ? [gradingId] : null,
+ ([id]) => RecordingApi.getSingle(parseInt(id))
+ );
+
+ if (!gradingId) {
+ router.back();
+
+ return (
+
+
+
+ );
+ }
+
+ if (!isLoadingGrading && (!grading || !isResponseSuccess(grading))) {
+ router.replace('/404');
+ return;
+ }
+
+ return (
+
+ {isLoadingGrading && (
+
+ )}
+ {!isLoadingGrading && grading && isResponseSuccess(grading) && (
+ egg.id === parseInt(gradingId)
+ )}
+ />
+ )}
+
+ );
+};
+
+export default DetailGrading;
diff --git a/src/app/production/recording/grading/layout.tsx b/src/app/production/recording/grading/layout.tsx
new file mode 100644
index 00000000..7220dfa1
--- /dev/null
+++ b/src/app/production/recording/grading/layout.tsx
@@ -0,0 +1,11 @@
+import SuspenseHelper from '@/components/helper/SuspenseHelper';
+
+const Layout = ({
+ children,
+}: Readonly<{
+ children: React.ReactNode;
+}>) => {
+ return {children};
+};
+
+export default Layout;
diff --git a/src/components/Card.tsx b/src/components/Card.tsx
index cde902f0..7b022971 100644
--- a/src/components/Card.tsx
+++ b/src/components/Card.tsx
@@ -3,6 +3,7 @@
import { HTMLAttributes, ReactNode } from 'react';
import { cn } from '@/lib/helper';
+import Image from 'next/image';
export interface CardProps
extends Omit, 'className'> {
@@ -106,7 +107,7 @@ const Card = ({
return (
-
{image && (
-
{
editUrl?: string;
onDelete?: () => void;
disableSubmit?: boolean;
+ onApprove?: () => void;
+ onReject?: () => void;
+ isApproveLoading?: boolean;
+ isRejectLoading?: boolean;
+ showApproveReject?: boolean;
}
export const FormActions = ({
@@ -17,25 +22,32 @@ export const FormActions = ({
editUrl,
onDelete,
disableSubmit = false,
+ onApprove,
+ onReject,
+ isApproveLoading = false,
+ isRejectLoading = false,
+ showApproveReject = false,
}: FormActionsProps) => {
return (
- {type !== 'add' && onDelete && (
+ {type !== 'add' && (
-
+ {onDelete && (
+
+ )}
{type !== 'edit' && editUrl && (
)}
+ {type === 'detail' &&
+ showApproveReject &&
+ (onApprove || onReject) && (
+ <>
+ {onApprove && (
+
+ )}
+ {onReject && (
+
+ )}
+ >
+ )}
)}
{type !== 'detail' && (
diff --git a/src/components/input/NumberInput.tsx b/src/components/input/NumberInput.tsx
index dd6c8c2b..e6e0e773 100644
--- a/src/components/input/NumberInput.tsx
+++ b/src/components/input/NumberInput.tsx
@@ -49,8 +49,8 @@ const NumberInput = ({
onValueChange={valueChangeHandler}
decimalScale={decimalScale}
allowNegative={allowNegative}
- startAdornment={inputPrefix}
- endAdornment={inputSuffix}
+ inputPrefix={inputPrefix}
+ inputSuffix={inputSuffix}
{...restProps}
/>
);
diff --git a/src/components/input/TextInput.tsx b/src/components/input/TextInput.tsx
index 4ee1f558..5936c85a 100644
--- a/src/components/input/TextInput.tsx
+++ b/src/components/input/TextInput.tsx
@@ -31,6 +31,8 @@ export interface TextInputProps {
errorMessage?: string;
startAdornment?: ReactNode;
endAdornment?: ReactNode;
+ inputPrefix?: ReactNode;
+ inputSuffix?: ReactNode;
onChange?: ChangeEventHandler
;
onBlur?: FocusEventHandler;
}
@@ -48,6 +50,8 @@ const TextInput = ({
errorMessage,
startAdornment,
endAdornment,
+ inputPrefix,
+ inputSuffix,
disabled = false,
required = false,
onChange,
@@ -85,39 +89,117 @@ const TextInput = ({
)}
-
- {startAdornment && startAdornment}
+ {inputPrefix || inputSuffix ? (
+
+ {inputPrefix && (
+
+ {inputPrefix}
+
+ )}
-
+
+ {startAdornment && startAdornment}
- {(isLoading || endAdornment) && (
-
- {isLoading &&
}
+
- {endAdornment && endAdornment}
+ {(isLoading || endAdornment) && (
+
+ {isLoading && }
+
+ {endAdornment && endAdornment}
+
+ )}
- )}
-
+
+ {inputSuffix && (
+
+ {inputSuffix}
+
+ )}
+
+ ) : (
+
+ {startAdornment && startAdornment}
+
+
+
+ {(isLoading || endAdornment) && (
+
+ {isLoading && }
+
+ {endAdornment && endAdornment}
+
+ )}
+
+ )}
{!isError && bottomLabel && (
{bottomLabel}
diff --git a/src/components/pages/ApprovalSteps.tsx b/src/components/pages/ApprovalSteps.tsx
index 7185e31b..b9590bc2 100644
--- a/src/components/pages/ApprovalSteps.tsx
+++ b/src/components/pages/ApprovalSteps.tsx
@@ -158,6 +158,7 @@ export const formatGroupedApprovalsToApprovalSteps = (
if (approvalGroup.approvals) {
switch (approvalGroup?.approvals[0]?.action) {
case 'CREATED':
+ case 'UPDATED':
case 'APPROVED':
approvalStatus = 'APPROVED';
break;
@@ -256,7 +257,7 @@ const useApprovalSteps = ({
moduleName: string;
moduleId: string;
params?: {
- page: number;
+ page?: number;
limit: number;
search?: string;
group_step_number?: boolean;
diff --git a/src/components/pages/inventory/movement/MovementTable.tsx b/src/components/pages/inventory/movement/MovementTable.tsx
index 6926ce4e..8ff39e3d 100644
--- a/src/components/pages/inventory/movement/MovementTable.tsx
+++ b/src/components/pages/inventory/movement/MovementTable.tsx
@@ -1,24 +1,46 @@
'use client';
-import { useState } from 'react';
+import { ChangeEventHandler, useState } from 'react';
import useSWR from 'swr';
-import { SortingState } from '@tanstack/react-table';
+import { SortingState, CellContext, ColumnDef } from '@tanstack/react-table';
import Table from '@/components/Table';
-import { useModal } from '@/components/Modal';
-import ConfirmationModal from '@/components/modal/ConfirmationModal';
+import { Icon } from '@iconify/react';
import { Movement } from '@/types/api/inventory/movement';
import { MovementApi } from '@/services/api/inventory';
import { cn } from '@/lib/helper';
+import { Product } from '@/types/api/master-data/product';
+import { Warehouse } from '@/types/api/master-data/warehouse';
import { isResponseSuccess } from '@/lib/api-helper';
import { useTableFilter } from '@/services/hooks/useTableFilter';
import { ROWS_OPTIONS } from '@/config/constant';
-import { TableToolbar } from '@/components/table/TableToolbar';
-import { TableRowSizeSelector } from '@/components/table/TableRowSizeSelector';
-import { OptionType } from '@/components/input/SelectInput';
+import { OptionType, useSelect } from '@/components/input/SelectInput';
+import Button from '@/components/Button';
+import DebouncedTextInput from '@/components/input/DebouncedTextInput';
+import SelectInput from '@/components/input/SelectInput';
import RowDropdownOptions from '@/components/table/RowDropdownOptions';
import RowCollapseOptions from '@/components/table/RowCollapseOptions';
-import { TableRowOptions } from '@/components/table/TableRowOptions';
+import RowOptionsMenuWrapper from '@/components/table/RowOptionsMenuWrapper';
+
+const RowOptionsMenu = ({
+ type = 'dropdown',
+ props,
+}: {
+ type: 'dropdown' | 'collapse';
+ props: CellContext
;
+}) => (
+
+
+
+);
const MovementTable = () => {
const {
@@ -28,30 +50,47 @@ const MovementTable = () => {
setPageSize,
toQueryString: getTableFilterQueryString,
} = useTableFilter({
- initial: { search: '' },
- paramMap: { page: 'page', pageSize: 'limit' },
+ initial: {
+ search: '',
+ product: '',
+ warehouse: '',
+ },
+ paramMap: {
+ page: 'page',
+ pageSize: 'limit',
+ product: 'product_id',
+ warehouse: 'warehouse_id',
+ },
});
const [sorting, setSorting] = useState([]);
- const [selectedMovement, setSelectedMovement] = useState<
- Movement | undefined
- >(undefined);
- const [isDeleteLoading, setIsDeleteLoading] = useState(false);
-
- const deleteModal = useModal();
const {
- data: movements,
- isLoading,
- mutate: refreshMovements,
- } = useSWR(
+ setInputValue: setProductInputValue,
+ options: productOptions,
+ isLoadingOptions: isLoadingProductOptions,
+ } = useSelect('/products', 'id', 'name');
+
+ const {
+ setInputValue: setWarehouseInputValue,
+ options: warehouseOptions,
+ isLoadingOptions: isLoadingWarehouseOptions,
+ } = useSelect('/warehouses', 'id', 'name');
+
+ const [selectedProduct, setSelectedProduct] = useState(
+ null
+ );
+ const [selectedWarehouse, setSelectedWarehouse] = useState(
+ null
+ );
+
+ const { data: movements, isLoading } = useSWR(
`${MovementApi.basePath}${getTableFilterQueryString()}`,
MovementApi.getAllFetcher
);
- const searchChangeHandler = (e: React.ChangeEvent) => {
+ const searchChangeHandler: ChangeEventHandler = (e) => {
updateFilter('search', e.target.value);
- setPage(1);
};
const pageSizeChangeHandler = (val: OptionType | OptionType[] | null) => {
@@ -60,167 +99,179 @@ const MovementTable = () => {
setPage(1);
};
- const confirmationModalDeleteClickHandler = async () => {
- setIsDeleteLoading(true);
- try {
- await MovementApi.delete(selectedMovement?.id as number);
- refreshMovements();
- deleteModal.closeModal();
- } finally {
- setIsDeleteLoading(false);
- }
+ const productChangeHandler = (val: OptionType | OptionType[] | null) => {
+ setSelectedProduct(val as OptionType);
+ updateFilter('product', val ? ((val as OptionType).value as string) : '');
};
+ const warehouseChangeHandler = (val: OptionType | OptionType[] | null) => {
+ setSelectedWarehouse(val as OptionType);
+ updateFilter('warehouse', val ? ((val as OptionType).value as string) : '');
+ };
+
+ const movementColumns: ColumnDef[] = [
+ {
+ header: '#',
+ cell: (props) =>
+ tableFilterState.pageSize * (tableFilterState.page - 1) +
+ props.row.index +
+ 1,
+ },
+ {
+ accessorFn: (row) => row.source_warehouse?.name,
+ header: 'Gudang Asal',
+ },
+ {
+ accessorFn: (row) => row.destination_warehouse?.name,
+ header: 'Gudang Tujuan',
+ },
+ {
+ accessorKey: 'transfer_reason',
+ header: 'Catatan',
+ },
+ {
+ accessorKey: 'transfer_date',
+ header: 'Tanggal',
+ cell: (props) =>
+ new Date(props.row.original.transfer_date).toLocaleDateString('id-ID'),
+ },
+ {
+ accessorFn: (row) => {
+ const totalCost = row.deliveries?.reduce(
+ (sum, d) => sum + (d.shipping_cost_total || 0),
+ 0
+ );
+ return totalCost?.toLocaleString('id-ID');
+ },
+ header: 'Biaya Pengiriman',
+ },
+ {
+ header: 'Aksi',
+ cell: (props) => {
+ const currentPageSize = props.table.getPaginationRowModel().rows.length;
+ const currentPageRows = props.table.getPaginationRowModel().flatRows;
+ const currentRowRelativeIndex =
+ currentPageRows.findIndex((r) => r.id === props.row.id) + 1;
+
+ const isLast2Rows = currentRowRelativeIndex > currentPageSize - 2;
+
+ return (
+ <>
+ {currentPageSize > 2 && (
+
+
+
+ )}
+
+ {currentPageSize <= 2 && (
+
+
+
+ )}
+ >
+ );
+ },
+ },
+ ];
+
return (
-
-
-
+
+
+
+
+ data={isResponseSuccess(movements) ? movements?.data : []}
+ columns={movementColumns}
+ pageSize={tableFilterState.pageSize}
+ page={isResponseSuccess(movements) ? movements?.meta?.page : 0}
+ totalItems={
+ isResponseSuccess(movements) ? movements?.meta?.total_results : 0
+ }
+ onPageChange={setPage}
+ isLoading={isLoading}
+ sorting={sorting}
+ setSorting={setSorting}
+ className={{
+ containerClassName: cn({
+ 'mb-20':
+ isResponseSuccess(movements) && movements?.data?.length === 0,
+ }),
+ tableWrapperClassName: 'overflow-x-auto min-h-full!',
+ tableClassName: 'font-inter w-full table-auto min-h-full!',
+ headerRowClassName: 'border-b border-b-gray-200',
+ headerColumnClassName:
+ 'px-6 py-3 text-xs font-semibold text-gray-500 last:flex last:flex-row last:justify-end',
+ bodyRowClassName: 'border-b border-b-gray-200',
+ bodyColumnClassName:
+ 'px-6 py-3 last:flex last:flex-row last:justify-end',
}}
- search={{
- value: tableFilterState.search,
- onChange: searchChangeHandler,
- placeholder: 'Cari Movement',
- }}
- />
-
-
-
- data={isResponseSuccess(movements) ? movements?.data : []}
- columns={[
- {
- header: '#',
- cell: (props) =>
- tableFilterState.pageSize * (tableFilterState.page - 1) +
- props.row.index +
- 1,
- },
- {
- accessorFn: (row) => row.source_warehouse?.name,
- header: 'Gudang Asal',
- },
- {
- accessorFn: (row) => row.destination_warehouse?.name,
- header: 'Gudang Tujuan',
- },
- {
- accessorKey: 'transfer_reason',
- header: 'Catatan',
- },
- {
- accessorKey: 'transfer_date',
- header: 'Tanggal',
- cell: (props) =>
- new Date(props.row.original.transfer_date).toLocaleDateString(
- 'id-ID'
- ),
- },
- {
- accessorFn: (row) => {
- const totalCost = row.deliveries?.reduce(
- (sum, d) => sum + (d.shipping_cost_total || 0),
- 0
- );
- return totalCost?.toLocaleString('id-ID');
- },
- header: 'Biaya Pengiriman',
- },
- {
- header: 'Aksi',
- cell: (props) => {
- const currentPageSize =
- props.table.getPaginationRowModel().rows.length;
- const currentPageRows =
- props.table.getPaginationRowModel().flatRows;
- const currentRowRelativeIndex =
- currentPageRows.findIndex((r) => r.id === props.row.id) + 1;
-
- const isLast2Rows = currentRowRelativeIndex > currentPageSize - 2;
-
- const deleteClickHandler = () => {
- setSelectedMovement(props.row.original);
- deleteModal.openModal();
- };
-
- return (
- <>
- {currentPageSize > 2 && (
-
-
-
- )}
-
- {currentPageSize <= 2 && (
-
-
-
- )}
- >
- );
- },
- },
- ]}
- pageSize={tableFilterState.pageSize}
- page={isResponseSuccess(movements) ? movements?.meta?.page : 0}
- totalItems={
- isResponseSuccess(movements) ? movements?.meta?.total_results : 0
- }
- onPageChange={setPage}
- isLoading={isLoading}
- sorting={sorting}
- setSorting={setSorting}
- className={{
- containerClassName: cn({
- 'mb-20':
- isResponseSuccess(movements) && movements?.data?.length === 0,
- }),
- tableWrapperClassName: 'overflow-x-auto min-h-full!',
- tableClassName: 'font-inter w-full table-auto min-h-full!',
- headerRowClassName: 'border-b border-b-gray-200',
- headerColumnClassName:
- 'px-6 py-3 text-xs font-semibold text-gray-500 last:flex last:flex-row last:justify-end',
- bodyRowClassName: 'border-b border-b-gray-200',
- bodyColumnClassName:
- 'px-6 py-3 last:flex last:flex-row last:justify-end',
- }}
- />
-
-
-
+ >
);
};
diff --git a/src/components/pages/inventory/movement/form/MovementForm.schema.ts b/src/components/pages/inventory/movement/form/MovementForm.schema.ts
index ed8fb479..39f7c669 100644
--- a/src/components/pages/inventory/movement/form/MovementForm.schema.ts
+++ b/src/components/pages/inventory/movement/form/MovementForm.schema.ts
@@ -1,34 +1,82 @@
import * as Yup from 'yup';
import { Movement } from '@/types/api/inventory/movement';
+type MovementFormSchemaType = {
+ transfer_reason: string;
+ transfer_date: string;
+ source_warehouse?: {
+ value: number;
+ label: string;
+ area?: string;
+ location?: string;
+ } | null;
+ source_warehouse_id: number;
+ destination_warehouse?: {
+ value: number;
+ label: string;
+ area?: string;
+ location?: string;
+ } | null;
+ destination_warehouse_id: number;
+ products: {
+ product?: {
+ value: number;
+ label: string;
+ } | null;
+ product_id: number;
+ product_qty: number | string;
+ }[];
+ deliveries: {
+ delivery_cost?: number | string;
+ delivery_cost_per_item?: number | string;
+ document?: File | string | null;
+ document_path?: string | null;
+ driver_name: string;
+ vehicle_plate: string;
+ supplier?: {
+ value: number;
+ label: string;
+ } | null;
+ supplier_id: number;
+ products: {
+ product?: {
+ value: number;
+ label: string;
+ } | null;
+ product_id: number;
+ product_qty: number | string;
+ }[];
+ }[];
+};
+
export type ProductSchema = {
- product: {
+ product?: {
value: number;
label: string;
} | null;
product_id: number;
- product_qty: number;
+ product_qty: number | string;
};
export type DeliverySchema = {
- delivery_cost?: number | undefined;
- delivery_cost_per_item?: number | undefined;
+ delivery_cost?: number | string;
+ delivery_cost_per_item?: number | string;
document?: File | string | null;
document_path?: string | null;
driver_name: string;
vehicle_plate: string;
- supplier: {
+ supplier?: {
value: number;
label: string;
} | null;
supplier_id: number;
products: {
- product: {
+ product?: {
value: number;
label: string;
} | null;
product_id: number;
- product_qty: number;
+ product_qty: number | string;
}[];
};
@@ -102,38 +150,37 @@ const DeliveryObjectSchema: Yup.ObjectSchema = Yup.object({
.required('Produk wajib diisi!'),
});
-export const MovementFormSchema = Yup.object({
- transfer_reason: Yup.string().required('Alasan transfer wajib diisi!'),
- transfer_date: Yup.string().required('Tanggal transfer wajib diisi!'),
- source_warehouse: Yup.object({
- value: Yup.number().min(1).required(),
- label: Yup.string().required(),
- area: Yup.string().optional(),
- location: Yup.string().optional(),
- }).nullable(),
- source_warehouse_id: Yup.number()
- .required('Gudang asal wajib diisi!')
- .typeError('Gudang asal wajib diisi!'),
- destination_warehouse: Yup.object({
- value: Yup.number().min(1).required(),
- label: Yup.string().required(),
- area: Yup.string().optional(),
- location: Yup.string().optional(),
- }).nullable(),
- destination_warehouse_id: Yup.number()
- .required('Gudang tujuan wajib diisi!')
- .typeError('Gudang tujuan wajib diisi!'),
- products: Yup.array()
- .of(ProductObjectSchema)
- .min(1, 'Minimal harus ada 1 produk!')
- .required('Produk wajib diisi!'),
- deliveries: Yup.array()
- .of(DeliveryObjectSchema)
- .min(1, 'Minimal harus ada 1 pengiriman!')
- .required('Pengiriman wajib diisi!'),
-});
-
-export const UpdateMovementFormSchema = MovementFormSchema;
+export const MovementFormSchema: Yup.ObjectSchema =
+ Yup.object({
+ transfer_reason: Yup.string().required('Alasan transfer wajib diisi!'),
+ transfer_date: Yup.string().required('Tanggal transfer wajib diisi!'),
+ source_warehouse: Yup.object({
+ value: Yup.number().min(1).required(),
+ label: Yup.string().required(),
+ area: Yup.string().optional(),
+ location: Yup.string().optional(),
+ }).nullable(),
+ source_warehouse_id: Yup.number()
+ .required('Gudang asal wajib diisi!')
+ .typeError('Gudang asal wajib diisi!'),
+ destination_warehouse: Yup.object({
+ value: Yup.number().min(1).required(),
+ label: Yup.string().required(),
+ area: Yup.string().optional(),
+ location: Yup.string().optional(),
+ }).nullable(),
+ destination_warehouse_id: Yup.number()
+ .required('Gudang tujuan wajib diisi!')
+ .typeError('Gudang tujuan wajib diisi!'),
+ products: Yup.array()
+ .of(ProductObjectSchema)
+ .min(1, 'Minimal harus ada 1 produk!')
+ .required('Produk wajib diisi!'),
+ deliveries: Yup.array()
+ .of(DeliveryObjectSchema)
+ .min(1, 'Minimal harus ada 1 pengiriman!')
+ .required('Pengiriman wajib diisi!'),
+ });
export type MovementFormValues = Yup.InferType;
diff --git a/src/components/pages/inventory/movement/form/MovementForm.tsx b/src/components/pages/inventory/movement/form/MovementForm.tsx
index d8239d14..438c09c6 100644
--- a/src/components/pages/inventory/movement/form/MovementForm.tsx
+++ b/src/components/pages/inventory/movement/form/MovementForm.tsx
@@ -8,28 +8,31 @@ import { Icon } from '@iconify/react';
import Button from '@/components/Button';
import TextInput from '@/components/input/TextInput';
import NumberInput from '@/components/input/NumberInput';
-import SelectInput, { OptionType } from '@/components/input/SelectInput';
-import { FormHeader } from '@/components/helper/form/FormHeader';
-import { FormActions } from '@/components/helper/form/FormActions';
+import SelectInput, {
+ OptionType,
+ useSelect,
+} from '@/components/input/SelectInput';
import {
CreateMovementPayload,
Movement,
} from '@/types/api/inventory/movement';
-import { isResponseSuccess } from '@/lib/api-helper';
+import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
+import { useRouter } from 'next/navigation';
import {
MovementFormSchema,
MovementFormValues,
- UpdateMovementFormSchema,
getMovementFormInitialValues,
ProductSchema,
DeliverySchema,
} from '@/components/pages/inventory/movement/form/MovementForm.schema';
-import { useMovementFormHandlers } from './useMovementFormHandlers';
import { SupplierApi, WarehouseApi } from '@/services/api/master-data';
import { ProductWarehouseApi } from '@/services/api/inventory';
import { toast } from 'react-hot-toast';
+import { MovementApi } from '@/services/api/inventory';
import FileInput from '@/components/input/FileInput';
import CheckboxInput from '@/components/input/CheckboxInput';
+import Badge from '@/components/Badge';
+import Card from '@/components/Card';
interface MovementFormProps {
type?: 'add' | 'edit' | 'detail';
@@ -37,7 +40,10 @@ interface MovementFormProps {
}
const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
- const [, setMovementFormErrorMessage] = useState('');
+ const router = useRouter();
+
+ // ===== STATE MANAGEMENT =====
+ const [movementFormErrorMessage, setMovementFormErrorMessage] = useState('');
const [
productWarehouseSelectInputValue,
setProductWarehouseSelectInputValue,
@@ -45,16 +51,112 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
const [selectedProducts, setSelectedProducts] = useState([]);
const [selectedDeliveries, setSelectedDeliveries] = useState([]);
- const {
- deleteModal,
- movementFormErrorMessage,
- isDeleteLoading,
- createMovementHandler,
- updateMovementHandler,
- deleteMovementClickHandler,
- confirmationModalDeleteClickHandler,
- } = useMovementFormHandlers(initialValues?.id);
+ // ===== FORM HANDLERS =====
+ const createMovementHandler = useCallback(
+ async (payload: CreateMovementPayload, documents: File[] = []) => {
+ const formData = new FormData();
+ formData.append('data', JSON.stringify(payload));
+ documents.forEach((file, index) => {
+ formData.append(`documents[${index}]`, file);
+ });
+ const res = await MovementApi.create(
+ formData as unknown as CreateMovementPayload
+ );
+ if (isResponseError(res)) {
+ setMovementFormErrorMessage(res.message);
+ return;
+ }
+ toast.success(res?.message as string);
+ router.push('/inventory/movement');
+ },
+ [router]
+ );
+
+ // ===== INTERFACES =====
+ interface WarehouseOptionType extends OptionType {
+ area?: string;
+ location?: string;
+ }
+
+ interface ProductWarehouseOptionType extends OptionType {
+ product_id: number;
+ warehouse_id: number;
+ warehouse_name: string;
+ quantity: number;
+ }
+
+ // ===== API DATA FETCHING =====
+ const allProductWarehousesUrl = `${ProductWarehouseApi.basePath}`;
+ const { data: allProductWarehouses } = useSWR(
+ allProductWarehousesUrl,
+ ProductWarehouseApi.getAllFetcher
+ );
+
+ // ===== USE SELECT HOOKS =====
+ const {
+ inputValue: warehouseSelectInputValue,
+ setInputValue: setWarehouseSelectInputValue,
+ isLoadingOptions: isLoadingWarehouses,
+ } = useSelect(WarehouseApi.basePath, 'id', 'name', 'search');
+
+ const {
+ setInputValue: setSupplierSelectInputValue,
+ options: supplierOptions,
+ isLoadingOptions: isLoadingSuppliers,
+ } = useSelect(SupplierApi.basePath, 'id', 'name', 'search');
+
+ const warehousesUrl = `${WarehouseApi.basePath}?${new URLSearchParams({ search: warehouseSelectInputValue }).toString()}`;
+ const { data: warehouses } = useSWR(
+ warehousesUrl,
+ WarehouseApi.getAllFetcher
+ );
+
+ // ===== DATA PROCESSING =====
+ const warehouseStockMap = useMemo(() => {
+ if (!isResponseSuccess(allProductWarehouses)) return new Map();
+
+ const stockMap = new Map<
+ number,
+ { totalQty: number; productCount: number }
+ >();
+
+ allProductWarehouses.data.forEach((pw) => {
+ const warehouseId = pw.warehouse.id;
+ const existing = stockMap.get(warehouseId) || {
+ totalQty: 0,
+ productCount: 0,
+ };
+
+ stockMap.set(warehouseId, {
+ totalQty: existing.totalQty + pw.quantity,
+ productCount: existing.productCount + 1,
+ });
+ });
+
+ return stockMap;
+ }, [allProductWarehouses]);
+
+ const warehouseOptions = useMemo(() => {
+ if (!isResponseSuccess(warehouses)) return [];
+
+ return (
+ warehouses?.data.map((w) => {
+ warehouseStockMap.get(w.id);
+ return {
+ value: w.id,
+ label: w.name,
+ area: w.area?.name,
+ location:
+ 'type' in w && (w.type === 'LOKASI' || w.type === 'KANDANG')
+ ? w.location?.name
+ : undefined,
+ };
+ }) || []
+ );
+ }, [warehouses, warehouseStockMap]);
+
+ // ===== FORM INITIALIZATION =====
const formikInitialValues = useMemo(
() => getMovementFormInitialValues(initialValues),
[initialValues]
@@ -62,8 +164,7 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
const formik = useFormik({
initialValues: formikInitialValues,
- validationSchema:
- type === 'edit' ? UpdateMovementFormSchema : MovementFormSchema,
+ validationSchema: MovementFormSchema,
validateOnChange: true,
validateOnBlur: true,
validateOnMount: false,
@@ -71,18 +172,18 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
onSubmit: async (values) => {
setMovementFormErrorMessage('');
const documents: File[] = [];
- const deliveriesPayload = values.deliveries.map((d, idx) => {
+ const deliveriesPayload = values.deliveries.map((d) => {
let documentIndex = 0;
if (d.document && d.document instanceof File) {
documents.push(d.document);
documentIndex = documents.length - 1;
- } else {
}
return {
- delivery_cost: d.delivery_cost ?? 0,
- delivery_cost_per_item: d.delivery_cost_per_item ?? 0,
+ delivery_cost: parseInt((d.delivery_cost || '').toString()) || 0,
+ delivery_cost_per_item:
+ parseInt((d.delivery_cost_per_item || '').toString()) || 0,
document_index: documentIndex,
document_path: d.document_path,
driver_name: d.driver_name,
@@ -90,7 +191,7 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
supplier_id: d.supplier_id,
products: d.products.map((p) => ({
product_id: p.product_id,
- product_qty: p.product_qty,
+ product_qty: parseInt(p.product_qty.toString()) || 0,
})),
};
});
@@ -102,7 +203,7 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
destination_warehouse_id: values.destination_warehouse_id,
products: values.products.map((p) => ({
product_id: p.product_id,
- product_qty: p.product_qty,
+ product_qty: parseInt(p.product_qty.toString()) || 0,
})),
deliveries: deliveriesPayload,
};
@@ -111,102 +212,43 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
case 'add':
await createMovementHandler(payload, documents);
break;
- case 'edit':
- await updateMovementHandler(
- initialValues?.id as number,
- payload,
- documents
- );
- break;
}
},
});
- const addProduct = () => {
- const newProducts = [
- ...(formik.values.products || []),
- {
- product: null,
- product_id: 0,
- product_qty: 0,
- },
- ];
- formik.setFieldValue('products', newProducts);
- };
+ // ===== PRODUCT WAREHOUSE FETCHING (after form initialization) =====
+ const getProductWarehousesUrl = useCallback(() => {
+ const productWarehouseParams = new URLSearchParams({
+ search: productWarehouseSelectInputValue,
+ });
+ if (formik.values.source_warehouse_id) {
+ productWarehouseParams.append(
+ 'warehouse_id',
+ formik.values.source_warehouse_id.toString()
+ );
+ }
+ return `${ProductWarehouseApi.basePath}?${productWarehouseParams.toString()}`;
+ }, [formik.values.source_warehouse_id, productWarehouseSelectInputValue]);
- const removeProduct = useCallback(
- (i: number) => {
- const updatedProducts =
- formik.values.products?.reduce((acc: ProductSchema[], item, index) => {
- if (index !== i) {
- acc.push(item);
- }
- return acc;
- }, []) ?? [];
+ const productWarehousesUrl = getProductWarehousesUrl();
+ const { data: productWarehouses, isLoading: isLoadingProductWarehouses } =
+ useSWR(
+ formik.values.source_warehouse_id ? productWarehousesUrl : null,
+ ProductWarehouseApi.getAllFetcher
+ );
- formik.setFieldValue('products', updatedProducts);
- },
- [formik]
- );
-
- const bulkRemoveProduct = useCallback(() => {
- const updatedProducts =
- formik.values.products?.filter(
- (_, idx) => !selectedProducts.includes(idx)
- ) ?? [];
- formik.setFieldValue('products', updatedProducts);
- setSelectedProducts([]);
- }, [formik, selectedProducts]);
-
- const addDelivery = () => {
- formik.setFieldValue('deliveries', [
- ...(formik.values.deliveries || []),
- {
- delivery_cost: undefined,
- delivery_cost_per_item: undefined,
- document: null,
- driver_name: '',
- vehicle_plate: '',
- supplier: null,
- supplier_id: 0,
- products: [
- {
- product: null,
- product_id: 0,
- product_qty: 0,
- },
- ],
- },
- ]);
- };
-
- const removeDelivery = useCallback(
- (i: number) => {
- const updatedDeliveries =
- formik.values.deliveries?.reduce(
- (acc: DeliverySchema[], item, index) => {
- if (index !== i) {
- acc.push(item);
- }
- return acc;
- },
- []
- ) ?? [];
-
- formik.setFieldValue('deliveries', updatedDeliveries);
- },
- [formik]
- );
-
- const bulkRemoveDelivery = useCallback(() => {
- const updatedDeliveries =
- formik.values.deliveries?.filter(
- (_, idx) => !selectedDeliveries.includes(idx)
- ) ?? [];
- formik.setFieldValue('deliveries', updatedDeliveries);
- setSelectedDeliveries([]);
- }, [formik, selectedDeliveries]);
+ const productWarehouseOptions = isResponseSuccess(productWarehouses)
+ ? productWarehouses?.data.map((pw) => ({
+ value: pw.product.id,
+ label: pw.product.name,
+ product_id: pw.product.id,
+ warehouse_id: pw.warehouse.id,
+ warehouse_name: pw.warehouse.name,
+ quantity: pw.quantity,
+ }))
+ : [];
+ // ===== HELPER FUNCTIONS =====
const isRepeaterInputError = (
arrayName: T,
column: T extends 'products' ? keyof ProductSchema : keyof DeliverySchema,
@@ -263,132 +305,112 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
};
};
- interface WarehouseOptionType extends OptionType {
- area?: string;
- location?: string;
- }
+ // ===== EVENT HANDLERS =====
+ // Product Handlers
+ const addProduct = () => {
+ const newProducts = [
+ ...(formik.values.products || []),
+ {
+ product: null,
+ product_id: 0,
+ product_qty: '',
+ },
+ ];
+ formik.setFieldValue('products', newProducts);
+ };
- interface ProductWarehouseOptionType extends OptionType {
- product_id: number;
- warehouse_id: number;
- warehouse_name: string;
- quantity: number;
- }
+ const removeProduct = useCallback(
+ (i: number) => {
+ const updatedProducts =
+ formik.values.products?.reduce((acc: ProductSchema[], item, index) => {
+ if (index !== i) {
+ acc.push(item);
+ }
+ return acc;
+ }, []) ?? [];
- const allProductWarehousesUrl = `${ProductWarehouseApi.basePath}`;
- const { data: allProductWarehouses } = useSWR(
- allProductWarehousesUrl,
- ProductWarehouseApi.getAllFetcher
+ formik.setFieldValue('products', updatedProducts);
+ },
+ [formik]
);
- const warehouseStockMap = useMemo(() => {
- if (!isResponseSuccess(allProductWarehouses)) return new Map();
+ const bulkRemoveProduct = useCallback(() => {
+ const updatedProducts =
+ formik.values.products?.filter(
+ (_, idx) => !selectedProducts.includes(idx)
+ ) ?? [];
+ formik.setFieldValue('products', updatedProducts);
+ setSelectedProducts([]);
+ }, [formik, selectedProducts]);
- const stockMap = new Map<
- number,
- { totalQty: number; productCount: number }
- >();
+ // Delivery Handlers
+ const addDelivery = () => {
+ formik.setFieldValue('deliveries', [
+ ...(formik.values.deliveries || []),
+ {
+ delivery_cost: '',
+ delivery_cost_per_item: '',
+ document: null,
+ driver_name: '',
+ vehicle_plate: '',
+ supplier: null,
+ supplier_id: 0,
+ products: [
+ {
+ product: null,
+ product_id: 0,
+ product_qty: '',
+ },
+ ],
+ },
+ ]);
+ };
- allProductWarehouses.data.forEach((pw) => {
- const warehouseId = pw.warehouse.id;
- const existing = stockMap.get(warehouseId) || {
- totalQty: 0,
- productCount: 0,
- };
+ const removeDelivery = useCallback(
+ (i: number) => {
+ const updatedDeliveries =
+ formik.values.deliveries?.reduce(
+ (acc: DeliverySchema[], item, index) => {
+ if (index !== i) {
+ acc.push(item);
+ }
+ return acc;
+ },
+ []
+ ) ?? [];
- stockMap.set(warehouseId, {
- totalQty: existing.totalQty + pw.quantity,
- productCount: existing.productCount + 1,
- });
- });
-
- return stockMap;
- }, [allProductWarehouses]);
-
- // Warehouse selection
- const [warehouseSelectInputValue, setWarehouseSelectInputValue] =
- useState('');
- const warehousesUrl = `${WarehouseApi.basePath}?${new URLSearchParams({ search: warehouseSelectInputValue }).toString()}`;
- const { data: warehouses, isLoading: isLoadingWarehouses } = useSWR(
- warehousesUrl,
- WarehouseApi.getAllFetcher
+ formik.setFieldValue('deliveries', updatedDeliveries);
+ },
+ [formik]
);
- const warehouseOptions = isResponseSuccess(warehouses)
- ? warehouses?.data.map((w) => {
- const stockInfo = warehouseStockMap.get(w.id);
- const stockLabel = stockInfo
- ? ` (Stock: ${stockInfo.totalQty.toLocaleString('id-ID')} items, ${stockInfo.productCount} produk)`
- : ' (Kosong)';
- return {
- value: w.id,
- label: `${w.name}${stockLabel}`,
- area: w.area?.name,
- location:
- 'type' in w && (w.type === 'LOKASI' || w.type === 'KANDANG')
- ? w.location?.name
- : undefined,
- };
- })
- : [];
+ const bulkRemoveDelivery = useCallback(() => {
+ const updatedDeliveries =
+ formik.values.deliveries?.filter(
+ (_, idx) => !selectedDeliveries.includes(idx)
+ ) ?? [];
+ formik.setFieldValue('deliveries', updatedDeliveries);
+ setSelectedDeliveries([]);
+ }, [formik, selectedDeliveries]);
- // Product Warehouse selection - Filter by source warehouse
- const productWarehouseParams = new URLSearchParams({
- search: productWarehouseSelectInputValue,
- });
- if (formik.values.source_warehouse_id) {
- productWarehouseParams.append(
- 'warehouse_id',
- formik.values.source_warehouse_id.toString()
- );
- }
- const productWarehousesUrl = `${ProductWarehouseApi.basePath}?${productWarehouseParams.toString()}`;
- const { data: productWarehouses, isLoading: isLoadingProductWarehouses } =
- useSWR(
- formik.values.source_warehouse_id ? productWarehousesUrl : null,
- ProductWarehouseApi.getAllFetcher
- );
- const productWarehouseOptions = isResponseSuccess(productWarehouses)
- ? productWarehouses?.data.map((pw) => ({
- value: pw.product.id,
- label: `${pw.product.name} - ${pw.warehouse.name} (Stock: ${pw.quantity.toLocaleString('id-ID')})`,
- product_id: pw.product.id,
- warehouse_id: pw.warehouse.id,
- warehouse_name: pw.warehouse.name,
- quantity: pw.quantity,
- }))
- : [];
-
- // Supplier selection
- const [supplierSelectInputValue, setSupplierSelectInputValue] = useState('');
- const suppliersUrl = `${SupplierApi.basePath}?${new URLSearchParams({ search: supplierSelectInputValue }).toString()}`;
- const { data: suppliers, isLoading: isLoadingSuppliers } = useSWR(
- suppliersUrl,
- SupplierApi.getAllFetcher
- );
- const supplierOptions = isResponseSuccess(suppliers)
- ? suppliers?.data.map((s) => ({ value: s.id, label: s.name }))
- : [];
-
- // Handle cost calculation when delivery_cost changes
+ // Cost Calculation Handlers
const handleDeliveryCostChange = useCallback(
- (idx: number, value: string) => {
- const numValue = parseFloat(value) || 0;
- formik.setFieldValue(`deliveries.${idx}.delivery_cost`, numValue);
+ (idx: number, value: number) => {
+ formik.setFieldValue(`deliveries.${idx}.delivery_cost`, value);
const delivery = formik.values.deliveries?.[idx];
if (delivery) {
const productQty = delivery.products.reduce(
- (sum, p) => sum + p.product_qty,
+ (sum, p) => sum + (parseInt(p.product_qty.toString()) || 0),
0
);
- if (productQty > 0 && numValue > 0) {
- const perItem = numValue / productQty;
+ if (productQty > 0 && value > 0) {
+ const perItem = value / productQty;
formik.setFieldValue(
`deliveries.${idx}.delivery_cost_per_item`,
perItem
);
- } else if (numValue === 0) {
+ } else if (value === 0) {
formik.setFieldValue(`deliveries.${idx}.delivery_cost_per_item`, 0);
}
}
@@ -396,25 +418,20 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
[formik]
);
- // Handle cost calculation when delivery_cost_per_item changes
const handleDeliveryCostPerItemChange = useCallback(
- (idx: number, value: string) => {
- const numValue = parseFloat(value) || 0;
- formik.setFieldValue(
- `deliveries.${idx}.delivery_cost_per_item`,
- numValue
- );
+ (idx: number, value: number) => {
+ formik.setFieldValue(`deliveries.${idx}.delivery_cost_per_item`, value);
const delivery = formik.values.deliveries?.[idx];
if (delivery) {
const productQty = delivery.products.reduce(
- (sum, p) => sum + p.product_qty,
+ (sum, p) => sum + (parseInt(p.product_qty.toString()) || 0),
0
);
- if (productQty > 0 && numValue > 0) {
- const totalCost = numValue * productQty;
+ if (productQty > 0 && value > 0) {
+ const totalCost = value * productQty;
formik.setFieldValue(`deliveries.${idx}.delivery_cost`, totalCost);
- } else if (numValue === 0) {
+ } else if (value === 0) {
formik.setFieldValue(`deliveries.${idx}.delivery_cost`, 0);
}
}
@@ -422,57 +439,23 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
[formik]
);
- // Auto-recalculate when product quantity changes
- useEffect(() => {
- formik.values.deliveries?.forEach((delivery, idx) => {
- const productQty = delivery.products.reduce(
- (sum, p) => sum + p.product_qty,
- 0
- );
+ const handleDeliveryCostChangeWrapper = useCallback(
+ (idx: number) => (e: React.ChangeEvent) => {
+ const value = parseFloat(e.target.value) || 0;
+ handleDeliveryCostChange(idx, value);
+ },
+ [handleDeliveryCostChange]
+ );
- // If delivery_cost is set, recalculate delivery_cost_per_item
- if (
- delivery.delivery_cost &&
- delivery.delivery_cost > 0 &&
- productQty > 0
- ) {
- const perItem = delivery.delivery_cost / productQty;
- if (Math.abs((delivery.delivery_cost_per_item || 0) - perItem) > 0.01) {
- formik.setFieldValue(
- `deliveries.${idx}.delivery_cost_per_item`,
- perItem
- );
- }
- }
- // If delivery_cost_per_item is set, recalculate delivery_cost
- else if (
- delivery.delivery_cost_per_item &&
- delivery.delivery_cost_per_item > 0 &&
- productQty > 0
- ) {
- const totalCost = delivery.delivery_cost_per_item * productQty;
- if (Math.abs((delivery.delivery_cost || 0) - totalCost) > 0.01) {
- formik.setFieldValue(`deliveries.${idx}.delivery_cost`, totalCost);
- }
- }
- });
- }, [
- formik.values.deliveries
- ?.map((d) => d.products.reduce((sum, p) => sum + p.product_qty, 0))
- .join(','),
- ]);
-
- useEffect(() => {
- if (
- formik.values.source_warehouse_id &&
- type !== 'edit' &&
- type !== 'detail'
- ) {
- formik.setFieldValue('products', []);
- formik.setFieldValue('deliveries', []);
- }
- }, [formik.values.source_warehouse_id]);
+ const handleDeliveryCostPerItemChangeWrapper = useCallback(
+ (idx: number) => (e: React.ChangeEvent) => {
+ const value = parseFloat(e.target.value) || 0;
+ handleDeliveryCostPerItemChange(idx, value);
+ },
+ [handleDeliveryCostPerItemChange]
+ );
+ // UTILITY FUNCTIONS
const getFilteredProductWarehouseOptions = useCallback(() => {
return (
formik.values.products
@@ -495,31 +478,92 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
[productWarehouseOptions, type]
);
- const getProductQtyAdornment = useCallback(
+ const getProductQtyBottomLabel = useCallback(
(productIdx: number) => {
- if (type === 'detail') return null;
+ if (type === 'detail') return undefined;
const product = formik.values.products?.[productIdx];
- if (!product || !product.product_id) return null;
+ if (!product || !product.product_id) return undefined;
const availableStock = getAvailableStock(product.product_id);
const requestedQty = Number(product.product_qty) || 0;
const remainingStock = availableStock - requestedQty;
if (requestedQty > 0) {
+ return `Sisa: ${remainingStock.toLocaleString('en-US')}`;
+ }
+
+ return `Tersedia: ${availableStock.toLocaleString('en-US')}`;
+ },
+ [formik.values.products, getAvailableStock, type]
+ );
+
+ const getDeliveryProductQtyBottomLabel = useCallback(
+ (deliveryIdx: number, productIdx: number) => {
+ if (type === 'detail') return undefined;
+ const delivery = formik.values.deliveries?.[deliveryIdx];
+ if (!delivery) return undefined;
+
+ const deliveryProduct = delivery.products[productIdx];
+ if (!deliveryProduct || !deliveryProduct.product_id) return undefined;
+
+ const relatedProduct = formik.values.products?.find(
+ (p) => p.product_id === deliveryProduct.product_id
+ );
+ if (!relatedProduct) return undefined;
+
+ const totalQtyUsed =
+ formik.values.deliveries?.reduce((total, d, dIdx) => {
+ const productQty = d.products.reduce((sum, p, pIdx) => {
+ if (
+ p.product_id === deliveryProduct.product_id &&
+ !(dIdx === deliveryIdx && pIdx === productIdx)
+ ) {
+ return sum + (Number(p.product_qty) || 0);
+ }
+ return sum;
+ }, 0);
+ return total + productQty;
+ }, 0) || 0;
+
+ const availableQty = Number(relatedProduct.product_qty) - totalQtyUsed;
+ return `Tersedia: ${availableQty.toLocaleString('en-US')}`;
+ },
+ [formik.values.deliveries, formik.values.products, type]
+ );
+
+ const getWarehouseStockAdornment = useCallback(
+ (warehouseId: number) => {
+ const stockInfo = warehouseStockMap.get(warehouseId);
+ if (!stockInfo) {
return (
-
- (sisa: {remainingStock.toLocaleString('id-ID')})
-
+
+ Kosong
+
);
}
+ const { productCount } = stockInfo;
+ let color: 'neutral' | 'success' | 'warning' = 'neutral';
+ if (productCount === 0) color = 'warning';
+ else if (productCount > 0) color = 'success';
+
return (
-
- (tersedia: {availableStock.toLocaleString('id-ID')})
-
+
+ Tersedia {productCount} produk
+
);
},
- [formik.values.products, getAvailableStock, type]
+ [warehouseStockMap]
);
const getProductQtyError = useCallback(
@@ -532,7 +576,7 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
const requestedQty = Number(product.product_qty) || 0;
if (requestedQty > availableStock) {
- return `Qty melebihi stok tersedia! Maksimal: ${availableStock.toLocaleString('id-ID')}`;
+ return `Qty melebihi stok tersedia! Maksimal: ${availableStock.toLocaleString('en-US')}`;
}
return null;
@@ -618,6 +662,7 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
[formik.values.deliveries, formik.values.products, type]
);
+ // ===== COMPUTED VALUES =====
const invalidQtyRows = useMemo(
() =>
type === 'detail'
@@ -650,847 +695,941 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
);
}, [formik.values.products, getProductQtyError, type]);
+ // ===== EFFECTS =====
+ useEffect(() => {
+ formik.values.deliveries?.forEach((delivery, idx) => {
+ const productQty = delivery.products.reduce(
+ (sum, p) => sum + (parseInt(p.product_qty.toString()) || 0),
+ 0
+ );
+
+ const deliveryCost =
+ parseInt((delivery.delivery_cost || '').toString()) || 0;
+ const deliveryCostPerItem =
+ parseInt((delivery.delivery_cost_per_item || '').toString()) || 0;
+
+ if (deliveryCost > 0 && productQty > 0) {
+ const perItem = deliveryCost / productQty;
+ if (Math.abs(deliveryCostPerItem - perItem) > 0.01) {
+ formik.setFieldValue(
+ `deliveries.${idx}.delivery_cost_per_item`,
+ perItem
+ );
+ }
+ } else if (deliveryCostPerItem > 0 && productQty > 0) {
+ const totalCost = deliveryCostPerItem * productQty;
+ if (Math.abs(deliveryCost - totalCost) > 0.01) {
+ formik.setFieldValue(`deliveries.${idx}.delivery_cost`, totalCost);
+ }
+ }
+ });
+ }, [
+ formik.values.deliveries
+ ?.map((d) =>
+ d.products.reduce(
+ (sum, p) => sum + (parseInt(p.product_qty.toString()) || 0),
+ 0
+ )
+ )
+ .join(','),
+ ]);
+
+ useEffect(() => {
+ if (
+ formik.values.source_warehouse_id &&
+ type !== 'edit' &&
+ type !== 'detail'
+ ) {
+ formik.setFieldValue('products', []);
+ formik.setFieldValue('deliveries', []);
+ }
+ }, [formik.values.source_warehouse_id]);
+
return (
<>
-
+
+
+
+ {type === 'add' && 'Tambah Movement'}
+ {type === 'edit' && 'Edit Movement'}
+ {type === 'detail' && 'Detail Movement'}
+
+