Merge branch 'development' into 'production'

Development

See merge request mbugroup/lti-web-client!382
This commit is contained in:
Adnan Zahir
2026-04-09 15:36:40 +07:00
11 changed files with 325 additions and 83 deletions
@@ -368,7 +368,9 @@ const DeliveryOrderFormModal = ({}: { initialValues?: Marketing }) => {
const currentProducts = deliveryOrderValues?.find( const currentProducts = deliveryOrderValues?.find(
(product) => product.id == id (product) => product.id == id
); );
setSelectedDeliveryProduct(values ?? currentProducts ?? null);
setSelectedDeliveryProduct(currentProducts ?? values ?? null);
if (id) { if (id) {
setStep(2); setStep(2);
} }
@@ -144,15 +144,15 @@ export const DeliveryProductToFieldValues = (
delivery: BaseDeliveryOrder delivery: BaseDeliveryOrder
): DeliveryOrderProductFormValues[] => { ): DeliveryOrderProductFormValues[] => {
const data = delivery.deliveries.map((item) => { const data = delivery.deliveries.map((item) => {
const soId = salesOrders.find( const salesOrder = salesOrders.find(
(so) => so.product_warehouse.id === item.product_warehouse.id (so) => so.product_warehouse.id === item.product_warehouse.id
)?.id; );
const warehouseOption = { const warehouseOption = {
value: item.product_warehouse.warehouse.id, value: item.product_warehouse.warehouse.id,
label: item.product_warehouse.warehouse.name, label: item.product_warehouse.warehouse.name,
}; };
return { return {
id: soId, id: salesOrder?.id,
unit_price: item.unit_price, unit_price: item.unit_price,
total_weight: item.total_weight, total_weight: item.total_weight,
qty: item.qty, qty: item.qty,
@@ -161,9 +161,21 @@ export const DeliveryProductToFieldValues = (
vehicle_number: item.vehicle_number, vehicle_number: item.vehicle_number,
delivery_date: formatDate(delivery.delivery_date, 'yyyy-MM-DD'), delivery_date: formatDate(delivery.delivery_date, 'yyyy-MM-DD'),
do_number: delivery.do_number, do_number: delivery.do_number,
marketing_product_id: soId, marketing_product_id: salesOrder?.id,
marketing_type: salesOrder?.marketing_type
? {
value: salesOrder?.marketing_type,
label: formatTitleCase(salesOrder?.marketing_type),
}
: null,
convertion_unit: salesOrder?.convertion_unit
? {
value: salesOrder?.convertion_unit.toLowerCase(),
label: formatTitleCase(salesOrder?.convertion_unit),
}
: null,
marketing_product: { marketing_product: {
id: soId, id: salesOrder?.id,
vehicle_number: item.vehicle_number, vehicle_number: item.vehicle_number,
warehouse_id: item.product_warehouse.warehouse.id, warehouse_id: item.product_warehouse.warehouse.id,
warehouse: warehouseOption, warehouse: warehouseOption,
@@ -183,6 +195,7 @@ export const DeliveryProductToFieldValues = (
}, },
} as DeliveryOrderProductFormValues; } as DeliveryOrderProductFormValues;
}); });
return data; return data;
}; };
export const mergeSOwithDO = ( export const mergeSOwithDO = (
@@ -194,6 +207,19 @@ export const mergeSOwithDO = (
const delivery = deliveryOrders.find( const delivery = deliveryOrders.find(
(d) => d?.marketing_product_id === so.id (d) => d?.marketing_product_id === so.id
); );
const isTelurQty =
so.marketing_type?.value?.toLowerCase() === 'telur' &&
so.convertion_unit?.value?.toLowerCase() === 'qty';
const salesOrderUnitPrice =
isTelurQty && Number(so.total_price || 0) > 0 && Number(so.qty || 0) > 0
? Number(so.total_price) / Number(so.qty)
: so.unit_price;
const salesOrderPricePerQty =
isTelurQty &&
Number(so.total_price || 0) > 0 &&
Number(so.total_weight || 0) > 0
? Number(so.total_price) / Number(so.total_weight)
: so.price_per_qty;
return { return {
...so, // nilai dasar dari sales order ...so, // nilai dasar dari sales order
@@ -201,30 +227,30 @@ export const mergeSOwithDO = (
delivery_date: delivery?.delivery_date || undefined, delivery_date: delivery?.delivery_date || undefined,
do_number: delivery?.do_number || undefined, do_number: delivery?.do_number || undefined,
vehicle_number: delivery?.vehicle_number || so.vehicle_number, vehicle_number: delivery?.vehicle_number || so.vehicle_number,
unit_price: autofill ? so.unit_price : delivery?.unit_price, unit_price: autofill ? delivery?.unit_price : salesOrderUnitPrice,
total_weight: autofill ? so.total_weight : delivery?.total_weight, total_weight: autofill ? delivery?.total_weight : so.total_weight,
qty: autofill ? so.qty : delivery?.qty, qty: autofill ? delivery?.qty : so.qty,
avg_weight: autofill ? so.avg_weight : delivery?.avg_weight, avg_weight: autofill ? delivery?.avg_weight : so.avg_weight,
total_price: autofill ? so.total_price : delivery?.total_price, total_price: autofill ? delivery?.total_price : so.total_price,
marketing_product: so, // jika ada, override marketing_product: so, // jika ada, override
uom: autofill ? so.uom : delivery?.uom, uom: autofill ? delivery?.uom : so.uom,
weight_per_convertion: autofill weight_per_convertion: autofill
? so.weight_per_convertion ? delivery?.weight_per_convertion
: delivery?.weight_per_convertion, : so.weight_per_convertion,
price_per_convertion: autofill price_per_convertion: autofill
? so.price_per_convertion ? delivery?.price_per_convertion
: delivery?.price_per_convertion, : so.price_per_convertion,
convertion_unit: autofill convertion_unit: autofill
? so.convertion_unit ? delivery?.convertion_unit
: delivery?.convertion_unit, : so.convertion_unit,
marketing_type: autofill ? so.marketing_type : delivery?.marketing_type, marketing_type: autofill ? delivery?.marketing_type : so.marketing_type,
total_peti: autofill ? so.total_peti : delivery?.total_peti, total_peti: autofill ? delivery?.total_peti : so.total_peti,
price_per_qty: autofill ? so.price_per_qty : delivery?.price_per_qty, price_per_qty: autofill ? delivery?.price_per_qty : salesOrderPricePerQty,
sisa_berat: autofill ? so.sisa_berat : delivery?.sisa_berat, sisa_berat: autofill ? delivery?.sisa_berat : so.sisa_berat,
price_sisa_berat: autofill price_sisa_berat: autofill
? so.price_sisa_berat ? delivery?.price_sisa_berat
: delivery?.price_sisa_berat, : so.price_sisa_berat,
week: autofill ? so.week : delivery?.week, week: autofill ? delivery?.week : so.week,
} as DeliveryOrderProductFormValues; } as DeliveryOrderProductFormValues;
}); });
}; };
@@ -32,6 +32,63 @@ import Dropdown from '@/components/Dropdown';
import { Icon } from '@iconify/react'; import { Icon } from '@iconify/react';
import { handleMarketingCalculation } from '@/lib/marketing-calculation'; import { handleMarketingCalculation } from '@/lib/marketing-calculation';
type PricingOption =
| string
| {
value: string;
label: string;
}
| null
| undefined;
type PricingSource =
| {
marketing_type?: PricingOption;
convertion_unit?: PricingOption;
total_price?: string | number | null;
qty?: string | number | null;
total_weight?: string | number | null;
unit_price?: string | number | null;
price_per_qty?: number | null;
}
| null
| undefined;
const getOptionValue = (value?: PricingOption) => {
if (!value) return undefined;
if (typeof value === 'string') return value.toLowerCase();
return value.value?.toLowerCase();
};
const isTelurQtyProduct = (value?: PricingSource) =>
getOptionValue(value?.marketing_type) === 'telur' &&
getOptionValue(value?.convertion_unit) === 'qty';
const getDisplayedUnitPrice = (value?: PricingSource) => {
if (
isTelurQtyProduct(value) &&
Number(value?.total_price || 0) > 0 &&
Number(value?.qty || 0) > 0
) {
return Number(value?.total_price) / Number(value?.qty);
}
return value?.unit_price ?? undefined;
};
const getDisplayedPricePerQty = (value?: PricingSource) => {
if (
isTelurQtyProduct(value) &&
Number(value?.total_price || 0) > 0 &&
Number(value?.total_weight || 0) > 0
) {
return Number(value?.total_price) / Number(value?.total_weight);
}
return value?.price_per_qty ?? null;
};
const DeliveryOrderProductForm = ({ const DeliveryOrderProductForm = ({
formState, formState,
salesOrders, salesOrders,
@@ -69,14 +126,18 @@ const DeliveryOrderProductForm = ({
Number(initialValues.total_peti) Number(initialValues.total_peti)
: 0; : 0;
const initialPricePerConvertion = // const initialPricePerConvertion =
initialValues?.total_price && // initialValues?.total_price &&
initialValues?.total_peti && // initialValues?.total_peti &&
Number(initialValues.total_peti) !== 0 // Number(initialValues.total_peti) !== 0
? (Number(initialValues.total_price) - // ? (Number(initialValues.total_price) -
initialSisaBerat * Number(initialValues.unit_price || 0)) / // initialSisaBerat * Number(initialValues.unit_price || 0)) /
Number(initialValues.total_peti) // Number(initialValues.total_peti)
: 0; // : 0;
const initialPricePerConvertion = initialValues?.unit_price
? Number(initialValues?.unit_price)
: 0;
const initialPriceSisaBerat = const initialPriceSisaBerat =
initialValues?.total_price && initialValues?.total_peti initialValues?.total_price && initialValues?.total_peti
@@ -154,6 +215,27 @@ const DeliveryOrderProductForm = ({
(item) => item.id === initialValues?.marketing_product_id (item) => item.id === initialValues?.marketing_product_id
); );
const defaultPricingSource: PricingSource = {
marketing_type:
initialValues?.marketing_type ?? salesOrder?.marketing_type ?? null,
convertion_unit:
initialValues?.convertion_unit ?? salesOrder?.convertion_unit ?? null,
total_price:
deliveryOrder?.total_price ??
initialValues?.total_price ??
salesOrder?.total_price,
qty: deliveryOrder?.qty ?? initialValues?.qty ?? salesOrder?.qty,
total_weight:
deliveryOrder?.total_weight ??
initialValues?.total_weight ??
salesOrder?.total_weight,
unit_price:
deliveryOrder?.unit_price ??
initialValues?.unit_price ??
salesOrder?.unit_price,
price_per_qty: initialValues?.price_per_qty ?? null,
};
const formik = useFormik<DeliveryOrderProductFormValues>({ const formik = useFormik<DeliveryOrderProductFormValues>({
enableReinitialize: true, enableReinitialize: true,
initialValues: { initialValues: {
@@ -167,8 +249,7 @@ const DeliveryOrderProductForm = ({
undefined, undefined,
marketing_product_id: marketing_product_id:
salesOrder?.id || initialValues?.marketing_product_id || undefined, salesOrder?.id || initialValues?.marketing_product_id || undefined,
unit_price: unit_price: getDisplayedUnitPrice(defaultPricingSource),
deliveryOrder?.unit_price ?? initialValues?.unit_price ?? undefined,
total_weight: total_weight:
deliveryOrder?.total_weight ?? initialValues?.total_weight ?? undefined, deliveryOrder?.total_weight ?? initialValues?.total_weight ?? undefined,
qty: deliveryOrder?.qty ?? initialValues?.qty ?? undefined, qty: deliveryOrder?.qty ?? initialValues?.qty ?? undefined,
@@ -186,7 +267,7 @@ const DeliveryOrderProductForm = ({
convertion_unit: initialValues?.convertion_unit || null, convertion_unit: initialValues?.convertion_unit || null,
marketing_type: initialValues?.marketing_type || null, marketing_type: initialValues?.marketing_type || null,
total_peti: initialValues?.total_peti ?? null, total_peti: initialValues?.total_peti ?? null,
price_per_qty: initialValues?.price_per_qty ?? null, price_per_qty: getDisplayedPricePerQty(defaultPricingSource),
sisa_berat: initialSisaBerat, sisa_berat: initialSisaBerat,
price_sisa_berat: initialPriceSisaBerat, price_sisa_berat: initialPriceSisaBerat,
week: initialValues?.week ?? null, week: initialValues?.week ?? null,
@@ -329,7 +410,11 @@ const DeliveryOrderProductForm = ({
if (!Boolean(initialValues.qty)) { if (!Boolean(initialValues.qty)) {
handleResetForm(); handleResetForm();
} else { } else {
setFormikValues(initialValues); setFormikValues({
...initialValues,
unit_price: getDisplayedUnitPrice(initialValues),
price_per_qty: getDisplayedPricePerQty(initialValues),
});
if (initialValues?.marketing_product_id) { if (initialValues?.marketing_product_id) {
setSelectedProduct({ setSelectedProduct({
value: initialValues?.id, value: initialValues?.id,
@@ -458,10 +543,11 @@ const DeliveryOrderProductForm = ({
marketing_product_id: selected.value as number, marketing_product_id: selected.value as number,
marketing_product: soFieldValues, marketing_product: soFieldValues,
qty: so.qty, qty: so.qty,
unit_price: so.unit_price, unit_price: getDisplayedUnitPrice(so),
total_price: so.total_price, total_price: so.total_price,
avg_weight: so.avg_weight, avg_weight: so.avg_weight,
total_weight: so.total_weight, total_weight: so.total_weight,
price_per_qty: getDisplayedPricePerQty(so),
vehicle_number: so.vehicle_number, vehicle_number: so.vehicle_number,
week: soFieldValues.week ?? null, week: soFieldValues.week ?? null,
}); });
@@ -761,12 +847,32 @@ const DeliveryOrderProductForm = ({
/> />
)} )}
{/* Harga per butir untuk TELUR + QTY */} {/* Harga Satuan */}
{formik.values.convertion_unit?.value.toLowerCase() !== 'peti' &&
formik.values.convertion_unit?.value.toLowerCase() !== 'kg' && (
<NumberInput
required
label={`Harga / ${formik.values.convertion_unit?.label.toLowerCase() !== 'qty' ? 'Kg' : 'Butir'} (Rp)`}
name='unit_price'
value={formik.values.unit_price}
onChange={(e) => {
const value = Number(e.target.value);
handleFieldChange('unit_price', value, () =>
setCurrentInput(e.target.name)
);
}}
isError={Boolean(formik.errors.unit_price)}
errorMessage={formik.errors.unit_price}
placeholder='Masukan Harga Satuan'
/>
)}
{/* Harga per kg untuk TELUR + QTY */}
{formik.values.marketing_type?.value.toLowerCase() === 'telur' && {formik.values.marketing_type?.value.toLowerCase() === 'telur' &&
formik.values.convertion_unit?.value.toLowerCase() === 'qty' && ( formik.values.convertion_unit?.value.toLowerCase() === 'qty' && (
<NumberInput <NumberInput
required required
label='Harga / Butir (Rp)' label='Harga / Kg (Rp)'
name='price_per_qty' name='price_per_qty'
value={formik.values.price_per_qty ?? undefined} value={formik.values.price_per_qty ?? undefined}
onChange={(e) => { onChange={(e) => {
@@ -780,27 +886,7 @@ const DeliveryOrderProductForm = ({
Boolean(formik.errors.price_per_qty) Boolean(formik.errors.price_per_qty)
} }
errorMessage={formik.errors.price_per_qty} errorMessage={formik.errors.price_per_qty}
placeholder='Masukan Harga per Butir' placeholder='Masukan Harga per Kg'
/>
)}
{/* Harga Satuan */}
{formik.values.convertion_unit?.value.toLowerCase() !== 'peti' &&
formik.values.convertion_unit?.value.toLowerCase() !== 'kg' && (
<NumberInput
required
label={`Harga / ${isResponseSuccess(productData) ? productData?.data?.uom?.name : 'Produk'} (Rp)`}
name='unit_price'
value={formik.values.unit_price}
onChange={(e) => {
const value = Number(e.target.value);
handleFieldChange('unit_price', value, () =>
setCurrentInput(e.target.name)
);
}}
isError={Boolean(formik.errors.unit_price)}
errorMessage={formik.errors.unit_price}
placeholder='Masukan Harga Satuan'
/> />
)} )}
@@ -157,11 +157,6 @@ const SalesOrderProductForm = ({
); );
}, [selectedProductWarehouse, formik.values.marketing_type]); }, [selectedProductWarehouse, formik.values.marketing_type]);
console.log({
initialValues,
values: formik.values,
});
// ===== Options ===== // ===== Options =====
const { const {
options: warehouseOptions, options: warehouseOptions,
@@ -5,8 +5,9 @@ import { Icon } from '@iconify/react';
import { useRef, useMemo } from 'react'; import { useRef, useMemo } from 'react';
import { formatCurrency, formatDate, formatNumber } from '@/lib/helper'; import { formatCurrency, formatDate, formatNumber } from '@/lib/helper';
import DeliveryOrderExport from '@/components/pages/marketing/pdf/DeliveryOrderExport'; import DeliveryOrderExport from '@/components/pages/marketing/pdf/DeliveryOrderExport';
import { Marketing, BaseDelivery } from '@/types/api/marketing/marketing'; import { Marketing } from '@/types/api/marketing/marketing';
import { Warehouse } from '@/types/api/master-data/warehouse'; import { Warehouse } from '@/types/api/master-data/warehouse';
import { DeliveryProductToFieldValues } from '@/components/pages/marketing/form/MarketingForm.schema';
type DeliveryOrderProductTableProps = { type DeliveryOrderProductTableProps = {
data: DeliveryOrderProductFormValues[]; data: DeliveryOrderProductFormValues[];
@@ -55,14 +56,17 @@ const DeliveryOrderProductTable = ({
const deliveryItems = useMemo(() => { const deliveryItems = useMemo(() => {
if (!hasDeliveryOrder) return []; if (!hasDeliveryOrder) return [];
return ( return (
marketing?.delivery_order?.flatMap((doItem) => marketing?.delivery_order?.flatMap((doItem) =>
doItem.deliveries.map((delivery) => ({ DeliveryProductToFieldValues(marketing?.sales_order, doItem).map(
...delivery, (delivery) => ({
do_number: doItem.do_number, ...delivery,
delivery_date: doItem.delivery_date, do_number: doItem.do_number,
warehouse: doItem.warehouse, delivery_date: doItem.delivery_date,
})) warehouse: doItem.warehouse,
})
)
) ?? [] ) ?? []
); );
}, [marketing?.delivery_order, hasDeliveryOrder]); }, [marketing?.delivery_order, hasDeliveryOrder]);
@@ -212,7 +216,7 @@ const DeliveryOrderProductTable = ({
}; };
const renderDeliveryOrderContent = ( const renderDeliveryOrderContent = (
item: BaseDelivery & { item: DeliveryOrderProductFormValues & {
do_number: string; do_number: string;
delivery_date: string; delivery_date: string;
warehouse: Warehouse; warehouse: Warehouse;
@@ -231,6 +235,24 @@ const DeliveryOrderProductTable = ({
<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'> <div className='flex w-full flex-row gap-1 items-center justify-between h-full'>
<div>Value</div> <div>Value</div>
{formType !== 'success' &&
(formType === 'add_delivery' ||
formType === 'edit_delivery' ||
formType === 'detail') && (
<div className='flex flex-row gap-1.5 items-center'>
<Button
type='button'
variant='ghost'
color='none'
onClick={() => {
onEditRef.current(item.id as number, item);
}}
className='p-0 hover:text-base-content'
>
<Icon icon='heroicons:pencil' width={20} height={20} />
</Button>
</div>
)}
</div> </div>
</th> </th>
</tr> </tr>
@@ -242,14 +264,14 @@ const DeliveryOrderProductTable = ({
<tr> <tr>
<td className='text-sm px-4 py-3'>Produk</td> <td className='text-sm px-4 py-3'>Produk</td>
<td className='text-sm px-4 py-3'> <td className='text-sm px-4 py-3'>
{item.product_warehouse?.product?.name} {item.marketing_product?.product_warehouse_data?.product.name}
</td> </td>
</tr> </tr>
<tr> <tr>
<td className='text-sm px-4 py-3'>Qty</td> <td className='text-sm px-4 py-3'>Qty</td>
<td className='text-sm px-4 py-3'> <td className='text-sm px-4 py-3'>
{item.qty {item.qty
? `${formatNumber(item.qty)} ${item.product_warehouse?.product?.uom?.name ?? ''}` ? `${formatNumber(Number(item.qty))} ${item.marketing_product?.product_warehouse_data?.product.uom.name ?? ''}`
: '-'} : '-'}
</td> </td>
</tr> </tr>
@@ -272,13 +294,13 @@ const DeliveryOrderProductTable = ({
<tr> <tr>
<td className='text-sm px-4 py-3'>Total Harga Satuan</td> <td className='text-sm px-4 py-3'>Total Harga Satuan</td>
<td className='text-sm px-4 py-3'> <td className='text-sm px-4 py-3'>
{formatCurrency(item.unit_price)} {formatCurrency(Number(item.unit_price))}
</td> </td>
</tr> </tr>
<tr> <tr>
<td className='text-sm px-4 py-3'>Total Penjualan</td> <td className='text-sm px-4 py-3'>Total Penjualan</td>
<td className='text-sm px-4 py-3'> <td className='text-sm px-4 py-3'>
{formatCurrency(item.total_price)} {formatCurrency(Number(item.total_price))}
</td> </td>
</tr> </tr>
</> </>
@@ -334,7 +356,9 @@ const DeliveryOrderProductTable = ({
<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'>
{hasDeliveryOrder {hasDeliveryOrder
? deliveryItems.map((item, index) => ( ? deliveryItems.map((item, index) => (
<div key={`do-table-${item.product_warehouse?.id}-${index}`}> <div
key={`do-table-${item.marketing_product?.product_warehouse?.value}-${index}`}
>
{formType === 'success' ? ( {formType === 'success' ? (
<div className='rounded-lg border border-tools-table-outline border-base-content/5'> <div className='rounded-lg border border-tools-table-outline border-base-content/5'>
<table <table
@@ -350,8 +374,11 @@ const DeliveryOrderProductTable = ({
</div> </div>
) : ( ) : (
<Card <Card
key={`do-table-${item.product_warehouse?.id}-${index}`} key={`do-table-${item.marketing_product?.product_warehouse?.value}-${index}`}
title={item.product_warehouse?.product?.name || 'Produk'} title={
item.marketing_product?.product_warehouse_data?.product
.name || 'Produk'
}
collapsible={true} collapsible={true}
defaultCollapsed={false} defaultCollapsed={false}
variant='bordered' variant='bordered'
@@ -48,6 +48,7 @@ import { useUiStore } from '@/stores/ui/ui.store';
import { usePathname } from 'next/navigation'; import { usePathname } from 'next/navigation';
import { Color } from '@/types/theme'; import { Color } from '@/types/theme';
import ButtonFilter from '@/components/helper/ButtonFilter'; import ButtonFilter from '@/components/helper/ButtonFilter';
import Dropdown from '@/components/Dropdown';
// ===== STATUS BADGE UTILITIES ===== // ===== STATUS BADGE UTILITIES =====
const statusTextMap: Record<string, string> = { const statusTextMap: Record<string, string> = {
@@ -352,6 +353,9 @@ const RecordingTable = () => {
const [isRejectLoading, setIsRejectLoading] = useState(false); const [isRejectLoading, setIsRejectLoading] = useState(false);
const [, setApprovalNotes] = useState(''); const [, setApprovalNotes] = useState('');
const [isLoadingExportingToExcel, setIsLoadingExportingToExcel] =
useState(false);
const singleDeleteModal = useModal(); const singleDeleteModal = useModal();
const approveModal = useModal(); const approveModal = useModal();
const rejectModal = useModal(); const rejectModal = useModal();
@@ -686,6 +690,14 @@ const RecordingTable = () => {
}); });
}, [selectedRowIds, recordings, isRecordingApproved]); }, [selectedRowIds, recordings, isRecordingApproved]);
const exportToExcelHandler = async () => {
setIsLoadingExportingToExcel(true);
await RecordingApi.exportToExcel(getTableFilterQueryString());
setIsLoadingExportingToExcel(false);
};
useEffect(() => { useEffect(() => {
if (isResponseSuccess(recordings) && recordings.data) { if (isResponseSuccess(recordings) && recordings.data) {
const newSelection: Record<string, boolean> = {}; const newSelection: Record<string, boolean> = {};
@@ -1313,6 +1325,50 @@ const RecordingTable = () => {
onClick={handleFilterModalOpen} onClick={handleFilterModalOpen}
className='px-3 py-2.5' className='px-3 py-2.5'
/> />
<Dropdown
align='end'
direction='bottom'
trigger={
<Button
variant='outline'
color='none'
className={cn(
'px-3 py-2.5 rounded-lg font-semibold text-sm gap-1.5',
'text-sm text-base-content/50 border border-base-content/10 shadow-button-soft'
)}
>
<Icon
width={20}
height={20}
icon='heroicons:cloud-arrow-down'
/>
Export
<div className='w-6.5 h-5 flex items-center justify-center border-l border-base-content/10'>
<Icon
width={14}
height={14}
icon='heroicons:chevron-down'
/>
</div>
</Button>
}
className={{
content:
'mt-1 rounded-xl border border-base-content/5 shadow-sm overflow-hidden',
}}
>
<Button
variant='ghost'
color='none'
onClick={exportToExcelHandler}
isLoading={isLoadingExportingToExcel}
className='w-full p-3 justify-start text-sm text-base-content/50 font-semibold text-nowrap'
>
<Icon icon='heroicons:table-cells' width={20} height={20} />
Export to Excel
</Button>
</Dropdown>
</div> </div>
</div> </div>
@@ -367,6 +367,9 @@ const PurchaseOrderAcceptApprovalForm = ({
); );
} else { } else {
formik.setFieldValue(`items.${idx}.expedition_vendor_id`, null); formik.setFieldValue(`items.${idx}.expedition_vendor_id`, null);
formik.setFieldValue(`items.${idx}.transport_per_item`, null);
formik.setFieldValue(`items.${idx}.vehicle_number`, null);
} }
}; };
@@ -553,6 +556,7 @@ const PurchaseOrderAcceptApprovalForm = ({
) )
} }
onBlur={formik.handleBlur} onBlur={formik.handleBlur}
disabled={!Boolean(formItem?.expedition_vendor)}
isError={ isError={
isRepeaterInputError(idx, 'vehicle_number').isError isRepeaterInputError(idx, 'vehicle_number').isError
} }
@@ -657,6 +661,7 @@ const PurchaseOrderAcceptApprovalForm = ({
thousandSeparator=',' thousandSeparator=','
decimalSeparator='.' decimalSeparator='.'
inputPrefix={'Rp'} inputPrefix={'Rp'}
disabled={!Boolean(formItem?.expedition_vendor)}
isError={ isError={
isRepeaterInputError(idx, 'transport_per_item') isRepeaterInputError(idx, 'transport_per_item')
.isError .isError
@@ -185,7 +185,12 @@ const PurchaseAcceptApprovalItemObjectSchema: Yup.ObjectSchema<PurchaseAcceptApp
.typeError('No. Surat jalan wajib diisi!'), .typeError('No. Surat jalan wajib diisi!'),
vehicle_number: Yup.string() vehicle_number: Yup.string()
.nullable() .nullable()
.optional() .when('expedition_vendor', {
is: (expeditionVendor?: { value?: number; label?: string } | null) =>
Boolean(expeditionVendor?.value),
then: (schema) => schema.required('Nomor kendaraan wajib diisi!'),
otherwise: (schema) => schema.optional(),
})
.typeError('Nomor kendaraan harus berupa plat nomor!'), .typeError('Nomor kendaraan harus berupa plat nomor!'),
expedition_vendor: Yup.object({ expedition_vendor: Yup.object({
value: Yup.number().min(1).required(), value: Yup.number().min(1).required(),
@@ -213,7 +218,13 @@ const PurchaseAcceptApprovalItemObjectSchema: Yup.ObjectSchema<PurchaseAcceptApp
.typeError('Jumlah diterima harus berupa angka!'), .typeError('Jumlah diterima harus berupa angka!'),
transport_per_item: Yup.mixed<string | number>() transport_per_item: Yup.mixed<string | number>()
.nullable() .nullable()
.optional() .when('expedition_vendor', {
is: (expeditionVendor?: { value?: number; label?: string } | null) =>
Boolean(expeditionVendor?.value),
then: (schema) =>
schema.required('Biaya transport per item wajib diisi!'),
otherwise: (schema) => schema.optional(),
})
.test( .test(
'is-valid-transport-per-item', 'is-valid-transport-per-item',
'Biaya transport per item harus berupa angka lebih dari atau sama dengan 0!', 'Biaya transport per item harus berupa angka lebih dari atau sama dengan 0!',
+26
View File
@@ -12,6 +12,8 @@ import {
NextDayRecording, NextDayRecording,
} from '@/types/api/production/recording'; } from '@/types/api/production/recording';
import { ProjectFlockKandang } from '@/types/api/production/project-flock-kandang'; import { ProjectFlockKandang } from '@/types/api/production/project-flock-kandang';
import { httpClient } from '@/services/http/client';
import { formatDate } from '@/lib/helper';
export const ProjectFlockKandangApi = new BaseApiService< export const ProjectFlockKandangApi = new BaseApiService<
ProjectFlockKandang, ProjectFlockKandang,
@@ -88,6 +90,30 @@ export class RecordingService extends BaseApiService<
} }
); );
} }
async exportToExcel(initialQueryString: string) {
const params = new URLSearchParams(initialQueryString);
params.set('export', 'excel');
const queryString = `?${params.toString()}`;
const res = await httpClient<Blob>(`${this.basePath}${queryString}`, {
method: 'GET',
responseType: 'blob',
});
const url = window.URL.createObjectURL(new Blob([res]));
const link = document.createElement('a');
link.href = url;
const fileName = `recording-${formatDate(Date.now(), 'DD-MM-YYYY')}.xlsx`;
link.setAttribute('download', fileName);
document.body.appendChild(link);
link.click();
link.remove();
}
} }
export const RecordingApi = new RecordingService('/production/recordings'); export const RecordingApi = new RecordingService('/production/recordings');
+7
View File
@@ -9,6 +9,13 @@ export type RequestOptions<B = unknown> = {
auth?: AuthMode; // 'cookie' | 'bearer' | 'none' auth?: AuthMode; // 'cookie' | 'bearer' | 'none'
token?: string; // required if auth === 'bearer' token?: string; // required if auth === 'bearer'
timeoutMs?: number; timeoutMs?: number;
responseType?:
| 'arraybuffer'
| 'blob'
| 'document'
| 'json'
| 'text'
| 'stream';
}; };
export class HttpError extends Error { export class HttpError extends Error {
+1
View File
@@ -40,6 +40,7 @@ export async function httpClient<T, B = unknown>(
data: opts.body, data: opts.body,
timeout: opts.timeoutMs ?? 10_000, timeout: opts.timeoutMs ?? 10_000,
withCredentials: isCookieAuth && !isBearerAuth, withCredentials: isCookieAuth && !isBearerAuth,
responseType: opts.responseType,
headers: { headers: {
...(isFormData ? {} : { 'Content-Type': 'application/json' }), ...(isFormData ? {} : { 'Content-Type': 'application/json' }),
...(opts.headers ?? {}), ...(opts.headers ?? {}),