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',
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,
@@ -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(),
}),
});
@@ -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}
/>
)}
@@ -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(),
}),
});
@@ -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}
+52 -54
View File
@@ -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
+2 -2
View File
@@ -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
View File
@@ -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
View File
@@ -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[];
+1 -1
View File
@@ -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[];
};