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

[FIX/FE] Adjustment Penjualan UI and Data Fetch

See merge request mbugroup/lti-web-client!317
This commit is contained in:
Rivaldi A N S
2026-02-09 03:04:46 +00:00
22 changed files with 287 additions and 488 deletions
@@ -276,6 +276,13 @@ const ExpensesTable = () => {
); );
}, },
}, },
{
accessorKey: 'reference_number',
header: 'Nomor Referensi',
cell: (props) => {
return props.row.original.reference_number ?? '-';
},
},
{ {
accessorKey: 'transaction_date', accessorKey: 'transaction_date',
header: 'Tanggal Pengajuan', header: 'Tanggal Pengajuan',
@@ -59,6 +59,10 @@ const DeliveryOrderFormModal = ({
const modalAction = searchParams.get('action'); const modalAction = searchParams.get('action');
const marketingId = searchParams.get('id'); const marketingId = searchParams.get('id');
const [currentModalAction, setCurrentModalAction] = useState<string | null>(
modalAction
);
const isModalActionForForm = const isModalActionForForm =
modalAction === 'add_delivery' || modalAction === 'add_delivery' ||
modalAction === 'edit_delivery' || modalAction === 'edit_delivery' ||
@@ -420,17 +424,7 @@ const DeliveryOrderFormModal = ({
const deliveryRejected = useMemo(() => { const deliveryRejected = useMemo(() => {
return ( return (
isResponseSuccess(marketing) && isResponseSuccess(marketing) &&
((marketing.data.latest_approval.step_number === 3 && marketing.data.latest_approval.action === 'REJECTED'
marketing.data.latest_approval.action === 'REJECTED') ||
(marketing.data.latest_approval.step_number === 2 &&
marketing.data.latest_approval.action === 'REJECTED'))
);
}, [marketing]);
const isPending = useMemo(() => {
return (
isResponseSuccess(marketing) &&
marketing.data.latest_approval.step_number === 1
); );
}, [marketing]); }, [marketing]);
@@ -441,6 +435,7 @@ const DeliveryOrderFormModal = ({
modalAction === 'edit_delivery' || modalAction === 'edit_delivery' ||
modalAction === 'detail' modalAction === 'detail'
) { ) {
setCurrentModalAction(modalAction);
formModal.openModal(); formModal.openModal();
} }
}, [modalAction]); }, [modalAction]);
@@ -562,9 +557,11 @@ const DeliveryOrderFormModal = ({
</th> </th>
</tr> </tr>
<tr> <tr>
<td className='text-sm px-4 py-3'>No. Sales Order</td> <td className='text-sm px-4 py-3'>No. Order</td>
<td className='text-sm px-4 py-3'> <td className='text-sm px-4 py-3'>
{marketing.data.so_number} {marketing.data.do_number
? marketing.data.do_number
: marketing.data.so_number}
</td> </td>
</tr> </tr>
<tr> <tr>
@@ -667,13 +664,7 @@ const DeliveryOrderFormModal = ({
<div className='px-4'> <div className='px-4'>
<MemoizedDeliveryOrderProductTable <MemoizedDeliveryOrderProductTable
marketing={marketing.data} marketing={marketing.data}
formType={ formType={deliveryRejected ? 'rejected' : modalAction}
deliveryRejected
? 'rejected'
: isPending
? 'pending'
: modalAction
}
data={deliveryOrderValues} data={deliveryOrderValues}
onEdit={handleEditDO} onEdit={handleEditDO}
onDelete={handleDeleteDO} onDelete={handleDeleteDO}
@@ -715,31 +706,32 @@ const DeliveryOrderFormModal = ({
/> />
</div> </div>
)} )}
{step === 1 && ( {step === 1 &&
<div className='w-full px-4 py-3 grid grid-cols-2 items-center justify-between gap-3 border-t border-base-content/10'> marketing?.data?.latest_approval?.step_number !== 3 && (
<Button <div className='w-full px-4 py-3 grid grid-cols-2 items-center justify-between gap-3 border-t border-base-content/10'>
type='button' <Button
variant='outline' type='button'
color='none' variant='outline'
onClick={() => rejectModal.openModal()} color='none'
disabled={deliveryRejected || isPending} onClick={() => rejectModal.openModal()}
className='p-3 border-base-content/10 shadow-button-soft rounded-lg text-sm text-base-content/50 font-semibold' disabled={deliveryRejected}
> className='p-3 border-base-content/10 shadow-button-soft rounded-lg text-sm text-base-content/50 font-semibold'
Reject >
</Button> Reject
<Button </Button>
type='button' <Button
color='primary' type='button'
onClick={() => { color='primary'
formRef.current?.requestSubmit(); onClick={() => {
}} formRef.current?.requestSubmit();
className='p-3 shadow-button-soft text-base-100 rounded-lg text-sm font-semibold' }}
disabled={deliveryRejected || isPending} className='p-3 shadow-button-soft text-base-100 rounded-lg text-sm font-semibold'
> disabled={deliveryRejected}
Approve >
</Button> Approve
</div> </Button>
)} </div>
)}
</div> </div>
</> </>
)} )}
@@ -749,8 +741,8 @@ const DeliveryOrderFormModal = ({
ref={successModal.ref} ref={successModal.ref}
iconPosition='left' iconPosition='left'
type='success' type='success'
text={`${modalAction === 'add' ? 'Data Berhasil Disimpan' : 'Data Berhasil Diubah'}`} text={`${currentModalAction === 'add' ? 'Data Berhasil Disimpan' : 'Data Berhasil Diubah'}`}
subtitleText={`${modalAction === 'add' ? 'Data delivery order telah berhasil disimpan.' : 'Data delivery order telah berhasil diubah.'}`} subtitleText={`${currentModalAction === 'add' ? 'Data delivery order telah berhasil disimpan.' : 'Data delivery order telah berhasil diubah.'}`}
primaryButton={{ primaryButton={{
text: 'Oke', text: 'Oke',
color: 'primary', color: 'primary',
@@ -760,14 +752,18 @@ const DeliveryOrderFormModal = ({
}, },
}} }}
> >
<MemoizedDeliveryOrderProductTable <div className='max-h-[50vh] overflow-y-auto'>
marketing={isResponseSuccess(marketing) ? marketing.data : undefined} <MemoizedDeliveryOrderProductTable
formType={'success'} marketing={
data={deliveryOrderValues} isResponseSuccess(marketing) ? marketing.data : undefined
onDelete={handleDeleteDO} }
onEdit={handleEditDO} formType={'success'}
onAddProductClick={handleAddDOClick} data={deliveryOrderValues}
/> onDelete={handleDeleteDO}
onEdit={handleEditDO}
onAddProductClick={handleAddDOClick}
/>
</div>
</ConfirmationModal> </ConfirmationModal>
<ConfirmationModalWithNotes <ConfirmationModalWithNotes
@@ -1,6 +1,6 @@
'use client'; 'use client';
import { RefObject } from 'react'; import { RefObject, useMemo } from 'react';
import { useFormik } from 'formik'; import { useFormik } from 'formik';
import { Icon } from '@iconify/react'; import { Icon } from '@iconify/react';
import Modal from '@/components/Modal'; import Modal from '@/components/Modal';
@@ -9,10 +9,12 @@ import SelectInput, {
OptionType, OptionType,
useSelect, useSelect,
} from '@/components/input/SelectInput'; } from '@/components/input/SelectInput';
import { CustomerApi, ProductApi } from '@/services/api/master-data';
import { MARKETING_APPROVAL_LINE } from '@/config/approval-line'; import { MARKETING_APPROVAL_LINE } from '@/config/approval-line';
import { MarketingFilter } from '@/types/api/marketing/marketing'; import { MarketingFilter } from '@/types/api/marketing/marketing';
import SelectInputCheckbox from '@/components/input/SelectInputCheckbox'; import SelectInputCheckbox from '@/components/input/SelectInputCheckbox';
import { MarketingApi } from '@/services/api/marketing/marketing';
import { isResponseSuccess } from '@/lib/api-helper';
import { BaseMarketing, BaseSalesOrder } from '@/types/api/marketing/marketing';
interface MarketingFilterModal { interface MarketingFilterModal {
ref: RefObject<HTMLDialogElement | null>; ref: RefObject<HTMLDialogElement | null>;
@@ -31,25 +33,59 @@ const MarketingFilterModal = ({
// ===== OPTIONS ===== // ===== OPTIONS =====
const { const {
options: productsOptions, rawData: productsRawData,
isLoadingOptions: isLoadingProductsOptions, isLoadingOptions: isLoadingProductsOptions,
setInputValue: setProductsInputValue, setInputValue: setProductsInputValue,
loadMore: loadMoreProducts, loadMore: loadMoreProducts,
} = useSelect(ProductApi.basePath, 'id', 'name', '', { } = useSelect<BaseMarketing>(MarketingApi.basePath, 'id', 'so_number', '', {
limit: 'limit', limit: 'limit',
}); });
const productsOptions = useMemo(() => {
if (!productsRawData || !isResponseSuccess(productsRawData)) return [];
const productsMap = new Map<number, { value: number; label: string }>();
productsRawData.data.forEach((deliveryOrder: BaseMarketing) => {
deliveryOrder.sales_order?.forEach((so: BaseSalesOrder) => {
const product = so.product_warehouse?.product;
if (product?.id && product?.name) {
productsMap.set(product.id, {
value: product.id,
label: product.name,
});
}
});
});
return Array.from(productsMap.values());
}, [productsRawData]);
const { const {
options: customersOptions, options: customersOptions,
isLoadingOptions: isLoadingCustomersOptions, isLoadingOptions: isLoadingCustomersOptions,
setInputValue: setCustomersInputValue, setInputValue: setCustomersInputValue,
loadMore: loadMoreCustomers, loadMore: loadMoreCustomers,
} = useSelect(CustomerApi.basePath, 'id', 'name', '', { } = useSelect(MarketingApi.basePath, 'customer.id', 'customer.name', '', {
limit: 'limit', limit: 'limit',
}); });
const statusOptions = MARKETING_APPROVAL_LINE.map((item) => ({
value: item.step_name.split(' ').join('_').toUpperCase(), const uniqueCustomersOptions = useMemo(() => {
label: item.step_name, const seen = new Set();
})); return customersOptions.filter((customer) => {
if (seen.has(customer.value)) return false;
seen.add(customer.value);
return true;
});
}, [customersOptions]);
const statusOptions = [
...MARKETING_APPROVAL_LINE.map((item) => ({
value: item.step_name.split(' ').join('_').toUpperCase(),
label: item.step_name,
})),
{ value: 'DITOLAK', label: 'Ditolak' },
];
const formik = useFormik<{ const formik = useFormik<{
product_ids: OptionType[]; product_ids: OptionType[];
@@ -151,7 +187,7 @@ const MarketingFilterModal = ({
label='Customer' label='Customer'
isClearable isClearable
placeholder='Pilih customer' placeholder='Pilih customer'
options={customersOptions} options={uniqueCustomersOptions}
isLoading={isLoadingCustomersOptions} isLoading={isLoadingCustomersOptions}
value={formik.values.customer_id} value={formik.values.customer_id}
onChange={customerChangeHandler} onChange={customerChangeHandler}
@@ -109,7 +109,9 @@ const RowsOptionsMenu = ({
className='p-3 justify-start text-sm font-semibold w-full' className='p-3 justify-start text-sm font-semibold w-full'
> >
<Icon icon='heroicons:truck' width={20} height={20} /> <Icon icon='heroicons:truck' width={20} height={20} />
Deliver Item {props.row.original.latest_approval.step_number == 2
? 'Deliver Item'
: 'Edit Delivery'}
</Button> </Button>
</RequirePermission> </RequirePermission>
</> </>
@@ -379,8 +381,13 @@ const MarketingTable = () => {
}, },
}, },
{ {
accessorKey: 'so_number', accessorKey: 'so_do_number',
header: 'No. Order', header: 'No. Order',
cell: (props) => {
return props.row.original.do_number
? props.row.original.do_number
: props.row.original.so_number;
},
}, },
{ {
accessorKey: 'so_date', accessorKey: 'so_date',
@@ -408,7 +415,7 @@ const MarketingTable = () => {
: approval?.step_number == 2 : approval?.step_number == 2
? 'info' ? 'info'
: approval?.step_number == 3 : approval?.step_number == 3
? 'warning' ? 'success'
: 'neutral' : 'neutral'
: 'neutral' : 'neutral'
} }
@@ -63,6 +63,10 @@ const SalesOrderFormModal = ({
const modalAction = searchParams.get('action'); const modalAction = searchParams.get('action');
const marketingId = searchParams.get('id'); const marketingId = searchParams.get('id');
const [currentModalAction, setCurrentModalAction] = useState<string | null>(
modalAction
);
const isModalActionForForm = const isModalActionForForm =
modalAction === 'add' || modalAction === 'add' ||
modalAction === 'edit' || modalAction === 'edit' ||
@@ -208,7 +212,7 @@ const SalesOrderFormModal = ({
convertion_unit: normalizedConvertionUnit, convertion_unit: normalizedConvertionUnit,
weight_per_convertion: weight_per_convertion:
product.weight_per_convertion ?? undefined, product.weight_per_convertion ?? undefined,
week: product.week?.value ?? undefined, week: product.week ?? undefined,
} as CreateSalesOrderProductPayload; } as CreateSalesOrderProductPayload;
}), }),
} as CreateSalesOrderPayload) } as CreateSalesOrderPayload)
@@ -412,6 +416,7 @@ const SalesOrderFormModal = ({
// ================== EFFECT ================== // ================== EFFECT ==================
useEffect(() => { useEffect(() => {
if (modalAction === 'add' || modalAction === 'edit') { if (modalAction === 'add' || modalAction === 'edit') {
setCurrentModalAction(modalAction);
formModal.openModal(); formModal.openModal();
} }
}, [modalAction]); }, [modalAction]);
@@ -724,8 +729,8 @@ const SalesOrderFormModal = ({
ref={successModal.ref} ref={successModal.ref}
iconPosition='left' iconPosition='left'
type='success' type='success'
text={`${modalAction === 'add' ? 'Data Berhasil Ditambahkan' : 'Data Berhasil Diubah'}`} text={`${currentModalAction === 'add' ? 'Data Berhasil Ditambahkan' : 'Data Berhasil Diubah'}`}
subtitleText={`${modalAction === 'add' ? 'Data sales order telah berhasil disimpan.' : 'Data sales order telah berhasil diubah.'}`} subtitleText={`${currentModalAction === 'add' ? 'Data sales order telah berhasil disimpan.' : 'Data sales order telah berhasil diubah.'}`}
primaryButton={{ primaryButton={{
text: 'Oke', text: 'Oke',
color: 'primary', color: 'primary',
@@ -735,13 +740,15 @@ const SalesOrderFormModal = ({
}, },
}} }}
> >
<MemoizedSalesOrderProductTable <div className='max-h-[50vh] overflow-y-auto'>
formType={'success'} <MemoizedSalesOrderProductTable
data={memoSalesOrder} formType={'success'}
onDelete={handleDeleteSO} data={memoSalesOrder}
onEdit={handleEditSO} onDelete={handleDeleteSO}
onAddProductClick={handleAddSOClick} onEdit={handleEditSO}
/> onAddProductClick={handleAddSOClick}
/>
</div>
</ConfirmationModal> </ConfirmationModal>
<ConfirmationModal <ConfirmationModal
@@ -128,12 +128,7 @@ export const SalesProductToFieldValues = (
label: formatTitleCase(product.convertion_unit), label: formatTitleCase(product.convertion_unit),
} }
: null, : null,
week: product.week week: product.week ?? null,
? {
value: product.week,
label: `Week ${product.week}`,
}
: null,
total_peti: product.total_peti, total_peti: product.total_peti,
weight_per_convertion: product.weight_per_convertion, weight_per_convertion: product.weight_per_convertion,
uom: product.product_warehouse.product.uom.name, uom: product.product_warehouse.product.uom.name,
@@ -30,13 +30,7 @@ type DeliveryOrderProductSchemaType = {
/** Harga per butir telur untuk TELUR + QTY */ /** Harga per butir telur untuk TELUR + QTY */
price_per_qty?: number | null | undefined; price_per_qty?: number | null | undefined;
/** Week untuk ayam pullet */ /** Week untuk ayam pullet */
week?: week?: number | null | undefined;
| {
value?: number;
label?: string;
}
| null
| undefined;
}; };
export const DeliveryOrderProductSchema: Yup.ObjectSchema<DeliveryOrderProductSchemaType> = export const DeliveryOrderProductSchema: Yup.ObjectSchema<DeliveryOrderProductSchemaType> =
@@ -79,26 +73,18 @@ export const DeliveryOrderProductSchema: Yup.ObjectSchema<DeliveryOrderProductSc
sisa_berat: Yup.number().nullable().optional().notRequired(), sisa_berat: Yup.number().nullable().optional().notRequired(),
price_sisa_berat: Yup.number().nullable().optional().notRequired(), price_sisa_berat: Yup.number().nullable().optional().notRequired(),
price_per_qty: Yup.number().nullable().optional().notRequired(), price_per_qty: Yup.number().nullable().optional().notRequired(),
week: Yup.object({ week: Yup.number()
value: Yup.number(),
label: Yup.string(),
})
.nullable() .nullable()
.default(null) .optional()
.notRequired()
.when('marketing_type', { .when('marketing_type', {
is: (marketingType: { value: string } | null | undefined) => is: (marketingType: { value: string } | null | undefined) =>
marketingType?.value?.toLowerCase() === 'ayam_pullet', marketingType?.value?.toLowerCase() === 'ayam_pullet',
then: (schema) => then: (schema) =>
schema schema
.shape({ .min(1, 'Week wajib diisi untuk Ayam Pullet!')
value: Yup.number().required( .required('Week wajib diisi untuk Ayam Pullet!')
'Week wajib diisi untuk Ayam Pullet!' .typeError('Week harus berupa angka!'),
),
label: Yup.string().required(
'Week wajib diisi untuk Ayam Pullet!'
),
})
.required('Week wajib diisi untuk Ayam Pullet!'),
otherwise: (schema) => schema.optional().notRequired(), otherwise: (schema) => schema.optional().notRequired(),
}), }),
}); });
@@ -511,19 +511,20 @@ 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' && (
<SelectInputRadio <NumberInput
required required
label='Minggu' label='Minggu'
options={optionsWeek} name='week'
value={ value={formik.values.week ?? undefined}
formik.values.week?.value onChange={(e) => {
? (formik.values.week as { value: number; label: string }) formik.setFieldValue('week', Number(e.target.value));
: null setCurrentInput(e.target.name);
}
onChange={(val) => {
formik.setFieldValue('week', val);
}} }}
placeholder='Pilih Week' onBlur={() => handleBlurField('week')}
isError={formik.touched.week && Boolean(formik.errors.week)}
errorMessage={formik.errors.week as string}
placeholder='Masukan Minggu'
decimalScale={0}
/> />
)} )}
@@ -37,13 +37,7 @@ type SalesOrderProductSchemaType = {
/** Harga per butir telur untuk TELUR + QTY */ /** Harga per butir telur untuk TELUR + QTY */
price_per_qty?: number | null | undefined; price_per_qty?: number | null | undefined;
/** Week untuk ayam pullet */ /** Week untuk ayam pullet */
week?: week?: number | null | undefined;
| {
value?: number;
label?: string;
}
| null
| undefined;
}; };
export const SalesOrderProductSchema: Yup.ObjectSchema<SalesOrderProductSchemaType> = export const SalesOrderProductSchema: Yup.ObjectSchema<SalesOrderProductSchemaType> =
@@ -102,26 +96,18 @@ export const SalesOrderProductSchema: Yup.ObjectSchema<SalesOrderProductSchemaTy
sisa_berat: Yup.number().nullable().optional().notRequired(), sisa_berat: Yup.number().nullable().optional().notRequired(),
price_sisa_berat: Yup.number().nullable().optional().notRequired(), price_sisa_berat: Yup.number().nullable().optional().notRequired(),
price_per_qty: Yup.number().nullable().optional().notRequired(), price_per_qty: Yup.number().nullable().optional().notRequired(),
week: Yup.object({ week: Yup.number()
value: Yup.number(),
label: Yup.string(),
})
.nullable() .nullable()
.default(null) .optional()
.notRequired()
.when('marketing_type', { .when('marketing_type', {
is: (marketingType: { value: string } | null | undefined) => is: (marketingType: { value: string } | null | undefined) =>
marketingType?.value?.toLowerCase() === 'ayam_pullet', marketingType?.value?.toLowerCase() === 'ayam_pullet',
then: (schema) => then: (schema) =>
schema schema
.shape({ .min(1, 'Week wajib diisi untuk Ayam Pullet!')
value: Yup.number().required( .required('Week wajib diisi untuk Ayam Pullet!')
'Week wajib diisi untuk Ayam Pullet!' .typeError('Week harus berupa angka!'),
),
label: Yup.string().required(
'Week wajib diisi untuk Ayam Pullet!'
),
})
.required('Week wajib diisi untuk Ayam Pullet!'),
otherwise: (schema) => schema.optional().notRequired(), otherwise: (schema) => schema.optional().notRequired(),
}), }),
}); });
@@ -467,19 +467,20 @@ 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' && (
<SelectInputRadio <NumberInput
required required
label='Minggu' label='Minggu'
options={optionsWeek} name='week'
value={ value={formik.values.week ?? undefined}
formik.values.week?.value onChange={(e) => {
? (formik.values.week as { value: number; label: string }) formik.setFieldValue('week', Number(e.target.value));
: null setCurrentInput(e.target.name);
}
onChange={(val) => {
formik.setFieldValue('week', val);
}} }}
placeholder='Pilih Week' onBlur={() => handleBlurField('week')}
isError={formik.touched.week && Boolean(formik.errors.week)}
errorMessage={formik.errors.week as string}
placeholder='Masukan Minggu'
decimalScale={0}
/> />
)} )}
@@ -1,5 +1,6 @@
import { DeliveryOrderProductFormValues } from '@/components/pages/marketing/form/repeater/delivery-order/DeliverOrderProduct.schema'; import { DeliveryOrderProductFormValues } from '@/components/pages/marketing/form/repeater/delivery-order/DeliverOrderProduct.schema';
import Button from '@/components/Button'; import Button from '@/components/Button';
import Card from '@/components/Card';
import { Icon } from '@iconify/react'; import { Icon } from '@iconify/react';
import { useRef } from 'react'; import { useRef } from 'react';
import { formatCurrency, formatDate, formatNumber } from '@/lib/helper'; import { formatCurrency, formatDate, formatNumber } from '@/lib/helper';
@@ -39,6 +40,8 @@ const DeliveryOrderProductTable = ({
const onDeleteRef = useRef(onDelete); const onDeleteRef = useRef(onDelete);
onDeleteRef.current = onDelete; onDeleteRef.current = onDelete;
const approvalStepNumber = marketing?.latest_approval?.step_number;
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'>
@@ -47,9 +50,20 @@ const DeliveryOrderProductTable = ({
(doItem) => doItem.do_number === item.do_number (doItem) => doItem.do_number === item.do_number
); );
return ( return (
<div <Card
className='rounded-lg border border-tools-table-outline border-base-content/5'
key={`table-${item.id}`} key={`table-${item.id}`}
title={
item.marketing_product?.product_warehouse?.label || 'Produk'
}
collapsible={true}
defaultCollapsed={false}
variant='bordered'
className={{
wrapper: 'w-full rounded-lg',
body: 'p-0',
title: 'px-2 py-1.5 font-normal text-sm',
collapsible: 'rounded-lg',
}}
> >
<table <table
style={{ style={{
@@ -58,12 +72,12 @@ const DeliveryOrderProductTable = ({
className='border-none w-full' className='border-none w-full'
> >
<tbody className='w-full'> <tbody className='w-full'>
<tr className='border-b border-tools-table-outline border-base-content/5'> <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'> <th className='w-1/3 text-start not-first:font-medium text-base-content/50 text-sm px-4 py-3'>
Label Label
</th> </th>
<th className='text-start font-medium text-base-content/50 text-sm px-4 py-3'> <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 mt-2'> <div className='flex w-full flex-row gap-1 items-center justify-between h-full'>
<div>Value</div> <div>Value</div>
{(formType === 'add_delivery' || {(formType === 'add_delivery' ||
formType === 'edit_delivery' || formType === 'edit_delivery' ||
@@ -105,16 +119,20 @@ const DeliveryOrderProductTable = ({
</th> </th>
</tr> </tr>
<> <>
<tr> {approvalStepNumber !== 1 && (
<td className='text-sm px-4 py-3'>Tanggal Pengiriman</td> <tr>
<td className='text-sm px-4 py-3'> <td className='text-sm px-4 py-3'>
{item.delivery_date ? ( Tanggal Pengiriman
formatDate(item.delivery_date, 'DD MMM YYYY') </td>
) : ( <td className='text-sm px-4 py-3'>
<span className='text-error'>Belum diisi</span> {item.delivery_date ? (
)} formatDate(item.delivery_date, 'DD MMM YYYY')
</td> ) : (
</tr> <span className='text-error'>Belum diisi</span>
)}
</td>
</tr>
)}
{item.do_number && ( {item.do_number && (
<tr> <tr>
<td className='text-sm px-4 py-3'>No. Pengiriman</td> <td className='text-sm px-4 py-3'>No. Pengiriman</td>
@@ -130,7 +148,9 @@ const DeliveryOrderProductTable = ({
<tr> <tr>
<td className='text-sm px-4 py-3'>Gudang</td> <td className='text-sm px-4 py-3'>Gudang</td>
<td className='text-sm px-4 py-3'> <td className='text-sm px-4 py-3'>
{item.marketing_product?.product_warehouse?.label} {doItem?.warehouse?.name ||
item.marketing_product?.product_warehouse_data
?.warehouse?.name}
</td> </td>
</tr> </tr>
<tr> <tr>
@@ -191,7 +211,7 @@ const DeliveryOrderProductTable = ({
</> </>
</tbody> </tbody>
</table> </table>
</div> </Card>
); );
})} })}
</div> </div>
@@ -1,6 +1,7 @@
'use client'; 'use client';
import Button from '@/components/Button'; import Button from '@/components/Button';
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, formatCurrency,
@@ -146,9 +147,18 @@ const SalesOrderProductTable = ({
<> <>
<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) => (
<div <Card
className='rounded-lg border border-tools-table-outline border-base-content/5'
key={`table-${item.id}`} key={`table-${item.id}`}
title={item.product_warehouse?.label || 'Produk'}
collapsible={true}
defaultCollapsed={false}
variant='bordered'
className={{
wrapper: 'w-full rounded-lg',
body: 'p-0',
title: 'px-2 py-1.5 font-normal text-sm',
collapsible: 'rounded-lg',
}}
> >
<table <table
style={{ style={{
@@ -157,12 +167,12 @@ const SalesOrderProductTable = ({
className='border-none w-full' className='border-none w-full'
> >
<tbody className='w-full'> <tbody className='w-full'>
<tr className='border-b border-tools-table-outline border-base-content/5'> <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'> <th className='w-1/3 text-start not-first:font-medium text-base-content/50 text-sm px-4 py-3'>
Label Label
</th> </th>
<th className='text-start font-medium text-base-content/50 text-sm px-4 py-3'> <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 mt-2'> <div className='flex w-full flex-row gap-1 items-center justify-between h-full'>
<div>Value</div> <div>Value</div>
{formType !== 'success' && ( {formType !== 'success' && (
<div className='flex flex-row gap-1.5 items-center'> <div className='flex flex-row gap-1.5 items-center'>
@@ -234,7 +244,7 @@ const SalesOrderProductTable = ({
'ayam_pullet' && ( 'ayam_pullet' && (
<tr> <tr>
<td className='text-sm px-4 py-3'>Tipe Konversi</td> <td className='text-sm px-4 py-3'>Tipe Konversi</td>
<td className='text-sm px-4 py-3'>{item.week?.label}</td> <td className='text-sm px-4 py-3'>Week {item.week}</td>
</tr> </tr>
)} )}
{item.convertion_unit?.value.toLowerCase() === 'peti' && ( {item.convertion_unit?.value.toLowerCase() === 'peti' && (
@@ -294,7 +304,7 @@ const SalesOrderProductTable = ({
</> </>
</tbody> </tbody>
</table> </table>
</div> </Card>
))} ))}
{formType != 'add_deliver' && {formType != 'add_deliver' &&
formType != 'edit_deliver' && formType != 'edit_deliver' &&
@@ -363,10 +363,6 @@ const ProjectFlockTable = ({ refresh }: { refresh?: () => void }) => {
accessorKey: 'location.name', accessorKey: 'location.name',
header: 'Lokasi', header: 'Lokasi',
}, },
{
accessorKey: 'fcr.name',
header: 'FCR',
},
{ {
accessorKey: 'category', accessorKey: 'category',
header: 'Kategori', header: 'Kategori',
@@ -16,11 +16,6 @@ type ProjectFlockFormSchemaType = {
label: string; label: string;
} | null; } | null;
category: string; category: string;
fcr: {
value: number | string;
label: string;
} | null;
fcr_id: number;
production_standard: { production_standard: {
value: number | string; value: number | string;
label: string; label: string;
@@ -96,15 +91,6 @@ export const ProjectFlockFormSchema: Yup.ObjectSchema<ProjectFlockFormSchemaType
.oneOf(['GROWING', 'LAYING'], 'Kategori wajib diisi!') .oneOf(['GROWING', 'LAYING'], 'Kategori wajib diisi!')
.required('Kategori wajib diisi!'), .required('Kategori wajib diisi!'),
// FCR
fcr: Yup.object({
value: Yup.number().required('ID FCR wajib diisi!'),
label: Yup.string().required('Nama FCR wajib diisi!'),
}).nullable(),
fcr_id: Yup.number()
.min(1, 'FCR wajib diisi!')
.required('FCR wajib diisi!'),
// Production Standard // Production Standard
production_standard: Yup.object({ production_standard: Yup.object({
value: Yup.number().required('ID Standar Produksi wajib diisi!'), value: Yup.number().required('ID Standar Produksi wajib diisi!'),
@@ -9,7 +9,6 @@ import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
import AlertErrorList from '@/components/helper/form/FormErrors'; import AlertErrorList from '@/components/helper/form/FormErrors';
import { import {
AreaApi, AreaApi,
FcrApi,
FlockApi, FlockApi,
KandangApi, KandangApi,
LocationApi, LocationApi,
@@ -284,13 +283,6 @@ const ProjectFlockForm = ({
: ((initialValues?.area?.id ?? '') as string), : ((initialValues?.area?.id ?? '') as string),
}); });
const {
options: optionsFcr,
isLoadingOptions: isLoadingFcrs,
setInputValue: setInputValueFcr,
loadMore: loadMoreFcr,
} = useSelect(FcrApi.basePath, 'id', 'name');
const { const {
options: optionsProductionStandards, options: optionsProductionStandards,
isLoadingOptions: isLoadingProductionStandards, isLoadingOptions: isLoadingProductionStandards,
@@ -505,12 +497,6 @@ const ProjectFlockForm = ({
label: initialValues.category, label: initialValues.category,
} }
: null, : null,
fcr: initialValues?.fcr
? {
value: initialValues.fcr?.id,
label: initialValues.fcr.name,
}
: null,
production_standard: initialValues?.production_standard production_standard: initialValues?.production_standard
? { ? {
value: initialValues.production_standard?.id, value: initialValues.production_standard?.id,
@@ -531,7 +517,6 @@ const ProjectFlockForm = ({
category: initialValues?.category as NonNullable< category: initialValues?.category as NonNullable<
'GROWING' | 'LAYING' | undefined 'GROWING' | 'LAYING' | undefined
>, >,
fcr_id: initialValues?.fcr?.id ?? 0,
production_standard_id: initialValues?.production_standard?.id ?? 0, production_standard_id: initialValues?.production_standard?.id ?? 0,
location_id: initialValues?.location?.id ?? 0, location_id: initialValues?.location?.id ?? 0,
kandang_ids: initialValues?.kandangs?.map( kandang_ids: initialValues?.kandangs?.map(
@@ -574,7 +559,6 @@ const ProjectFlockForm = ({
flock_name: values.flock_name as string, flock_name: values.flock_name as string,
area_id: values.area_id as number, area_id: values.area_id as number,
category: values.category as string, category: values.category as string,
fcr_id: values.fcr_id as number,
production_standard_id: values.production_standard_id as number, production_standard_id: values.production_standard_id as number,
location_id: values.location_id as number, location_id: values.location_id as number,
kandang_ids: values.kandang_ids as number[], kandang_ids: values.kandang_ids as number[],
@@ -996,25 +980,6 @@ const ProjectFlockForm = ({
isClearable isClearable
isDisabled={formType != 'add'} isDisabled={formType != 'add'}
/> />
<SelectInput
required
label='FCR'
placeholder='Pilih FCR'
value={formik.values.fcr as OptionType}
onChange={(val) => {
optionChangeHandler(val, 'fcr');
}}
onInputChange={setInputValueFcr}
onMenuScrollToBottom={loadMoreFcr}
options={optionsFcr}
isLoading={isLoadingFcrs}
isError={
formik.touched.fcr_id && Boolean(formik.errors.fcr_id)
}
errorMessage={formik.errors.fcr_id as string}
isClearable
isDisabled={formType != 'add'}
/>
<SelectInput <SelectInput
required required
label='Kategori' label='Kategori'
@@ -31,8 +31,7 @@ import {
RecordingApi, RecordingApi,
ProjectFlockApi, ProjectFlockApi,
} from '@/services/api/production'; } from '@/services/api/production';
import { FcrApi, ProductionStandardApi } from '@/services/api/master-data'; import { ProductionStandardApi } from '@/services/api/master-data';
import { FcrWithStandards, FcrStandard } from '@/types/api/master-data/fcr';
import { import {
ProductionStandard, ProductionStandard,
StandardDetails, StandardDetails,
@@ -87,24 +86,6 @@ interface RecordingFormProps {
initialValues?: Recording; initialValues?: Recording;
} }
const fcrStandardColumns: ColumnDef<FcrStandard>[] = [
{
accessorKey: 'weight',
header: 'Weight',
cell: (props) => formatNumber(props.getValue() as number),
},
{
accessorKey: 'fcr_number',
header: 'FCR Number',
cell: (props) => formatNumber(props.getValue() as number),
},
{
accessorKey: 'mortality',
header: 'Mortality',
cell: (props) => formatNumber(props.getValue() as number),
},
];
const productionStandardColumns: ColumnDef<StandardDetails>[] = [ const productionStandardColumns: ColumnDef<StandardDetails>[] = [
{ {
accessorKey: 'week', accessorKey: 'week',
@@ -253,36 +234,14 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
const approveModal = useModal(); const approveModal = useModal();
const rejectModal = useModal(); const rejectModal = useModal();
const deleteModal = useModal(); const deleteModal = useModal();
const fcrStandardModal = useModal();
const productionStandardModal = useModal(); const productionStandardModal = useModal();
const [fcrStandards, setFcrStandards] = useState<FcrStandard[]>([]);
const [productionStandards, setProductionStandards] = const [productionStandards, setProductionStandards] =
useState<ProductionStandard | null>(null); useState<ProductionStandard | null>(null);
const [isFcrModalOpen, setIsFcrModalOpen] = useState(false);
const [isProductionStandardModalOpen, setIsProductionStandardModalOpen] = const [isProductionStandardModalOpen, setIsProductionStandardModalOpen] =
useState(false); useState(false);
useEffect(() => {
const checkFcrModalOpen = () => {
const isOpen = fcrStandardModal.ref.current?.open || false;
setIsFcrModalOpen(isOpen);
};
checkFcrModalOpen();
const observer = new MutationObserver(checkFcrModalOpen);
if (fcrStandardModal.ref.current) {
observer.observe(fcrStandardModal.ref.current, {
attributes: true,
attributeFilter: ['open'],
});
}
return () => observer.disconnect();
}, [fcrStandardModal.ref]);
useEffect(() => { useEffect(() => {
const checkProductionStandardModalOpen = () => { const checkProductionStandardModalOpen = () => {
const isOpen = productionStandardModal.ref.current?.open || false; const isOpen = productionStandardModal.ref.current?.open || false;
@@ -460,24 +419,6 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
? projectFlockKandangLookupData.data ? projectFlockKandangLookupData.data
: undefined; : undefined;
const fcrId = useMemo(() => {
if (type === 'add') {
return projectFlockKandangLookup?.project_flock?.fcr?.id;
}
return initialValues?.project_flock?.fcr?.id;
}, [type, projectFlockKandangLookup, initialValues]);
const { data: fcr, isLoading: isLoadingFcrStandards } = useSWR(
isFcrModalOpen && fcrId ? `fcr-detail-${fcrId}` : null,
() => FcrApi.getSingle(fcrId!)
);
useEffect(() => {
if (fcr?.status === 'success') {
setFcrStandards((fcr.data as FcrWithStandards).fcr_standards || []);
}
}, [fcr]);
const productionStandardId = useMemo(() => { const productionStandardId = useMemo(() => {
if (type === 'add') { if (type === 'add') {
return projectFlockKandangLookup?.project_flock?.production_standard_id; return projectFlockKandangLookup?.project_flock?.production_standard_id;
@@ -606,7 +547,7 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
isLoadingOptions: isLoadingEggProducts, isLoadingOptions: isLoadingEggProducts,
loadMore: loadMoreEggProducts, loadMore: loadMoreEggProducts,
} = useSelect(ProductWarehouseApi.basePath, 'id', 'product.name', 'search', { } = useSelect(ProductWarehouseApi.basePath, 'id', 'product.name', 'search', {
search: 'telur', type: 'TELUR',
location_id: eggProductsLocationId, location_id: eggProductsLocationId,
kandang_id: eggProductsKandangId, kandang_id: eggProductsKandangId,
}); });
@@ -886,20 +827,10 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
if (isResponseSuccess(eggProductsData) && selectedKandang) { if (isResponseSuccess(eggProductsData) && selectedKandang) {
const data = eggProductsData.data as unknown as ProductWarehouse[]; const data = eggProductsData.data as unknown as ProductWarehouse[];
data.forEach((product) => { data.forEach((product) => {
const productName = product.product.name; options.push({
value: product.id,
if ( label: product.product.name,
productName.toLowerCase().includes('telur') || });
productName.toLowerCase().includes('egg') ||
productName.toLowerCase().includes('pecah') ||
productName.toLowerCase().includes('konsumsi') ||
productName.toLowerCase().includes('baik')
) {
options.push({
value: product.id,
label: product.product.name,
});
}
}); });
} }
@@ -1952,24 +1883,6 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
: '-'} : '-'}
</p> </p>
</div> </div>
<div>
<span className='text-sm text-gray-600'>Standard FCR</span>
<div className='mt-1'>
<Badge
variant='soft'
color='primary'
className={{
badge:
'cursor-pointer hover:opacity-80 transition-opacity whitespace-nowrap',
}}
onClick={() => fcrStandardModal.openModal()}
>
{projectFlockKandangLookup?.project_flock?.fcr?.name ||
initialValues?.project_flock?.fcr?.name ||
'-'}
</Badge>
</div>
</div>
<div> <div>
<span className='text-sm text-gray-600'> <span className='text-sm text-gray-600'>
Standard Produksi Standard Produksi
@@ -2160,22 +2073,6 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
</div> </div>
</div> </div>
)} )}
<div>
<span className='text-sm text-gray-600'>Standard FCR</span>
<div className='mt-1'>
<Badge
variant='soft'
color='primary'
className={{
badge:
'cursor-pointer hover:opacity-80 transition-opacity whitespace-nowrap',
}}
onClick={() => fcrStandardModal.openModal()}
>
{initialValues.project_flock?.fcr?.name || '-'}
</Badge>
</div>
</div>
<div> <div>
<span className='text-sm text-gray-600'> <span className='text-sm text-gray-600'>
Standard Produksi Standard Produksi
@@ -2227,21 +2124,6 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
<tr>
<td className='py-3 font-medium'>FCR (g)</td>
<td className='text-center py-3'>
<span className='font-semibold'>
{initialValues.fcr_value != null
? `${formatNumber(initialValues.fcr_value)} g`
: '-'}
</span>
</td>
<td className='text-center py-3 text-gray-600'>
{initialValues.project_flock?.fcr?.fcr_std != null
? `${formatNumber(initialValues.project_flock?.fcr?.fcr_std)} g`
: '-'}
</td>
</tr>
<tr> <tr>
<td className='py-3 font-medium'>Feed Intake (g)</td> <td className='py-3 font-medium'>Feed Intake (g)</td>
<td className='text-center py-3'> <td className='text-center py-3'>
@@ -3283,62 +3165,6 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
</> </>
)} )}
{/* FCR Standard Modal */}
<Modal
ref={fcrStandardModal.ref}
closeOnBackdrop={true}
className={{
modal: 'p-0',
modalBox: 'p-0 rounded-2xl xl:max-w-4/12 max-w-sm',
}}
>
<div className='space-y-6'>
{/* Modal Header */}
<div className='flex items-center justify-between gap-2 py-3 border-b border-gray-300 px-4'>
<div className='flex items-center gap-2 text-primary'>
<Icon icon='mdi:chart-line' width={20} height={20} />
<h3 className='font-semibold'>Detail Standard FCR</h3>
</div>
<Button
variant='link'
onClick={fcrStandardModal.closeModal}
className='text-gray-500 hover:text-gray-700 transition-colors cursor-pointer'
>
<Icon icon='heroicons:x-mark' width={20} height={20} />
</Button>
</div>
<div className='px-4'>
{isLoadingFcrStandards ? (
<div className='flex justify-center py-8'>
<span className='loading loading-spinner loading-lg'></span>
</div>
) : fcrStandards.length > 0 ? (
<Table<FcrStandard>
data={fcrStandards}
columns={fcrStandardColumns}
pageSize={100}
className={{
tableWrapperClassName: 'overflow-x-auto',
tableClassName: 'w-full table-auto text-sm',
headerRowClassName: 'border-b border-b-gray-200',
headerColumnClassName:
'px-4 py-3 text-xs font-semibold text-gray-500 whitespace-nowrap border-l border-l-gray-200 border-r border-r-gray-200 border-t border-t-gray-200 border-gray-200 border-b-0',
bodyRowClassName:
'hover:bg-gray-50 transition-colors border-b border-gray-200 first:border-t first:border-t-gray-200 border-l border-l-gray-200 border-r border-r-gray-200',
bodyColumnClassName:
'px-4 py-3 text-xs text-gray-900 whitespace-nowrap',
paginationClassName: 'hidden',
}}
/>
) : (
<p className='text-sm text-gray-500'>
Tidak ada data FCR standards
</p>
)}
</div>
</div>
</Modal>
{/* Production Standard Modal */} {/* Production Standard Modal */}
<Modal <Modal
closeOnBackdrop={true} closeOnBackdrop={true}
+52 -54
View File
@@ -1,7 +1,8 @@
'use client'; 'use client';
import { ChangeEventHandler, useCallback, useState } from 'react'; import { ChangeEventHandler, useCallback, useMemo, useState } from 'react';
import useSWR from 'swr'; import useSWR from 'swr';
import useSWRInfinite from 'swr/infinite';
import { CellContext, ColumnDef, SortingState } from '@tanstack/react-table'; import { CellContext, ColumnDef, SortingState } from '@tanstack/react-table';
import toast from 'react-hot-toast'; import toast from 'react-hot-toast';
@@ -17,16 +18,19 @@ import RowCollapseOptions from '@/components/table/RowCollapseOptions';
import RowOptionsMenuWrapper from '@/components/table/RowOptionsMenuWrapper'; import RowOptionsMenuWrapper from '@/components/table/RowOptionsMenuWrapper';
import RequirePermission from '@/components/helper/RequirePermission'; import RequirePermission from '@/components/helper/RequirePermission';
import StatusBadge from '@/components/helper/StatusBadge'; import StatusBadge from '@/components/helper/StatusBadge';
import PurchaseOrderInvoice from '@/components/pages/purchase/order/PurchaseOrderInvoice';
import { cn, formatDate } from '@/lib/helper'; import { cn, formatDate } from '@/lib/helper';
import { isResponseSuccess } from '@/lib/api-helper'; import { isResponseSuccess } from '@/lib/api-helper';
import { BaseApiResponse } from '@/types/api/api-general';
import { useTableFilter } from '@/services/hooks/useTableFilter'; import { useTableFilter } from '@/services/hooks/useTableFilter';
import { ROWS_OPTIONS } from '@/config/constant'; import { ROWS_OPTIONS } from '@/config/constant';
import { Purchase } from '@/types/api/purchase/purchase'; import { Purchase } from '@/types/api/purchase/purchase';
import { PurchaseApi } from '@/services/api/purchase'; import { PurchaseApi } from '@/services/api/purchase';
import { ExpenseApi } from '@/services/api/expense';
import { Expense } from '@/types/api/expense';
import { Color } from '@/types/theme'; import { Color } from '@/types/theme';
import Link from 'next/link';
// ===== STATUS BADGE UTILITIES ===== // ===== STATUS BADGE UTILITIES =====
const statusTextMap: Record<string, string> = { const statusTextMap: Record<string, string> = {
@@ -159,27 +163,33 @@ const PurchaseTable = () => {
PurchaseApi.getAllFetcher PurchaseApi.getAllFetcher
); );
const [isDownloadingInvoice, setIsDownloadingInvoice] = useState(false); const getKey = (
const [invoicePurchaseData, setInvoicePurchaseData] = pageIndex: number,
useState<Purchase | null>(null); previousPageData: BaseApiResponse<Expense>[] | null
) => {
const handleDownloadInvoice = async (purchaseId: number) => { if (pageIndex > 0 && !previousPageData) return null;
setIsDownloadingInvoice(true); return `${ExpenseApi.basePath}?page=${pageIndex + 1}&limit=100`;
try {
const response = await PurchaseApi.getSingle(purchaseId);
if (isResponseSuccess(response) && response.data) {
setInvoicePurchaseData(response.data);
setTimeout(() => {
setInvoicePurchaseData(null);
}, 1000);
}
} catch {
toast.error('Gagal mengambil data purchase order.');
} finally {
setIsDownloadingInvoice(false);
}
}; };
const { data: expensesPages } = useSWRInfinite(
getKey,
ExpenseApi.getAllFetcher
);
const expenseMap = useMemo(() => {
const map = new Map<string, number>();
if (!expensesPages) return map;
expensesPages.forEach((page) => {
if (isResponseSuccess(page)) {
page.data.forEach((expense: Expense) => {
map.set(expense.reference_number, expense.id);
});
}
});
return map;
}, [expensesPages]);
// ===== TABLE COLUMNS DEFINITION ===== // ===== TABLE COLUMNS DEFINITION =====
const purchaseColumns: ColumnDef<Purchase>[] = [ const purchaseColumns: ColumnDef<Purchase>[] = [
{ {
@@ -191,37 +201,34 @@ const PurchaseTable = () => {
}, },
{ {
accessorKey: 'po_expedition', accessorKey: 'po_expedition',
header: 'PO Ekspedisi', header: 'Ekspedisi PO',
cell: (props) => { cell: (props) => {
const purchase = props.row.original; const poExpedition = props.row.original.po_expedition;
if (!poExpedition || poExpedition.length === 0) return '-';
if (!purchase.po_number || purchase.po_number === 'Belum dibuat') {
return <span>-</span>;
}
return ( return (
<Button <ul className='list-disc pl-4'>
color='primary' {poExpedition.map((exp, index) => {
className='w-fit min-w-32 flex items-center justify-start gap-1 px-2 py-1 text-sm font-mono' const expenseId = expenseMap.get(exp.refrence);
onClick={() => handleDownloadInvoice(purchase.id)} if (expenseId) {
disabled={isDownloadingInvoice} return (
> <li key={index}>
<Icon <Link
icon={ href={`/expense/detail/?expenseId=${expenseId}`}
isDownloadingInvoice className='p-0 h-auto text-primary underline'
? 'eos-icons:loading' >
: 'material-symbols:file-open-outline' {exp.refrence}
</Link>
</li>
);
} }
width={16} return <li key={index}>{exp.refrence}</li>;
height={16} })}
/> </ul>
{purchase.po_number}
</Button>
); );
}, },
}, },
{ {
accessorKey: 'supplier.name', accessorKey: 'supplier',
header: 'Vendor', header: 'Vendor',
cell: (props) => props.row.original.supplier.name, cell: (props) => props.row.original.supplier.name,
}, },
@@ -505,15 +512,6 @@ const PurchaseTable = () => {
onClick: confirmationModalDeleteClickHandler, onClick: confirmationModalDeleteClickHandler,
}} }}
/> />
{invoicePurchaseData && (
<div className='hidden'>
<PurchaseOrderInvoice
data={invoicePurchaseData}
triggerDownloadOnMount={true}
/>
</div>
)}
</> </>
); );
}; };
@@ -1,6 +1,6 @@
'use client'; 'use client';
import { useMemo, useState, useEffect, useCallback, useRef } from 'react'; import { useMemo, useState } from 'react';
import { import {
Page, Page,
Text, Text,
@@ -235,16 +235,11 @@ const pdfStyles = StyleSheet.create({
interface PurchaseOrderInvoiceProps { interface PurchaseOrderInvoiceProps {
data?: Purchase; data?: Purchase;
className?: string; className?: string;
triggerDownloadOnMount?: boolean;
} }
const PurchaseOrderInvoice = ({ const PurchaseOrderInvoice = ({ data }: PurchaseOrderInvoiceProps) => {
data,
triggerDownloadOnMount,
}: PurchaseOrderInvoiceProps) => {
const [, setIsGeneratingPDF] = useState(false); const [, setIsGeneratingPDF] = useState(false);
const purchaseData = data; const purchaseData = data;
const hasDownloadedRef = useRef(false);
const grandTotal = useMemo(() => { const grandTotal = useMemo(() => {
return ( return (
@@ -255,7 +250,7 @@ const PurchaseOrderInvoice = ({
); );
}, [purchaseData?.items]); }, [purchaseData?.items]);
const handleDownloadPDF = useCallback(async () => { const handleDownloadPDF = async () => {
if (!purchaseData) { if (!purchaseData) {
toast.error('No purchase order data available'); toast.error('No purchase order data available');
return; return;
@@ -515,20 +510,7 @@ const PurchaseOrderInvoice = ({
} finally { } finally {
setIsGeneratingPDF(false); setIsGeneratingPDF(false);
} }
}, [purchaseData]); };
useEffect(() => {
if (triggerDownloadOnMount && purchaseData && !hasDownloadedRef.current) {
hasDownloadedRef.current = true;
handleDownloadPDF();
}
}, [triggerDownloadOnMount, purchaseData]);
useEffect(() => {
if (!triggerDownloadOnMount) {
hasDownloadedRef.current = false;
}
}, [triggerDownloadOnMount]);
if (!purchaseData) { if (!purchaseData) {
return ( return (
@@ -538,10 +520,6 @@ const PurchaseOrderInvoice = ({
); );
} }
if (triggerDownloadOnMount) {
return null;
}
return purchaseData?.po_number && return purchaseData?.po_number &&
purchaseData.po_number !== 'Belum dibuat' ? ( purchaseData.po_number !== 'Belum dibuat' ? (
<Button <Button
+2 -2
View File
@@ -15,7 +15,7 @@ export type MarketingFormValues = {
total_price?: string | number; total_price?: string | number;
marketing_type?: { value: string; label: string } | null; marketing_type?: { value: string; label: string } | null;
convertion_unit?: { value: string; label: string } | null; convertion_unit?: { value: string; label: string } | null;
week?: { value?: number; label?: string } | null; week?: number | null;
weight_per_convertion?: number | null; weight_per_convertion?: number | null;
price_per_convertion?: number | null; price_per_convertion?: number | null;
total_peti?: number | null; total_peti?: number | null;
@@ -100,7 +100,7 @@ export const calculateAyamPullet = (
): void => { ): void => {
const { values, setFieldValue } = ctx; const { values, setFieldValue } = ctx;
const unitPrice = Number(values.unit_price || 0); const unitPrice = Number(values.unit_price || 0);
const week = Number(values.week?.value || 0); const week = Number(values.week || 0);
const qty = Number(values.qty || 0); const qty = Number(values.qty || 0);
const avgWeight = Number(values.avg_weight || 0); const avgWeight = Number(values.avg_weight || 0);
const totalWeight = Number(values.total_weight || 0); const totalWeight = Number(values.total_weight || 0);
+2
View File
@@ -17,6 +17,8 @@ export type BaseMarketing = {
status?: string; status?: string;
so_number: string; so_number: string;
so_date: string; so_date: string;
do_number?: string;
do_date?: string;
customer: Customer; customer: Customer;
sales_person: CreatedUser; sales_person: CreatedUser;
notes: string; notes: string;
-4
View File
@@ -1,5 +1,4 @@
import { Area } from '@/types/api/master-data/area'; import { Area } from '@/types/api/master-data/area';
import { Fcr } from '@/types/api/master-data/fcr';
import { Flock } from '@/types/api/master-data/flock'; import { Flock } from '@/types/api/master-data/flock';
import { Kandang } from '@/types/api/master-data/kandang'; import { Kandang } from '@/types/api/master-data/kandang';
import { Location } from '@/types/api/master-data/location'; import { Location } from '@/types/api/master-data/location';
@@ -16,8 +15,6 @@ export type BaseProjectFlock = {
area: Area; area: Area;
area_id: number; area_id: number;
category: string; category: string;
fcr: Fcr;
fcr_id: number;
production_standard: ProductionStandard; production_standard: ProductionStandard;
production_standard_id: number; production_standard_id: number;
location: Location; location: Location;
@@ -51,7 +48,6 @@ export type CreateProjectFlockPayload = {
flock_name: string; flock_name: string;
area_id: number; area_id: number;
category: string; category: string;
fcr_id: number;
production_standard_id: number; production_standard_id: number;
location_id: number; location_id: number;
kandang_ids: number[]; kandang_ids: number[];
+1 -1
View File
@@ -76,7 +76,7 @@ export type BasePurchase = {
items?: PurchaseItem[]; items?: PurchaseItem[];
latest_approval?: BaseApproval; latest_approval?: BaseApproval;
requester_name?: string; requester_name?: string;
po_expedition?: string[]; po_expedition?: { id: number; refrence: string }[];
created_user?: CreatedUser; created_user?: CreatedUser;
products?: PurchaseItemProduct[]; products?: PurchaseItemProduct[];
}; };