From 215580215e68738c4b74d3595bd2d821c37d86f7 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Wed, 29 Oct 2025 17:26:06 +0700 Subject: [PATCH 001/168] feat(FE-212): add types for purchase creation and updates --- src/types/api/purchasing/purchasing.d.ts | 47 ++++++++++++++++++++++++ 1 file changed, 47 insertions(+) create mode 100644 src/types/api/purchasing/purchasing.d.ts diff --git a/src/types/api/purchasing/purchasing.d.ts b/src/types/api/purchasing/purchasing.d.ts new file mode 100644 index 00000000..479efd9c --- /dev/null +++ b/src/types/api/purchasing/purchasing.d.ts @@ -0,0 +1,47 @@ +import { BaseMetadata } from '@/types/api/api-general'; +import { Supplier } from '@/types/api/master-data/supplier'; +import { Warehouse } from '@/types/api/master-data/warehouse'; + +export type BasePurchase = { + id: number; + pr_number: string; + po_number: string; + po_date: string; + supplier: Supplier; + warehouse: Warehouse[]; + credit_term: number; + due_date: string; + grand_total: number; + notes?: string | null; + deleted_at?: string | null; + created_by: number; +}; + +export type Purchase = BaseMetadata & BasePurchase; + +export type CreatePurchasePayload = { + pr_number: string; + po_number: string; + po_date: string; + supplier_id: number; + warehouse_ids: number[]; + credit_term: number; + due_date: string; + grand_total: number; + notes?: string | null; + purchase_items: { + product_id: number; + product_warehouse_id?: number | null; + received_date?: string | null; + travel_number?: string | null; + travel_number_docs?: string | null; + vehicle_number?: string | null; + sub_qty: number; + total_qty: number; + total_used: number; + price: number; + total_price: number; + }[]; +}; + +export type UpdatePurchasePayload = CreatePurchasePayload; From 8b09a8d315007e22465bd66ac0d5bf3fbe002262 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Wed, 29 Oct 2025 17:26:26 +0700 Subject: [PATCH 002/168] feat(FE-212): implement PurchaseApi service for purchase requests --- src/services/api/purchasing.ts | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 src/services/api/purchasing.ts diff --git a/src/services/api/purchasing.ts b/src/services/api/purchasing.ts new file mode 100644 index 00000000..71e6f60f --- /dev/null +++ b/src/services/api/purchasing.ts @@ -0,0 +1,12 @@ +import { BaseApiService } from './base'; +import { + CreatePurchasePayload, + Purchase, + UpdatePurchasePayload, +} from '@/types/api/purchasing/purchasing'; + +export const PurchaseApi = new BaseApiService< + Purchase, + CreatePurchasePayload, + UpdatePurchasePayload +>('/purchase-requests'); From c6fcb17b4d784b93f2b9a23a7a9da9539ab5800c Mon Sep 17 00:00:00 2001 From: rstubryan Date: Wed, 29 Oct 2025 20:31:31 +0700 Subject: [PATCH 003/168] feat(FE-212): add validation schemas for purchase request and update forms --- .../purchase/form/PurchaseForm.schema.ts | 165 ++++++++++++++++++ src/types/api/purchasing/purchasing.d.ts | 16 +- 2 files changed, 167 insertions(+), 14 deletions(-) create mode 100644 src/components/pages/purchase/form/PurchaseForm.schema.ts diff --git a/src/components/pages/purchase/form/PurchaseForm.schema.ts b/src/components/pages/purchase/form/PurchaseForm.schema.ts new file mode 100644 index 00000000..49292e95 --- /dev/null +++ b/src/components/pages/purchase/form/PurchaseForm.schema.ts @@ -0,0 +1,165 @@ +import * as Yup from 'yup'; +import { CreatePurchaseRequestPayload } from '@/types/api/purchasing/purchasing'; +import { Supplier } from '@/types/api/master-data/supplier'; +import { Warehouse } from '@/types/api/master-data/warehouse'; + +export const PurchaseRequestFormSchema = Yup.object({ + supplier: Yup.object({ + value: Yup.number().min(1).required(), + label: Yup.string().required(), + }).nullable(), + supplier_id: Yup.number() + .default(0) + .typeError('Supplier wajib diisi!') + .test( + 'is-valid-supplier', + 'Supplier wajib diisi!', + (value) => value !== undefined && value !== null && value > 0 + ) + .required('Supplier wajib diisi!'), + warehouse: Yup.array() + .of( + Yup.object({ + value: Yup.number().min(1).required(), + label: Yup.string().required(), + }) + ) + .min(1, 'Minimal harus ada 1 warehouse!') + .required('Warehouse wajib diisi!') + .nullable(), + warehouse_ids: Yup.array() + .of(Yup.number().min(1)) + .min(1, 'Minimal harus ada 1 warehouse!') + .required('Warehouse wajib diisi!'), + credit_term: Yup.number() + .required('Termin kredit wajib diisi!') + .min(0, 'Termin kredit tidak boleh negatif!') + .typeError('Termin kredit harus berupa angka!'), + notes: Yup.string().optional().nullable(), + purchase_items: Yup.array() + .of( + Yup.object({ + product_id: Yup.number() + .required('Produk wajib diisi!') + .min(1, 'Produk wajib diisi!') + .typeError('Produk harus berupa angka!'), + product_warehouse_id: Yup.number() + .optional() + .nullable() + .typeError('Product warehouse harus berupa angka!'), + total_qty: Yup.number() + .required('Jumlah total wajib diisi!') + .min(0, 'Jumlah total tidak boleh negatif!') + .typeError('Jumlah total harus berupa angka!'), + price: Yup.number() + .required('Harga wajib diisi!') + .min(0, 'Harga tidak boleh negatif!') + .typeError('Harga harus berupa angka!'), + }) + ) + .min(1, 'Minimal harus ada 1 item pembelian!') + .required('Item pembelian wajib diisi!'), +}); + +export const UpdatePurchaseRequestFormSchema = Yup.object({ + supplier: Yup.object({ + value: Yup.number().min(1).required(), + label: Yup.string().required(), + }).nullable(), + supplier_id: Yup.number() + .default(0) + .typeError('Supplier wajib diisi!') + .test( + 'is-valid-supplier', + 'Supplier wajib diisi!', + (value) => value !== undefined && value !== null && value > 0 + ) + .required('Supplier wajib diisi!'), + warehouse: Yup.array() + .of( + Yup.object({ + value: Yup.number().min(1).required(), + label: Yup.string().required(), + }) + ) + .min(1, 'Minimal harus ada 1 warehouse!') + .required('Warehouse wajib diisi!') + .nullable(), + warehouse_ids: Yup.array() + .of(Yup.number().min(1)) + .min(1, 'Minimal harus ada 1 warehouse!') + .required('Warehouse wajib diisi!'), + credit_term: Yup.number() + .required('Termin kredit wajib diisi!') + .min(0, 'Termin kredit tidak boleh negatif!') + .typeError('Termin kredit harus berupa angka!'), + notes: Yup.string().optional().nullable(), + purchase_items: Yup.array() + .of( + Yup.object({ + product_id: Yup.number() + .required('Produk wajib diisi!') + .min(1, 'Produk wajib diisi!') + .typeError('Produk harus berupa angka!'), + product_warehouse_id: Yup.number() + .optional() + .nullable() + .typeError('Product warehouse harus berupa angka!'), + total_qty: Yup.number() + .required('Jumlah total wajib diisi!') + .min(0, 'Jumlah total tidak boleh negatif!') + .typeError('Jumlah total harus berupa angka!'), + price: Yup.number() + .required('Harga wajib diisi!') + .min(0, 'Harga tidak boleh negatif!') + .typeError('Harga harus berupa angka!'), + }) + ) + .min(1, 'Minimal harus ada 1 item pembelian!') + .required('Item pembelian wajib diisi!'), +}); + +export type PurchaseRequestFormValues = Yup.InferType< + typeof PurchaseRequestFormSchema +>; + +type PurchaseRequestFormData = Partial & { + supplier?: Supplier; + warehouse?: Warehouse[]; + purchase_items?: CreatePurchaseRequestPayload['purchase_items']; +}; + +export const getPurchaseRequestFormInitialValues = ( + initialValues?: PurchaseRequestFormData +): PurchaseRequestFormValues => ({ + supplier: initialValues?.supplier + ? { + value: initialValues.supplier.id, + label: initialValues.supplier.name, + } + : null, + supplier_id: initialValues?.supplier_id ?? 0, + warehouse: + initialValues?.warehouse?.map((warehouse) => ({ + value: warehouse.id, + label: warehouse.name, + })) ?? [], + warehouse_ids: initialValues?.warehouse_ids ?? [], + credit_term: initialValues?.credit_term ?? 0, + notes: initialValues?.notes ?? '', + purchase_items: initialValues?.purchase_items?.map( + (item: NonNullable[0]) => ({ + product_id: item.product_id, + product_warehouse_id: item.product_warehouse_id, + total_qty: item.total_qty, + price: item.price, + }) + ) ?? [ + { + product_id: 0, + product_warehouse_id: null, + total_qty: 0, + price: 0, + }, + ], +}); diff --git a/src/types/api/purchasing/purchasing.d.ts b/src/types/api/purchasing/purchasing.d.ts index 479efd9c..eea53e1d 100644 --- a/src/types/api/purchasing/purchasing.d.ts +++ b/src/types/api/purchasing/purchasing.d.ts @@ -19,29 +19,17 @@ export type BasePurchase = { export type Purchase = BaseMetadata & BasePurchase; -export type CreatePurchasePayload = { - pr_number: string; - po_number: string; - po_date: string; +export type CreatePurchaseRequestPayload = { supplier_id: number; warehouse_ids: number[]; credit_term: number; - due_date: string; - grand_total: number; notes?: string | null; purchase_items: { product_id: number; product_warehouse_id?: number | null; - received_date?: string | null; - travel_number?: string | null; - travel_number_docs?: string | null; - vehicle_number?: string | null; - sub_qty: number; total_qty: number; - total_used: number; price: number; - total_price: number; }[]; }; -export type UpdatePurchasePayload = CreatePurchasePayload; +export type UpdatePurchaseRequestPayload = CreatePurchaseRequestPayload; From 3daf1a518edab9812bb13380e3aef8e6ea32543d Mon Sep 17 00:00:00 2001 From: rstubryan Date: Wed, 29 Oct 2025 21:11:40 +0700 Subject: [PATCH 004/168] feat(FE-208): add 'Purchase' link to navigation constants --- src/config/constant.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/config/constant.ts b/src/config/constant.ts index b5a12fb4..2331084e 100644 --- a/src/config/constant.ts +++ b/src/config/constant.ts @@ -40,6 +40,12 @@ export const MAIN_DRAWER_LINKS: MAIN_DRAWER_MENU[] = [ ], }, + { + title: 'Purchase', + link: '/purchase', + icon: 'gg:shopping-cart', + }, + { title: 'Persediaan', link: '/inventory', From 7ba7b884a47de6a9998df4887d9092bfdb0e96c0 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Wed, 29 Oct 2025 21:12:24 +0700 Subject: [PATCH 005/168] feat(FE-212): rename purchasing files and update validation schemas for purchase requests --- ...chema.ts => PurchaseRequestForm.schema.ts} | 72 +++---------------- src/services/api/purchasing.ts | 2 +- .../purchase.d.ts} | 2 +- 3 files changed, 10 insertions(+), 66 deletions(-) rename src/components/pages/purchase/form/{PurchaseForm.schema.ts => PurchaseRequestForm.schema.ts} (59%) rename src/types/api/{purchasing/purchasing.d.ts => purchase/purchase.d.ts} (95%) diff --git a/src/components/pages/purchase/form/PurchaseForm.schema.ts b/src/components/pages/purchase/form/PurchaseRequestForm.schema.ts similarity index 59% rename from src/components/pages/purchase/form/PurchaseForm.schema.ts rename to src/components/pages/purchase/form/PurchaseRequestForm.schema.ts index 49292e95..3b21b97a 100644 --- a/src/components/pages/purchase/form/PurchaseForm.schema.ts +++ b/src/components/pages/purchase/form/PurchaseRequestForm.schema.ts @@ -1,7 +1,7 @@ import * as Yup from 'yup'; -import { CreatePurchaseRequestPayload } from '@/types/api/purchasing/purchasing'; import { Supplier } from '@/types/api/master-data/supplier'; import { Warehouse } from '@/types/api/master-data/warehouse'; +import { CreatePurchaseRequestPayload } from '@/types/api/purchase/purchase'; export const PurchaseRequestFormSchema = Yup.object({ supplier: Yup.object({ @@ -33,7 +33,7 @@ export const PurchaseRequestFormSchema = Yup.object({ .required('Warehouse wajib diisi!'), credit_term: Yup.number() .required('Termin kredit wajib diisi!') - .min(0, 'Termin kredit tidak boleh negatif!') + .min(1, 'Termin kredit tidak boleh negatif!') .typeError('Termin kredit harus berupa angka!'), notes: Yup.string().optional().nullable(), purchase_items: Yup.array() @@ -44,16 +44,16 @@ export const PurchaseRequestFormSchema = Yup.object({ .min(1, 'Produk wajib diisi!') .typeError('Produk harus berupa angka!'), product_warehouse_id: Yup.number() - .optional() - .nullable() + .required('Product warehouse wajib diisi!') + .min(1, 'Product warehouse wajib diisi!') .typeError('Product warehouse harus berupa angka!'), total_qty: Yup.number() .required('Jumlah total wajib diisi!') - .min(0, 'Jumlah total tidak boleh negatif!') + .min(1, 'Jumlah total tidak boleh negatif!') .typeError('Jumlah total harus berupa angka!'), price: Yup.number() .required('Harga wajib diisi!') - .min(0, 'Harga tidak boleh negatif!') + .min(1, 'Harga tidak boleh negatif!') .typeError('Harga harus berupa angka!'), }) ) @@ -61,63 +61,7 @@ export const PurchaseRequestFormSchema = Yup.object({ .required('Item pembelian wajib diisi!'), }); -export const UpdatePurchaseRequestFormSchema = Yup.object({ - supplier: Yup.object({ - value: Yup.number().min(1).required(), - label: Yup.string().required(), - }).nullable(), - supplier_id: Yup.number() - .default(0) - .typeError('Supplier wajib diisi!') - .test( - 'is-valid-supplier', - 'Supplier wajib diisi!', - (value) => value !== undefined && value !== null && value > 0 - ) - .required('Supplier wajib diisi!'), - warehouse: Yup.array() - .of( - Yup.object({ - value: Yup.number().min(1).required(), - label: Yup.string().required(), - }) - ) - .min(1, 'Minimal harus ada 1 warehouse!') - .required('Warehouse wajib diisi!') - .nullable(), - warehouse_ids: Yup.array() - .of(Yup.number().min(1)) - .min(1, 'Minimal harus ada 1 warehouse!') - .required('Warehouse wajib diisi!'), - credit_term: Yup.number() - .required('Termin kredit wajib diisi!') - .min(0, 'Termin kredit tidak boleh negatif!') - .typeError('Termin kredit harus berupa angka!'), - notes: Yup.string().optional().nullable(), - purchase_items: Yup.array() - .of( - Yup.object({ - product_id: Yup.number() - .required('Produk wajib diisi!') - .min(1, 'Produk wajib diisi!') - .typeError('Produk harus berupa angka!'), - product_warehouse_id: Yup.number() - .optional() - .nullable() - .typeError('Product warehouse harus berupa angka!'), - total_qty: Yup.number() - .required('Jumlah total wajib diisi!') - .min(0, 'Jumlah total tidak boleh negatif!') - .typeError('Jumlah total harus berupa angka!'), - price: Yup.number() - .required('Harga wajib diisi!') - .min(0, 'Harga tidak boleh negatif!') - .typeError('Harga harus berupa angka!'), - }) - ) - .min(1, 'Minimal harus ada 1 item pembelian!') - .required('Item pembelian wajib diisi!'), -}); +export const UpdatePurchaseRequestFormSchema = PurchaseRequestFormSchema; export type PurchaseRequestFormValues = Yup.InferType< typeof PurchaseRequestFormSchema @@ -157,7 +101,7 @@ export const getPurchaseRequestFormInitialValues = ( ) ?? [ { product_id: 0, - product_warehouse_id: null, + product_warehouse_id: 0, total_qty: 0, price: 0, }, diff --git a/src/services/api/purchasing.ts b/src/services/api/purchasing.ts index 71e6f60f..6f0fd8ee 100644 --- a/src/services/api/purchasing.ts +++ b/src/services/api/purchasing.ts @@ -3,7 +3,7 @@ import { CreatePurchasePayload, Purchase, UpdatePurchasePayload, -} from '@/types/api/purchasing/purchasing'; +} from '@/types/api/purchase/purchasing'; export const PurchaseApi = new BaseApiService< Purchase, diff --git a/src/types/api/purchasing/purchasing.d.ts b/src/types/api/purchase/purchase.d.ts similarity index 95% rename from src/types/api/purchasing/purchasing.d.ts rename to src/types/api/purchase/purchase.d.ts index eea53e1d..afb6cc60 100644 --- a/src/types/api/purchasing/purchasing.d.ts +++ b/src/types/api/purchase/purchase.d.ts @@ -26,7 +26,7 @@ export type CreatePurchaseRequestPayload = { notes?: string | null; purchase_items: { product_id: number; - product_warehouse_id?: number | null; + product_warehouse_id: number; total_qty: number; price: number; }[]; From b2c38cd06f29994ad7fca785af14178e50fe4196 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Wed, 29 Oct 2025 21:12:42 +0700 Subject: [PATCH 006/168] feat(FE-208): create PurchaseRequestForm component and add AddPurchaseRequest page --- src/app/purchase/add/page.tsx | 11 + .../purchase/form/PurchaseRequestForm.tsx | 546 ++++++++++++++++++ .../form/usePurchaseRequestFormHandlers.ts | 70 +++ 3 files changed, 627 insertions(+) create mode 100644 src/app/purchase/add/page.tsx create mode 100644 src/components/pages/purchase/form/PurchaseRequestForm.tsx create mode 100644 src/components/pages/purchase/form/usePurchaseRequestFormHandlers.ts diff --git a/src/app/purchase/add/page.tsx b/src/app/purchase/add/page.tsx new file mode 100644 index 00000000..c69d3b10 --- /dev/null +++ b/src/app/purchase/add/page.tsx @@ -0,0 +1,11 @@ +import PurchaseRequestForm from '@/components/pages/purchase/form/PurchaseRequestForm'; + +const AddPurchaseRequest = () => { + return ( +
+ +
+ ); +}; + +export default AddPurchaseRequest; diff --git a/src/components/pages/purchase/form/PurchaseRequestForm.tsx b/src/components/pages/purchase/form/PurchaseRequestForm.tsx new file mode 100644 index 00000000..03da06c6 --- /dev/null +++ b/src/components/pages/purchase/form/PurchaseRequestForm.tsx @@ -0,0 +1,546 @@ +'use client'; + +import { useMemo, useState } from 'react'; +import { useFormik } from 'formik'; +import useSWR from 'swr'; +import { Icon } from '@iconify/react'; +import Button from '@/components/Button'; +import TextInput from '@/components/input/TextInput'; +import ConfirmationModal from '@/components/modal/ConfirmationModal'; +import { FormHeader } from '@/components/helper/form/FormHeader'; +import { FormActions } from '@/components/helper/form/FormActions'; + +import { + PurchaseRequestFormSchema, + PurchaseRequestFormValues, + getPurchaseRequestFormInitialValues, + UpdatePurchaseRequestFormSchema, +} from './PurchaseRequestForm.schema'; +import { SupplierApi } from '@/services/api/master-data'; +import { WarehouseApi } from '@/services/api/master-data'; +import { isResponseSuccess } from '@/lib/api-helper'; +import { usePurchaseRequestFormHandlers } from './usePurchaseRequestFormHandlers'; + +import Card from '@/components/Card'; +import { + CreatePurchaseRequestPayload, + Purchase, +} from '@/types/api/purchase/purchase'; + +interface PurchaseRequestFormProps { + type?: 'add' | 'edit' | 'detail'; + initialValues?: Purchase; +} + +const PurchaseRequestForm = ({ + type = 'add', + initialValues, +}: PurchaseRequestFormProps) => { + const [selectedPurchaseItems, setSelectedPurchaseItems] = useState( + [] + ); + + const { + deleteModal, + purchaseRequestFormErrorMessage, + isDeleteLoading, + createPurchaseRequestHandler, + updatePurchaseRequestHandler, + deletePurchaseRequestClickHandler, + confirmationModalDeleteClickHandler, + } = usePurchaseRequestFormHandlers(initialValues?.id); + + // ===== API DATA FETCHING ===== + const { data: suppliers, isLoading: isLoadingSuppliers } = useSWR( + SupplierApi.basePath, + SupplierApi.getAllFetcher + ); + + const { data: warehouses, isLoading: isLoadingWarehouses } = useSWR( + WarehouseApi.basePath, + WarehouseApi.getAllFetcher + ); + + // ===== DATA PROCESSING ===== + const supplierOptions = useMemo(() => { + if (!isResponseSuccess(suppliers)) return []; + return ( + suppliers?.data.map((supplier) => ({ + value: supplier.id, + label: supplier.name, + })) || [] + ); + }, [suppliers]); + + const warehouseOptions = useMemo(() => { + if (!isResponseSuccess(warehouses)) return []; + return ( + warehouses?.data.map((warehouse) => ({ + value: warehouse.id, + label: warehouse.name, + })) || [] + ); + }, [warehouses]); + + const formikInitialValues = useMemo( + () => getPurchaseRequestFormInitialValues(initialValues), + [initialValues] + ); + + const formik = useFormik({ + initialValues: formikInitialValues, + validationSchema: + type === 'edit' + ? UpdatePurchaseRequestFormSchema + : PurchaseRequestFormSchema, + validateOnChange: true, + validateOnBlur: true, + onSubmit: async (values) => { + const payload: CreatePurchaseRequestPayload = { + supplier_id: values.supplier_id, + warehouse_ids: + (values.warehouse_ids?.filter( + (id) => id !== undefined && id !== null + ) as number[]) || [], + credit_term: values.credit_term || 0, + notes: values.notes || '', + purchase_items: (values.purchase_items || []).map((item) => ({ + product_id: item.product_id, + product_warehouse_id: item.product_warehouse_id, + total_qty: + typeof item.total_qty === 'number' + ? item.total_qty + : parseFloat(String(item.total_qty)) || 0, + price: + typeof item.price === 'number' + ? item.price + : parseFloat(String(item.price)) || 0, + })), + }; + + switch (type) { + case 'add': + await createPurchaseRequestHandler(payload); + break; + case 'edit': + await updatePurchaseRequestHandler( + initialValues?.id as number, + payload + ); + break; + } + }, + }); + + // ===== EVENT HANDLERS ===== + const supplierChangeHandler = (val: string) => { + const supplierId = parseInt(val) || 0; + formik.setFieldValue('supplier_id', supplierId); + + const selectedSupplier = supplierOptions.find( + (option) => option.value === supplierId + ); + if (selectedSupplier) { + formik.setFieldValue('supplier', selectedSupplier); + } else { + formik.setFieldValue('supplier', null); + } + }; + + const warehouseChangeHandler = (val: string) => { + const warehouseId = parseInt(val) || 0; + const currentWarehouseIds = formik.values.warehouse_ids || []; + + if (warehouseId > 0 && !currentWarehouseIds.includes(warehouseId)) { + const newWarehouseIds = [...currentWarehouseIds, warehouseId].filter( + (id) => id !== undefined && id !== null + ); + formik.setFieldValue('warehouse_ids', newWarehouseIds); + + const selectedWarehouse = warehouseOptions.find( + (option) => option.value === warehouseId + ); + if (selectedWarehouse) { + const currentWarehouses = formik.values.warehouse || []; + formik.setFieldValue('warehouse', [ + ...currentWarehouses, + selectedWarehouse, + ]); + } + } + }; + + // Purchase Items Handlers + const addPurchaseItem = () => { + const newPurchaseItems = [ + ...(formik.values.purchase_items || []), + { + product_id: 0, + product_warehouse_id: null, + total_qty: 0, + price: 0, + }, + ]; + formik.setFieldValue('purchase_items', newPurchaseItems); + }; + + const removePurchaseItem = (idx: number) => { + const updatedPurchaseItems = formik.values.purchase_items?.filter( + (_, i) => i !== idx + ); + formik.setFieldValue('purchase_items', updatedPurchaseItems); + }; + + const removeSelectedPurchaseItems = () => { + const updatedPurchaseItems = formik.values.purchase_items?.filter( + (_, idx) => !selectedPurchaseItems.includes(idx) + ); + formik.setFieldValue('purchase_items', updatedPurchaseItems); + setSelectedPurchaseItems([]); + }; + + const handlePurchaseItemChange = ( + idx: number, + field: string, + value: string | number + ) => { + const numValue = typeof value === 'string' ? parseFloat(value) || 0 : value; + formik.setFieldValue(`purchase_items.${idx}.${field}`, numValue); + }; + + return ( + <> +
+ +
+ {/* Basic Info Card */} + +
+ supplierChangeHandler(e.target.value)} + onBlur={formik.handleBlur} + isError={ + formik.touched.supplier_id && + Boolean(formik.errors.supplier_id) + } + errorMessage={formik.errors.supplier_id as string} + readOnly={type === 'detail'} + type='number' + placeholder='Masukkan Supplier ID' + /> + + warehouseChangeHandler(e.target.value)} + onBlur={formik.handleBlur} + readOnly={type === 'detail'} + type='number' + placeholder='Tambahkan Warehouse ID' + /> + + + +
+ +
+
+
+ + {/* Purchase Items Table */} + +
+ + + + {type !== 'detail' && ( + + )} + + + + + {type !== 'detail' && } + + + + {formik.values.purchase_items?.map((item, idx) => ( + + {type !== 'detail' && ( + + )} + + + + + {type !== 'detail' && ( + + )} + + ))} + +
+ 0 + } + onChange={(e) => { + if (e.target.checked) { + setSelectedPurchaseItems( + formik.values.purchase_items?.map( + (_, idx) => idx + ) ?? [] + ); + } else { + setSelectedPurchaseItems([]); + } + }} + /> + + Product ID + * + Product Warehouse ID + Total Qty + * + + Price + * + Action
+ { + if (e.target.checked) { + setSelectedPurchaseItems([ + ...selectedPurchaseItems, + idx, + ]); + } else { + setSelectedPurchaseItems( + selectedPurchaseItems.filter((i) => i !== idx) + ); + } + }} + /> + + + handlePurchaseItemChange( + idx, + 'product_id', + e.target.value + ) + } + onBlur={formik.handleBlur} + type='number' + placeholder='Product ID' + readOnly={type === 'detail'} + className={{ + wrapper: 'min-w-24', + }} + /> + + + handlePurchaseItemChange( + idx, + 'product_warehouse_id', + e.target.value + ) + } + onBlur={formik.handleBlur} + type='number' + placeholder='Product Warehouse ID' + readOnly={type === 'detail'} + className={{ + wrapper: 'min-w-24', + }} + /> + + + handlePurchaseItemChange( + idx, + 'total_qty', + e.target.value + ) + } + onBlur={formik.handleBlur} + type='number' + placeholder='Total Qty' + readOnly={type === 'detail'} + className={{ + wrapper: 'min-w-24', + }} + /> + + + handlePurchaseItemChange( + idx, + 'price', + e.target.value + ) + } + onBlur={formik.handleBlur} + type='number' + placeholder='Price' + readOnly={type === 'detail'} + className={{ + wrapper: 'min-w-24', + }} + /> + +
+ +
+
+
+ {type !== 'detail' && ( +
+ {selectedPurchaseItems.length > 0 && ( + + )} + +
+ )} +
+ + {/* Action buttons */} + + type={type} + formik={formik} + editUrl={ + initialValues + ? `/purchase/detail/edit/?purchaseId=${initialValues.id}` + : undefined + } + onDelete={deletePurchaseRequestClickHandler} + /> + {purchaseRequestFormErrorMessage && ( +
+ + {purchaseRequestFormErrorMessage} +
+ )} + +
+ + {type !== 'add' && ( + + )} + + ); +}; + +export default PurchaseRequestForm; diff --git a/src/components/pages/purchase/form/usePurchaseRequestFormHandlers.ts b/src/components/pages/purchase/form/usePurchaseRequestFormHandlers.ts new file mode 100644 index 00000000..0594c0f8 --- /dev/null +++ b/src/components/pages/purchase/form/usePurchaseRequestFormHandlers.ts @@ -0,0 +1,70 @@ +import { useCallback, useState } from 'react'; +import { useRouter } from 'next/navigation'; +import { toast } from 'react-hot-toast'; +import { useModal } from '@/components/Modal'; +import { PurchaseApi } from '@/services/api/purchasing'; +import { + CreatePurchaseRequestPayload, + UpdatePurchaseRequestPayload, +} from '@/types/api/purchase/purchasing'; +import { isResponseError } from '@/lib/api-helper'; + +export const usePurchaseRequestFormHandlers = (initialValuesId?: number) => { + const router = useRouter(); + const deleteModal = useModal(); + const [purchaseRequestFormErrorMessage, setPurchaseRequestFormErrorMessage] = + useState(''); + const [isDeleteLoading, setIsDeleteLoading] = useState(false); + + const createPurchaseRequestHandler = useCallback( + async (payload: CreatePurchaseRequestPayload) => { + const res = await PurchaseApi.create(payload); + if (isResponseError(res)) { + setPurchaseRequestFormErrorMessage(res.message); + return; + } + toast.success(res?.message as string); + router.push('/purchase'); + }, + [router] + ); + + const updatePurchaseRequestHandler = useCallback( + async (purchaseRequestId: number, payload: UpdatePurchaseRequestPayload) => { + const res = await PurchaseApi.update(purchaseRequestId, payload); + if (res?.status === 'error') { + setPurchaseRequestFormErrorMessage(res.message); + return; + } + toast.success(res?.message as string); + router.refresh(); + router.push('/purchase'); + }, + [router] + ); + + const deletePurchaseRequestClickHandler = useCallback(() => { + deleteModal.openModal(); + }, [deleteModal]); + + const confirmationModalDeleteClickHandler = useCallback(async () => { + if (!initialValuesId) return; + + setIsDeleteLoading(true); + await PurchaseApi.delete(initialValuesId); + deleteModal.closeModal(); + toast.success('Successfully delete Purchase Request!'); + setIsDeleteLoading(false); + router.push('/purchase'); + }, [deleteModal, initialValuesId, router]); + + return { + deleteModal, + purchaseRequestFormErrorMessage, + isDeleteLoading, + createPurchaseRequestHandler, + updatePurchaseRequestHandler, + deletePurchaseRequestClickHandler, + confirmationModalDeleteClickHandler, + }; +}; \ No newline at end of file From b0e8a460fddc30d65650785243cd9d0a6ac0ac36 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Thu, 30 Oct 2025 09:29:38 +0700 Subject: [PATCH 007/168] refactor(FE-208,212): update PurchaseRequestForm schema and component to handle warehouse IDs --- .../form/PurchaseRequestForm.schema.ts | 55 ++++++------ .../purchase/form/PurchaseRequestForm.tsx | 84 +++++++++---------- src/types/api/purchase/purchase.d.ts | 2 +- 3 files changed, 68 insertions(+), 73 deletions(-) diff --git a/src/components/pages/purchase/form/PurchaseRequestForm.schema.ts b/src/components/pages/purchase/form/PurchaseRequestForm.schema.ts index 3b21b97a..204d3e46 100644 --- a/src/components/pages/purchase/form/PurchaseRequestForm.schema.ts +++ b/src/components/pages/purchase/form/PurchaseRequestForm.schema.ts @@ -9,28 +9,9 @@ export const PurchaseRequestFormSchema = Yup.object({ label: Yup.string().required(), }).nullable(), supplier_id: Yup.number() - .default(0) - .typeError('Supplier wajib diisi!') - .test( - 'is-valid-supplier', - 'Supplier wajib diisi!', - (value) => value !== undefined && value !== null && value > 0 - ) - .required('Supplier wajib diisi!'), - warehouse: Yup.array() - .of( - Yup.object({ - value: Yup.number().min(1).required(), - label: Yup.string().required(), - }) - ) - .min(1, 'Minimal harus ada 1 warehouse!') - .required('Warehouse wajib diisi!') - .nullable(), - warehouse_ids: Yup.array() - .of(Yup.number().min(1)) - .min(1, 'Minimal harus ada 1 warehouse!') - .required('Warehouse wajib diisi!'), + .required('Supplier wajib diisi!') + .min(1, 'Supplier wajib diisi!') + .typeError('Supplier wajib diisi!'), credit_term: Yup.number() .required('Termin kredit wajib diisi!') .min(1, 'Termin kredit tidak boleh negatif!') @@ -39,6 +20,14 @@ export const PurchaseRequestFormSchema = Yup.object({ purchase_items: Yup.array() .of( Yup.object({ + warehouse: Yup.object({ + value: Yup.number().min(1).required(), + label: Yup.string().required(), + }).nullable(), + warehouse_ids: Yup.number() + .required('Warehouse wajib diisi!') + .min(1, 'Produk wajib diisi!') + .typeError('Warehouse harus berupa angka!'), product_id: Yup.number() .required('Produk wajib diisi!') .min(1, 'Produk wajib diisi!') @@ -58,7 +47,8 @@ export const PurchaseRequestFormSchema = Yup.object({ }) ) .min(1, 'Minimal harus ada 1 item pembelian!') - .required('Item pembelian wajib diisi!'), + .required('Item pembelian wajib diisi!') + .typeError('Item pembelian wajib diisi!'), }); export const UpdatePurchaseRequestFormSchema = PurchaseRequestFormSchema; @@ -83,16 +73,21 @@ export const getPurchaseRequestFormInitialValues = ( } : null, supplier_id: initialValues?.supplier_id ?? 0, - warehouse: - initialValues?.warehouse?.map((warehouse) => ({ - value: warehouse.id, - label: warehouse.name, - })) ?? [], - warehouse_ids: initialValues?.warehouse_ids ?? [], credit_term: initialValues?.credit_term ?? 0, notes: initialValues?.notes ?? '', purchase_items: initialValues?.purchase_items?.map( (item: NonNullable[0]) => ({ + warehouse: initialValues?.warehouse?.find( + (w) => w.id === item.warehouse_ids + ) + ? { + value: item.warehouse_ids, + label: + initialValues.warehouse.find((w) => w.id === item.warehouse_ids) + ?.name || '', + } + : null, + warehouse_ids: item.warehouse_ids, product_id: item.product_id, product_warehouse_id: item.product_warehouse_id, total_qty: item.total_qty, @@ -100,6 +95,8 @@ export const getPurchaseRequestFormInitialValues = ( }) ) ?? [ { + warehouse: null, + warehouse_ids: 0, product_id: 0, product_warehouse_id: 0, total_qty: 0, diff --git a/src/components/pages/purchase/form/PurchaseRequestForm.tsx b/src/components/pages/purchase/form/PurchaseRequestForm.tsx index 03da06c6..bd0399d8 100644 --- a/src/components/pages/purchase/form/PurchaseRequestForm.tsx +++ b/src/components/pages/purchase/form/PurchaseRequestForm.tsx @@ -98,13 +98,10 @@ const PurchaseRequestForm = ({ onSubmit: async (values) => { const payload: CreatePurchaseRequestPayload = { supplier_id: values.supplier_id, - warehouse_ids: - (values.warehouse_ids?.filter( - (id) => id !== undefined && id !== null - ) as number[]) || [], credit_term: values.credit_term || 0, notes: values.notes || '', purchase_items: (values.purchase_items || []).map((item) => ({ + warehouse_ids: item.warehouse_ids, product_id: item.product_id, product_warehouse_id: item.product_warehouse_id, total_qty: @@ -147,36 +144,14 @@ const PurchaseRequestForm = ({ } }; - const warehouseChangeHandler = (val: string) => { - const warehouseId = parseInt(val) || 0; - const currentWarehouseIds = formik.values.warehouse_ids || []; - - if (warehouseId > 0 && !currentWarehouseIds.includes(warehouseId)) { - const newWarehouseIds = [...currentWarehouseIds, warehouseId].filter( - (id) => id !== undefined && id !== null - ); - formik.setFieldValue('warehouse_ids', newWarehouseIds); - - const selectedWarehouse = warehouseOptions.find( - (option) => option.value === warehouseId - ); - if (selectedWarehouse) { - const currentWarehouses = formik.values.warehouse || []; - formik.setFieldValue('warehouse', [ - ...currentWarehouses, - selectedWarehouse, - ]); - } - } - }; - // Purchase Items Handlers const addPurchaseItem = () => { const newPurchaseItems = [ ...(formik.values.purchase_items || []), { + warehouse_ids: 0, product_id: 0, - product_warehouse_id: null, + product_warehouse_id: 0, total_qty: 0, price: 0, }, @@ -204,8 +179,13 @@ const PurchaseRequestForm = ({ field: string, value: string | number ) => { - const numValue = typeof value === 'string' ? parseFloat(value) || 0 : value; - formik.setFieldValue(`purchase_items.${idx}.${field}`, numValue); + if (field === 'warehouse_ids') { + formik.setFieldValue(`purchase_items.${idx}.${field}`, value); + } else { + const numValue = + typeof value === 'string' ? parseFloat(value) || 0 : value; + formik.setFieldValue(`purchase_items.${idx}.${field}`, numValue); + } }; return ( @@ -233,7 +213,7 @@ const PurchaseRequestForm = ({ > supplierChangeHandler(e.target.value)} @@ -248,19 +228,9 @@ const PurchaseRequestForm = ({ placeholder='Masukkan Supplier ID' /> - warehouseChangeHandler(e.target.value)} - onBlur={formik.handleBlur} - readOnly={type === 'detail'} - type='number' - placeholder='Tambahkan Warehouse ID' - /> - )} + + Warehouse ID + * + Product ID * - Product Warehouse ID + + Product Warehouse ID + * + Total Qty * @@ -367,6 +344,27 @@ const PurchaseRequestForm = ({ /> )} + + + handlePurchaseItemChange( + idx, + 'warehouse_ids', + e.target.value + ) + } + onBlur={formik.handleBlur} + type='number' + placeholder='Warehouse IDs' + readOnly={type === 'detail'} + className={{ + wrapper: 'min-w-24', + }} + /> + Date: Thu, 30 Oct 2025 10:39:23 +0700 Subject: [PATCH 008/168] refactor(FE-208,212): enhance PurchaseRequestForm with product and product warehouse fields --- .../form/PurchaseRequestForm.schema.ts | 50 ++++++++++++++----- .../purchase/form/PurchaseRequestForm.tsx | 36 ++++++++----- .../form/usePurchaseRequestFormHandlers.ts | 9 ++-- src/services/api/purchasing.ts | 10 ++-- 4 files changed, 73 insertions(+), 32 deletions(-) diff --git a/src/components/pages/purchase/form/PurchaseRequestForm.schema.ts b/src/components/pages/purchase/form/PurchaseRequestForm.schema.ts index 204d3e46..c885af99 100644 --- a/src/components/pages/purchase/form/PurchaseRequestForm.schema.ts +++ b/src/components/pages/purchase/form/PurchaseRequestForm.schema.ts @@ -2,6 +2,8 @@ import * as Yup from 'yup'; import { Supplier } from '@/types/api/master-data/supplier'; import { Warehouse } from '@/types/api/master-data/warehouse'; import { CreatePurchaseRequestPayload } from '@/types/api/purchase/purchase'; +import { Product } from '@/types/api/master-data/product'; +import { ProductWarehouse } from '@/types/api/inventory/product-warehouse'; export const PurchaseRequestFormSchema = Yup.object({ supplier: Yup.object({ @@ -28,10 +30,18 @@ export const PurchaseRequestFormSchema = Yup.object({ .required('Warehouse wajib diisi!') .min(1, 'Produk wajib diisi!') .typeError('Warehouse harus berupa angka!'), + product: Yup.object({ + value: Yup.number().min(1).required(), + label: Yup.string().required(), + }).nullable(), product_id: Yup.number() .required('Produk wajib diisi!') .min(1, 'Produk wajib diisi!') .typeError('Produk harus berupa angka!'), + product_warehouse: Yup.object({ + value: Yup.number().min(1).required(), + label: Yup.string().required(), + }).nullable(), product_warehouse_id: Yup.number() .required('Product warehouse wajib diisi!') .min(1, 'Product warehouse wajib diisi!') @@ -57,10 +67,16 @@ export type PurchaseRequestFormValues = Yup.InferType< typeof PurchaseRequestFormSchema >; -type PurchaseRequestFormData = Partial & { +type PurchaseRequestFormData = { supplier?: Supplier; - warehouse?: Warehouse[]; - purchase_items?: CreatePurchaseRequestPayload['purchase_items']; + supplier_id?: number; + credit_term?: number; + notes?: string | null; + purchase_items?: (CreatePurchaseRequestPayload['purchase_items'][0] & { + warehouse?: Warehouse; + product?: Product; + product_warehouse?: ProductWarehouse; + })[]; }; export const getPurchaseRequestFormInitialValues = ( @@ -73,22 +89,30 @@ export const getPurchaseRequestFormInitialValues = ( } : null, supplier_id: initialValues?.supplier_id ?? 0, - credit_term: initialValues?.credit_term ?? 0, + credit_term: initialValues?.credit_term ?? 1, notes: initialValues?.notes ?? '', purchase_items: initialValues?.purchase_items?.map( - (item: NonNullable[0]) => ({ - warehouse: initialValues?.warehouse?.find( - (w) => w.id === item.warehouse_ids - ) + (item: NonNullable[0]) => ({ + warehouse: item.warehouse ? { - value: item.warehouse_ids, - label: - initialValues.warehouse.find((w) => w.id === item.warehouse_ids) - ?.name || '', + value: item.warehouse.id, + label: item.warehouse.name, } : null, warehouse_ids: item.warehouse_ids, + product: item.product + ? { + value: item.product.id, + label: item.product.name, + } + : null, product_id: item.product_id, + product_warehouse: item.product_warehouse + ? { + value: item.product_warehouse.id, + label: item.product_warehouse.product.name, + } + : null, product_warehouse_id: item.product_warehouse_id, total_qty: item.total_qty, price: item.price, @@ -97,7 +121,9 @@ export const getPurchaseRequestFormInitialValues = ( { warehouse: null, warehouse_ids: 0, + product: null, product_id: 0, + product_warehouse: null, product_warehouse_id: 0, total_qty: 0, price: 0, diff --git a/src/components/pages/purchase/form/PurchaseRequestForm.tsx b/src/components/pages/purchase/form/PurchaseRequestForm.tsx index bd0399d8..2c5c7b97 100644 --- a/src/components/pages/purchase/form/PurchaseRequestForm.tsx +++ b/src/components/pages/purchase/form/PurchaseRequestForm.tsx @@ -97,21 +97,18 @@ const PurchaseRequestForm = ({ validateOnBlur: true, onSubmit: async (values) => { const payload: CreatePurchaseRequestPayload = { - supplier_id: values.supplier_id, + supplier_id: values.supplier_id || 0, credit_term: values.credit_term || 0, notes: values.notes || '', purchase_items: (values.purchase_items || []).map((item) => ({ - warehouse_ids: item.warehouse_ids, - product_id: item.product_id, - product_warehouse_id: item.product_warehouse_id, - total_qty: - typeof item.total_qty === 'number' - ? item.total_qty - : parseFloat(String(item.total_qty)) || 0, + warehouse_ids: item.warehouse_ids || 0, + product_id: item.product_id || 0, + product_warehouse_id: item.product_warehouse_id || 0, + total_qty: item.total_qty || 0, price: typeof item.price === 'number' ? item.price - : parseFloat(String(item.price)) || 0, + : parseFloat(item.price) || 0, })), }; @@ -149,8 +146,11 @@ const PurchaseRequestForm = ({ const newPurchaseItems = [ ...(formik.values.purchase_items || []), { + warehouse: null, warehouse_ids: 0, + product: null, product_id: 0, + product_warehouse: null, product_warehouse_id: 0, total_qty: 0, price: 0, @@ -179,12 +179,23 @@ const PurchaseRequestForm = ({ field: string, value: string | number ) => { - if (field === 'warehouse_ids') { - formik.setFieldValue(`purchase_items.${idx}.${field}`, value); - } else { + const integerFields = [ + 'warehouse_ids', + 'product_id', + 'product_warehouse_id', + 'total_qty', + ]; + const floatFields = ['price']; + + if (integerFields.includes(field)) { + const numValue = typeof value === 'string' ? parseInt(value) || 0 : value; + formik.setFieldValue(`purchase_items.${idx}.${field}`, numValue); + } else if (floatFields.includes(field)) { const numValue = typeof value === 'string' ? parseFloat(value) || 0 : value; formik.setFieldValue(`purchase_items.${idx}.${field}`, numValue); + } else { + formik.setFieldValue(`purchase_items.${idx}.${field}`, value); } }; @@ -388,6 +399,7 @@ const PurchaseRequestForm = ({ diff --git a/src/components/pages/purchase/form/usePurchaseRequestFormHandlers.ts b/src/components/pages/purchase/form/usePurchaseRequestFormHandlers.ts index 0594c0f8..c5fb1e48 100644 --- a/src/components/pages/purchase/form/usePurchaseRequestFormHandlers.ts +++ b/src/components/pages/purchase/form/usePurchaseRequestFormHandlers.ts @@ -6,7 +6,7 @@ import { PurchaseApi } from '@/services/api/purchasing'; import { CreatePurchaseRequestPayload, UpdatePurchaseRequestPayload, -} from '@/types/api/purchase/purchasing'; +} from '@/types/api/purchase/purchase'; import { isResponseError } from '@/lib/api-helper'; export const usePurchaseRequestFormHandlers = (initialValuesId?: number) => { @@ -30,7 +30,10 @@ export const usePurchaseRequestFormHandlers = (initialValuesId?: number) => { ); const updatePurchaseRequestHandler = useCallback( - async (purchaseRequestId: number, payload: UpdatePurchaseRequestPayload) => { + async ( + purchaseRequestId: number, + payload: UpdatePurchaseRequestPayload + ) => { const res = await PurchaseApi.update(purchaseRequestId, payload); if (res?.status === 'error') { setPurchaseRequestFormErrorMessage(res.message); @@ -67,4 +70,4 @@ export const usePurchaseRequestFormHandlers = (initialValuesId?: number) => { deletePurchaseRequestClickHandler, confirmationModalDeleteClickHandler, }; -}; \ No newline at end of file +}; diff --git a/src/services/api/purchasing.ts b/src/services/api/purchasing.ts index 6f0fd8ee..2ed23193 100644 --- a/src/services/api/purchasing.ts +++ b/src/services/api/purchasing.ts @@ -1,12 +1,12 @@ import { BaseApiService } from './base'; import { - CreatePurchasePayload, + CreatePurchaseRequestPayload, Purchase, - UpdatePurchasePayload, -} from '@/types/api/purchase/purchasing'; + UpdatePurchaseRequestPayload, +} from '@/types/api/purchase/purchase'; export const PurchaseApi = new BaseApiService< Purchase, - CreatePurchasePayload, - UpdatePurchasePayload + CreatePurchaseRequestPayload, + UpdatePurchaseRequestPayload >('/purchase-requests'); From 2307035717c7297634010a49368960368b72a860 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Thu, 30 Oct 2025 12:00:22 +0700 Subject: [PATCH 009/168] feat(FE-Storyless): add custom control component to SelectInput for adornment support --- src/components/input/SelectInput.tsx | 92 +++++++++++++++++----------- 1 file changed, 57 insertions(+), 35 deletions(-) diff --git a/src/components/input/SelectInput.tsx b/src/components/input/SelectInput.tsx index f3eca7d1..d37dcab4 100644 --- a/src/components/input/SelectInput.tsx +++ b/src/components/input/SelectInput.tsx @@ -9,6 +9,8 @@ import Select, { InputActionMeta, MultiValue, SingleValue, + components as ReactSelectComponents, + ControlProps, } from 'react-select'; import CreatableSelect from 'react-select/creatable'; import makeAnimated from 'react-select/animated'; @@ -64,6 +66,33 @@ interface SelectInputProps extends SelectInputBaseProps { const animatedComponents = makeAnimated(); +const CustomControl = < + Option, + IsMulti extends boolean, + Group extends GroupBase