feat(FE-177): Integrate API sales order and fixing sales order initial state

This commit is contained in:
randy-ar
2025-11-17 15:59:31 +07:00
parent d3c4706d87
commit a9bdb6c36e
9 changed files with 277 additions and 308 deletions
@@ -215,7 +215,7 @@ const SalesOrderTable = () => {
), ),
}, },
{ {
accessorKey: 'name', accessorKey: 'so_number',
header: 'No. Order', header: 'No. Order',
}, },
{ {
@@ -226,7 +226,7 @@ const SalesOrderTable = () => {
}, },
}, },
{ {
accessorKey: 'approval.step_name', accessorKey: 'latest_approval.step_name',
header: 'Status', header: 'Status',
}, },
{ {
@@ -11,7 +11,10 @@ import ApprovalSteps, {
import Table from '@/components/Table'; import Table from '@/components/Table';
import { MARKETING_APPROVAL_LINE } from '@/config/approval-line'; import { MARKETING_APPROVAL_LINE } from '@/config/approval-line';
import { cn, formatCurrency, formatDate, formatNumber } from '@/lib/helper'; import { cn, formatCurrency, formatDate, formatNumber } from '@/lib/helper';
import { MarketingApi } from '@/services/api/marketing/marketing'; import {
MarketingApi,
SalesOrderApi,
} from '@/services/api/marketing/marketing';
import { BaseSalesOrder, Marketing } from '@/types/api/marketing/marketing'; import { BaseSalesOrder, Marketing } from '@/types/api/marketing/marketing';
import { Icon } from '@iconify/react'; import { Icon } from '@iconify/react';
import { useState } from 'react'; import { useState } from 'react';
@@ -77,7 +80,7 @@ const SalesOrderDetail = ({
const confirmationModalApproveClickHandler = async () => { const confirmationModalApproveClickHandler = async () => {
setIsLoading(true); setIsLoading(true);
const res = await MarketingApi.singleApproval( const res = await SalesOrderApi.singleApproval(
initialValues?.id as number, initialValues?.id as number,
approvalAction approvalAction
); );
@@ -24,7 +24,7 @@ import { CustomerApi } from '@/services/api/master-data';
import { useFormik } from 'formik'; import { useFormik } from 'formik';
import { SalesOrderFormValues, SalesOrderSchema } from './MarketingForm.schema'; import { SalesOrderFormValues, SalesOrderSchema } from './MarketingForm.schema';
import { isResponseError, isResponseSuccess } from '@/lib/api-helper'; import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
import { MarketingApi } from '@/services/api/marketing/marketing'; import { SalesOrderApi } from '@/services/api/marketing/marketing';
import { SalesOrderProductFormValues } from './repeater/sales-order/SalesOrderProduct.schema'; import { SalesOrderProductFormValues } from './repeater/sales-order/SalesOrderProduct.schema';
import ConfirmationModal from '@/components/modal/ConfirmationModal'; import ConfirmationModal from '@/components/modal/ConfirmationModal';
import toast from 'react-hot-toast'; import toast from 'react-hot-toast';
@@ -36,6 +36,8 @@ const MarketingProductToFieldValues = (
product: BaseSalesOrder product: BaseSalesOrder
): SalesOrderProductFormValues => { ): SalesOrderProductFormValues => {
return { return {
id: product.id,
vehicle_number: product.vehicle_number,
kandang_id: product.product_warehouse.warehouse.id, kandang_id: product.product_warehouse.warehouse.id,
kandang: { kandang: {
value: product.product_warehouse.warehouse.id, value: product.product_warehouse.warehouse.id,
@@ -70,87 +72,89 @@ const SalesForm = ({
const [isLoading, setIsLoading] = useState(false); const [isLoading, setIsLoading] = useState(false);
const [selectedMarketingProduct, setSelectedMarketingProduct] = const [selectedMarketingProduct, setSelectedMarketingProduct] =
useState<SalesOrderProductFormValues | null>(null); useState<SalesOrderProductFormValues | null>(null);
const [rawMarketingProducts, setRawMarketingProducts] = useState<
SalesOrderProductFormValues[]
>(
initialValues?.sales_order.map((item) =>
MarketingProductToFieldValues(item)
) || []
);
const [selectedCustomer, setSelectedCustomer] = useState<OptionType | null>(
initialValues?.customer
? { value: initialValues.customer.id, label: initialValues.customer.name }
: null
);
const [rowSelection, setRowSelection] = useState<Record<string, boolean>>({}); const [rowSelection, setRowSelection] = useState<Record<string, boolean>>({});
const selectedRowIds = Object.keys(rowSelection).map((item) => const selectedRowIds = Object.keys(rowSelection).map((item) =>
parseInt(item) parseInt(item)
); );
const [grandTotal, setGrandTotal] = useState<number>(
initialValues?.sales_order
?.map((item) => item.total_price)
.reduce((a, b) => a + b, 0) ?? 0
);
const { const {
options: customerOptions, options: customerOptions,
isLoadingOptions: isLoadingCustomerOptions, isLoadingOptions: isLoadingCustomerOptions,
} = useSelect<Customer>(CustomerApi.basePath, 'id', 'name'); } = useSelect<Customer>(CustomerApi.basePath, 'id', 'name');
const handleDeleteProduct = useCallback( const formikInitialValues = useMemo(() => {
(product_warehouse_id: number, kandang_id: number) => { return {
setRawMarketingProducts((prev) => so_date: initialValues?.so_date || undefined,
prev.filter( notes: initialValues?.notes || undefined,
(p) => customer_id: initialValues?.customer?.id || undefined,
p.product_warehouse_id !== product_warehouse_id && sales_person_id: initialValues?.sales_person?.id || 1,
p.kandang_id !== kandang_id customer: initialValues?.customer
) ? {
); value: initialValues.customer.id,
}, label: initialValues.customer.name,
[] }
); : null,
const handleBulkDeleteProduct = () => { sales_order:
setRawMarketingProducts((prev) => initialValues?.sales_order?.map((product) =>
prev.filter( MarketingProductToFieldValues(product)
(product) => ) ?? [],
!selectedRowIds.includes( };
parseInt(`${product.product_warehouse_id}${product.kandang_id}`) }, [initialValues]);
)
)
);
};
const handleDelete = () => {
deleteModal.openModal();
};
const handleAddProductClick = useCallback(() => { const formik = useFormik<SalesOrderFormValues>({
setSelectedMarketingProduct(null); // Pastikan form tambah enableReinitialize: true,
addProductModal.openModal(); initialValues: formikInitialValues,
}, [addProductModal]); validationSchema: SalesOrderSchema,
const handleAddSubmitProduct = useCallback( validateOnMount: true,
async (values: SalesOrderProductFormValues) => { onSubmit: async (values) => {
setRawMarketingProducts((prev) => [...prev, values]); console.log('VALUES');
formik.setValues({ console.log(values);
...formik.values, const payload = {
sales_order: [...formik.values.sales_order, values], customer_id: values.customer_id as number,
}); sales_person_id: values.sales_person_id as number,
setGrandTotal((prev) => prev + (values.total_price as number)); date: formatDate(values.so_date as string, 'yyyy-MM-DD'),
addProductModal.closeModal(); notes: values.notes as string,
marketing_products: values.sales_order.map((product) => {
return {
vehicle_number: product.vehicle_number as string,
kandang_id: product.kandang_id as number,
product_warehouse_id: product.product_warehouse_id as number,
unit_price: parseFloat(product.unit_price as string),
total_weight: parseFloat(product.total_weight as string),
qty: parseFloat(product.qty as string),
avg_weight: parseFloat(product.avg_weight as string),
total_price: parseFloat(product.total_price as string),
} as CreateSalesOrderProductPayload;
}),
} as CreateSalesOrderPayload;
console.log('PAYLOAD');
console.log(payload);
switch (formType) {
case 'add':
await createMarketingHandler(payload);
break;
case 'edit':
await updateMarketingHandler(payload);
break;
default:
break;
}
afterSubmit?.();
}, },
[rawMarketingProducts.length, addProductModal] });
);
const handleChangeCustomer = useCallback( const grandTotal = useMemo(() => {
(val: OptionType | OptionType[] | null) => { return formik.values.sales_order.reduce(
setSelectedCustomer(val as OptionType); (total, product) =>
formik.setFieldValue('customer_id', (val as OptionType)?.value); total + parseFloat((product.total_price as string) || '0'),
formik.setFieldValue('customer', val as OptionType); 0
}, );
[selectedCustomer, setSelectedCustomer] }, [formik.values.sales_order]);
);
const createMarketingHandler = async (values: CreateSalesOrderPayload) => { const createMarketingHandler = async (values: CreateSalesOrderPayload) => {
console.log(values); console.log(values);
const createMarketingRes = await MarketingApi.create(values); const createMarketingRes = await SalesOrderApi.create(values);
if (isResponseSuccess(createMarketingRes)) { if (isResponseSuccess(createMarketingRes)) {
toast.success(createMarketingRes?.message as string); toast.success(createMarketingRes?.message as string);
router.push('/marketing/sales-orders'); router.push('/marketing/sales-orders');
@@ -161,7 +165,7 @@ const SalesForm = ({
}; };
const updateMarketingHandler = async (values: CreateSalesOrderPayload) => { const updateMarketingHandler = async (values: CreateSalesOrderPayload) => {
console.log(values); console.log(values);
const updateMarketingRes = await MarketingApi.update( const updateMarketingRes = await SalesOrderApi.update(
initialValues?.id as number, initialValues?.id as number,
values values
); );
@@ -176,7 +180,7 @@ const SalesForm = ({
const deleteMarketingHandler = async () => { const deleteMarketingHandler = async () => {
setIsLoading(true); setIsLoading(true);
console.log(initialValues?.id); console.log(initialValues?.id);
const deleteMarketingRes = await MarketingApi.delete( const deleteMarketingRes = await SalesOrderApi.delete(
initialValues?.id as number initialValues?.id as number
); );
if (isResponseSuccess(deleteMarketingRes)) { if (isResponseSuccess(deleteMarketingRes)) {
@@ -191,84 +195,59 @@ const SalesForm = ({
router.push('/marketing/sales-orders'); router.push('/marketing/sales-orders');
}; };
const formikInitialValues = useMemo(() => { const handleDeleteProduct = useCallback(
return { (id: number) => {
so_date: initialValues?.so_date || undefined, const currentProducts = formik.values.sales_order;
notes: initialValues?.notes || undefined, formik.setFieldValue(
customer_id: initialValues?.customer?.id || undefined, 'sales_order',
sales_person_id: initialValues?.sales_person?.id || 1, currentProducts.filter((p) => p.id != id)
customer: { );
value: initialValues?.customer?.id as number,
label: initialValues?.customer?.name as string,
},
sales_order:
initialValues?.sales_order?.map((product) =>
MarketingProductToFieldValues(product)
) ?? [],
};
}, [initialValues]);
const formik = useFormik<SalesOrderFormValues>({
enableReinitialize: true,
initialValues: formikInitialValues,
validationSchema: SalesOrderSchema,
validateOnMount: true,
onSubmit: async (values) => {
const payload = {
customer_id: values.customer_id as number,
sales_person_id: values.sales_person_id as number,
date: formatDate(values.so_date as string, 'yyyy-MM-DD'),
notes: values.notes as string,
marketing_products: values.sales_order.map((product) => {
return {
vehicle_number: 'D 1234 XXXX',
kandang_id: product.kandang_id as number,
product_warehouse_id: product.product_warehouse_id as number,
unit_price: parseFloat(product.unit_price as string),
total_weight: parseFloat(product.total_weight as string),
qty: parseFloat(product.qty as string),
avg_weight: parseFloat(product.avg_weight as string),
total_price: parseFloat(product.total_price as string),
} as CreateSalesOrderProductPayload;
}),
} as CreateSalesOrderPayload;
switch (formType) {
case 'add':
createMarketingHandler(payload);
break;
case 'edit':
updateMarketingHandler(payload);
break;
default:
break;
}
afterSubmit?.();
}, },
}); [formik]
);
const { setValues: formikSetValues } = formik; const handleBulkDeleteProduct = useCallback(() => {
const currentProducts = formik.values.sales_order;
useEffect(() => { formik.setFieldValue(
formikSetValues(formik.initialValues); 'sales_order',
}, [formikSetValues, formik.initialValues]); currentProducts.filter(
(product) => !selectedRowIds.includes(product.id ?? -1)
useEffect(() => { )
// Hitung Grand Total baru
const newGrandTotal = rawMarketingProducts.reduce(
(total, product) => total + parseFloat(product.total_price as string),
0
); );
// Perbarui nilai formik.values.marketing_products
formik.setFieldValue('marketing_products', rawMarketingProducts, false);
// Parameter ketiga (false) untuk menghindari validasi secara langsung
// Perbarui state grandTotal
setGrandTotal(newGrandTotal);
// Reset row selection setiap kali daftar produk berubah (opsional, tapi disarankan)
setRowSelection({}); setRowSelection({});
}, [rawMarketingProducts]); }, [formik, selectedRowIds]);
const handleDelete = () => {
deleteModal.openModal();
};
const handleAddProductClick = useCallback(() => {
setSelectedMarketingProduct(null);
addProductModal.openModal();
}, [addProductModal]);
const handleAddSubmitProduct = useCallback(
async (values: SalesOrderProductFormValues) => {
const currentProducts = formik.values.sales_order;
const newValues = {
...values,
id: values.id ?? Date.now(),
};
formik.setFieldValue('sales_order', [...currentProducts, newValues]);
addProductModal.closeModal();
},
[formik, addProductModal]
);
const handleChangeCustomer = useCallback(
(val: OptionType | OptionType[] | null) => {
formik.setFieldValue('customer_id', (val as OptionType)?.value);
formik.setFieldValue('customer', val as OptionType);
},
[formik]
);
return ( return (
<> <>
@@ -292,7 +271,7 @@ const SalesForm = ({
label='Pelanggan' label='Pelanggan'
options={customerOptions} options={customerOptions}
isLoading={isLoadingCustomerOptions} isLoading={isLoadingCustomerOptions}
value={selectedCustomer} value={formik.values.customer}
onChange={handleChangeCustomer} onChange={handleChangeCustomer}
isError={ isError={
formik.touched.customer_id && Boolean(formik.errors.customer_id) formik.touched.customer_id && Boolean(formik.errors.customer_id)
@@ -318,13 +297,14 @@ const SalesForm = ({
wrapper: 'bg-white w-full', wrapper: 'bg-white w-full',
}} }}
> >
{JSON.stringify(formik.values.sales_order)} {/* <div className='text-blue-500'>{JSON.stringify(initialValues)}</div>
<div className='text-green-500'> <div className='text-green-500'>{JSON.stringify(formik.values)}</div>
{JSON.stringify(formik.values.sales_order)} <div className='text-red-500'>{JSON.stringify(formik.errors)}</div> */}
</div>
<span className='text-red-500'>{JSON.stringify(formik.errors)}</span>
<SalesOrderProductTable <SalesOrderProductTable
data={rawMarketingProducts} data={formik.values.sales_order}
rowSelection={rowSelection}
setRowSelection={setRowSelection}
selectedRowIds={selectedRowIds}
onDelete={handleDeleteProduct} onDelete={handleDeleteProduct}
onBulkDelete={handleBulkDeleteProduct} onBulkDelete={handleBulkDeleteProduct}
onAddProductClick={handleAddProductClick} onAddProductClick={handleAddProductClick}
@@ -345,7 +325,7 @@ const SalesForm = ({
<div className='flex flex-col h-full justify-between items-end py-6'> <div className='flex flex-col h-full justify-between items-end py-6'>
<span>Total Penjualan</span> <span>Total Penjualan</span>
<span className='text-lg font-semibold'> <span className='text-lg font-semibold'>
{formatCurrency(grandTotal)} {formatCurrency(grandTotal)}{' '}
</span> </span>
</div> </div>
</div> </div>
@@ -356,6 +336,7 @@ const SalesForm = ({
<Button <Button
type='submit' type='submit'
disabled={!formik.isValid || formik.isSubmitting} disabled={!formik.isValid || formik.isSubmitting}
isLoading={formik.isSubmitting}
> >
Submit Submit
</Button> </Button>
@@ -363,7 +344,12 @@ const SalesForm = ({
</form> </form>
{formType == 'edit' && ( {formType == 'edit' && (
<div className='flex flex-row justify-start'> <div className='flex flex-row justify-start'>
<Button type='button' color='error' onClick={handleDelete}> <Button
type='button'
color='error'
onClick={handleDelete}
isLoading={isLoading}
>
<Icon icon='mdi:trash' width={24} height={24} /> <Icon icon='mdi:trash' width={24} height={24} />
Hapus Hapus
</Button> </Button>
@@ -403,6 +389,7 @@ const SalesForm = ({
text={`Apakah anda yakin ingin menghapus data penjualan ini?`} text={`Apakah anda yakin ingin menghapus data penjualan ini?`}
secondaryButton={{ secondaryButton={{
text: 'Tidak', text: 'Tidak',
onClick: deleteModal.closeModal,
}} }}
primaryButton={{ primaryButton={{
text: 'Ya', text: 'Ya',
@@ -1,6 +1,7 @@
import * as Yup from 'yup'; import * as Yup from 'yup';
type SalesOrderProductSchemaType = { type SalesOrderProductSchemaType = {
id?: number | undefined;
kandang_id?: number; kandang_id?: number;
kandang?: { kandang?: {
value: number; value: number;
@@ -16,10 +17,13 @@ type SalesOrderProductSchemaType = {
qty: string | number | undefined; qty: string | number | undefined;
avg_weight: string | number | undefined; avg_weight: string | number | undefined;
total_price: string | number | undefined; total_price: string | number | undefined;
vehicle_number?: string | undefined;
}; };
export const SalesOrderProductSchema: Yup.ObjectSchema<SalesOrderProductSchemaType> = export const SalesOrderProductSchema: Yup.ObjectSchema<SalesOrderProductSchemaType> =
Yup.object({ Yup.object({
id: Yup.number(),
vehicle_number: Yup.string().required('Nomor Kendaraan wajib diisi!'),
kandang: Yup.object({ kandang: Yup.object({
value: Yup.number().min(1).required(), value: Yup.number().min(1).required(),
label: Yup.string().required(), label: Yup.string().required(),
@@ -4,8 +4,8 @@ import { useFormik } from 'formik';
import { import {
SalesOrderProductFormValues, SalesOrderProductFormValues,
SalesOrderProductSchema, SalesOrderProductSchema,
} from './SalesOrderProduct.schema'; } from '@/components/pages/marketing/form/repeater/sales-order/SalesOrderProduct.schema';
import { RefObject, useEffect, useState } from 'react'; import { RefObject, useState } from 'react';
import SelectInput, { import SelectInput, {
OptionType, OptionType,
useSelect, useSelect,
@@ -17,75 +17,24 @@ import { ProductWarehouseApi } from '@/services/api/inventory';
import NumberInput from '@/components/input/NumberInput'; import NumberInput from '@/components/input/NumberInput';
import Button from '@/components/Button'; import Button from '@/components/Button';
import { isResponseSuccess } from '@/lib/api-helper'; import { isResponseSuccess } from '@/lib/api-helper';
import { formatVechicleNumber } from '@/lib/helper';
import PatternInput from '@/components/input/PatternInput';
import Alert from '@/components/Alert';
const SalesOrderProductForm = ({ const SalesOrderProductForm = ({
initialValues, initialValues,
modalRef,
onSubmitForm, onSubmitForm,
}: { }: {
initialValues?: SalesOrderProductFormValues; initialValues?: SalesOrderProductFormValues;
modalRef?: RefObject<HTMLDialogElement | null>; modalRef?: RefObject<HTMLDialogElement | null>;
onSubmitForm?: (value: SalesOrderProductFormValues) => Promise<void>; onSubmitForm?: (value: SalesOrderProductFormValues) => Promise<void>;
}) => { }) => {
// State
const [selectedOptionsKandang, setSelectedOptionsKandang] =
useState<OptionType | null>(null);
const [selectedOptionsWarehouse, setSelectedOptionsWarehouse] = useState<
OptionType | null | undefined
>(undefined);
const [formErrorMessage, setFormErrorMessage] = useState(''); const [formErrorMessage, setFormErrorMessage] = useState('');
// Options Data
const {
options: kandangSourceOptions,
rawData: kandangSourceRawData,
isLoadingOptions: isLoadingKandangSourceOptions,
} = useSelect<Kandang>(KandangApi.basePath, 'id', 'name');
const {
options: warehouseSourceOptions,
rawData: warehouseSourceRawData,
isLoadingOptions: isLoadingWarehouseSourceOptions,
} = useSelect<ProductWarehouse>(
ProductWarehouseApi.basePath,
'id',
'product.name',
'search',
{
warehouse_id: selectedOptionsKandang?.value?.toString() ?? '',
}
);
// Handler
const kandangChangeHandler = (val: OptionType | OptionType[] | null) => {
setSelectedOptionsKandang(val as OptionType);
formik.setFieldValue('kandang', val as OptionType);
formik.setFieldValue('kandang_id', (val as OptionType)?.value);
formik.setFieldValue('product_warehouse_id', null);
formik.setFieldValue('qty', null);
warehouseChangeHandler(null);
};
const warehouseChangeHandler = (val: OptionType | OptionType[] | null) => {
setSelectedOptionsWarehouse(val as OptionType);
formik.setFieldValue('product_warehouse', val as OptionType);
formik.setFieldValue('product_warehouse_id', (val as OptionType)?.value);
if (isResponseSuccess(warehouseSourceRawData)) {
const productWarehouse = warehouseSourceRawData?.data.find(
(item: ProductWarehouse) => item.id === (val as OptionType)?.value
);
if (selectedOptionsWarehouse?.value !== null) {
formik.setFieldValue('qty', productWarehouse?.quantity);
handleBlurField('qty');
} else {
formik.setFieldValue('qty', null);
}
}
};
// Formik
const formik = useFormik<SalesOrderProductFormValues>({ const formik = useFormik<SalesOrderProductFormValues>({
enableReinitialize: true, enableReinitialize: true,
initialValues: { initialValues: {
vehicle_number: initialValues?.vehicle_number || undefined,
kandang_id: initialValues?.kandang_id || undefined, kandang_id: initialValues?.kandang_id || undefined,
kandang: initialValues?.kandang || undefined, kandang: initialValues?.kandang || undefined,
product_warehouse: initialValues?.product_warehouse || undefined, product_warehouse: initialValues?.product_warehouse || undefined,
@@ -99,34 +48,59 @@ const SalesOrderProductForm = ({
validationSchema: SalesOrderProductSchema, validationSchema: SalesOrderProductSchema,
onSubmit: async (values) => { onSubmit: async (values) => {
setFormErrorMessage(''); setFormErrorMessage('');
if ( onSubmitForm?.(values);
isResponseSuccess(kandangSourceRawData) && handleResetForm();
isResponseSuccess(warehouseSourceRawData)
) {
const productWarehouse = warehouseSourceRawData?.data.find(
(item: ProductWarehouse) => item.id === values.product_warehouse_id
);
const kandang = kandangSourceRawData?.data.find(
(item: Kandang) => item.id === values.kandang_id
);
onSubmitForm?.(values);
handleResetForm();
}
}, },
}); });
const { setValues: formikSetValues } = formik;
useEffect(() => { const {
formikSetValues(formik.initialValues); options: kandangSourceOptions,
}, [formikSetValues, formik.initialValues]); isLoadingOptions: isLoadingKandangSourceOptions,
} = useSelect<Kandang>(KandangApi.basePath, 'id', 'name');
const {
options: warehouseSourceOptions,
rawData: warehouseSourceRawData,
isLoadingOptions: isLoadingWarehouseSourceOptions,
} = useSelect<ProductWarehouse>(
ProductWarehouseApi.basePath,
'id',
'product.name',
'search',
{
warehouse_id: formik.values.kandang_id?.toString() ?? '',
}
);
const kandangChangeHandler = (val: OptionType | OptionType[] | null) => {
formik.setFieldValue('kandang', val as OptionType);
formik.setFieldValue('kandang_id', (val as OptionType)?.value);
formik.setFieldValue('product_warehouse_id', null);
formik.setFieldValue('product_warehouse', null);
formik.setFieldValue('qty', null);
};
const warehouseChangeHandler = (val: OptionType | OptionType[] | null) => {
formik.setFieldValue('product_warehouse', val as OptionType);
const newId = (val as OptionType)?.value;
formik.setFieldValue('product_warehouse_id', newId);
if (isResponseSuccess(warehouseSourceRawData) && newId) {
const productWarehouse = warehouseSourceRawData?.data.find(
(item: ProductWarehouse) => item.id === newId
);
formik.setFieldValue('qty', productWarehouse?.quantity);
handleBlurField('qty');
} else {
formik.setFieldValue('qty', null);
}
};
const handleResetForm = () => { const handleResetForm = () => {
setSelectedOptionsKandang(null);
setSelectedOptionsWarehouse(null);
setFormErrorMessage(''); setFormErrorMessage('');
formik.resetForm({ formik.resetForm({
values: { values: {
vehicle_number: '',
kandang_id: undefined, kandang_id: undefined,
kandang: null, kandang: null,
product_warehouse: null, product_warehouse: null,
@@ -145,7 +119,7 @@ const SalesOrderProductForm = ({
formik.values; formik.values;
if (field === 'unit_price' || field === 'total_price' || field === 'qty') { if (field === 'unit_price' || field === 'total_price' || field === 'qty') {
if (qty && unit_price && field === 'unit_price') { if (qty && unit_price && (field === 'unit_price' || field === 'qty')) {
formik.setFieldValue( formik.setFieldValue(
'total_price', 'total_price',
(qty as number) * (unit_price as number) (qty as number) * (unit_price as number)
@@ -159,7 +133,7 @@ const SalesOrderProductForm = ({
} }
if (field === 'avg_weight' || field === 'total_weight' || field === 'qty') { if (field === 'avg_weight' || field === 'total_weight' || field === 'qty') {
if (qty && avg_weight && field === 'avg_weight') { if (qty && avg_weight && (field === 'avg_weight' || field === 'qty')) {
formik.setFieldValue( formik.setFieldValue(
'total_weight', 'total_weight',
(qty as number) * (avg_weight as number) (qty as number) * (avg_weight as number)
@@ -180,8 +154,15 @@ const SalesOrderProductForm = ({
onSubmit={formik.handleSubmit} onSubmit={formik.handleSubmit}
onReset={handleResetForm} onReset={handleResetForm}
> >
{formErrorMessage && (
<div onClick={() => setFormErrorMessage('')} className='my-3 w-full'>
<Alert color='error'>
{formErrorMessage ? formErrorMessage : ''}
</Alert>
</div>
)}
<div className='grid grid-cols-2 gap-4 z-200'> <div className='grid grid-cols-2 gap-4 z-200'>
{/* <PatternInput <PatternInput
name='vehicle_number' name='vehicle_number'
label='No. Polisi' label='No. Polisi'
format='AA #### AAA' format='AA #### AAA'
@@ -198,16 +179,15 @@ const SalesOrderProductForm = ({
Boolean(formik.errors.vehicle_number) Boolean(formik.errors.vehicle_number)
} }
errorMessage={formik.errors.vehicle_number} errorMessage={formik.errors.vehicle_number}
/> */} />
<SelectInput <SelectInput
required required
label='Kandang' label='Kandang'
options={kandangSourceOptions} options={kandangSourceOptions}
isLoading={isLoadingKandangSourceOptions} isLoading={isLoadingKandangSourceOptions}
value={selectedOptionsKandang} value={formik.values.kandang}
onChange={kandangChangeHandler} onChange={kandangChangeHandler}
isClearable isClearable
menuPortalTarget={modalRef?.current}
isError={ isError={
formik.touched.kandang_id && Boolean(formik.errors.kandang_id) formik.touched.kandang_id && Boolean(formik.errors.kandang_id)
} }
@@ -219,12 +199,11 @@ const SalesOrderProductForm = ({
label='Produk' label='Produk'
options={warehouseSourceOptions} options={warehouseSourceOptions}
isLoading={isLoadingWarehouseSourceOptions} isLoading={isLoadingWarehouseSourceOptions}
value={selectedOptionsWarehouse} value={formik.values.product_warehouse}
onChange={warehouseChangeHandler} onChange={warehouseChangeHandler}
isClearable isClearable
menuPortalTarget={modalRef?.current}
placeholder='Pilih Kandang Terlebih Dahulu' placeholder='Pilih Kandang Terlebih Dahulu'
isDisabled={!selectedOptionsKandang?.value} isDisabled={!formik.values.kandang_id}
isError={ isError={
formik.touched.product_warehouse_id && formik.touched.product_warehouse_id &&
Boolean(formik.errors.product_warehouse_id) Boolean(formik.errors.product_warehouse_id)
@@ -3,39 +3,38 @@
import Button from '@/components/Button'; import Button from '@/components/Button';
import Table from '@/components/Table'; import Table from '@/components/Table';
import { SalesOrderProductFormValues } from '@/components/pages/marketing/form/repeater/sales-order/SalesOrderProduct.schema'; import { SalesOrderProductFormValues } from '@/components/pages/marketing/form/repeater/sales-order/SalesOrderProduct.schema';
import { cn, formatCurrency, formatNumber } from '@/lib/helper'; import {
cn,
formatCurrency,
formatNumber,
formatVechicleNumber,
} from '@/lib/helper';
import { Icon } from '@iconify/react'; import { Icon } from '@iconify/react';
import { useMemo, useState } from 'react'; import { useMemo, useState } from 'react';
import * as TanStack from '@tanstack/react-table'; import * as TanStack from '@tanstack/react-table';
import CheckboxInput from '@/components/input/CheckboxInput'; import CheckboxInput from '@/components/input/CheckboxInput';
// Hapus import Modal, useModal, dan MarketingProductForm
// Tentukan Tipe Props baru yang diterima dari SalesForm
type SalesOrderProductTableProps = { type SalesOrderProductTableProps = {
data: SalesOrderProductFormValues[]; data: SalesOrderProductFormValues[];
onDelete: (product_warehouse_id: number, kandang_id: number) => void; rowSelection: Record<string, boolean>;
onBulkDelete: (selectedIds: number[]) => void; setRowSelection: React.Dispatch<
onAddProductClick: () => void; // Prop baru untuk memanggil modal di parent React.SetStateAction<Record<string, boolean>>
>;
selectedRowIds: number[];
onDelete: (id: number) => void;
onBulkDelete: () => void;
onAddProductClick: () => void;
}; };
const SalesOrderProductTable = ({ const SalesOrderProductTable = ({
data, data,
rowSelection,
setRowSelection,
selectedRowIds,
onDelete, onDelete,
onBulkDelete, onBulkDelete,
onAddProductClick, onAddProductClick,
}: SalesOrderProductTableProps) => { }: SalesOrderProductTableProps) => {
const [rowSelection, setRowSelection] = useState<Record<string, boolean>>({});
const selectedRowIds = Object.keys(rowSelection).map((item) =>
parseInt(item)
);
const handleBulkDeleteClick = () => {
onBulkDelete(selectedRowIds);
setRowSelection({});
};
const columns = useMemo( const columns = useMemo(
() => [ () => [
{ {
@@ -67,6 +66,11 @@ const SalesOrderProductTable = ({
</div> </div>
), ),
}, },
{
accessorFn: (row: SalesOrderProductFormValues) =>
formatVechicleNumber(row.vehicle_number as string),
header: 'No. Polisi',
},
{ {
accessorFn: (row: SalesOrderProductFormValues) => row.kandang?.label, accessorFn: (row: SalesOrderProductFormValues) => row.kandang?.label,
header: 'Kandang', header: 'Kandang',
@@ -110,22 +114,10 @@ const SalesOrderProductTable = ({
<Button <Button
color='error' color='error'
className='p-1' className='p-1'
disabled={
!props.row.original.product_warehouse_id ||
!props.row.original.kandang_id
}
onClick={() => { onClick={() => {
// PANGGIL CALLBACK PARENT (onDelete) onDelete(props.row.original.id as number);
if (
props.row.original.product_warehouse_id &&
props.row.original.kandang_id
) {
onDelete(
props.row.original.product_warehouse_id,
props.row.original.kandang_id
);
}
}} }}
type='button'
> >
<Icon icon='mdi:trash' width={16} height={16} /> <Icon icon='mdi:trash' width={16} height={16} />
</Button> </Button>
@@ -180,7 +172,7 @@ const SalesOrderProductTable = ({
variant='outline' variant='outline'
color='error' color='error'
className='justify-start w-fit py-1 text-sm' className='justify-start w-fit py-1 text-sm'
onClick={handleBulkDeleteClick} onClick={onBulkDelete}
> >
<Icon icon='mdi:trash' width={16} height={16} /> <Icon icon='mdi:trash' width={16} height={16} />
Hapus Hapus
@@ -191,8 +183,6 @@ const SalesOrderProductTable = ({
</Button> </Button>
)} )}
</div> </div>
{/* Modal dan Form dihapus dari sini */}
</> </>
); );
}; };
+2
View File
@@ -211,6 +211,7 @@ export const dummyProductWarehouses: ProductWarehouse[] = [
// Helper untuk Sales Order (SO) Item // Helper untuk Sales Order (SO) Item
const soItem1: BaseSalesOrder = { const soItem1: BaseSalesOrder = {
vehicle_number: 'B 1234 ABC',
id: 101, id: 101,
marketing_id: 1, marketing_id: 1,
product_warehouse_id: 1, product_warehouse_id: 1,
@@ -222,6 +223,7 @@ const soItem1: BaseSalesOrder = {
product_warehouse: dummyProductWarehouses[0] as ProductWarehouse, product_warehouse: dummyProductWarehouses[0] as ProductWarehouse,
}; };
const soItem2: BaseSalesOrder = { const soItem2: BaseSalesOrder = {
vehicle_number: 'D 5678 EFG',
id: 102, id: 102,
marketing_id: 2, marketing_id: 2,
product_warehouse_id: 2, product_warehouse_id: 2,
+34 -31
View File
@@ -20,7 +20,7 @@ const createDummyResponse = <T>(data: T): BaseApiResponse<T> => ({
data: data, data: data,
}); });
export class MarketingService extends BaseApiService< export class SalesOrderService extends BaseApiService<
Marketing, Marketing,
CreateSalesOrderPayload, CreateSalesOrderPayload,
UpdateSalesOrderPayload UpdateSalesOrderPayload
@@ -29,40 +29,40 @@ export class MarketingService extends BaseApiService<
super(basePath); super(basePath);
} }
/** // /**
* Override: Mengambil semua data Marketing dari dummyMarketings // * Override: Mengambil semua data Marketing dari dummyMarketings
*/ // */
async getAllFetcher(endpoint: string): Promise<BaseApiResponse<Marketing[]>> { // async getAllFetcher(endpoint: string): Promise<BaseApiResponse<Marketing[]>> {
// Simulasi delay jaringan // // Simulasi delay jaringan
await sleep(500); // await sleep(500);
// Filter data marketing yang valid (jika menggunakan BaseMarketing[]) // // Filter data marketing yang valid (jika menggunakan BaseMarketing[])
const data = dummyMarketings as Marketing[]; // const data = dummyMarketings as Marketing[];
return createDummyResponse<Marketing[]>(data); // return createDummyResponse<Marketing[]>(data);
} // }
/** // /**
* Override: Mengambil satu data Marketing berdasarkan ID dari dummyMarketings // * Override: Mengambil satu data Marketing berdasarkan ID dari dummyMarketings
*/ // */
async getSingle(id: number): Promise<BaseApiResponse<Marketing> | undefined> { // async getSingle(id: number): Promise<BaseApiResponse<Marketing> | undefined> {
// Simulasi delay jaringan // // Simulasi delay jaringan
await sleep(300); // await sleep(300);
const foundData = dummyMarketings.find((m) => m.id == id); // const foundData = dummyMarketings.find((m) => m.id == id);
if (foundData) { // if (foundData) {
// Data ditemukan, kembalikan respons sukses // // Data ditemukan, kembalikan respons sukses
return createDummyResponse<Marketing>(foundData as Marketing); // return createDummyResponse<Marketing>(foundData as Marketing);
} else { // } else {
// Data tidak ditemukan, simulasi respons error // // Data tidak ditemukan, simulasi respons error
return { // return {
code: 404, // code: 404,
status: 'error', // status: 'error',
message: 'Marketing data not found (MOCK)', // message: 'Marketing data not found (MOCK)',
}; // };
} // }
} // }
/** /**
* Approve single marketing data * Approve single marketing data
@@ -111,4 +111,7 @@ export class MarketingService extends BaseApiService<
} }
} }
export const MarketingApi = new MarketingService('/marketing/sales-orders'); export const SalesOrderApi = new SalesOrderService('/marketing/sales-orders');
export const MarketingApi = new BaseApiService<Marketing, unknown, unknown>(
'/marketing'
);
+1
View File
@@ -35,6 +35,7 @@ export type BaseSalesOrder = {
total_weight: number; total_weight: number;
total_price: number; total_price: number;
product_warehouse: ProductWarehouse; product_warehouse: ProductWarehouse;
vehicle_number: string;
}; };
export type BaseDeliveryOrder = { export type BaseDeliveryOrder = {