mirror of
https://gitlab.com/mbugroup/lti-web-client.git
synced 2026-05-20 13:32:00 +00:00
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:
@@ -276,6 +276,13 @@ const ExpensesTable = () => {
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
accessorKey: 'reference_number',
|
||||
header: 'Nomor Referensi',
|
||||
cell: (props) => {
|
||||
return props.row.original.reference_number ?? '-';
|
||||
},
|
||||
},
|
||||
{
|
||||
accessorKey: 'transaction_date',
|
||||
header: 'Tanggal Pengajuan',
|
||||
|
||||
@@ -59,6 +59,10 @@ const DeliveryOrderFormModal = ({
|
||||
const modalAction = searchParams.get('action');
|
||||
const marketingId = searchParams.get('id');
|
||||
|
||||
const [currentModalAction, setCurrentModalAction] = useState<string | null>(
|
||||
modalAction
|
||||
);
|
||||
|
||||
const isModalActionForForm =
|
||||
modalAction === 'add_delivery' ||
|
||||
modalAction === 'edit_delivery' ||
|
||||
@@ -420,17 +424,7 @@ const DeliveryOrderFormModal = ({
|
||||
const deliveryRejected = useMemo(() => {
|
||||
return (
|
||||
isResponseSuccess(marketing) &&
|
||||
((marketing.data.latest_approval.step_number === 3 &&
|
||||
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.data.latest_approval.action === 'REJECTED'
|
||||
);
|
||||
}, [marketing]);
|
||||
|
||||
@@ -441,6 +435,7 @@ const DeliveryOrderFormModal = ({
|
||||
modalAction === 'edit_delivery' ||
|
||||
modalAction === 'detail'
|
||||
) {
|
||||
setCurrentModalAction(modalAction);
|
||||
formModal.openModal();
|
||||
}
|
||||
}, [modalAction]);
|
||||
@@ -562,9 +557,11 @@ const DeliveryOrderFormModal = ({
|
||||
</th>
|
||||
</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'>
|
||||
{marketing.data.so_number}
|
||||
{marketing.data.do_number
|
||||
? marketing.data.do_number
|
||||
: marketing.data.so_number}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
@@ -667,13 +664,7 @@ const DeliveryOrderFormModal = ({
|
||||
<div className='px-4'>
|
||||
<MemoizedDeliveryOrderProductTable
|
||||
marketing={marketing.data}
|
||||
formType={
|
||||
deliveryRejected
|
||||
? 'rejected'
|
||||
: isPending
|
||||
? 'pending'
|
||||
: modalAction
|
||||
}
|
||||
formType={deliveryRejected ? 'rejected' : modalAction}
|
||||
data={deliveryOrderValues}
|
||||
onEdit={handleEditDO}
|
||||
onDelete={handleDeleteDO}
|
||||
@@ -715,31 +706,32 @@ const DeliveryOrderFormModal = ({
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{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'>
|
||||
<Button
|
||||
type='button'
|
||||
variant='outline'
|
||||
color='none'
|
||||
onClick={() => rejectModal.openModal()}
|
||||
disabled={deliveryRejected || isPending}
|
||||
className='p-3 border-base-content/10 shadow-button-soft rounded-lg text-sm text-base-content/50 font-semibold'
|
||||
>
|
||||
Reject
|
||||
</Button>
|
||||
<Button
|
||||
type='button'
|
||||
color='primary'
|
||||
onClick={() => {
|
||||
formRef.current?.requestSubmit();
|
||||
}}
|
||||
className='p-3 shadow-button-soft text-base-100 rounded-lg text-sm font-semibold'
|
||||
disabled={deliveryRejected || isPending}
|
||||
>
|
||||
Approve
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
{step === 1 &&
|
||||
marketing?.data?.latest_approval?.step_number !== 3 && (
|
||||
<div className='w-full px-4 py-3 grid grid-cols-2 items-center justify-between gap-3 border-t border-base-content/10'>
|
||||
<Button
|
||||
type='button'
|
||||
variant='outline'
|
||||
color='none'
|
||||
onClick={() => rejectModal.openModal()}
|
||||
disabled={deliveryRejected}
|
||||
className='p-3 border-base-content/10 shadow-button-soft rounded-lg text-sm text-base-content/50 font-semibold'
|
||||
>
|
||||
Reject
|
||||
</Button>
|
||||
<Button
|
||||
type='button'
|
||||
color='primary'
|
||||
onClick={() => {
|
||||
formRef.current?.requestSubmit();
|
||||
}}
|
||||
className='p-3 shadow-button-soft text-base-100 rounded-lg text-sm font-semibold'
|
||||
disabled={deliveryRejected}
|
||||
>
|
||||
Approve
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
@@ -749,8 +741,8 @@ const DeliveryOrderFormModal = ({
|
||||
ref={successModal.ref}
|
||||
iconPosition='left'
|
||||
type='success'
|
||||
text={`${modalAction === 'add' ? 'Data Berhasil Disimpan' : 'Data Berhasil Diubah'}`}
|
||||
subtitleText={`${modalAction === 'add' ? 'Data delivery order telah berhasil disimpan.' : 'Data delivery order telah berhasil diubah.'}`}
|
||||
text={`${currentModalAction === 'add' ? 'Data Berhasil Disimpan' : 'Data Berhasil Diubah'}`}
|
||||
subtitleText={`${currentModalAction === 'add' ? 'Data delivery order telah berhasil disimpan.' : 'Data delivery order telah berhasil diubah.'}`}
|
||||
primaryButton={{
|
||||
text: 'Oke',
|
||||
color: 'primary',
|
||||
@@ -760,14 +752,18 @@ const DeliveryOrderFormModal = ({
|
||||
},
|
||||
}}
|
||||
>
|
||||
<MemoizedDeliveryOrderProductTable
|
||||
marketing={isResponseSuccess(marketing) ? marketing.data : undefined}
|
||||
formType={'success'}
|
||||
data={deliveryOrderValues}
|
||||
onDelete={handleDeleteDO}
|
||||
onEdit={handleEditDO}
|
||||
onAddProductClick={handleAddDOClick}
|
||||
/>
|
||||
<div className='max-h-[50vh] overflow-y-auto'>
|
||||
<MemoizedDeliveryOrderProductTable
|
||||
marketing={
|
||||
isResponseSuccess(marketing) ? marketing.data : undefined
|
||||
}
|
||||
formType={'success'}
|
||||
data={deliveryOrderValues}
|
||||
onDelete={handleDeleteDO}
|
||||
onEdit={handleEditDO}
|
||||
onAddProductClick={handleAddDOClick}
|
||||
/>
|
||||
</div>
|
||||
</ConfirmationModal>
|
||||
|
||||
<ConfirmationModalWithNotes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
'use client';
|
||||
|
||||
import { RefObject } from 'react';
|
||||
import { RefObject, useMemo } from 'react';
|
||||
import { useFormik } from 'formik';
|
||||
import { Icon } from '@iconify/react';
|
||||
import Modal from '@/components/Modal';
|
||||
@@ -9,10 +9,12 @@ import SelectInput, {
|
||||
OptionType,
|
||||
useSelect,
|
||||
} from '@/components/input/SelectInput';
|
||||
import { CustomerApi, ProductApi } from '@/services/api/master-data';
|
||||
import { MARKETING_APPROVAL_LINE } from '@/config/approval-line';
|
||||
import { MarketingFilter } from '@/types/api/marketing/marketing';
|
||||
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 {
|
||||
ref: RefObject<HTMLDialogElement | null>;
|
||||
@@ -31,25 +33,59 @@ const MarketingFilterModal = ({
|
||||
|
||||
// ===== OPTIONS =====
|
||||
const {
|
||||
options: productsOptions,
|
||||
rawData: productsRawData,
|
||||
isLoadingOptions: isLoadingProductsOptions,
|
||||
setInputValue: setProductsInputValue,
|
||||
loadMore: loadMoreProducts,
|
||||
} = useSelect(ProductApi.basePath, 'id', 'name', '', {
|
||||
} = useSelect<BaseMarketing>(MarketingApi.basePath, 'id', 'so_number', '', {
|
||||
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 {
|
||||
options: customersOptions,
|
||||
isLoadingOptions: isLoadingCustomersOptions,
|
||||
setInputValue: setCustomersInputValue,
|
||||
loadMore: loadMoreCustomers,
|
||||
} = useSelect(CustomerApi.basePath, 'id', 'name', '', {
|
||||
} = useSelect(MarketingApi.basePath, 'customer.id', 'customer.name', '', {
|
||||
limit: 'limit',
|
||||
});
|
||||
const statusOptions = MARKETING_APPROVAL_LINE.map((item) => ({
|
||||
value: item.step_name.split(' ').join('_').toUpperCase(),
|
||||
label: item.step_name,
|
||||
}));
|
||||
|
||||
const uniqueCustomersOptions = useMemo(() => {
|
||||
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<{
|
||||
product_ids: OptionType[];
|
||||
@@ -151,7 +187,7 @@ const MarketingFilterModal = ({
|
||||
label='Customer'
|
||||
isClearable
|
||||
placeholder='Pilih customer'
|
||||
options={customersOptions}
|
||||
options={uniqueCustomersOptions}
|
||||
isLoading={isLoadingCustomersOptions}
|
||||
value={formik.values.customer_id}
|
||||
onChange={customerChangeHandler}
|
||||
|
||||
@@ -109,7 +109,9 @@ const RowsOptionsMenu = ({
|
||||
className='p-3 justify-start text-sm font-semibold w-full'
|
||||
>
|
||||
<Icon icon='heroicons:truck' width={20} height={20} />
|
||||
Deliver Item
|
||||
{props.row.original.latest_approval.step_number == 2
|
||||
? 'Deliver Item'
|
||||
: 'Edit Delivery'}
|
||||
</Button>
|
||||
</RequirePermission>
|
||||
</>
|
||||
@@ -379,8 +381,13 @@ const MarketingTable = () => {
|
||||
},
|
||||
},
|
||||
{
|
||||
accessorKey: 'so_number',
|
||||
accessorKey: 'so_do_number',
|
||||
header: 'No. Order',
|
||||
cell: (props) => {
|
||||
return props.row.original.do_number
|
||||
? props.row.original.do_number
|
||||
: props.row.original.so_number;
|
||||
},
|
||||
},
|
||||
{
|
||||
accessorKey: 'so_date',
|
||||
@@ -408,7 +415,7 @@ const MarketingTable = () => {
|
||||
: approval?.step_number == 2
|
||||
? 'info'
|
||||
: approval?.step_number == 3
|
||||
? 'warning'
|
||||
? 'success'
|
||||
: 'neutral'
|
||||
: 'neutral'
|
||||
}
|
||||
|
||||
@@ -63,6 +63,10 @@ const SalesOrderFormModal = ({
|
||||
const modalAction = searchParams.get('action');
|
||||
const marketingId = searchParams.get('id');
|
||||
|
||||
const [currentModalAction, setCurrentModalAction] = useState<string | null>(
|
||||
modalAction
|
||||
);
|
||||
|
||||
const isModalActionForForm =
|
||||
modalAction === 'add' ||
|
||||
modalAction === 'edit' ||
|
||||
@@ -208,7 +212,7 @@ const SalesOrderFormModal = ({
|
||||
convertion_unit: normalizedConvertionUnit,
|
||||
weight_per_convertion:
|
||||
product.weight_per_convertion ?? undefined,
|
||||
week: product.week?.value ?? undefined,
|
||||
week: product.week ?? undefined,
|
||||
} as CreateSalesOrderProductPayload;
|
||||
}),
|
||||
} as CreateSalesOrderPayload)
|
||||
@@ -412,6 +416,7 @@ const SalesOrderFormModal = ({
|
||||
// ================== EFFECT ==================
|
||||
useEffect(() => {
|
||||
if (modalAction === 'add' || modalAction === 'edit') {
|
||||
setCurrentModalAction(modalAction);
|
||||
formModal.openModal();
|
||||
}
|
||||
}, [modalAction]);
|
||||
@@ -724,8 +729,8 @@ const SalesOrderFormModal = ({
|
||||
ref={successModal.ref}
|
||||
iconPosition='left'
|
||||
type='success'
|
||||
text={`${modalAction === 'add' ? 'Data Berhasil Ditambahkan' : 'Data Berhasil Diubah'}`}
|
||||
subtitleText={`${modalAction === 'add' ? 'Data sales order telah berhasil disimpan.' : 'Data sales order telah berhasil diubah.'}`}
|
||||
text={`${currentModalAction === 'add' ? 'Data Berhasil Ditambahkan' : 'Data Berhasil Diubah'}`}
|
||||
subtitleText={`${currentModalAction === 'add' ? 'Data sales order telah berhasil disimpan.' : 'Data sales order telah berhasil diubah.'}`}
|
||||
primaryButton={{
|
||||
text: 'Oke',
|
||||
color: 'primary',
|
||||
@@ -735,13 +740,15 @@ const SalesOrderFormModal = ({
|
||||
},
|
||||
}}
|
||||
>
|
||||
<MemoizedSalesOrderProductTable
|
||||
formType={'success'}
|
||||
data={memoSalesOrder}
|
||||
onDelete={handleDeleteSO}
|
||||
onEdit={handleEditSO}
|
||||
onAddProductClick={handleAddSOClick}
|
||||
/>
|
||||
<div className='max-h-[50vh] overflow-y-auto'>
|
||||
<MemoizedSalesOrderProductTable
|
||||
formType={'success'}
|
||||
data={memoSalesOrder}
|
||||
onDelete={handleDeleteSO}
|
||||
onEdit={handleEditSO}
|
||||
onAddProductClick={handleAddSOClick}
|
||||
/>
|
||||
</div>
|
||||
</ConfirmationModal>
|
||||
|
||||
<ConfirmationModal
|
||||
|
||||
@@ -128,12 +128,7 @@ export const SalesProductToFieldValues = (
|
||||
label: formatTitleCase(product.convertion_unit),
|
||||
}
|
||||
: null,
|
||||
week: product.week
|
||||
? {
|
||||
value: product.week,
|
||||
label: `Week ${product.week}`,
|
||||
}
|
||||
: null,
|
||||
week: product.week ?? null,
|
||||
total_peti: product.total_peti,
|
||||
weight_per_convertion: product.weight_per_convertion,
|
||||
uom: product.product_warehouse.product.uom.name,
|
||||
|
||||
+7
-21
@@ -30,13 +30,7 @@ type DeliveryOrderProductSchemaType = {
|
||||
/** Harga per butir telur untuk TELUR + QTY */
|
||||
price_per_qty?: number | null | undefined;
|
||||
/** Week untuk ayam pullet */
|
||||
week?:
|
||||
| {
|
||||
value?: number;
|
||||
label?: string;
|
||||
}
|
||||
| null
|
||||
| undefined;
|
||||
week?: number | null | undefined;
|
||||
};
|
||||
|
||||
export const DeliveryOrderProductSchema: Yup.ObjectSchema<DeliveryOrderProductSchemaType> =
|
||||
@@ -79,26 +73,18 @@ export const DeliveryOrderProductSchema: Yup.ObjectSchema<DeliveryOrderProductSc
|
||||
sisa_berat: Yup.number().nullable().optional().notRequired(),
|
||||
price_sisa_berat: Yup.number().nullable().optional().notRequired(),
|
||||
price_per_qty: Yup.number().nullable().optional().notRequired(),
|
||||
week: Yup.object({
|
||||
value: Yup.number(),
|
||||
label: Yup.string(),
|
||||
})
|
||||
week: Yup.number()
|
||||
.nullable()
|
||||
.default(null)
|
||||
.optional()
|
||||
.notRequired()
|
||||
.when('marketing_type', {
|
||||
is: (marketingType: { value: string } | null | undefined) =>
|
||||
marketingType?.value?.toLowerCase() === 'ayam_pullet',
|
||||
then: (schema) =>
|
||||
schema
|
||||
.shape({
|
||||
value: Yup.number().required(
|
||||
'Week wajib diisi untuk Ayam Pullet!'
|
||||
),
|
||||
label: Yup.string().required(
|
||||
'Week wajib diisi untuk Ayam Pullet!'
|
||||
),
|
||||
})
|
||||
.required('Week wajib diisi untuk Ayam Pullet!'),
|
||||
.min(1, 'Week wajib diisi untuk Ayam Pullet!')
|
||||
.required('Week wajib diisi untuk Ayam Pullet!')
|
||||
.typeError('Week harus berupa angka!'),
|
||||
otherwise: (schema) => schema.optional().notRequired(),
|
||||
}),
|
||||
});
|
||||
|
||||
+11
-10
@@ -511,19 +511,20 @@ const DeliveryOrderProductForm = ({
|
||||
{/* Konversi Satuan Week Pullet */}
|
||||
{formik.values.marketing_type?.value.toLowerCase() ===
|
||||
'ayam_pullet' && (
|
||||
<SelectInputRadio
|
||||
<NumberInput
|
||||
required
|
||||
label='Minggu'
|
||||
options={optionsWeek}
|
||||
value={
|
||||
formik.values.week?.value
|
||||
? (formik.values.week as { value: number; label: string })
|
||||
: null
|
||||
}
|
||||
onChange={(val) => {
|
||||
formik.setFieldValue('week', val);
|
||||
name='week'
|
||||
value={formik.values.week ?? undefined}
|
||||
onChange={(e) => {
|
||||
formik.setFieldValue('week', Number(e.target.value));
|
||||
setCurrentInput(e.target.name);
|
||||
}}
|
||||
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}
|
||||
/>
|
||||
)}
|
||||
|
||||
|
||||
+7
-21
@@ -37,13 +37,7 @@ type SalesOrderProductSchemaType = {
|
||||
/** Harga per butir telur untuk TELUR + QTY */
|
||||
price_per_qty?: number | null | undefined;
|
||||
/** Week untuk ayam pullet */
|
||||
week?:
|
||||
| {
|
||||
value?: number;
|
||||
label?: string;
|
||||
}
|
||||
| null
|
||||
| undefined;
|
||||
week?: number | null | undefined;
|
||||
};
|
||||
|
||||
export const SalesOrderProductSchema: Yup.ObjectSchema<SalesOrderProductSchemaType> =
|
||||
@@ -102,26 +96,18 @@ export const SalesOrderProductSchema: Yup.ObjectSchema<SalesOrderProductSchemaTy
|
||||
sisa_berat: Yup.number().nullable().optional().notRequired(),
|
||||
price_sisa_berat: Yup.number().nullable().optional().notRequired(),
|
||||
price_per_qty: Yup.number().nullable().optional().notRequired(),
|
||||
week: Yup.object({
|
||||
value: Yup.number(),
|
||||
label: Yup.string(),
|
||||
})
|
||||
week: Yup.number()
|
||||
.nullable()
|
||||
.default(null)
|
||||
.optional()
|
||||
.notRequired()
|
||||
.when('marketing_type', {
|
||||
is: (marketingType: { value: string } | null | undefined) =>
|
||||
marketingType?.value?.toLowerCase() === 'ayam_pullet',
|
||||
then: (schema) =>
|
||||
schema
|
||||
.shape({
|
||||
value: Yup.number().required(
|
||||
'Week wajib diisi untuk Ayam Pullet!'
|
||||
),
|
||||
label: Yup.string().required(
|
||||
'Week wajib diisi untuk Ayam Pullet!'
|
||||
),
|
||||
})
|
||||
.required('Week wajib diisi untuk Ayam Pullet!'),
|
||||
.min(1, 'Week wajib diisi untuk Ayam Pullet!')
|
||||
.required('Week wajib diisi untuk Ayam Pullet!')
|
||||
.typeError('Week harus berupa angka!'),
|
||||
otherwise: (schema) => schema.optional().notRequired(),
|
||||
}),
|
||||
});
|
||||
|
||||
+11
-10
@@ -467,19 +467,20 @@ const SalesOrderProductForm = ({
|
||||
{/* Konversi Satuan Week Pullet */}
|
||||
{formik.values.marketing_type?.value.toLowerCase() ===
|
||||
'ayam_pullet' && (
|
||||
<SelectInputRadio
|
||||
<NumberInput
|
||||
required
|
||||
label='Minggu'
|
||||
options={optionsWeek}
|
||||
value={
|
||||
formik.values.week?.value
|
||||
? (formik.values.week as { value: number; label: string })
|
||||
: null
|
||||
}
|
||||
onChange={(val) => {
|
||||
formik.setFieldValue('week', val);
|
||||
name='week'
|
||||
value={formik.values.week ?? undefined}
|
||||
onChange={(e) => {
|
||||
formik.setFieldValue('week', Number(e.target.value));
|
||||
setCurrentInput(e.target.name);
|
||||
}}
|
||||
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 Button from '@/components/Button';
|
||||
import Card from '@/components/Card';
|
||||
import { Icon } from '@iconify/react';
|
||||
import { useRef } from 'react';
|
||||
import { formatCurrency, formatDate, formatNumber } from '@/lib/helper';
|
||||
@@ -39,6 +40,8 @@ const DeliveryOrderProductTable = ({
|
||||
const onDeleteRef = useRef(onDelete);
|
||||
onDeleteRef.current = onDelete;
|
||||
|
||||
const approvalStepNumber = marketing?.latest_approval?.step_number;
|
||||
|
||||
return (
|
||||
<>
|
||||
<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
|
||||
);
|
||||
return (
|
||||
<div
|
||||
className='rounded-lg border border-tools-table-outline border-base-content/5'
|
||||
<Card
|
||||
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
|
||||
style={{
|
||||
@@ -58,12 +72,12 @@ const DeliveryOrderProductTable = ({
|
||||
className='border-none 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'>
|
||||
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 mt-2'>
|
||||
<div className='flex w-full flex-row gap-1 items-center justify-between h-full'>
|
||||
<div>Value</div>
|
||||
{(formType === 'add_delivery' ||
|
||||
formType === 'edit_delivery' ||
|
||||
@@ -105,16 +119,20 @@ const DeliveryOrderProductTable = ({
|
||||
</th>
|
||||
</tr>
|
||||
<>
|
||||
<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>
|
||||
{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>
|
||||
@@ -130,7 +148,9 @@ const DeliveryOrderProductTable = ({
|
||||
<tr>
|
||||
<td className='text-sm px-4 py-3'>Gudang</td>
|
||||
<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>
|
||||
</tr>
|
||||
<tr>
|
||||
@@ -191,7 +211,7 @@ const DeliveryOrderProductTable = ({
|
||||
</>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
'use client';
|
||||
|
||||
import Button from '@/components/Button';
|
||||
import Card from '@/components/Card';
|
||||
import { SalesOrderProductFormValues } from '@/components/pages/marketing/form/repeater/sales-order/SalesOrderProduct.schema';
|
||||
import {
|
||||
formatCurrency,
|
||||
@@ -146,9 +147,18 @@ const SalesOrderProductTable = ({
|
||||
<>
|
||||
<div className='size-full flex flex-col relative overflow-x-hidden gap-3'>
|
||||
{data.map((item) => (
|
||||
<div
|
||||
className='rounded-lg border border-tools-table-outline border-base-content/5'
|
||||
<Card
|
||||
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
|
||||
style={{
|
||||
@@ -157,12 +167,12 @@ const SalesOrderProductTable = ({
|
||||
className='border-none 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'>
|
||||
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 mt-2'>
|
||||
<div className='flex w-full flex-row gap-1 items-center justify-between h-full'>
|
||||
<div>Value</div>
|
||||
{formType !== 'success' && (
|
||||
<div className='flex flex-row gap-1.5 items-center'>
|
||||
@@ -234,7 +244,7 @@ const SalesOrderProductTable = ({
|
||||
'ayam_pullet' && (
|
||||
<tr>
|
||||
<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>
|
||||
)}
|
||||
{item.convertion_unit?.value.toLowerCase() === 'peti' && (
|
||||
@@ -294,7 +304,7 @@ const SalesOrderProductTable = ({
|
||||
</>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</Card>
|
||||
))}
|
||||
{formType != 'add_deliver' &&
|
||||
formType != 'edit_deliver' &&
|
||||
|
||||
@@ -363,10 +363,6 @@ const ProjectFlockTable = ({ refresh }: { refresh?: () => void }) => {
|
||||
accessorKey: 'location.name',
|
||||
header: 'Lokasi',
|
||||
},
|
||||
{
|
||||
accessorKey: 'fcr.name',
|
||||
header: 'FCR',
|
||||
},
|
||||
{
|
||||
accessorKey: 'category',
|
||||
header: 'Kategori',
|
||||
|
||||
@@ -16,11 +16,6 @@ type ProjectFlockFormSchemaType = {
|
||||
label: string;
|
||||
} | null;
|
||||
category: string;
|
||||
fcr: {
|
||||
value: number | string;
|
||||
label: string;
|
||||
} | null;
|
||||
fcr_id: number;
|
||||
production_standard: {
|
||||
value: number | string;
|
||||
label: string;
|
||||
@@ -96,15 +91,6 @@ export const ProjectFlockFormSchema: Yup.ObjectSchema<ProjectFlockFormSchemaType
|
||||
.oneOf(['GROWING', 'LAYING'], '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: Yup.object({
|
||||
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 {
|
||||
AreaApi,
|
||||
FcrApi,
|
||||
FlockApi,
|
||||
KandangApi,
|
||||
LocationApi,
|
||||
@@ -284,13 +283,6 @@ const ProjectFlockForm = ({
|
||||
: ((initialValues?.area?.id ?? '') as string),
|
||||
});
|
||||
|
||||
const {
|
||||
options: optionsFcr,
|
||||
isLoadingOptions: isLoadingFcrs,
|
||||
setInputValue: setInputValueFcr,
|
||||
loadMore: loadMoreFcr,
|
||||
} = useSelect(FcrApi.basePath, 'id', 'name');
|
||||
|
||||
const {
|
||||
options: optionsProductionStandards,
|
||||
isLoadingOptions: isLoadingProductionStandards,
|
||||
@@ -505,12 +497,6 @@ const ProjectFlockForm = ({
|
||||
label: initialValues.category,
|
||||
}
|
||||
: null,
|
||||
fcr: initialValues?.fcr
|
||||
? {
|
||||
value: initialValues.fcr?.id,
|
||||
label: initialValues.fcr.name,
|
||||
}
|
||||
: null,
|
||||
production_standard: initialValues?.production_standard
|
||||
? {
|
||||
value: initialValues.production_standard?.id,
|
||||
@@ -531,7 +517,6 @@ const ProjectFlockForm = ({
|
||||
category: initialValues?.category as NonNullable<
|
||||
'GROWING' | 'LAYING' | undefined
|
||||
>,
|
||||
fcr_id: initialValues?.fcr?.id ?? 0,
|
||||
production_standard_id: initialValues?.production_standard?.id ?? 0,
|
||||
location_id: initialValues?.location?.id ?? 0,
|
||||
kandang_ids: initialValues?.kandangs?.map(
|
||||
@@ -574,7 +559,6 @@ const ProjectFlockForm = ({
|
||||
flock_name: values.flock_name as string,
|
||||
area_id: values.area_id as number,
|
||||
category: values.category as string,
|
||||
fcr_id: values.fcr_id as number,
|
||||
production_standard_id: values.production_standard_id as number,
|
||||
location_id: values.location_id as number,
|
||||
kandang_ids: values.kandang_ids as number[],
|
||||
@@ -996,25 +980,6 @@ const ProjectFlockForm = ({
|
||||
isClearable
|
||||
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
|
||||
required
|
||||
label='Kategori'
|
||||
|
||||
@@ -31,8 +31,7 @@ import {
|
||||
RecordingApi,
|
||||
ProjectFlockApi,
|
||||
} from '@/services/api/production';
|
||||
import { FcrApi, ProductionStandardApi } from '@/services/api/master-data';
|
||||
import { FcrWithStandards, FcrStandard } from '@/types/api/master-data/fcr';
|
||||
import { ProductionStandardApi } from '@/services/api/master-data';
|
||||
import {
|
||||
ProductionStandard,
|
||||
StandardDetails,
|
||||
@@ -87,24 +86,6 @@ interface RecordingFormProps {
|
||||
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>[] = [
|
||||
{
|
||||
accessorKey: 'week',
|
||||
@@ -253,36 +234,14 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
|
||||
const approveModal = useModal();
|
||||
const rejectModal = useModal();
|
||||
const deleteModal = useModal();
|
||||
const fcrStandardModal = useModal();
|
||||
const productionStandardModal = useModal();
|
||||
|
||||
const [fcrStandards, setFcrStandards] = useState<FcrStandard[]>([]);
|
||||
const [productionStandards, setProductionStandards] =
|
||||
useState<ProductionStandard | null>(null);
|
||||
|
||||
const [isFcrModalOpen, setIsFcrModalOpen] = useState(false);
|
||||
const [isProductionStandardModalOpen, setIsProductionStandardModalOpen] =
|
||||
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(() => {
|
||||
const checkProductionStandardModalOpen = () => {
|
||||
const isOpen = productionStandardModal.ref.current?.open || false;
|
||||
@@ -460,24 +419,6 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
|
||||
? projectFlockKandangLookupData.data
|
||||
: 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(() => {
|
||||
if (type === 'add') {
|
||||
return projectFlockKandangLookup?.project_flock?.production_standard_id;
|
||||
@@ -606,7 +547,7 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
|
||||
isLoadingOptions: isLoadingEggProducts,
|
||||
loadMore: loadMoreEggProducts,
|
||||
} = useSelect(ProductWarehouseApi.basePath, 'id', 'product.name', 'search', {
|
||||
search: 'telur',
|
||||
type: 'TELUR',
|
||||
location_id: eggProductsLocationId,
|
||||
kandang_id: eggProductsKandangId,
|
||||
});
|
||||
@@ -886,20 +827,10 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
|
||||
if (isResponseSuccess(eggProductsData) && selectedKandang) {
|
||||
const data = eggProductsData.data as unknown as ProductWarehouse[];
|
||||
data.forEach((product) => {
|
||||
const productName = product.product.name;
|
||||
|
||||
if (
|
||||
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,
|
||||
});
|
||||
}
|
||||
options.push({
|
||||
value: product.id,
|
||||
label: product.product.name,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1952,24 +1883,6 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
|
||||
: '-'}
|
||||
</p>
|
||||
</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>
|
||||
<span className='text-sm text-gray-600'>
|
||||
Standard Produksi
|
||||
@@ -2160,22 +2073,6 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
|
||||
</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>
|
||||
<span className='text-sm text-gray-600'>
|
||||
Standard Produksi
|
||||
@@ -2227,21 +2124,6 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
|
||||
</tr>
|
||||
</thead>
|
||||
<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>
|
||||
<td className='py-3 font-medium'>Feed Intake (g)</td>
|
||||
<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 */}
|
||||
<Modal
|
||||
closeOnBackdrop={true}
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
'use client';
|
||||
|
||||
import { ChangeEventHandler, useCallback, useState } from 'react';
|
||||
import { ChangeEventHandler, useCallback, useMemo, useState } from 'react';
|
||||
import useSWR from 'swr';
|
||||
import useSWRInfinite from 'swr/infinite';
|
||||
import { CellContext, ColumnDef, SortingState } from '@tanstack/react-table';
|
||||
import toast from 'react-hot-toast';
|
||||
|
||||
@@ -17,16 +18,19 @@ import RowCollapseOptions from '@/components/table/RowCollapseOptions';
|
||||
import RowOptionsMenuWrapper from '@/components/table/RowOptionsMenuWrapper';
|
||||
import RequirePermission from '@/components/helper/RequirePermission';
|
||||
import StatusBadge from '@/components/helper/StatusBadge';
|
||||
import PurchaseOrderInvoice from '@/components/pages/purchase/order/PurchaseOrderInvoice';
|
||||
|
||||
import { cn, formatDate } from '@/lib/helper';
|
||||
import { isResponseSuccess } from '@/lib/api-helper';
|
||||
import { BaseApiResponse } from '@/types/api/api-general';
|
||||
|
||||
import { useTableFilter } from '@/services/hooks/useTableFilter';
|
||||
import { ROWS_OPTIONS } from '@/config/constant';
|
||||
import { Purchase } from '@/types/api/purchase/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 Link from 'next/link';
|
||||
|
||||
// ===== STATUS BADGE UTILITIES =====
|
||||
const statusTextMap: Record<string, string> = {
|
||||
@@ -159,27 +163,33 @@ const PurchaseTable = () => {
|
||||
PurchaseApi.getAllFetcher
|
||||
);
|
||||
|
||||
const [isDownloadingInvoice, setIsDownloadingInvoice] = useState(false);
|
||||
const [invoicePurchaseData, setInvoicePurchaseData] =
|
||||
useState<Purchase | null>(null);
|
||||
|
||||
const handleDownloadInvoice = async (purchaseId: number) => {
|
||||
setIsDownloadingInvoice(true);
|
||||
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 getKey = (
|
||||
pageIndex: number,
|
||||
previousPageData: BaseApiResponse<Expense>[] | null
|
||||
) => {
|
||||
if (pageIndex > 0 && !previousPageData) return null;
|
||||
return `${ExpenseApi.basePath}?page=${pageIndex + 1}&limit=100`;
|
||||
};
|
||||
|
||||
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 =====
|
||||
const purchaseColumns: ColumnDef<Purchase>[] = [
|
||||
{
|
||||
@@ -191,37 +201,34 @@ const PurchaseTable = () => {
|
||||
},
|
||||
{
|
||||
accessorKey: 'po_expedition',
|
||||
header: 'PO Ekspedisi',
|
||||
header: 'Ekspedisi PO',
|
||||
cell: (props) => {
|
||||
const purchase = props.row.original;
|
||||
|
||||
if (!purchase.po_number || purchase.po_number === 'Belum dibuat') {
|
||||
return <span>-</span>;
|
||||
}
|
||||
|
||||
const poExpedition = props.row.original.po_expedition;
|
||||
if (!poExpedition || poExpedition.length === 0) return '-';
|
||||
return (
|
||||
<Button
|
||||
color='primary'
|
||||
className='w-fit min-w-32 flex items-center justify-start gap-1 px-2 py-1 text-sm font-mono'
|
||||
onClick={() => handleDownloadInvoice(purchase.id)}
|
||||
disabled={isDownloadingInvoice}
|
||||
>
|
||||
<Icon
|
||||
icon={
|
||||
isDownloadingInvoice
|
||||
? 'eos-icons:loading'
|
||||
: 'material-symbols:file-open-outline'
|
||||
<ul className='list-disc pl-4'>
|
||||
{poExpedition.map((exp, index) => {
|
||||
const expenseId = expenseMap.get(exp.refrence);
|
||||
if (expenseId) {
|
||||
return (
|
||||
<li key={index}>
|
||||
<Link
|
||||
href={`/expense/detail/?expenseId=${expenseId}`}
|
||||
className='p-0 h-auto text-primary underline'
|
||||
>
|
||||
{exp.refrence}
|
||||
</Link>
|
||||
</li>
|
||||
);
|
||||
}
|
||||
width={16}
|
||||
height={16}
|
||||
/>
|
||||
{purchase.po_number}
|
||||
</Button>
|
||||
return <li key={index}>{exp.refrence}</li>;
|
||||
})}
|
||||
</ul>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
accessorKey: 'supplier.name',
|
||||
accessorKey: 'supplier',
|
||||
header: 'Vendor',
|
||||
cell: (props) => props.row.original.supplier.name,
|
||||
},
|
||||
@@ -505,15 +512,6 @@ const PurchaseTable = () => {
|
||||
onClick: confirmationModalDeleteClickHandler,
|
||||
}}
|
||||
/>
|
||||
|
||||
{invoicePurchaseData && (
|
||||
<div className='hidden'>
|
||||
<PurchaseOrderInvoice
|
||||
data={invoicePurchaseData}
|
||||
triggerDownloadOnMount={true}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
'use client';
|
||||
|
||||
import { useMemo, useState, useEffect, useCallback, useRef } from 'react';
|
||||
import { useMemo, useState } from 'react';
|
||||
import {
|
||||
Page,
|
||||
Text,
|
||||
@@ -235,16 +235,11 @@ const pdfStyles = StyleSheet.create({
|
||||
interface PurchaseOrderInvoiceProps {
|
||||
data?: Purchase;
|
||||
className?: string;
|
||||
triggerDownloadOnMount?: boolean;
|
||||
}
|
||||
|
||||
const PurchaseOrderInvoice = ({
|
||||
data,
|
||||
triggerDownloadOnMount,
|
||||
}: PurchaseOrderInvoiceProps) => {
|
||||
const PurchaseOrderInvoice = ({ data }: PurchaseOrderInvoiceProps) => {
|
||||
const [, setIsGeneratingPDF] = useState(false);
|
||||
const purchaseData = data;
|
||||
const hasDownloadedRef = useRef(false);
|
||||
|
||||
const grandTotal = useMemo(() => {
|
||||
return (
|
||||
@@ -255,7 +250,7 @@ const PurchaseOrderInvoice = ({
|
||||
);
|
||||
}, [purchaseData?.items]);
|
||||
|
||||
const handleDownloadPDF = useCallback(async () => {
|
||||
const handleDownloadPDF = async () => {
|
||||
if (!purchaseData) {
|
||||
toast.error('No purchase order data available');
|
||||
return;
|
||||
@@ -515,20 +510,7 @@ const PurchaseOrderInvoice = ({
|
||||
} finally {
|
||||
setIsGeneratingPDF(false);
|
||||
}
|
||||
}, [purchaseData]);
|
||||
|
||||
useEffect(() => {
|
||||
if (triggerDownloadOnMount && purchaseData && !hasDownloadedRef.current) {
|
||||
hasDownloadedRef.current = true;
|
||||
handleDownloadPDF();
|
||||
}
|
||||
}, [triggerDownloadOnMount, purchaseData]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!triggerDownloadOnMount) {
|
||||
hasDownloadedRef.current = false;
|
||||
}
|
||||
}, [triggerDownloadOnMount]);
|
||||
};
|
||||
|
||||
if (!purchaseData) {
|
||||
return (
|
||||
@@ -538,10 +520,6 @@ const PurchaseOrderInvoice = ({
|
||||
);
|
||||
}
|
||||
|
||||
if (triggerDownloadOnMount) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return purchaseData?.po_number &&
|
||||
purchaseData.po_number !== 'Belum dibuat' ? (
|
||||
<Button
|
||||
|
||||
@@ -15,7 +15,7 @@ export type MarketingFormValues = {
|
||||
total_price?: string | number;
|
||||
marketing_type?: { 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;
|
||||
price_per_convertion?: number | null;
|
||||
total_peti?: number | null;
|
||||
@@ -100,7 +100,7 @@ export const calculateAyamPullet = (
|
||||
): void => {
|
||||
const { values, setFieldValue } = ctx;
|
||||
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 avgWeight = Number(values.avg_weight || 0);
|
||||
const totalWeight = Number(values.total_weight || 0);
|
||||
|
||||
+2
@@ -17,6 +17,8 @@ export type BaseMarketing = {
|
||||
status?: string;
|
||||
so_number: string;
|
||||
so_date: string;
|
||||
do_number?: string;
|
||||
do_date?: string;
|
||||
customer: Customer;
|
||||
sales_person: CreatedUser;
|
||||
notes: string;
|
||||
|
||||
-4
@@ -1,5 +1,4 @@
|
||||
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 { Kandang } from '@/types/api/master-data/kandang';
|
||||
import { Location } from '@/types/api/master-data/location';
|
||||
@@ -16,8 +15,6 @@ export type BaseProjectFlock = {
|
||||
area: Area;
|
||||
area_id: number;
|
||||
category: string;
|
||||
fcr: Fcr;
|
||||
fcr_id: number;
|
||||
production_standard: ProductionStandard;
|
||||
production_standard_id: number;
|
||||
location: Location;
|
||||
@@ -51,7 +48,6 @@ export type CreateProjectFlockPayload = {
|
||||
flock_name: string;
|
||||
area_id: number;
|
||||
category: string;
|
||||
fcr_id: number;
|
||||
production_standard_id: number;
|
||||
location_id: number;
|
||||
kandang_ids: number[];
|
||||
|
||||
Vendored
+1
-1
@@ -76,7 +76,7 @@ export type BasePurchase = {
|
||||
items?: PurchaseItem[];
|
||||
latest_approval?: BaseApproval;
|
||||
requester_name?: string;
|
||||
po_expedition?: string[];
|
||||
po_expedition?: { id: number; refrence: string }[];
|
||||
created_user?: CreatedUser;
|
||||
products?: PurchaseItemProduct[];
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user