Merge branch 'fix/adjustment-penjualan' into 'development'

[FIX/FE] Adjustment Penjualan UI and Data Fetch (2/2)

See merge request mbugroup/lti-web-client!318
This commit is contained in:
Rivaldi A N S
2026-02-09 07:16:56 +00:00
8 changed files with 569 additions and 463 deletions
@@ -111,6 +111,7 @@ const DeliveryOrderFormModal = ({
const successModal = useModal(); const successModal = useModal();
const rejectModal = useModal(); const rejectModal = useModal();
const deleteModal = useModal(); const deleteModal = useModal();
const approveModal = useModal();
const formRef = useRef<HTMLFormElement>(null); const formRef = useRef<HTMLFormElement>(null);
const textareaRef = useRef<HTMLTextAreaElement>(null); const textareaRef = useRef<HTMLTextAreaElement>(null);
@@ -333,6 +334,33 @@ const DeliveryOrderFormModal = ({
refreshApproval(); refreshApproval();
}; };
const approveMarketingHandler = async (notes: string) => {
if (!marketingId) {
toast.error(`Tidak ada data yang valid untuk di approve.`);
approveModal.closeModal();
return;
}
const approveMarketingRes = await SalesOrderApi.singleApproval(
Number(marketingId),
'APPROVED',
notes
);
if (isResponseSuccess(approveMarketingRes)) {
approveModal.closeModal();
toast.success(approveMarketingRes?.message as string);
closeModalHandler();
router.push('/marketing');
}
if (isResponseError(approveMarketingRes)) {
approveModal.closeModal();
toast.error(approveMarketingRes?.message as string);
}
refreshMarketing();
refreshApproval();
};
const deleteClickHandler = () => { const deleteClickHandler = () => {
deleteModal.openModal(); deleteModal.openModal();
}; };
@@ -380,7 +408,77 @@ const DeliveryOrderFormModal = ({
}, },
[prevButtonHandler] [prevButtonHandler]
); );
const handleUpdateDO = useCallback(
const isApprovalStep3Approved = useMemo(() => {
return (
isResponseSuccess(marketing) &&
marketing.data.latest_approval?.step_number === 3 &&
marketing.data.latest_approval?.action === 'APPROVED'
);
}, [marketing]);
const handleUpdateDOWithAPI = useCallback(
async (id: number, values: DeliveryOrderProductFormValues) => {
if (!marketingId) {
toast.error('Marketing ID tidak ditemukan');
return;
}
setIsLoading(true);
const updatedDeliveryValues = deliveryOrderValues.map((product) =>
product.id === id ? { ...product, ...values } : product
);
const payload = {
marketing_id: Number(marketingId),
delivery_products: updatedDeliveryValues
.map((product) => {
if (Boolean(product.delivery_date)) {
return {
marketing_product_id: product.marketing_product_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),
delivery_date: formatDate(
product.delivery_date as string,
'yyyy-MM-DD'
),
vehicle_number: product.vehicle_number,
};
}
})
.filter((item) => Boolean(item)),
} as UpdateDeliveryOrderPayload;
const updateDeliveryRes = await DeliveryOrderApi.update(
Number(marketingId),
payload
);
if (isResponseSuccess(updateDeliveryRes)) {
toast.success(updateDeliveryRes?.message as string);
closeModalHandler();
}
if (isResponseError(updateDeliveryRes)) {
setFormErrorMessage(updateDeliveryRes?.message as string);
}
setIsLoading(false);
},
[
marketingId,
deliveryOrderValues,
formik.values.sales_order,
prevButtonHandler,
refreshMarketing,
]
);
const handleUpdateDOLocal = useCallback(
async (id: number, values: DeliveryOrderProductFormValues) => { async (id: number, values: DeliveryOrderProductFormValues) => {
setDeliveryOrderValues((prev) => setDeliveryOrderValues((prev) =>
prev.map((product) => prev.map((product) =>
@@ -463,7 +561,26 @@ const DeliveryOrderFormModal = ({
); );
formik.setValues(filledInitialValues); formik.setValues(filledInitialValues);
setStep(1);
if (modalAction === 'add_delivery') {
// add delivery
const firstDeliveryItem = filledInitialValues.delivery_order?.[0];
if (firstDeliveryItem) {
setSelectedDeliveryProduct(firstDeliveryItem);
}
setStep(2); // Langsung ke form delivery
} else if (modalAction === 'edit_delivery') {
// edit delivery
const firstDeliveryItem = filledInitialValues.delivery_order?.[0];
if (firstDeliveryItem) {
setSelectedDeliveryProduct(firstDeliveryItem);
setStep(2); // Langsung ke form edit
} else {
setStep(1); // Jika belum ada data, tampilkan detail view
}
} else {
setStep(1); // Detail view
}
} }
if (isResponseError(marketing)) { if (isResponseError(marketing)) {
@@ -474,7 +591,7 @@ const DeliveryOrderFormModal = ({
}; };
getFilledInitialValues(); getFilledInitialValues();
}, [marketingId, marketing]); }, [marketingId, marketing, modalAction]);
// Reset error message when step changes // Reset error message when step changes
useEffect(() => { useEffect(() => {
@@ -679,7 +796,12 @@ const DeliveryOrderFormModal = ({
exisitingValues={deliveryOrderValues} exisitingValues={deliveryOrderValues}
onSubmitForm={handleAddSubmitDO} onSubmitForm={handleAddSubmitDO}
initialValues={selectedDeliveryProduct ?? undefined} initialValues={selectedDeliveryProduct ?? undefined}
onUpdateForm={handleUpdateDO} onUpdateForm={
isApprovalStep3Approved
? handleUpdateDOWithAPI
: handleUpdateDOLocal
}
isLoading={isLoading}
/> />
)} )}
</div> </div>
@@ -723,7 +845,15 @@ const DeliveryOrderFormModal = ({
type='button' type='button'
color='primary' color='primary'
onClick={() => { onClick={() => {
formRef.current?.requestSubmit(); // Jika masih di step 1 approval, gunakan single approval API
if (
marketing?.data?.latest_approval?.step_number === 1
) {
approveModal.openModal();
} else {
// Jika sudah di step 2/3, gunakan form submit (delivery products)
formRef.current?.requestSubmit();
}
}} }}
className='p-3 shadow-button-soft text-base-100 rounded-lg text-sm font-semibold' className='p-3 shadow-button-soft text-base-100 rounded-lg text-sm font-semibold'
disabled={deliveryRejected} disabled={deliveryRejected}
@@ -741,8 +871,8 @@ const DeliveryOrderFormModal = ({
ref={successModal.ref} ref={successModal.ref}
iconPosition='left' iconPosition='left'
type='success' type='success'
text={`${currentModalAction === 'add' ? 'Data Berhasil Disimpan' : 'Data Berhasil Diubah'}`} text={`${currentModalAction === 'add_delivery' ? 'Data Berhasil Disimpan' : 'Data Berhasil Diubah'}`}
subtitleText={`${currentModalAction === 'add' ? 'Data delivery order telah berhasil disimpan.' : 'Data delivery order telah berhasil diubah.'}`} subtitleText={`${currentModalAction === 'add_delivery' ? 'Data delivery order telah berhasil disimpan.' : 'Data delivery order telah berhasil diubah.'}`}
primaryButton={{ primaryButton={{
text: 'Oke', text: 'Oke',
color: 'primary', color: 'primary',
@@ -795,6 +925,21 @@ const DeliveryOrderFormModal = ({
onClick: confirmationModalDeleteClickHandler, onClick: confirmationModalDeleteClickHandler,
}} }}
/> />
<ConfirmationModalWithNotes
ref={approveModal.ref}
type={'success'}
text={`Apakah anda yakin ingin approve data penjualan?`}
secondaryButton={{
text: 'Tidak',
onClick: approveModal.closeModal,
}}
primaryButton={{
text: 'Ya',
color: 'success',
onClick: approveMarketingHandler,
}}
/>
</> </>
); );
}; };
@@ -394,7 +394,7 @@ const SalesOrderFormModal = ({
} }
formik.setFieldValue('sales_order', updatedProducts); formik.setFieldValue('sales_order', updatedProducts);
console.log(formik.values); setSelectedMarketingProduct(null);
nextButtonHandler(); nextButtonHandler();
}, },
[memoSalesOrder, nextButtonHandler] [memoSalesOrder, nextButtonHandler]
@@ -418,6 +418,15 @@ const SalesOrderFormModal = ({
if (modalAction === 'add' || modalAction === 'edit') { if (modalAction === 'add' || modalAction === 'edit') {
setCurrentModalAction(modalAction); setCurrentModalAction(modalAction);
formModal.openModal(); formModal.openModal();
if (modalAction === 'add') {
formik.resetForm();
setStep(1);
setSelectedMarketingProduct(null);
setSelectedDeliveryProduct(null);
setFormErrorMessage('');
setFormErrorList([]);
}
} }
}, [modalAction]); }, [modalAction]);
@@ -36,6 +36,7 @@ const DeliveryOrderProductForm = ({
exisitingValues, exisitingValues,
onSubmitForm, onSubmitForm,
onUpdateForm, onUpdateForm,
isLoading,
}: { }: {
formState: 'add' | 'edit'; formState: 'add' | 'edit';
salesOrders: BaseSalesOrder[]; salesOrders: BaseSalesOrder[];
@@ -46,6 +47,7 @@ const DeliveryOrderProductForm = ({
id: number, id: number,
value: DeliveryOrderProductFormValues value: DeliveryOrderProductFormValues
) => Promise<void>; ) => Promise<void>;
isLoading?: boolean;
}) => { }) => {
const [formikErrorMessage, setFormErrorMessage] = useState(''); const [formikErrorMessage, setFormErrorMessage] = useState('');
const [selectedProduct, setSelectedProduct] = useState<OptionType | null>( const [selectedProduct, setSelectedProduct] = useState<OptionType | null>(
@@ -178,6 +180,25 @@ const DeliveryOrderProductForm = ({
}, },
}); });
const hasWeekField = useMemo(() => {
const marketingType = formik.values.marketing_type?.value?.toLowerCase();
if (marketingType === 'ayam_pullet') {
return true;
}
if (formik.values.marketing_product?.product_warehouse_data) {
return Boolean(
formik.values.marketing_product?.product_warehouse_data?.week !==
undefined &&
formik.values.marketing_product?.product_warehouse_data?.week !==
null &&
formik.values.marketing_product?.product_warehouse_data?.week > 0
);
}
return false;
}, [formik.values.marketing_product, formik.values.marketing_type]);
const handleResetForm = () => { const handleResetForm = () => {
setFormErrorMessage(''); setFormErrorMessage('');
formik.resetForm({ formik.resetForm({
@@ -362,20 +383,24 @@ const DeliveryOrderProductForm = ({
avg_weight: '', avg_weight: '',
total_weight: '', total_weight: '',
vehicle_number: '', vehicle_number: '',
week: null,
}); });
return; return;
} }
const soFieldValues = SalesProductToFieldValues(so);
formik.setValues({ formik.setValues({
...formik.values, ...formik.values,
marketing_product_id: selected.value as number, marketing_product_id: selected.value as number,
marketing_product: SalesProductToFieldValues(so), marketing_product: soFieldValues,
qty: so.qty, qty: so.qty,
unit_price: so.unit_price, unit_price: so.unit_price,
total_price: so.total_price, total_price: so.total_price,
avg_weight: so.avg_weight, avg_weight: so.avg_weight,
total_weight: so.total_weight, total_weight: so.total_weight,
vehicle_number: so.vehicle_number, vehicle_number: so.vehicle_number,
week: soFieldValues.week ?? null,
}); });
}} }}
startAdornment={ startAdornment={
@@ -509,10 +534,14 @@ const DeliveryOrderProductForm = ({
)} )}
{/* Konversi Satuan Week Pullet */} {/* Konversi Satuan Week Pullet */}
{formik.values.marketing_type?.value.toLowerCase() === {(formik.values.marketing_type?.value.toLowerCase() ===
'ayam_pullet' && ( 'ayam_pullet' ||
hasWeekField) && (
<NumberInput <NumberInput
required required={
formik.values.marketing_type?.value.toLowerCase() ===
'ayam_pullet'
}
label='Minggu' label='Minggu'
name='week' name='week'
value={formik.values.week ?? undefined} value={formik.values.week ?? undefined}
@@ -793,8 +822,8 @@ const DeliveryOrderProductForm = ({
<div className='absolute sm:w-full bottom-0 right-0 p-4'> <div className='absolute sm:w-full bottom-0 right-0 p-4'>
<Button <Button
type='submit' type='submit'
isLoading={formik.isSubmitting} isLoading={formik.isSubmitting || isLoading}
disabled={formik.isSubmitting} disabled={formik.isSubmitting || isLoading}
className='w-full p-3 rounded-lg text-base-100 text-sm font-semibold' className='w-full p-3 rounded-lg text-base-100 text-sm font-semibold'
> >
Submit Submit
@@ -117,6 +117,19 @@ const SalesOrderProductForm = ({
isInitialValid: false, isInitialValid: false,
}); });
const hasWeekField = useMemo(() => {
const marketingType = formik.values.marketing_type?.value?.toLowerCase();
if (marketingType === 'ayam_pullet') {
return true;
}
return Boolean(
selectedProductWarehouse?.week !== undefined &&
selectedProductWarehouse?.week !== null &&
selectedProductWarehouse?.week > 0
);
}, [selectedProductWarehouse, formik.values.marketing_type]);
// ===== Options ===== // ===== Options =====
const { const {
options: kandangSourceOptions, options: kandangSourceOptions,
@@ -180,10 +193,20 @@ const SalesOrderProductForm = ({
setSelectedProductWarehouse(productWarehouse || null); setSelectedProductWarehouse(productWarehouse || null);
formik.setFieldValue('qty', productWarehouse?.quantity); formik.setFieldValue('qty', productWarehouse?.quantity);
formik.setFieldValue('uom', productWarehouse?.product?.uom?.name || ''); formik.setFieldValue('uom', productWarehouse?.product?.uom?.name || '');
if (
productWarehouse?.week !== undefined &&
productWarehouse?.week !== null &&
productWarehouse?.week > 0
) {
formik.setFieldValue('week', productWarehouse.week);
} else {
formik.setFieldValue('week', null);
}
handleBlurField('qty'); handleBlurField('qty');
} else { } else {
formik.setFieldValue('qty', ''); formik.setFieldValue('qty', '');
formik.setFieldValue('uom', ''); formik.setFieldValue('uom', '');
formik.setFieldValue('week', null);
} }
}; };
@@ -465,10 +488,14 @@ const SalesOrderProductForm = ({
)} )}
{/* Konversi Satuan Week Pullet */} {/* Konversi Satuan Week Pullet */}
{formik.values.marketing_type?.value.toLowerCase() === {(formik.values.marketing_type?.value.toLowerCase() ===
'ayam_pullet' && ( 'ayam_pullet' ||
hasWeekField) && (
<NumberInput <NumberInput
required required={
formik.values.marketing_type?.value.toLowerCase() ===
'ayam_pullet'
}
label='Minggu' label='Minggu'
name='week' name='week'
value={formik.values.week ?? undefined} value={formik.values.week ?? undefined}
@@ -19,6 +19,7 @@ type DeliveryOrderProductTableProps = {
| 'detail' | 'detail'
| 'rejected' | 'rejected'
| 'pending' | 'pending'
| 'success'
| string | string
| null; | null;
marketing?: Marketing; marketing?: Marketing;
@@ -32,7 +33,6 @@ const DeliveryOrderProductTable = ({
formType, formType,
onEdit, onEdit,
onDelete, onDelete,
onAddProductClick,
marketing, marketing,
}: DeliveryOrderProductTableProps) => { }: DeliveryOrderProductTableProps) => {
const onEditRef = useRef(onEdit); const onEditRef = useRef(onEdit);
@@ -42,178 +42,193 @@ const DeliveryOrderProductTable = ({
const approvalStepNumber = marketing?.latest_approval?.step_number; const approvalStepNumber = marketing?.latest_approval?.step_number;
const renderTableContent = (item: DeliveryOrderProductFormValues) => {
const doItem = marketing?.delivery_order?.find(
(doItem) => doItem.do_number === item.do_number
);
return (
<>
<tr className='border-b border-tools-table-outline border-base-content/5'>
<th className='w-1/3 text-start not-first:font-medium text-base-content/50 text-sm px-4 py-3'>
Label
</th>
<th className='text-start font-medium text-base-content/50 text-sm px-4 py-3'>
<div className='flex w-full flex-row gap-1 items-center justify-between h-full'>
<div>Value</div>
{formType !== 'success' &&
(formType === 'add_delivery' ||
formType === 'edit_delivery' ||
formType === 'detail') && (
<div className='flex flex-row gap-1.5 items-center'>
<Button
type='button'
variant='ghost'
color='none'
onClick={() => {
onEditRef.current(item.id as number, item);
}}
className='p-0 hover:text-base-content'
>
<Icon icon='heroicons:pencil' width={20} height={20} />
</Button>
</div>
)}
</div>
</th>
</tr>
<>
<tr>
<td className='text-sm px-4 py-3'>Gudang</td>
<td className='text-sm px-4 py-3'>
{doItem?.warehouse?.name ||
item.marketing_product?.product_warehouse_data?.warehouse?.name}
</td>
</tr>
<tr>
<td className='text-sm px-4 py-3'>Produk</td>
<td className='text-sm px-4 py-3'>
{item.marketing_product?.product_warehouse?.label}
</td>
</tr>
<tr>
<td className='text-sm px-4 py-3'>Qty</td>
<td className='text-sm px-4 py-3'>
{item.qty
? `${formatNumber(parseFloat(item.qty as string))} ${item.marketing_product?.uom ?? ''}`
: '-'}
</td>
</tr>
{Number(item.avg_weight ?? 0) > 0 && (
<tr>
<td className='text-sm px-4 py-3'>Avg Bobot</td>
<td className='text-sm px-4 py-3'>
{formatNumber(Number(item.avg_weight))} Kg
</td>
</tr>
)}
{Number(item.total_weight ?? 0) > 0 && (
<tr>
<td className='text-sm px-4 py-3'>Total Bobot</td>
<td className='text-sm px-4 py-3'>
{formatNumber(Number(item.total_weight))}
</td>
</tr>
)}
<tr>
<td className='text-sm px-4 py-3'>Total Harga Satuan</td>
<td className='text-sm px-4 py-3'>
{formatCurrency(parseFloat(item.unit_price as string))}
</td>
</tr>
<tr>
<td className='text-sm px-4 py-3'>Total Penjualan</td>
<td className='text-sm px-4 py-3'>
{formatCurrency(parseFloat(item.total_price as string))}
</td>
</tr>
</>
<tr className='border-b border-t border-tools-table-outline border-base-content/5'>
<th className='w-1/3 text-start not-first:font-medium text-base-content/50 text-sm px-4 py-3'>
Label
</th>
<th className='text-start font-medium text-base-content/50 text-sm px-4 py-3'>
<div className='flex w-full flex-row gap-1 items-center justify-between h-full'>
<div>Value</div>
</div>
</th>
</tr>
<>
{approvalStepNumber !== 1 && (
<tr>
<td className='text-sm px-4 py-3'>Tanggal Pengiriman</td>
<td className='text-sm px-4 py-3'>
{item.delivery_date ? (
formatDate(item.delivery_date, 'DD MMM YYYY')
) : formType === 'add_delivery' ||
formType === 'edit_delivery' ||
formType === 'detail' ? (
<span
className='text-error hover:text-error/70 cursor-pointer hover:underline underline-offset-4'
onClick={() => {
onEditRef.current(item.id as number, item);
}}
>
Belum diisi
</span>
) : (
<span className='text-error'>Belum diisi</span>
)}
</td>
</tr>
)}
{item.do_number && (
<tr>
<td className='text-sm px-4 py-3'>No. Pengiriman</td>
<td className='text-sm px-4 py-3'>{item.do_number}</td>
</tr>
)}
<tr>
<td className='text-sm px-4 py-3'>No. Polisi</td>
<td className='text-sm px-4 py-3'>{item.vehicle_number}</td>
</tr>
{doItem && (
<tr>
<td className='text-sm px-4 py-3'>Dokumen Pengiriman</td>
<td className='text-sm px-4 py-3'>
<DeliveryOrderExport data={marketing} deliveryOrder={doItem} />
</td>
</tr>
)}
</>
</>
);
};
return ( return (
<> <>
<div className='size-full flex flex-col relative overflow-x-hidden gap-3'> <div className='size-full flex flex-col relative overflow-x-hidden gap-3'>
{data.map((item) => { {data.map((item) => (
const doItem = marketing?.delivery_order?.find( <div key={`table-${item.id}`}>
(doItem) => doItem.do_number === item.do_number {formType === 'success' ? (
); <div className='rounded-lg border border-tools-table-outline border-base-content/5'>
return ( <table
<Card style={{
key={`table-${item.id}`} borderRadius: '0.5rem',
title={ }}
item.marketing_product?.product_warehouse?.label || 'Produk' className='border-none w-full'
} >
collapsible={true} <tbody className='w-full'>{renderTableContent(item)}</tbody>
defaultCollapsed={false} </table>
variant='bordered' </div>
className={{ ) : (
wrapper: 'w-full rounded-lg', <Card
body: 'p-0', key={`table-${item.id}`}
title: 'px-2 py-1.5 font-normal text-sm', title={
collapsible: 'rounded-lg', item.marketing_product?.product_warehouse?.label || 'Produk'
}} }
> collapsible={true}
<table defaultCollapsed={false}
style={{ variant='bordered'
borderRadius: '0.5rem', className={{
wrapper: 'w-full rounded-lg',
body: 'p-0',
title: 'px-2 py-1.5 font-normal text-sm',
collapsible: 'rounded-lg',
}} }}
className='border-none w-full'
> >
<tbody className='w-full'> <table
<tr className='border-b border-t border-tools-table-outline border-base-content/5'> style={{
<th className='w-1/3 text-start not-first:font-medium text-base-content/50 text-sm px-4 py-3'> borderRadius: '0.5rem',
Label }}
</th> className='border-none w-full'
<th className='text-start font-medium text-base-content/50 text-sm px-4 py-3'> >
<div className='flex w-full flex-row gap-1 items-center justify-between h-full'> <tbody className='w-full'>{renderTableContent(item)}</tbody>
<div>Value</div> </table>
{(formType === 'add_delivery' || </Card>
formType === 'edit_delivery' || )}
formType === 'detail') && ( </div>
<div className='flex flex-row gap-1.5 items-center'> ))}
<Button
type='button'
variant='ghost'
color='none'
onClick={() => {
onEditRef.current(item.id as number, item);
}}
className='p-0 hover:text-base-content'
>
<Icon
icon='heroicons:pencil'
width={20}
height={20}
/>
</Button>
<Button
type='button'
variant='ghost'
color='none'
onClick={() => {
onDeleteRef.current(item.id as number);
}}
className='p-0 text-error hover:text-base-content'
>
<Icon
icon='heroicons:trash'
width={20}
height={20}
/>
</Button>
</div>
)}
</div>
</th>
</tr>
<>
{approvalStepNumber !== 1 && (
<tr>
<td className='text-sm px-4 py-3'>
Tanggal Pengiriman
</td>
<td className='text-sm px-4 py-3'>
{item.delivery_date ? (
formatDate(item.delivery_date, 'DD MMM YYYY')
) : (
<span className='text-error'>Belum diisi</span>
)}
</td>
</tr>
)}
{item.do_number && (
<tr>
<td className='text-sm px-4 py-3'>No. Pengiriman</td>
<td className='text-sm px-4 py-3'>{item.do_number}</td>
</tr>
)}
<tr>
<td className='text-sm px-4 py-3'>No. Polisi</td>
<td className='text-sm px-4 py-3'>
{item.vehicle_number}
</td>
</tr>
<tr>
<td className='text-sm px-4 py-3'>Gudang</td>
<td className='text-sm px-4 py-3'>
{doItem?.warehouse?.name ||
item.marketing_product?.product_warehouse_data
?.warehouse?.name}
</td>
</tr>
<tr>
<td className='text-sm px-4 py-3'>Produk</td>
<td className='text-sm px-4 py-3'>
{item.marketing_product?.product_warehouse?.label}
</td>
</tr>
<tr>
<td className='text-sm px-4 py-3'>Qty</td>
<td className='text-sm px-4 py-3'>
{item.qty
? `${formatNumber(parseFloat(item.qty as string))} ${item.marketing_product?.uom ?? ''}`
: '-'}
</td>
</tr>
<tr>
<td className='text-sm px-4 py-3'>Avg Bobot</td>
<td className='text-sm px-4 py-3'>
{item.avg_weight
? formatNumber(
parseFloat(item.avg_weight as string)
) + ' Kg'
: '-'}
</td>
</tr>
<tr>
<td className='text-sm px-4 py-3'>Total Bobot</td>
<td className='text-sm px-4 py-3'>
{formatNumber(parseFloat(item.total_weight as string))}
</td>
</tr>
<tr>
<td className='text-sm px-4 py-3'>Total Harga Satuan</td>
<td className='text-sm px-4 py-3'>
{formatCurrency(parseFloat(item.unit_price as string))}
</td>
</tr>
<tr>
<td className='text-sm px-4 py-3'>Total Penjualan</td>
<td className='text-sm px-4 py-3'>
{formatCurrency(parseFloat(item.total_price as string))}
</td>
</tr>
{doItem && (
<tr>
<td className='text-sm px-4 py-3'>
Dokumen Pengiriman
</td>
<td className='text-sm px-4 py-3'>
<DeliveryOrderExport
data={marketing}
deliveryOrder={doItem}
/>
</td>
</tr>
)}
</>
</tbody>
</table>
</Card>
);
})}
</div> </div>
</> </>
); );
@@ -3,15 +3,9 @@
import Button from '@/components/Button'; import Button from '@/components/Button';
import Card from '@/components/Card'; import Card from '@/components/Card';
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 { import { formatCurrency, formatNumber } from '@/lib/helper';
formatCurrency,
formatNumber,
formatVechicleNumber,
} from '@/lib/helper';
import { Icon } from '@iconify/react'; import { Icon } from '@iconify/react';
import { useMemo, useRef } from 'react'; import { useRef } from 'react';
import * as TanStack from '@tanstack/react-table';
import CheckboxInput from '@/components/input/CheckboxInput';
type SalesOrderProductTableProps = { type SalesOrderProductTableProps = {
data: SalesOrderProductFormValues[]; data: SalesOrderProductFormValues[];
@@ -33,278 +27,168 @@ const SalesOrderProductTable = ({
const onEditRef = useRef(onEdit); const onEditRef = useRef(onEdit);
onEditRef.current = onEdit; onEditRef.current = onEdit;
const columns = useMemo( const renderTableContent = (item: SalesOrderProductFormValues) => (
() => [ <>
{ <tr className='border-b border-tools-table-outline border-base-content/5'>
id: 'select', <th className='w-1/3 text-start not-first:font-medium text-base-content/50 text-sm px-4 py-3'>
header: ({ Label
table, </th>
}: { <th className='text-start font-medium text-base-content/50 text-sm px-4 py-3'>
table: TanStack.Table<SalesOrderProductFormValues>; <div className='flex w-full flex-row gap-1 items-center justify-between h-full'>
}) => ( <div>Value</div>
<div className='w-full flex flex-row justify-center'> {formType !== 'success' && (
<CheckboxInput <div className='flex flex-row gap-1.5 items-center'>
name='allRow' <Button
checked={table.getIsAllRowsSelected()} type='button'
indeterminate={table.getIsSomeRowsSelected()} variant='ghost'
onChange={table.getToggleAllRowsSelectedHandler()} color='none'
/> onClick={() => {
onEditRef.current(item.id as number);
}}
className='p-0 hover:text-base-content'
>
<Icon icon='heroicons:pencil' width={20} height={20} />
</Button>
<Button
type='button'
variant='ghost'
color='none'
onClick={() => {
onDeleteRef.current(item.id as number);
}}
className='p-0 text-error hover:text-base-content'
>
<Icon icon='heroicons:trash' width={20} height={20} />
</Button>
</div>
)}
</div> </div>
), </th>
cell: ({ row }: { row: TanStack.Row<SalesOrderProductFormValues> }) => ( </tr>
<div> <>
<CheckboxInput <tr>
name='row' <td className='text-sm px-4 py-3'>No. Polisi</td>
checked={row.getIsSelected()} <td className='text-sm px-4 py-3'>{item.vehicle_number}</td>
disabled={!row.getCanSelect()} </tr>
indeterminate={row.getIsSomeSelected()} <tr>
onChange={row.getToggleSelectedHandler()} <td className='text-sm px-4 py-3'>Gudang</td>
value={`${row.original.product_warehouse_id}${row.original.kandang_id}`} <td className='text-sm px-4 py-3'>{item.kandang?.label}</td>
/> </tr>
</div> <tr>
), <td className='text-sm px-4 py-3'>Kategori</td>
}, <td className='text-sm px-4 py-3'>{item.marketing_type?.label}</td>
{ </tr>
accessorFn: (row: SalesOrderProductFormValues) => <tr>
formatVechicleNumber(row.vehicle_number as string), <td className='text-sm px-4 py-3'>Produk</td>
header: 'No. Polisi', <td className='text-sm px-4 py-3'>{item.product_warehouse?.label}</td>
}, </tr>
{ {item.marketing_type?.value.toLowerCase() === 'telur' && (
accessorFn: (row: SalesOrderProductFormValues) => row.kandang?.label, <tr>
header: 'Kandang', <td className='text-sm px-4 py-3'>Tipe Konversi</td>
}, <td className='text-sm px-4 py-3'>{item.convertion_unit?.label}</td>
{ </tr>
accessorFn: (row: SalesOrderProductFormValues) => )}
row.product_warehouse?.label, {item.marketing_type?.value.toLowerCase() === 'ayam_pullet' && (
header: 'Produk', <tr>
}, <td className='text-sm px-4 py-3'>Tipe Konversi</td>
{ <td className='text-sm px-4 py-3'>Week {item.week}</td>
accessorFn: (row: SalesOrderProductFormValues) => </tr>
formatCurrency(parseFloat(row.unit_price as string)), )}
header: 'Harga Satuan (Rp)', {item.convertion_unit?.value.toLowerCase() === 'peti' && (
}, <tr>
{ <td className='text-sm px-4 py-3'>Total Peti</td>
accessorFn: (row: SalesOrderProductFormValues) => <td className='text-sm px-4 py-3'>
formatNumber(parseFloat(row.total_weight as string), undefined, 0, 5), {item.total_peti} {item.convertion_unit?.label}
header: 'Total Bobot (Kg)', </td>
}, </tr>
{ )}
accessorFn: (row: SalesOrderProductFormValues) => {item.marketing_type?.value.toLowerCase() !== 'trading' && (
formatNumber(parseFloat(row.qty as string)), <>
header: 'Kuantitas', <tr>
cell: ({ row }: { row: TanStack.Row<SalesOrderProductFormValues> }) => <td className='text-sm px-4 py-3'>Total Bobot</td>
formatNumber( <td className='text-sm px-4 py-3'>
parseFloat(row.original.qty as string), {item.total_weight
undefined, ? formatNumber(parseFloat(item.total_weight as string)) +
0, ' Kg'
5 : '0 Kg'}
) + </td>
' ' + </tr>
(row.original.uom ?? ''), <tr>
}, <td className='text-sm px-4 py-3'>Avg Bobot</td>
{ <td className='text-sm px-4 py-3'>
accessorFn: (row: SalesOrderProductFormValues) => {item.avg_weight
formatNumber(parseFloat(row.avg_weight as string), undefined, 0, 5), ? formatNumber(parseFloat(item.avg_weight as string)) + ' Kg'
header: 'Avg. Bobot (Kg)', : '0 Kg'}
}, </td>
{ </tr>
accessorFn: (row: SalesOrderProductFormValues) => </>
formatCurrency(parseFloat(row.total_price as string)), )}
header: 'Total Penjualan (Rp)', <tr>
}, <td className='text-sm px-4 py-3'>
{ {item.marketing_type?.value === 'telur'
header: 'Aksi', ? 'Total Butir Telur'
cell: ( : 'Qty'}
props: TanStack.CellContext<SalesOrderProductFormValues, unknown> </td>
) => ( <td className='text-sm px-4 py-3'>
<div className='flex flex-row gap-1 items-center justify-end h-full mt-2'> {`${formatNumber(parseFloat(item.qty as string))} ${item.uom || ''}`}
<Button </td>
color='warning' </tr>
className='p-1' <tr>
onClick={() => onEditRef.current(props.row.original.id as number)} <td className='text-sm px-4 py-3'>Harga Satuan</td>
type='button' <td className='text-sm px-4 py-3'>
> {formatCurrency(parseFloat(item.unit_price as string))}
<Icon icon='mdi:pencil' width={16} height={16} /> Edit </td>
</Button> </tr>
<Button <tr>
color='error' <td className='text-sm px-4 py-3'>Total Penjualan</td>
className='p-1' <td className='text-sm px-4 py-3'>
onClick={() => {formatCurrency(parseFloat(item.total_price as string))}
onDeleteRef.current(props.row.original.id as number) </td>
} </tr>
type='button' </>
> </>
<Icon icon='mdi:trash' width={16} height={16} /> Hapus
</Button>
</div>
),
},
],
[]
); );
return ( return (
<> <>
<div className='size-full flex flex-col relative overflow-x-hidden gap-3'> <div className='size-full flex flex-col relative overflow-x-hidden gap-3'>
{data.map((item) => ( {data.map((item) => (
<Card <div key={`table-${item.id}`}>
key={`table-${item.id}`} {formType === 'success' ? (
title={item.product_warehouse?.label || 'Produk'} <div className='rounded-lg border border-tools-table-outline border-base-content/5'>
collapsible={true} <table
defaultCollapsed={false} style={{
variant='bordered' borderRadius: '0.5rem',
className={{ }}
wrapper: 'w-full rounded-lg', className='border-none w-full'
body: 'p-0', >
title: 'px-2 py-1.5 font-normal text-sm', <tbody className='w-full'>{renderTableContent(item)}</tbody>
collapsible: 'rounded-lg', </table>
}} </div>
> ) : (
<table <Card
style={{ title={item.product_warehouse?.label || 'Produk'}
borderRadius: '0.5rem', collapsible={true}
}} defaultCollapsed={false}
className='border-none w-full' variant='bordered'
> className={{
<tbody className='w-full'> wrapper: 'w-full rounded-lg',
<tr className='border-b border-t border-tools-table-outline border-base-content/5'> body: 'p-0',
<th className='w-1/3 text-start not-first:font-medium text-base-content/50 text-sm px-4 py-3'> title: 'px-2 py-1.5 font-normal text-sm',
Label collapsible: 'rounded-lg',
</th> }}
<th className='text-start font-medium text-base-content/50 text-sm px-4 py-3'> >
<div className='flex w-full flex-row gap-1 items-center justify-between h-full'> <table
<div>Value</div> style={{
{formType !== 'success' && ( borderRadius: '0.5rem',
<div className='flex flex-row gap-1.5 items-center'> }}
<Button className='border-none w-full'
type='button' >
variant='ghost' <tbody className='w-full'>{renderTableContent(item)}</tbody>
color='none' </table>
onClick={() => { </Card>
onEditRef.current(item.id as number); )}
}} </div>
className='p-0 hover:text-base-content'
>
<Icon
icon='heroicons:pencil'
width={20}
height={20}
/>
</Button>
<Button
type='button'
variant='ghost'
color='none'
onClick={() => {
onDeleteRef.current(item.id as number);
}}
className='p-0 text-error hover:text-base-content'
>
<Icon
icon='heroicons:trash'
width={20}
height={20}
/>
</Button>
</div>
)}
</div>
</th>
</tr>
<>
<tr>
<td className='text-sm px-4 py-3'>No. Polisi</td>
<td className='text-sm px-4 py-3'>{item.vehicle_number}</td>
</tr>
<tr>
<td className='text-sm px-4 py-3'>Gudang</td>
<td className='text-sm px-4 py-3'>{item.kandang?.label}</td>
</tr>
<tr>
<td className='text-sm px-4 py-3'>Kategori</td>
<td className='text-sm px-4 py-3'>
{item.marketing_type?.label}
</td>
</tr>
<tr>
<td className='text-sm px-4 py-3'>Produk</td>
<td className='text-sm px-4 py-3'>
{item.product_warehouse?.label}
</td>
</tr>
{item.marketing_type?.value.toLowerCase() === 'telur' && (
<tr>
<td className='text-sm px-4 py-3'>Tipe Konversi</td>
<td className='text-sm px-4 py-3'>
{item.convertion_unit?.label}
</td>
</tr>
)}
{item.marketing_type?.value.toLowerCase() ===
'ayam_pullet' && (
<tr>
<td className='text-sm px-4 py-3'>Tipe Konversi</td>
<td className='text-sm px-4 py-3'>Week {item.week}</td>
</tr>
)}
{item.convertion_unit?.value.toLowerCase() === 'peti' && (
<tr>
<td className='text-sm px-4 py-3'>Total Peti</td>
<td className='text-sm px-4 py-3'>
{item.total_peti} {item.convertion_unit?.label}
</td>
</tr>
)}
{item.marketing_type?.value.toLowerCase() !== 'trading' && (
<>
<tr>
<td className='text-sm px-4 py-3'>Total Bobot</td>
<td className='text-sm px-4 py-3'>
{item.total_weight
? formatNumber(
parseFloat(item.total_weight as string)
) + ' Kg'
: '0 Kg'}
</td>
</tr>
<tr>
<td className='text-sm px-4 py-3'>Avg Bobot</td>
<td className='text-sm px-4 py-3'>
{item.avg_weight
? formatNumber(
parseFloat(item.avg_weight as string)
) + ' Kg'
: '0 Kg'}
</td>
</tr>
</>
)}
<tr>
<td className='text-sm px-4 py-3'>
{item.marketing_type?.value === 'telur'
? 'Total Butir Telur'
: 'Qty'}
</td>
<td className='text-sm px-4 py-3'>
{`${formatNumber(parseFloat(item.qty as string))} ${item.uom || ''}`}
</td>
</tr>
<tr>
<td className='text-sm px-4 py-3'>Harga Satuan</td>
<td className='text-sm px-4 py-3'>
{formatCurrency(parseFloat(item.unit_price as string))}
</td>
</tr>
<tr>
<td className='text-sm px-4 py-3'>Total Penjualan</td>
<td className='text-sm px-4 py-3'>
{formatCurrency(parseFloat(item.total_price as string))}
</td>
</tr>
</>
</tbody>
</table>
</Card>
))} ))}
{formType != 'add_deliver' && {formType != 'add_deliver' &&
formType != 'edit_deliver' && formType != 'edit_deliver' &&
+1 -5
View File
@@ -512,13 +512,9 @@ export const FILTER_TYPE_OPTIONS = [
]; ];
export const MARKETING_TYPE_OPTIONS = [ export const MARKETING_TYPE_OPTIONS = [
{
label: 'Ayam Pullet',
value: 'AYAM_PULLET',
},
{ {
label: 'Ayam', label: 'Ayam',
value: 'AYAM', value: 'AYAM,AYAM_PULLET',
}, },
{ {
label: 'Trading', label: 'Trading',
+1
View File
@@ -11,6 +11,7 @@ export type BaseProductWarehouse = {
quantity: number; quantity: number;
product: Product; product: Product;
warehouse: Warehouse; warehouse: Warehouse;
week?: number | null;
}; };
export type ProductWarehouse = BaseMetadata & BaseProductWarehouse; export type ProductWarehouse = BaseMetadata & BaseProductWarehouse;