From 5d88af1a31cdfe7f1a5c9567c4a6cc275081aeba Mon Sep 17 00:00:00 2001 From: rstubryan Date: Wed, 12 Nov 2025 20:27:06 +0700 Subject: [PATCH] feat(FE-208,212): implement table-based UI for Purchase Order acceptance and staff approval forms --- .../order/PurchaseOrderAcceptApprovalForm.tsx | 756 ++++++++++-------- .../order/PurchaseOrderStaffApprovalForm.tsx | 561 +++++++------ 2 files changed, 761 insertions(+), 556 deletions(-) diff --git a/src/components/pages/purchase/form/order/PurchaseOrderAcceptApprovalForm.tsx b/src/components/pages/purchase/form/order/PurchaseOrderAcceptApprovalForm.tsx index dcdf603d..a1bf68b3 100644 --- a/src/components/pages/purchase/form/order/PurchaseOrderAcceptApprovalForm.tsx +++ b/src/components/pages/purchase/form/order/PurchaseOrderAcceptApprovalForm.tsx @@ -9,6 +9,8 @@ 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 Table from '@/components/Table'; +import * as TanStack from '@tanstack/react-table'; import { PurchaseRequisitionsAcceptApprovalFormDefaultValues, @@ -21,6 +23,7 @@ import { CreateAcceptApprovalRequisitionsPayload, Purchase, } from '@/types/api/purchase/purchase'; +import { PurchaseRequisitionsAcceptApprovalFormValues } from './PurchaseOrderForm.schema'; import DateInput from '@/components/input/DateInput'; interface PurchaseOrderAcceptApprovalFormProps { @@ -67,7 +70,7 @@ const PurchaseOrderAcceptApprovalForm = ({ } // ===== UTILITY FUNCTIONS ===== - const getPurchaseItemError = ( + const isRepeaterInputError = ( idx: number, field: | 'purchase_item_id' @@ -392,332 +395,445 @@ const PurchaseOrderAcceptApprovalForm = ({ } }; + // ===== TABLE COLUMNS DEFINITION ===== + const columns = useMemo( + () => [ + { + accessorKey: 'purchase_item', + header: () => ( +
+ Item + * +
+ ), + cell: (props: TanStack.CellContext) => { + const idx = props.row.index; + const item = formik.values.items?.[idx]; + + return ( + purchaseItemChangeHandler(idx, val)} + options={getPurchaseItemOptions()} + isError={ + isRepeaterInputError(idx, 'purchase_item_id').isError + } + errorMessage={ + isRepeaterInputError(idx, 'purchase_item_id') + .errorMessage + } + placeholder='Pilih Item...' + className={{ + wrapper: 'min-w-52 md:min-w-72 lg:min-w-80', + }} + /> + ); + }, + }, + { + accessorKey: 'warehouse_id', + header: () => ( +
+ Gudang Tujuan + * +
+ ), + cell: (props: TanStack.CellContext) => { + const idx = props.row.index; + const item = formik.values.items?.[idx]; + + return ( + warehouseChangeHandler(idx, val)} + options={getWarehouseOptions()} + isError={ + isRepeaterInputError(idx, 'warehouse_id').isError + } + errorMessage={ + isRepeaterInputError(idx, 'warehouse_id').errorMessage + } + placeholder='Pilih Gudang...' + className={{ + wrapper: 'min-w-52 md:min-w-72 lg:min-w-80', + }} + /> + ); + }, + }, + { + accessorKey: 'travel_number', + header: () => ( +
+ No. Surat Jalan + * +
+ ), + cell: (props: TanStack.CellContext) => { + const idx = props.row.index; + const item = formik.values.items?.[idx]; + + return ( + + formik.setFieldValue( + `items.${idx}.travel_number`, + e.target.value + ) + } + onBlur={formik.handleBlur} + isError={ + isRepeaterInputError(idx, 'travel_number').isError + } + errorMessage={ + isRepeaterInputError(idx, 'travel_number') + .errorMessage + } + placeholder='Masukkan no. surat jalan' + className={{ + wrapper: 'min-w-40 md:min-w-52 lg:min-w-64', + }} + /> + ); + }, + }, + { + accessorKey: 'travel_document_path', + header: () => ( +
+ Dokumen Surat Jalan + * +
+ ), + cell: (props: TanStack.CellContext) => { + const idx = props.row.index; + const item = formik.values.items?.[idx]; + + return ( + + formik.setFieldValue( + `items.${idx}.travel_document_path`, + e.target.value + ) + } + onBlur={formik.handleBlur} + isError={ + isRepeaterInputError(idx, 'travel_document_path') + .isError + } + errorMessage={ + isRepeaterInputError(idx, 'travel_document_path') + .errorMessage + } + placeholder='Masukkan path dokumen' + className={{ + wrapper: 'min-w-52 md:min-w-72 lg:min-w-80', + }} + /> + ); + }, + }, + { + accessorKey: 'vehicle_number', + header: () => ( +
+ Nomor Kendaraan + * +
+ ), + cell: (props: TanStack.CellContext) => { + const idx = props.row.index; + const item = formik.values.items?.[idx]; + + return ( + + formik.setFieldValue( + `items.${idx}.vehicle_number`, + e.target.value + ) + } + onBlur={formik.handleBlur} + isError={ + isRepeaterInputError(idx, 'vehicle_number').isError + } + errorMessage={ + isRepeaterInputError(idx, 'vehicle_number') + .errorMessage + } + placeholder='Masukkan nomor kendaraan' + className={{ + wrapper: 'min-w-40 md:min-w-52 lg:min-w-64', + }} + /> + ); + }, + }, + { + accessorKey: 'expedition_vendor_id', + header: () => ( +
+ Vendor Ekspedisi + * +
+ ), + cell: (props: TanStack.CellContext) => { + const idx = props.row.index; + const item = formik.values.items?.[idx]; + + return ( + + expeditionVendorChangeHandler(idx, val) + } + options={getExpeditionVendorOptions()} + isError={ + isRepeaterInputError(idx, 'expedition_vendor_id') + .isError + } + errorMessage={ + isRepeaterInputError(idx, 'expedition_vendor_id') + .errorMessage + } + placeholder='Pilih Vendor...' + className={{ + wrapper: 'min-w-48 md:min-w-64 lg:min-w-72', + }} + /> + ); + }, + }, + { + accessorKey: 'received_qty', + header: () => ( +
+ Jumlah Diterima + * +
+ ), + cell: (props: TanStack.CellContext) => { + const idx = props.row.index; + const item = formik.values.items?.[idx]; + + return ( + + handlePurchaseItemChange( + idx, + 'received_qty', + e.target.value + ) + } + onBlur={formik.handleBlur} + placeholder='Masukkan jumlah diterima' + allowNegative={false} + decimalScale={0} + thousandSeparator=',' + decimalSeparator='.' + isError={ + isRepeaterInputError(idx, 'received_qty').isError + } + errorMessage={ + isRepeaterInputError(idx, 'received_qty').errorMessage + } + className={{ + wrapper: 'min-w-40 md:min-w-52 lg:min-w-64', + }} + /> + ); + }, + }, + { + accessorKey: 'transport_per_item', + header: () => ( +
+ Transport/Item + * +
+ ), + cell: (props: TanStack.CellContext) => { + const idx = props.row.index; + const item = formik.values.items?.[idx]; + + return ( + + handlePurchaseItemChange( + idx, + 'transport_per_item', + e.target.value + ) + } + onBlur={formik.handleBlur} + placeholder='Masukkan transport/item' + allowNegative={false} + decimalScale={2} + thousandSeparator=',' + decimalSeparator='.' + inputPrefix={'Rp'} + isError={ + isRepeaterInputError(idx, 'transport_per_item') + .isError + } + errorMessage={ + isRepeaterInputError(idx, 'transport_per_item') + .errorMessage + } + className={{ + wrapper: 'min-w-40 md:min-w-52 lg:min-w-64', + }} + /> + ); + }, + }, + { + accessorKey: 'transport_total', + header: () => ( +
+ Total Transport + * +
+ ), + cell: (props: TanStack.CellContext) => { + const idx = props.row.index; + const item = formik.values.items?.[idx]; + + return ( + + handlePurchaseItemChange( + idx, + 'transport_total', + e.target.value + ) + } + onBlur={formik.handleBlur} + placeholder='Masukkan total transport' + allowNegative={false} + decimalScale={2} + thousandSeparator=',' + decimalSeparator='.' + inputPrefix={'Rp'} + isError={ + isRepeaterInputError(idx, 'transport_total').isError + } + errorMessage={ + isRepeaterInputError(idx, 'transport_total') + .errorMessage + } + className={{ + wrapper: 'min-w-40 md:min-w-52 lg:min-w-64', + }} + /> + ); + }, + }, + ], + [formik.values.items, formik.handleBlur, purchaseItemChangeHandler, warehouseChangeHandler, expeditionVendorChangeHandler, handlePurchaseItemChange, getPurchaseItemOptions, getWarehouseOptions, getExpeditionVendorOptions] + ); + + const tableData = useMemo(() => formik.values.items || [], [formik.values.items]); + return (

Konfirmasi Penerimaan Produk

-
- - - - - - - - - - - - - - - - - {formik.values.items?.map((item, idx) => { - const selectedPurchaseItem = purchaseItems.find( - (p) => p.value === item.purchase_item_id - ); - return ( - - - - - - - - - - - - - ); - })} - -
- Item - * - - Tanggal Diterima - * - - Gudang Tujuan - * - - No. Surat Jalan - * - - Dokumen Surat Jalan - * - - Nomor Kendaraan - * - - Vendor Ekspedisi - * - - Jumlah Diterima - * - - Transport/Item - * - - Total Transport - * -
- purchaseItemChangeHandler(idx, val)} - options={getPurchaseItemOptions()} - isError={ - getPurchaseItemError(idx, 'purchase_item_id').isError - } - errorMessage={ - getPurchaseItemError(idx, 'purchase_item_id') - .errorMessage - } - placeholder='Pilih Item...' - className={{ - wrapper: 'min-w-52 md:min-w-72 lg:min-w-80', - }} - /> - - - formik.setFieldValue( - `items.${idx}.received_date`, - e.target.value - ) - } - onBlur={formik.handleBlur} - isError={ - getPurchaseItemError(idx, 'received_date').isError - } - errorMessage={ - getPurchaseItemError(idx, 'received_date') - .errorMessage - } - className={{ - wrapper: 'min-w-40 md:min-w-52 lg:min-w-64', - }} - /> - - warehouseChangeHandler(idx, val)} - options={getWarehouseOptions()} - isError={ - getPurchaseItemError(idx, 'warehouse_id').isError - } - errorMessage={ - getPurchaseItemError(idx, 'warehouse_id').errorMessage - } - placeholder='Pilih Gudang...' - className={{ - wrapper: 'min-w-52 md:min-w-72 lg:min-w-80', - }} - /> - - - formik.setFieldValue( - `items.${idx}.travel_number`, - e.target.value - ) - } - onBlur={formik.handleBlur} - isError={ - getPurchaseItemError(idx, 'travel_number').isError - } - errorMessage={ - getPurchaseItemError(idx, 'travel_number') - .errorMessage - } - placeholder='Masukkan no. surat jalan' - className={{ - wrapper: 'min-w-40 md:min-w-52 lg:min-w-64', - }} - /> - - - formik.setFieldValue( - `items.${idx}.travel_document_path`, - e.target.value - ) - } - onBlur={formik.handleBlur} - isError={ - getPurchaseItemError(idx, 'travel_document_path') - .isError - } - errorMessage={ - getPurchaseItemError(idx, 'travel_document_path') - .errorMessage - } - placeholder='Masukkan path dokumen' - className={{ - wrapper: 'min-w-52 md:min-w-72 lg:min-w-80', - }} - /> - - - formik.setFieldValue( - `items.${idx}.vehicle_number`, - e.target.value - ) - } - onBlur={formik.handleBlur} - isError={ - getPurchaseItemError(idx, 'vehicle_number').isError - } - errorMessage={ - getPurchaseItemError(idx, 'vehicle_number') - .errorMessage - } - placeholder='Masukkan nomor kendaraan' - className={{ - wrapper: 'min-w-40 md:min-w-52 lg:min-w-64', - }} - /> - - - expeditionVendorChangeHandler(idx, val) - } - options={getExpeditionVendorOptions()} - isError={ - getPurchaseItemError(idx, 'expedition_vendor_id') - .isError - } - errorMessage={ - getPurchaseItemError(idx, 'expedition_vendor_id') - .errorMessage - } - placeholder='Pilih Vendor...' - className={{ - wrapper: 'min-w-48 md:min-w-64 lg:min-w-72', - }} - /> - - - handlePurchaseItemChange( - idx, - 'received_qty', - e.target.value - ) - } - onBlur={formik.handleBlur} - placeholder='Masukkan jumlah diterima' - allowNegative={false} - decimalScale={0} - thousandSeparator=',' - decimalSeparator='.' - isError={ - getPurchaseItemError(idx, 'received_qty').isError - } - errorMessage={ - getPurchaseItemError(idx, 'received_qty').errorMessage - } - className={{ - wrapper: 'min-w-40 md:min-w-52 lg:min-w-64', - }} - /> - - - handlePurchaseItemChange( - idx, - 'transport_per_item', - e.target.value - ) - } - onBlur={formik.handleBlur} - placeholder='Masukkan transport/item' - allowNegative={false} - decimalScale={2} - thousandSeparator=',' - decimalSeparator='.' - inputPrefix={'Rp'} - isError={ - getPurchaseItemError(idx, 'transport_per_item') - .isError - } - errorMessage={ - getPurchaseItemError(idx, 'transport_per_item') - .errorMessage - } - className={{ - wrapper: 'min-w-40 md:min-w-52 lg:min-w-64', - }} - /> - - - handlePurchaseItemChange( - idx, - 'transport_total', - e.target.value - ) - } - onBlur={formik.handleBlur} - placeholder='Masukkan total transport' - allowNegative={false} - decimalScale={2} - thousandSeparator=',' - decimalSeparator='.' - inputPrefix={'Rp'} - isError={ - getPurchaseItemError(idx, 'transport_total').isError - } - errorMessage={ - getPurchaseItemError(idx, 'transport_total') - .errorMessage - } - className={{ - wrapper: 'min-w-40 md:min-w-52 lg:min-w-64', - }} - /> -
+ + {/* Date Inputs Section - Above Table */} +
+

Tanggal Penerimaan Produk

+ {formik.values.items?.map((item, idx) => ( +
+
+ + + formik.setFieldValue( + `items.${idx}.received_date`, + e.target.value + ) + } + onBlur={formik.handleBlur} + isError={isRepeaterInputError(idx, 'received_date').isError} + errorMessage={ + isRepeaterInputError(idx, 'received_date').errorMessage + } + placeholder='Pilih tanggal diterima' + className={{ + wrapper: 'w-full max-w-xs', + }} + /> +
+
+ ))}
+ + + + Belum ada data item... + + + } + />
{ @@ -300,6 +301,268 @@ const PurchaseOrderStaffApprovalForm = ({ } }; + // ===== TABLE COLUMNS DEFINITION ===== + const columns = useMemo( + () => [ + { + accessorKey: 'purchase_item', + header: () => ( +
+ Item + * +
+ ), + cell: (props: TanStack.CellContext) => { + const idx = props.row.index; + const item = formik.values.items?.[idx]; + + return ( + + purchaseItemChangeHandler(idx, val) + } + options={getPurchaseItemOptions()} + isError={ + isRepeaterInputError(idx, 'purchase_item_id') + .isError + } + errorMessage={ + isRepeaterInputError(idx, 'purchase_item_id') + .errorMessage + } + placeholder='Pilih Item...' + className={{ + wrapper: 'min-w-52 md:min-w-72 lg:min-w-80', + }} + /> + ); + }, + }, + { + accessorKey: 'warehouse', + header: 'Gudang', + cell: (props: TanStack.CellContext) => { + const idx = props.row.index; + const selectedItem = purchaseItems.find( + (p) => p.value === formik.values.items?.[idx]?.purchase_item_id + ); + + return ( + + ); + }, + }, + { + accessorKey: 'product_name', + header: 'Produk', + cell: (props: TanStack.CellContext) => { + const idx = props.row.index; + const selectedItem = purchaseItems.find( + (p) => p.value === formik.values.items?.[idx]?.purchase_item_id + ); + + return ( + + ); + }, + }, + { + accessorKey: 'product_category', + header: 'Jenis Produk', + cell: (props: TanStack.CellContext) => { + const idx = props.row.index; + const selectedItem = purchaseItems.find( + (p) => p.value === formik.values.items?.[idx]?.purchase_item_id + ); + + return ( + + ); + }, + }, + { + accessorKey: 'quantity', + header: 'Jumlah', + cell: (props: TanStack.CellContext) => { + const idx = props.row.index; + const selectedItem = purchaseItems.find( + (p) => p.value === formik.values.items?.[idx]?.purchase_item_id + ); + + return ( + + ); + }, + }, + { + accessorKey: 'uom', + header: 'Satuan', + cell: (props: TanStack.CellContext) => { + const idx = props.row.index; + const selectedItem = purchaseItems.find( + (p) => p.value === formik.values.items?.[idx]?.purchase_item_id + ); + + return ( + + ); + }, + }, + { + accessorKey: 'price', + header: () => ( +
+ Harga Satuan + * +
+ ), + cell: (props: TanStack.CellContext) => { + const idx = props.row.index; + const item = formik.values.items?.[idx]; + + return ( + + handlePurchaseItemChange( + idx, + 'price', + e.target.value + ) + } + onBlur={formik.handleBlur} + placeholder='Masukkan harga satuan' + allowNegative={false} + decimalScale={2} + thousandSeparator=',' + decimalSeparator='.' + inputPrefix={'Rp'} + isError={isRepeaterInputError(idx, 'price').isError} + errorMessage={ + isRepeaterInputError(idx, 'price').errorMessage + } + className={{ + wrapper: 'min-w-48 md:min-w-64 lg:min-w-72', + }} + /> + ); + }, + }, + { + accessorKey: 'total_price', + header: () => ( +
+ Total (Rp.) + * +
+ ), + cell: (props: TanStack.CellContext) => { + const idx = props.row.index; + const item = formik.values.items?.[idx]; + + return ( + + handlePurchaseItemChange( + idx, + 'total_price', + e.target.value + ) + } + onBlur={formik.handleBlur} + placeholder='Masukkan total harga' + allowNegative={false} + decimalScale={2} + thousandSeparator=',' + decimalSeparator='.' + inputPrefix={'Rp'} + isError={ + isRepeaterInputError(idx, 'total_price').isError + } + errorMessage={ + isRepeaterInputError(idx, 'total_price') + .errorMessage + } + className={{ + wrapper: 'min-w-48 md:min-w-64 lg:min-w-72', + }} + /> + ); + }, + }, + ], + [formik.values.items, formik.handleBlur, purchaseItemChangeHandler, handlePurchaseItemChange, getPurchaseItemOptions, purchaseItems] + ); + + const tableData = useMemo(() => formik.values.items || [], [formik.values.items]); + return ( <>
-

Konfirmasi Approve Pembelian

-
-
- - - - - - - - - - - - - - {formik.values.items?.map((item, idx) => { - const selectedPurchaseItem = purchaseItems.find( - (p) => p.value === item.purchase_item_id - ); - return ( - - - - - - - - - - - ); - })} - -
- Item - * - GudangProdukJenis ProdukJumlahSatuan - Harga Satuan - * - - Total (Rp.) - * -
- - purchaseItemChangeHandler(idx, val) - } - options={getPurchaseItemOptions()} - isError={ - getPurchaseItemError(idx, 'purchase_item_id') - .isError - } - errorMessage={ - getPurchaseItemError(idx, 'purchase_item_id') - .errorMessage - } - placeholder='Pilih Item...' - className={{ - wrapper: 'min-w-52 md:min-w-72 lg:min-w-80', - }} - /> - - - - - - - - - - - - - handlePurchaseItemChange( - idx, - 'price', - e.target.value - ) - } - onBlur={formik.handleBlur} - placeholder='Masukkan harga satuan' - allowNegative={false} - decimalScale={2} - thousandSeparator=',' - decimalSeparator='.' - inputPrefix={'Rp'} - isError={getPurchaseItemError(idx, 'price').isError} - errorMessage={ - getPurchaseItemError(idx, 'price').errorMessage - } - className={{ - wrapper: 'min-w-48 md:min-w-64 lg:min-w-72', - }} - /> - - - handlePurchaseItemChange( - idx, - 'total_price', - e.target.value - ) - } - onBlur={formik.handleBlur} - placeholder='Masukkan total harga' - allowNegative={false} - decimalScale={2} - thousandSeparator=',' - decimalSeparator='.' - inputPrefix={'Rp'} - isError={ - getPurchaseItemError(idx, 'total_price').isError - } - errorMessage={ - getPurchaseItemError(idx, 'total_price') - .errorMessage - } - className={{ - wrapper: 'min-w-48 md:min-w-64 lg:min-w-72', - }} - /> -
-
-
- -
- - {/* Action buttons */} -
-
- - - +

+ Konfirmasi Approve Pembelian +

+ + + Belum ada data item... + + } + /> +
+ +
+ + {/* Action buttons */} +
+
+ + +
+
{purchaseOrderFormErrorMessage && (