Merge branch 'fix/cleanup-warning-and-adjust-form-field' into 'development'

[FIX/FE] Cleanup Warning and Refactoring All Form Field (No Warning on Linter)

See merge request mbugroup/lti-web-client!341
This commit is contained in:
Rivaldi A N S
2026-03-06 04:47:39 +00:00
50 changed files with 1812 additions and 1170 deletions
+3 -1
View File
@@ -108,7 +108,9 @@ const Drawer = ({
if (closeOnBackdropClick) {
setOpen(false);
}
onBackdropClick && onBackdropClick();
if (onBackdropClick) {
onBackdropClick();
}
};
return (
+5 -1
View File
@@ -31,7 +31,11 @@ export const useModal = (isNestingModal = false) => {
}, []);
const toggle = useCallback(() => {
open ? closeModal() : openModal();
if (open) {
closeModal();
} else {
openModal();
}
}, [open, closeModal, openModal]);
useEffect(() => {
@@ -308,7 +308,16 @@ const SalesClosingTable = ({ projectFlockId }: SalesClosingTableProps) => {
// },
// },
],
[]
[
summary,
totals.avgActualPrice,
totals.avgSalesPrice,
totals.avgWeight,
totals.totalActualPrice,
totals.totalQuantity,
totals.totalSalesPrice,
totals.totalWeight,
]
);
return (
@@ -150,33 +150,39 @@ const DashboardProduction = () => {
},
});
const { resetForm } = formik;
const handleResetFilter = useCallback(() => {
formik.resetForm();
resetForm();
resetFilterValues(); // Clear stored filter values
setAnalysisMode('OVERVIEW');
setEndpointUrl('/dashboards');
setSelectedLocationIds([]);
}, [resetFilterValues, filterValues, selectedLocationIds]);
}, [resetForm, resetFilterValues]);
const handleApplyFilter = (values: DashboardFilter) => {
// Build query params object, only include non-empty values
const params: Record<string, string> = {};
const handleApplyFilter = useCallback(
(values: DashboardFilter) => {
// Build query params object, only include non-empty values
const params: Record<string, string> = {};
if (values.start_date) params.start_date = values.start_date;
if (values.end_date) params.end_date = values.end_date;
if (values.analysis_mode) params.analysis_mode = values.analysis_mode;
if (values.location_ids.length > 0)
params.location_ids = values.location_ids.toString();
if (values.flock_ids.length > 0)
params.flock_ids = values.flock_ids.toString();
if (values.kandang_ids.length > 0)
params.kandang_ids = values.kandang_ids.toString();
if (values.comparison_type) params.comparison_type = values.comparison_type;
if (values.start_date) params.start_date = values.start_date;
if (values.end_date) params.end_date = values.end_date;
if (values.analysis_mode) params.analysis_mode = values.analysis_mode;
if (values.location_ids.length > 0)
params.location_ids = values.location_ids.toString();
if (values.flock_ids.length > 0)
params.flock_ids = values.flock_ids.toString();
if (values.kandang_ids.length > 0)
params.kandang_ids = values.kandang_ids.toString();
if (values.comparison_type)
params.comparison_type = values.comparison_type;
setEndpointUrl(`/dashboards?${new URLSearchParams(params).toString()}`);
filterModal.closeModal();
refreshDashboardProductionData();
};
setEndpointUrl(`/dashboards?${new URLSearchParams(params).toString()}`);
filterModal.closeModal();
refreshDashboardProductionData();
},
[filterModal, refreshDashboardProductionData]
);
// ===== Load filter from store on mount =====
useEffect(() => {
@@ -190,20 +196,20 @@ const DashboardProduction = () => {
kandang_ids: normalizeToArray(filterValues.kandang),
comparison_type: filterValues.comparisonType,
});
}, [filterValues]);
}, [filterValues, handleApplyFilter]);
// ===== Formik Error List =====
const { formErrorList, close, handleFormSubmit } = useFormikErrorList(formik);
// ===== Export PDF =====
const handleExportPDF = async () => {
const handleExportPDF = useCallback(async () => {
await generateDashboardPDF({
filterValues: formik.values,
allStatsRef,
allChartsRef,
setExporting,
});
};
}, [formik.values]);
// ===== Register Navbar Actions =====
const openFilterModalRef = useRef(filterModal.openModal);
@@ -253,7 +259,7 @@ const DashboardProduction = () => {
</Dropdown>
</div>
);
}, [formik.values, exporting, setNavbarActions]);
}, [formik.values, exporting, setNavbarActions, handleExportPDF]);
// Cleanup only on unmount
useEffect(() => {
@@ -409,14 +409,14 @@ const DashboardLineChart = ({
axisLine={{ stroke: '#C1C1C180', opacity: 0.5 }}
domain={(() => {
// Calculate dynamic domain based on visible data
let seriesData: DashboardChartsSeries[] = [];
// let seriesData: DashboardChartsSeries[] = [];
let dataset: DashboardChartsDataset[] = [];
if (
analysisMode === 'OVERVIEW' &&
isOverviewCharts(data.charts)
) {
seriesData = data.charts[chartData]?.series || [];
// seriesData = data.charts[chartData]?.series || [];
dataset = data.charts[chartData]?.dataset || [];
} else if (
analysisMode === 'COMPARISON' &&
@@ -426,7 +426,7 @@ const DashboardLineChart = ({
data.charts.farm ||
data.charts.flock ||
data.charts.kandang;
seriesData = comparisonChart?.series || [];
// seriesData = comparisonChart?.series || [];
dataset = comparisonChart?.dataset || [];
}
@@ -1,6 +1,6 @@
'use client';
import { useCallback, useEffect, useState } from 'react';
import { useCallback, useEffect, useRef, useState } from 'react';
import useSWR from 'swr';
import { Icon } from '@iconify/react';
@@ -75,6 +75,12 @@ const ExpenseKandangsTable = ({
.filter((id): id is number => id !== undefined)
)
);
const rowSelectionRef = useRef(rowSelection);
const prevRowSelectionRef = useRef<Record<string, boolean>>({});
useEffect(() => {
rowSelectionRef.current = rowSelection;
}, [rowSelection]);
const kandangsColumns: ColumnDef<Kandang>[] = [
{
@@ -133,33 +139,43 @@ const ExpenseKandangsTable = ({
useEffect(() => {
setOpen(isResponseSuccess(kandangs) ? kandangs.data.length > 0 : false);
}, [kandangs, isResponseSuccess]);
}, [kandangs]);
useEffect(() => {
if (Object.keys(rowSelection).length !== 0 && isResponseSuccess(kandangs)) {
const formattedSelectedKandangs = Object.keys(rowSelection).map(
(item) => {
const selectedKandang = kandangs.data.find(
(kandang) => kandang.id === parseInt(item)
);
const currentKeys = Object.keys(rowSelection).sort().join(',');
const prevKeys = Object.keys(prevRowSelectionRef.current).sort().join(',');
return {
id: parseInt(item),
name: selectedKandang?.name ?? 'Kandang tidak ditemukan!',
};
}
);
if (currentKeys !== prevKeys) {
prevRowSelectionRef.current = { ...rowSelection };
onChange(formattedSelectedKandangs);
} else {
onChange([]);
if (
Object.keys(rowSelection).length !== 0 &&
isResponseSuccess(kandangs)
) {
const formattedSelectedKandangs = Object.keys(rowSelection).map(
(item) => {
const selectedKandang = kandangs.data.find(
(kandang) => kandang.id === parseInt(item)
);
return {
id: parseInt(item),
name: selectedKandang?.name ?? 'Kandang tidak ditemukan!',
};
}
);
onChange(formattedSelectedKandangs);
} else if (Object.keys(rowSelection).length === 0) {
onChange([]);
}
}
}, [rowSelection]);
}, [rowSelection, kandangs, onChange]);
useEffect(() => {
if (
selectedKandangs.length === 0 &&
Object.keys(rowSelection).length !== 0
Object.keys(rowSelectionRef.current).length !== 0
) {
setRowSelection({});
}
@@ -1,6 +1,6 @@
'use client';
import { useCallback, useEffect, useState } from 'react';
import { useCallback, useState } from 'react';
import { useRouter } from 'next/navigation';
import { useFormik } from 'formik';
import toast from 'react-hot-toast';
@@ -90,6 +90,7 @@ const ExpenseRealizationForm = ({
const formik = useFormik<ExpenseRealizationFormValues>({
initialValues: getExpenseRealizationFormInitialValues(initialValues),
enableReinitialize: true,
validationSchema:
type === 'edit'
? UpdateExpenseRealizationFormSchema
@@ -143,7 +144,6 @@ const ExpenseRealizationForm = ({
},
});
const { setValues: formikSetValues } = formik;
const { formErrorList, close, handleFormSubmit } = useFormikErrorList(formik);
const {
@@ -254,10 +254,6 @@ const ExpenseRealizationForm = ({
formik.setFieldValue('documents', newRequestDocuments);
};
useEffect(() => {
formikSetValues(getExpenseRealizationFormInitialValues(initialValues));
}, [formikSetValues, getExpenseRealizationFormInitialValues, initialValues]);
return (
<section className='w-full'>
<header className='flex flex-col gap-4'>
@@ -1,6 +1,6 @@
'use client';
import { useCallback, useEffect, useState } from 'react';
import { useCallback, useState } from 'react';
import { useRouter } from 'next/navigation';
import { useFormik } from 'formik';
import { toast } from 'react-hot-toast';
@@ -102,6 +102,7 @@ const ExpenseRequestForm = ({
const formik = useFormik<ExpenseRequestFormValues>({
initialValues: getExpenseFormInitialValues(initialValues),
enableReinitialize: true,
validationSchema:
type === 'edit'
? UpdateExpenseRequestFormSchema
@@ -171,7 +172,7 @@ const ExpenseRequestForm = ({
},
});
const { setValues: formikSetValues } = formik;
const { setFieldValue, setFieldTouched } = formik;
const {
setInputValue: setLocationInputValue,
@@ -186,8 +187,8 @@ const ExpenseRequestForm = ({
} = useSelect<Supplier>(SupplierApi.basePath, 'id', 'name');
const categoryChangeHandler = (val: OptionType | OptionType[] | null) => {
formik.setFieldTouched('category', true);
formik.setFieldValue('category', val);
setFieldTouched('category', true);
setFieldValue('category', val);
};
const locationChangeHandler = useCallback(
@@ -195,12 +196,12 @@ const ExpenseRequestForm = ({
const location = val as OptionType | null;
const locationId = location ? Number(location.value) : 0;
formik.setFieldTouched('location', true);
formik.setFieldValue('location', location);
formik.setFieldTouched('location_id', true);
formik.setFieldValue('location_id', locationId);
setFieldTouched('location', true);
setFieldValue('location', location);
setFieldTouched('location_id', true);
setFieldValue('location_id', locationId);
},
[]
[setFieldTouched, setFieldValue]
);
const kandangsChangeHandler = (
@@ -343,10 +344,6 @@ const ExpenseRequestForm = ({
formik.handleSubmit(e);
};
useEffect(() => {
formikSetValues(getExpenseFormInitialValues(initialValues));
}, [formikSetValues, getExpenseFormInitialValues, initialValues]);
return (
<>
<section className='w-full'>
+20 -15
View File
@@ -2,7 +2,6 @@ import Button from '@/components/Button';
import Card from '@/components/Card';
import { FormHeader } from '@/components/helper/form/FormHeader';
import RequirePermission from '@/components/helper/RequirePermission';
import DebouncedTextInput from '@/components/input/DebouncedTextInput';
import { useModal } from '@/components/Modal';
import ConfirmationModal from '@/components/modal/ConfirmationModal';
import Table from '@/components/Table';
@@ -26,11 +25,13 @@ const FinanceDetail = ({ finance }: { finance: Finance }) => {
const informasiUmum = [
{
label: 'ID',
value: finance.payment_code,
value: finance.payment_code || '-',
},
{
label: 'Jenis Transaksi',
value: formatTitleCase(finance.transaction_type.split('_').join(' ')),
value: formatTitleCase(
(finance.transaction_type || '').split('_').join(' ')
),
},
{
label: 'Pihak',
@@ -38,11 +39,13 @@ const FinanceDetail = ({ finance }: { finance: Finance }) => {
},
{
label: 'Tanggal',
value: formatDate(finance.payment_date, 'DD MMM yyyy'),
value: finance.payment_date
? formatDate(finance.payment_date, 'DD MMM yyyy')
: '-',
},
{
label: 'Metode Pembayaran',
value: finance.payment_method,
value: finance.payment_method || '-',
},
{
label: 'Catatan',
@@ -61,22 +64,22 @@ const FinanceDetail = ({ finance }: { finance: Finance }) => {
: '-',
},
{
label: `Rekening ${formatTitleCase(finance.party?.type)}`,
value: finance.party?.account_number,
label: `Rekening ${formatTitleCase(finance.party?.type || '')}`,
value: finance.party?.account_number || '-',
},
{
label: 'Nominal',
value: formatCurrency(
finance.transaction_type === 'INJECTION'
? finance.nominal
: Math.abs(finance.nominal)
? finance.nominal || 0
: Math.abs(finance.nominal || 0)
),
},
].filter((item) => {
// Hide party account number row if transaction type is INJECTION
if (
FINANCE_INJECTION_STATUS.includes(finance.transaction_type) &&
item.label === `Rekening ${formatTitleCase(finance.party?.type)}`
FINANCE_INJECTION_STATUS.includes(finance.transaction_type || '') &&
item.label === `Rekening ${formatTitleCase(finance.party?.type || '')}`
) {
return false;
}
@@ -150,7 +153,7 @@ const FinanceDetail = ({ finance }: { finance: Finance }) => {
</Card>
<div className='flex flex-row gap-2 justify-end'>
{FINANCE_TRANSACTION_STATUS.includes(finance.transaction_type) &&
{FINANCE_TRANSACTION_STATUS.includes(finance.transaction_type || '') &&
finance.party?.type !== 'SUPPLIER' && (
<RequirePermission permissions='lti.finance.payments.update'>
<Button
@@ -163,7 +166,9 @@ const FinanceDetail = ({ finance }: { finance: Finance }) => {
</Button>
</RequirePermission>
)}
{FINANCE_INITIAL_BALANCE_STATUS.includes(finance.transaction_type) && (
{FINANCE_INITIAL_BALANCE_STATUS.includes(
finance.transaction_type || ''
) && (
<RequirePermission permissions='lti.finance.initial_balances.update'>
<Button
color='warning'
@@ -175,7 +180,7 @@ const FinanceDetail = ({ finance }: { finance: Finance }) => {
</Button>
</RequirePermission>
)}
{FINANCE_INJECTION_STATUS.includes(finance.transaction_type) && (
{FINANCE_INJECTION_STATUS.includes(finance.transaction_type || '') && (
<RequirePermission permissions='lti.finance.injections.update'>
<Button
color='warning'
@@ -201,7 +206,7 @@ const FinanceDetail = ({ finance }: { finance: Finance }) => {
<ConfirmationModal
ref={deleteModal.ref}
type='error'
text={`Apakah anda yakin ingin menghapus data Finance ini (${finance?.payment_code})?`}
text={`Apakah anda yakin ingin menghapus data Finance ini (${finance?.payment_code || ''})?`}
secondaryButton={{
text: 'Tidak',
}}
@@ -594,7 +594,7 @@ const FinanceTable = () => {
},
},
],
[]
[deleteModal]
);
useEffect(() => {
@@ -282,6 +282,9 @@ const InventoryAdjustmentTable = () => {
if (recordingOption) {
return recordingOption.label;
}
if (subtypeValue === 'RECORDING_DEPLETION_OUT') {
return 'Recording Depletion';
}
return subtypeValue || '-';
};
@@ -26,6 +26,11 @@ export type InventoryAdjustmentFormSchemaType = {
label: string;
} | null;
product_id: number;
depletion_product: {
value: number;
label: string;
} | null;
depletion_product_id: number;
transaction_type: string;
transaction_subtype: string;
qty: number | string;
@@ -80,6 +85,13 @@ export const InventoryAdjustmentFormSchema: Yup.ObjectSchema<InventoryAdjustment
.min(1, 'Produk wajib diisi!')
.required('Produk wajib diisi!')
.typeError('Produk wajib diisi!'),
depletion_product: Yup.object({
value: Yup.number().min(1).required(),
label: Yup.string().required(),
}).nullable(),
depletion_product_id: Yup.number()
.default(0)
.typeError('Jenis deplesi harus berupa angka'),
transaction_type: Yup.string()
.min(1, 'Tipe transaksi wajib diisi!')
.oneOf(
@@ -74,6 +74,8 @@ const InventoryAdjustmentForm = ({
useState<OptionType | null>(null);
const [selectedTransactionSubtype, setSelectedTransactionSubtype] =
useState<OptionType | null>(null);
const [selectedDepletionProduct, setSelectedDepletionProduct] =
useState<OptionType | null>(null);
const [selectedProjectFlockLocationId, setSelectedProjectFlockLocationId] =
useState<string>('');
@@ -185,6 +187,15 @@ const InventoryAdjustmentForm = ({
rawData: products,
} = useSelect<Product>(ProductApi.basePath, 'id', 'name', 'search');
const {
setInputValue: setDepletionProductInputValue,
options: depletionProductOptions,
isLoadingOptions: isLoadingDepletionProductOptions,
loadMore: loadMoreDepletionProducts,
} = useSelect<Product>(ProductApi.basePath, 'id', 'name', 'search', {
is_depletion: 'true',
});
const productOptions = useMemo(() => {
if (!isResponseSuccess(products)) return [];
@@ -241,6 +252,8 @@ const InventoryAdjustmentForm = ({
project_flock_kandang_id: 0,
product: null,
product_id: 0,
depletion_product: null,
depletion_product_id: 0,
transaction_type: '',
transaction_subtype: '',
qty: '',
@@ -260,7 +273,10 @@ const InventoryAdjustmentForm = ({
setInventoryAdjustmentFormErrorMessage('');
const payload: CreateInventoryAdjustmentPayload = {
project_flock_kandang_id: values.project_flock_kandang_id,
product_id: values.product_id,
product_id:
values.depletion_product_id > 0
? values.depletion_product_id
: values.product_id,
transaction_subtype: values.transaction_subtype,
qty: Number(values.qty),
price: Number(values.price),
@@ -275,6 +291,8 @@ const InventoryAdjustmentForm = ({
},
});
const { setFieldValue, setFieldTouched, resetForm, setValues } = formik;
const transactionSubtypeOptions = useMemo(() => {
const transactionType = selectedTransactionType?.value;
@@ -321,19 +339,31 @@ const InventoryAdjustmentForm = ({
useEffect(() => {
if (selectedTransactionType?.value === 'RECORDING' && selectedProduct) {
setSelectedTransactionSubtype(null);
formik.setFieldValue('transaction_subtype', '');
setFieldValue('transaction_subtype', '');
}
}, [selectedProduct, selectedTransactionType]);
}, [setFieldValue, selectedProduct, selectedTransactionType]);
const isDepletionProductVisible = useMemo(() => {
return selectedTransactionSubtype?.value === 'RECORDING_DEPLETION_IN';
}, [selectedTransactionSubtype]);
useEffect(() => {
if (!isDepletionProductVisible) {
setSelectedDepletionProduct(null);
setFieldValue('depletion_product', null);
setFieldValue('depletion_product_id', 0);
}
}, [isDepletionProductVisible, setFieldValue]);
// ===== EVENT HANDLERS =====
const locationChangeHandler = (val: OptionType | OptionType[] | null) => {
const location = val as OptionType | null;
const locationId = location ? Number(location.value) : 0;
formik.setFieldTouched('location', true);
formik.setFieldValue('location', location);
formik.setFieldTouched('location_id', true);
formik.setFieldValue('location_id', locationId);
setFieldTouched('location', true);
setFieldValue('location', location);
setFieldTouched('location_id', true);
setFieldValue('location_id', locationId);
setSelectedLocation(location);
setSelectedProjectFlock(null);
@@ -348,10 +378,10 @@ const InventoryAdjustmentForm = ({
const projectFlock = val as OptionType | null;
const projectFlockId = Number(projectFlock?.value);
formik.setFieldTouched('project_flock', true);
formik.setFieldValue('project_flock', projectFlock);
formik.setFieldTouched('project_flock_id', true);
formik.setFieldValue('project_flock_id', projectFlockId);
setFieldTouched('project_flock', true);
setFieldValue('project_flock', projectFlock);
setFieldTouched('project_flock_id', true);
setFieldValue('project_flock_id', projectFlockId);
setSelectedProjectFlock(projectFlock);
setSelectedKandang(null);
@@ -362,44 +392,58 @@ const InventoryAdjustmentForm = ({
const kandang = val as OptionType | null;
const kandangId = Number(kandang?.value);
formik.setFieldTouched('kandang', true);
formik.setFieldValue('kandang', kandang);
formik.setFieldTouched('kandang_id', true);
formik.setFieldValue('kandang_id', kandangId);
setFieldTouched('kandang', true);
setFieldValue('kandang', kandang);
setFieldTouched('kandang_id', true);
setFieldValue('kandang_id', kandangId);
setSelectedKandang(kandang);
setSelectedProduct(null);
formik.setFieldTouched('project_flock_kandang', true);
formik.setFieldTouched('project_flock_kandang_id', true);
setFieldTouched('project_flock_kandang', true);
setFieldTouched('project_flock_kandang_id', true);
};
const productChangeHandler = (val: OptionType | OptionType[] | null) => {
const product = val as OptionType | null;
const productId = (product?.value as number) ?? 0;
formik.setFieldTouched('product', true);
formik.setFieldValue('product', product);
formik.setFieldTouched('product_id', true);
formik.setFieldValue('product_id', productId);
setFieldTouched('product', true);
setFieldValue('product', product);
setFieldTouched('product_id', true);
setFieldValue('product_id', productId);
setSelectedProduct(product);
};
const depletionProductChangeHandler = (
val: OptionType | OptionType[] | null
) => {
const depletionProduct = val as OptionType | null;
const depletionProductId = (depletionProduct?.value as number) ?? 0;
setFieldTouched('depletion_product', true);
setFieldValue('depletion_product', depletionProduct);
setFieldTouched('depletion_product_id', true);
setFieldValue('depletion_product_id', depletionProductId);
setSelectedDepletionProduct(depletionProduct);
};
useEffect(() => {
const transactionType = formik.values.transaction_type;
if (!transactionType) {
setSelectedTransactionSubtype(null);
formik.setFieldValue('transaction_subtype', '');
setFieldValue('transaction_subtype', '');
return;
}
setSelectedTransactionSubtype(null);
formik.setFieldValue('transaction_subtype', '');
formik.setFieldTouched('transaction_subtype', true);
setFieldValue('transaction_subtype', '');
setFieldTouched('transaction_subtype', true);
if (transactionType === 'PEMBELIAN') {
formik.setFieldValue(
setFieldValue(
'transaction_subtype',
TRANSACTION_SUBTYPE_OPTIONS.PEMBELIAN.value
);
@@ -408,7 +452,7 @@ const InventoryAdjustmentForm = ({
label: TRANSACTION_SUBTYPE_OPTIONS.PEMBELIAN.label,
});
} else if (transactionType === 'PENJUALAN') {
formik.setFieldValue(
setFieldValue(
'transaction_subtype',
TRANSACTION_SUBTYPE_OPTIONS.PENJUALAN.value
);
@@ -417,7 +461,7 @@ const InventoryAdjustmentForm = ({
label: TRANSACTION_SUBTYPE_OPTIONS.PENJUALAN.label,
});
}
}, [formik.values.transaction_type]);
}, [setFieldTouched, setFieldValue, formik.values.transaction_type]);
const transactionTypeChangeHandler = (
val: OptionType | OptionType[] | null
@@ -425,8 +469,8 @@ const InventoryAdjustmentForm = ({
const typeOption = val as OptionType | null;
const selectedType = typeOption?.value as string;
formik.setFieldValue('transaction_type', selectedType);
formik.setFieldTouched('transaction_type', true);
setFieldValue('transaction_type', selectedType);
setFieldTouched('transaction_type', true);
setSelectedTransactionType(typeOption);
};
@@ -437,20 +481,21 @@ const InventoryAdjustmentForm = ({
const subtypeOption = val as OptionType | null;
const selectedSubtype = subtypeOption?.value as string;
formik.setFieldTouched('transaction_subtype', true);
formik.setFieldValue('transaction_subtype', selectedSubtype);
setFieldTouched('transaction_subtype', true);
setFieldValue('transaction_subtype', selectedSubtype);
setSelectedTransactionSubtype(subtypeOption);
};
const resetHandler = () => {
formik.resetForm();
resetForm();
setSelectedLocation(null);
setSelectedProjectFlock(null);
setSelectedKandang(null);
setSelectedProduct(null);
setSelectedTransactionType(null);
setSelectedTransactionSubtype(null);
setSelectedDepletionProduct(null);
setSelectedProjectFlockLocationId('');
};
@@ -460,14 +505,18 @@ const InventoryAdjustmentForm = ({
projectFlockKandangLookup.project_flock_kandang_id;
if (formik.values.project_flock_kandang_id !== projectFlockKandangId) {
formik.setFieldValue('project_flock_kandang_id', projectFlockKandangId);
formik.setFieldValue('project_flock_kandang', {
setFieldValue('project_flock_kandang_id', projectFlockKandangId);
setFieldValue('project_flock_kandang', {
value: projectFlockKandangId,
label: `${projectFlockKandangLookup.project_flock.flock_name} - ${projectFlockKandangLookup.kandang.name}`,
});
}
}
}, [projectFlockKandangLookup, formik.values.project_flock_kandang_id]);
}, [
projectFlockKandangLookup,
formik.values.project_flock_kandang_id,
setFieldValue,
]);
useEffect(() => {
if (initialValues && type === 'detail') {
@@ -519,7 +568,7 @@ const InventoryAdjustmentForm = ({
});
}
formik.setValues({
setValues({
location: initialValues.location
? {
value: initialValues.location.id,
@@ -550,6 +599,8 @@ const InventoryAdjustmentForm = ({
}
: null,
product_id: initialValues.product_warehouse?.product?.id ?? 0,
depletion_product: null,
depletion_product_id: 0,
transaction_type: transactionType,
transaction_subtype: transactionSubtype,
qty: initialValues.qty ?? '',
@@ -557,7 +608,7 @@ const InventoryAdjustmentForm = ({
notes: initialValues.notes ?? '',
});
}
}, [formik.setValues, initialValues, type]);
}, [setValues, initialValues, type]);
// ===== Formik Error List =====
const { formErrorList, close, handleFormSubmit } = useFormikErrorList(formik);
@@ -729,6 +780,29 @@ const InventoryAdjustmentForm = ({
isSearchable
/>
{/* RECORDING_DEPLETION_IN */}
{isDepletionProductVisible && (
<SelectInput
required
label='Jenis Deplesi'
value={selectedDepletionProduct}
onChange={depletionProductChangeHandler}
onInputChange={setDepletionProductInputValue}
options={depletionProductOptions}
onMenuScrollToBottom={loadMoreDepletionProducts}
isLoading={isLoadingDepletionProductOptions}
isError={
formik.touched.depletion_product_id &&
Boolean(formik.errors.depletion_product_id)
}
errorMessage={formik.errors.depletion_product_id as string}
isDisabled={type === 'detail'}
placeholder='Pilih Jenis Deplesi'
isClearable
isSearchable
/>
)}
{/* Number Input Quantity */}
<NumberInput
className={{
@@ -1,7 +1,6 @@
'use client';
import AlertErrorList from '@/components/helper/form/FormErrors';
import { OptionType } from '@/components/input/SelectInput';
import Modal, { useModal } from '@/components/Modal';
import ConfirmationModal from '@/components/modal/ConfirmationModal';
import { DeliveryOrderProductFormValues } from '@/components/pages/marketing/form/repeater/delivery-order/DeliverOrderProduct.schema';
@@ -112,16 +111,34 @@ const DeliveryOrderFormModal = ({}: { initialValues?: Marketing }) => {
useState<DeliveryOrderProductFormValues | null>(null);
const [deliveryOrderValues, setDeliveryOrderValues] = useState<
DeliveryOrderProductFormValues[]
>(
isResponseSuccess(marketing)
? mergeSOwithDO(
marketing?.data.sales_order?.map(SalesProductToFieldValues) ?? [],
marketing?.data.delivery_order?.flatMap((delivery) =>
DeliveryProductToFieldValues(marketing.data.sales_order, delivery)
) ?? [],
true
)
: []
>([]);
const getDeliveryOrderValues = useCallback(
(marketingData: Marketing): DeliveryOrderProductFormValues[] => {
const hasDeliveryOrder =
marketingData.delivery_order &&
marketingData.delivery_order.length > 0 &&
marketingData.delivery_order.some(
(doItem) => doItem.deliveries && doItem.deliveries.length > 0
);
if (hasDeliveryOrder) {
return (
marketingData.delivery_order?.flatMap((delivery) =>
DeliveryProductToFieldValues(marketingData.sales_order, delivery)
) ?? []
);
}
return mergeSOwithDO(
marketingData.sales_order?.map(SalesProductToFieldValues) ?? [],
marketingData.delivery_order?.flatMap((delivery) =>
DeliveryProductToFieldValues(marketingData.sales_order, delivery)
) ?? [],
true
);
},
[]
);
// ================== SETUP FORMIK ==================
@@ -130,14 +147,8 @@ const DeliveryOrderFormModal = ({}: { initialValues?: Marketing }) => {
>(() => {
if (!isResponseSuccess(marketing))
return {} as SalesOrderFormValues & DeliveryOrderFormValues;
const deliveryValues = mergeSOwithDO(
marketing?.data.sales_order?.map(SalesProductToFieldValues) ?? [],
marketing?.data.delivery_order?.flatMap((delivery) =>
DeliveryProductToFieldValues(marketing.data.sales_order, delivery)
) ?? [],
true
);
const deliveryValues = getDeliveryOrderValues(marketing.data);
setDeliveryOrderValues(deliveryValues);
return {
@@ -163,7 +174,7 @@ const DeliveryOrderFormModal = ({}: { initialValues?: Marketing }) => {
) ?? [],
delivery_order: deliveryValues,
};
}, [marketing]);
}, [marketing, getDeliveryOrderValues]);
const formik = useFormik<SalesOrderFormValues & DeliveryOrderFormValues>({
enableReinitialize: true,
@@ -648,9 +659,8 @@ const DeliveryOrderFormModal = ({}: { initialValues?: Marketing }) => {
<tr>
<td className='text-sm px-4 py-3'>No. Order</td>
<td className='text-sm px-4 py-3'>
{marketing.data.do_number
? marketing.data.do_number
: marketing.data.so_number}
{marketing.data.do_number ||
marketing.data.so_number}
</td>
</tr>
<tr>
@@ -765,6 +775,7 @@ const DeliveryOrderFormModal = ({}: { initialValues?: Marketing }) => {
<MemoizedDeliveryOrderProductForm
formState={'edit'}
salesOrders={marketing?.data?.sales_order ?? []}
deliveryOrders={marketing?.data?.delivery_order ?? []}
exisitingValues={deliveryOrderValues}
onSubmitForm={handleAddSubmitDO}
initialValues={selectedDeliveryProduct ?? undefined}
@@ -10,7 +10,10 @@ import NumberInput from '@/components/input/NumberInput';
import PatternInput from '@/components/input/PatternInput';
import { formatTitleCase, formatVechicleNumber } from '@/lib/helper';
import DateInput from '@/components/input/DateInput';
import { BaseSalesOrder } from '@/types/api/marketing/marketing';
import {
BaseSalesOrder,
BaseDeliveryOrder,
} from '@/types/api/marketing/marketing';
import { SalesProductToFieldValues } from '@/components/pages/marketing/form/MarketingForm.schema';
import * as Yup from 'yup';
import { isResponseSuccess } from '@/lib/api-helper';
@@ -32,6 +35,7 @@ import { handleMarketingCalculation } from '@/lib/marketing-calculation';
const DeliveryOrderProductForm = ({
formState,
salesOrders,
deliveryOrders,
initialValues,
exisitingValues,
onUpdateForm,
@@ -39,6 +43,7 @@ const DeliveryOrderProductForm = ({
}: {
formState: 'add' | 'edit';
salesOrders: BaseSalesOrder[];
deliveryOrders?: BaseDeliveryOrder[];
initialValues?: DeliveryOrderProductFormValues;
exisitingValues?: DeliveryOrderProductFormValues[];
onSubmitForm?: (value: DeliveryOrderProductFormValues) => Promise<void>;
@@ -115,6 +120,36 @@ const DeliveryOrderProductForm = ({
})
?.filter((item) => item != null) as OptionType[];
const hasDeliveryOrder = useMemo(() => {
return (
deliveryOrders &&
deliveryOrders.length > 0 &&
deliveryOrders.some(
(doItem) => doItem.deliveries && doItem.deliveries.length > 0
)
);
}, [deliveryOrders]);
const deliveryOrder = useMemo(() => {
if (!hasDeliveryOrder || !deliveryOrders) return null;
for (const doItem of deliveryOrders) {
const found = doItem.deliveries.find(
(d) =>
d.product_warehouse.id ===
initialValues?.marketing_product?.product_warehouse_id
);
if (found) {
return {
...found,
delivery_date: doItem.delivery_date,
do_number: doItem.do_number,
};
}
}
return null;
}, [deliveryOrders, hasDeliveryOrder, initialValues]);
const salesOrder = salesOrders.find(
(item) => item.id === initialValues?.marketing_product_id
);
@@ -122,15 +157,25 @@ const DeliveryOrderProductForm = ({
const formik = useFormik<DeliveryOrderProductFormValues>({
enableReinitialize: true,
initialValues: {
delivery_date: initialValues?.delivery_date || undefined,
vehicle_number: initialValues?.vehicle_number || undefined,
delivery_date:
deliveryOrder?.delivery_date ||
initialValues?.delivery_date ||
undefined,
vehicle_number:
deliveryOrder?.vehicle_number ||
initialValues?.vehicle_number ||
undefined,
marketing_product_id:
salesOrder?.id || initialValues?.marketing_product_id || undefined,
unit_price: initialValues?.unit_price || undefined,
total_weight: initialValues?.total_weight || undefined,
qty: initialValues?.qty || undefined,
avg_weight: initialValues?.avg_weight || undefined,
total_price: initialValues?.total_price || undefined,
unit_price:
deliveryOrder?.unit_price ?? initialValues?.unit_price ?? undefined,
total_weight:
deliveryOrder?.total_weight ?? initialValues?.total_weight ?? undefined,
qty: deliveryOrder?.qty ?? initialValues?.qty ?? undefined,
avg_weight:
deliveryOrder?.avg_weight ?? initialValues?.avg_weight ?? undefined,
total_price:
deliveryOrder?.total_price ?? initialValues?.total_price ?? undefined,
marketing_product: initialValues?.marketing_product || undefined,
uom: initialValues?.uom || '',
weight_per_convertion:
@@ -2,10 +2,11 @@ import { DeliveryOrderProductFormValues } from '@/components/pages/marketing/for
import Button from '@/components/Button';
import Card from '@/components/Card';
import { Icon } from '@iconify/react';
import { useRef } from 'react';
import { useRef, useMemo } from 'react';
import { formatCurrency, formatDate, formatNumber } from '@/lib/helper';
import DeliveryOrderExport from '@/components/pages/marketing/pdf/DeliveryOrderExport';
import { Marketing } from '@/types/api/marketing/marketing';
import { Marketing, BaseDelivery } from '@/types/api/marketing/marketing';
import { Warehouse } from '@/types/api/master-data/warehouse';
type DeliveryOrderProductTableProps = {
data: DeliveryOrderProductFormValues[];
@@ -42,7 +43,31 @@ const DeliveryOrderProductTable = ({
const approvalStepNumber = marketing?.latest_approval?.step_number;
const renderTableContent = (item: DeliveryOrderProductFormValues) => {
const hasDeliveryOrder = useMemo(() => {
return (
marketing?.delivery_order &&
marketing.delivery_order.length > 0 &&
marketing.delivery_order.some(
(doItem) => doItem.deliveries && doItem.deliveries.length > 0
)
);
}, [marketing?.delivery_order]);
const deliveryItems = useMemo(() => {
if (!hasDeliveryOrder) return [];
return (
marketing?.delivery_order?.flatMap((doItem) =>
doItem.deliveries.map((delivery) => ({
...delivery,
do_number: doItem.do_number,
delivery_date: doItem.delivery_date,
warehouse: doItem.warehouse,
}))
) ?? []
);
}, [marketing?.delivery_order, hasDeliveryOrder]);
const renderSalesOrderContent = (item: DeliveryOrderProductFormValues) => {
const doItem = marketing?.delivery_order?.find(
(doItem) => doItem.do_number === item.do_number
);
@@ -185,50 +210,217 @@ const DeliveryOrderProductTable = ({
);
};
const renderDeliveryOrderContent = (
item: BaseDelivery & {
do_number: string;
delivery_date: string;
warehouse: Warehouse;
}
) => {
const parentDoItem = marketing?.delivery_order?.find(
(doItem) => doItem.do_number === item.do_number
);
return (
<>
<tr className='border-b border-tools-table-outline border-base-content/5'>
<th className='w-1/3 text-start not-first:font-medium text-base-content/50 text-sm px-4 py-3'>
Label
</th>
<th className='text-start font-medium text-base-content/50 text-sm px-4 py-3'>
<div className='flex w-full flex-row gap-1 items-center justify-between h-full'>
<div>Value</div>
</div>
</th>
</tr>
<>
<tr>
<td className='text-sm px-4 py-3'>Gudang</td>
<td className='text-sm px-4 py-3'>{item.warehouse?.name}</td>
</tr>
<tr>
<td className='text-sm px-4 py-3'>Produk</td>
<td className='text-sm px-4 py-3'>
{item.product_warehouse?.product?.name}
</td>
</tr>
<tr>
<td className='text-sm px-4 py-3'>Qty</td>
<td className='text-sm px-4 py-3'>
{item.qty
? `${formatNumber(item.qty)} ${item.product_warehouse?.product?.uom?.name ?? ''}`
: '-'}
</td>
</tr>
{Number(item.avg_weight ?? 0) > 0 && (
<tr>
<td className='text-sm px-4 py-3'>Avg Bobot</td>
<td className='text-sm px-4 py-3'>
{formatNumber(Number(item.avg_weight))} Kg
</td>
</tr>
)}
{Number(item.total_weight ?? 0) > 0 && (
<tr>
<td className='text-sm px-4 py-3'>Total Bobot</td>
<td className='text-sm px-4 py-3'>
{formatNumber(Number(item.total_weight))}
</td>
</tr>
)}
<tr>
<td className='text-sm px-4 py-3'>Total Harga Satuan</td>
<td className='text-sm px-4 py-3'>
{formatCurrency(item.unit_price)}
</td>
</tr>
<tr>
<td className='text-sm px-4 py-3'>Total Penjualan</td>
<td className='text-sm px-4 py-3'>
{formatCurrency(item.total_price)}
</td>
</tr>
</>
<tr className='border-b border-t border-tools-table-outline border-base-content/5'>
<th className='w-1/3 text-start not-first:font-medium text-base-content/50 text-sm px-4 py-3'>
Label
</th>
<th className='text-start font-medium text-base-content/50 text-sm px-4 py-3'>
<div className='flex w-full flex-row gap-1 items-center justify-between h-full'>
<div>Value</div>
</div>
</th>
</tr>
<>
{approvalStepNumber !== 1 && (
<tr>
<td className='text-sm px-4 py-3'>Tanggal Pengiriman</td>
<td className='text-sm px-4 py-3'>
{item.delivery_date
? formatDate(item.delivery_date, 'DD MMM YYYY')
: '-'}
</td>
</tr>
)}
{item.do_number && (
<tr>
<td className='text-sm px-4 py-3'>No. Pengiriman</td>
<td className='text-sm px-4 py-3'>{item.do_number}</td>
</tr>
)}
<tr>
<td className='text-sm px-4 py-3'>No. Polisi</td>
<td className='text-sm px-4 py-3'>{item.vehicle_number}</td>
</tr>
{parentDoItem && (
<tr>
<td className='text-sm px-4 py-3'>Dokumen Pengiriman</td>
<td className='text-sm px-4 py-3'>
<DeliveryOrderExport
data={marketing}
deliveryOrder={parentDoItem}
/>
</td>
</tr>
)}
</>
</>
);
};
return (
<>
<div className='size-full flex flex-col relative overflow-x-hidden gap-3'>
{data.map((item) => (
<div key={`table-${item.id}`}>
{formType === 'success' ? (
<div className='rounded-lg border border-tools-table-outline border-base-content/5'>
<table
style={{
borderRadius: '0.5rem',
}}
className='border-none w-full'
>
<tbody className='w-full'>{renderTableContent(item)}</tbody>
</table>
{hasDeliveryOrder
? deliveryItems.map((item, index) => (
<div key={`do-table-${item.product_warehouse?.id}-${index}`}>
{formType === 'success' ? (
<div className='rounded-lg border border-tools-table-outline border-base-content/5'>
<table
style={{
borderRadius: '0.5rem',
}}
className='border-none w-full'
>
<tbody className='w-full'>
{renderDeliveryOrderContent(item)}
</tbody>
</table>
</div>
) : (
<Card
key={`do-table-${item.product_warehouse?.id}-${index}`}
title={item.product_warehouse?.product?.name || '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={{
borderRadius: '0.5rem',
}}
className='border-none w-full'
>
<tbody className='w-full'>
{renderDeliveryOrderContent(item)}
</tbody>
</table>
</Card>
)}
</div>
) : (
<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={{
borderRadius: '0.5rem',
}}
className='border-none w-full'
>
<tbody className='w-full'>{renderTableContent(item)}</tbody>
</table>
</Card>
)}
</div>
))}
))
: data.map((item) => (
<div key={`table-${item.id}`}>
{formType === 'success' ? (
<div className='rounded-lg border border-tools-table-outline border-base-content/5'>
<table
style={{
borderRadius: '0.5rem',
}}
className='border-none w-full'
>
<tbody className='w-full'>
{renderSalesOrderContent(item)}
</tbody>
</table>
</div>
) : (
<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={{
borderRadius: '0.5rem',
}}
className='border-none w-full'
>
<tbody className='w-full'>
{renderSalesOrderContent(item)}
</tbody>
</table>
</Card>
)}
</div>
))}
</div>
</>
);
@@ -1,7 +1,7 @@
import Button from '@/components/Button';
import { BaseDeliveryOrder, Marketing } from '@/types/api/marketing/marketing';
import { Icon } from '@iconify/react';
import { Document, Image, Page, pdf, Text, View } from '@react-pdf/renderer';
import { Document, Page, pdf, Text, View } from '@react-pdf/renderer';
import { useMemo, useState } from 'react';
import { formatDate, formatNumber, formatVechicleNumber } from '@/lib/helper';
import pdfStyles from '@/components/pages/marketing/pdf/styles/MarketingPDFStyles';
@@ -143,7 +143,7 @@ const AreasTable = () => {
useEffect(() => {
setTableState('areas-table', pathname);
}, [pathname]);
}, [pathname, setTableState]);
const searchChangeHandler: ChangeEventHandler<HTMLInputElement> = (e) => {
setSearchValue(e.target.value);
@@ -143,7 +143,7 @@ const BanksTable = () => {
useEffect(() => {
setTableState('banks-table', pathname);
}, [pathname]);
}, [pathname, setTableState]);
const searchChangeHandler: ChangeEventHandler<HTMLInputElement> = (e) => {
setSearchValue(e.target.value);
@@ -145,7 +145,7 @@ const CustomersTable = () => {
useEffect(() => {
setTableState('customers-table', pathname);
}, [pathname]);
}, [pathname, setTableState]);
const searchChangeHandler: ChangeEventHandler<HTMLInputElement> = (e) => {
setSearchValue(e.target.value);
@@ -145,7 +145,7 @@ const FlockTable = () => {
useEffect(() => {
setTableState('flocks-table', pathname);
}, [pathname]);
}, [pathname, setTableState]);
const searchChangeHandler: ChangeEventHandler<HTMLInputElement> = (e) => {
setSearchValue(e.target.value);
@@ -128,7 +128,7 @@ const NonstocksTable = () => {
useEffect(() => {
setTableState('nonstocks-table', pathname);
}, [pathname]);
}, [pathname, setTableState]);
const [sorting, setSorting] = useState<SortingState>([]);
@@ -1,12 +1,6 @@
'use client';
import {
ChangeEventHandler,
useEffect,
useMemo,
useRef,
useState,
} from 'react';
import { ChangeEventHandler, useEffect, useMemo, useState } from 'react';
import { usePathname } from 'next/navigation';
import useSWR from 'swr';
import { CellContext, ColumnDef, SortingState } from '@tanstack/react-table';
@@ -222,7 +216,7 @@ const ProductCategoryTable = () => {
useEffect(() => {
setTableState('product-category-table', pathname);
}, [pathname]);
}, [pathname, setTableState]);
return (
<>
@@ -222,7 +222,7 @@ const ProductsTable = () => {
useEffect(() => {
setTableState('product-table', pathname);
}, [pathname]);
}, [pathname, setTableState]);
const searchChangeHandler: ChangeEventHandler<HTMLInputElement> = (e) => {
setSearchValue(e.target.value);
@@ -178,14 +178,7 @@ const ProductionStandardForm = ({
const router = useRouter();
// ===== Store =====
const {
formData,
setFormData,
addDetail,
updateDetail,
deleteDetail,
clearCache,
} = useFormStore();
const { formData, setFormData, clearCache } = useFormStore();
// ===== Formik =====
// Initial values - only recalculate when initialValue changes (for edit/detail mode)
@@ -23,7 +23,6 @@ import RequirePermission from '@/components/helper/RequirePermission';
import PopoverButton from '@/components/popover/PopoverButton';
import PopoverContent from '@/components/popover/PopoverContent';
import SupplierTableSkeleton from '@/components/pages/master-data/supplier/skeleton/SupplierTableSkeleton';
import SelectInput from '@/components/input/SelectInput';
import { OptionType } from '@/components/input/SelectInput';
import ButtonFilter from '@/components/helper/ButtonFilter';
@@ -167,6 +166,8 @@ const SuppliersTable = () => {
},
});
const { setFieldValue } = formik;
// ===== CATEGORY OPTIONS (SAPRONAK or BOP) =====
const categoryOptions = useMemo(
() => [
@@ -191,9 +192,9 @@ const SuppliersTable = () => {
const option = val as OptionType | null;
const categoryId = option?.value ? String(option.value) : null;
formik.setFieldValue('category_id', categoryId);
setFieldValue('category_id', categoryId);
},
[formik]
[setFieldValue]
);
const handleFilterFlagChange = useCallback(
@@ -206,9 +207,9 @@ const SuppliersTable = () => {
? false
: null;
formik.setFieldValue('flag', boolValue);
setFieldValue('flag', boolValue);
},
[formik]
[setFieldValue]
);
// ===== FILTER HELPERS =====
@@ -238,9 +239,9 @@ const SuppliersTable = () => {
if (filterModal.open) {
const flagBoolValue =
tableFilterState.flagFilter === 'EKSPEDISI' ? true : false;
formik.setFieldValue('flag', flagBoolValue);
setFieldValue('flag', flagBoolValue);
}
}, [filterModal.open, tableFilterState.flagFilter]);
}, [filterModal.open, tableFilterState.flagFilter, setFieldValue]);
useEffect(() => {
updateFilter('search', searchValue);
@@ -508,6 +509,7 @@ const SuppliersTable = () => {
options={flagOptions}
value={flagValue}
onChange={handleFilterFlagChange}
isClearable
className={{ wrapper: 'w-full' }}
/>
</div>
@@ -167,29 +167,26 @@ const SupplierForm = ({
},
});
const { setValues: formikSetValues } = formik;
const { setFieldValue } = formik;
// Initialize Formik
useEffect(() => {
formikSetValues(formikInitialValues);
if (formType != 'add') {
const hatcheryArrays = formikInitialValues.hatchery?.split(',');
const hatcheryCreatedOptions = hatcheryArrays?.map((item) => ({
if (formType !== 'add' && initialValues?.hatchery) {
const hatcheryArrays = initialValues.hatchery.split(',');
const hatcheryCreatedOptions = hatcheryArrays.map((item) => ({
value: item,
label: item,
}));
setHatcheryOptionValues(hatcheryCreatedOptions ?? []);
setHatcheryOptionValues(hatcheryCreatedOptions);
}
}, [formikSetValues, formikInitialValues, setHatcheryOptionValues]);
}, [formType, initialValues?.hatchery]);
useEffect(() => {
const commaSeparatedValues = hatcheryOptionsValues
.map((item) => item.value)
.join(',');
formikSetValues({
...formik.values,
hatchery: commaSeparatedValues,
});
}, [hatcheryOptionsValues, formikSetValues]);
setFieldValue('hatchery', commaSeparatedValues);
}, [hatcheryOptionsValues, setFieldValue]);
// Option Handler
const typeChangeHandler = (val: OptionType | OptionType[] | null) => {
@@ -128,7 +128,7 @@ const UomsTable = () => {
useEffect(() => {
setTableState('uoms-table', pathname);
}, [pathname]);
}, [pathname, setTableState]);
const [sorting, setSorting] = useState<SortingState>([]);
@@ -169,6 +169,8 @@ const WarehousesTable = () => {
},
});
const { setFieldValue } = formik;
// ===== AREA OPTIONS =====
const {
setInputValue: setAreaInputValue,
@@ -245,9 +247,13 @@ const WarehousesTable = () => {
if (filterModal.open) {
const activeProjectFlockValue =
tableFilterState.activeProjectFlockFilter === 'true' ? true : false; // Default ke false (Semua Kandang)
formik.setFieldValue('active_project_flock', activeProjectFlockValue);
setFieldValue('active_project_flock', activeProjectFlockValue);
}
}, [filterModal.open]);
}, [
filterModal.open,
tableFilterState.activeProjectFlockFilter,
setFieldValue,
]);
useEffect(() => {
updateFilter('search', searchValue);
@@ -40,7 +40,6 @@ import { RecordingApi } from '@/services/api/production';
import { isResponseSuccess } from '@/lib/api-helper';
import { useTableFilter } from '@/services/hooks/useTableFilter';
import toast from 'react-hot-toast';
import Badge from '@/components/Badge';
import StatusBadge from '@/components/helper/StatusBadge';
import CheckboxInput from '@/components/input/CheckboxInput';
import { useUiStore } from '@/stores/ui/ui.store';
@@ -532,7 +531,7 @@ const RecordingTable = () => {
useEffect(() => {
setTableState('recording-table', pathname);
}, [pathname]);
}, [pathname, setTableState]);
const searchChangeHandler = useCallback(
(e: React.ChangeEvent<HTMLInputElement>) => {
@@ -1119,14 +1118,11 @@ const RecordingTable = () => {
},
],
[
isRecordingApproved,
tableFilterState.pageSize,
tableFilterState.page,
selectedRecording,
singleDeleteModal,
approveModal,
rejectModal,
rowSelection,
setRowSelection,
setApprovalNotes,
setSelectedRecording,
@@ -1007,6 +1007,8 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
});
// ===== HELPER FUNCTIONS =====
const { setFieldValue } = formik;
const getAvailableStock = useCallback(
(productWarehouseId: number) => {
if ((type as 'add' | 'edit' | 'detail') === 'detail') return 0;
@@ -1098,16 +1100,10 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
const isAlreadyRecorded = recordedProjectFlockKandangIds.has(
projectFlockKandangLookup.project_flock_kandang_id
);
let color: 'neutral' | 'success' | 'warning' | 'error';
if (isAlreadyRecorded) {
color = 'warning';
} else {
color = 'success';
}
const color = isAlreadyRecorded ? 'warning' : 'success';
return (
<span className={'whitespace-nowrap text-xs'}>
<span className={`whitespace-nowrap text-xs text-${color}`}>
Periode {projectFlockKandangLookup.project_flock?.period}
</span>
);
@@ -1411,9 +1407,9 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
}
if (formik.values.project_flock_kandang_id !== projectFlockKandangId) {
formik.setFieldValue('project_flock_kandang_id', projectFlockKandangId);
setFieldValue('project_flock_kandang_id', projectFlockKandangId);
formik.setFieldValue('project_flock_kandang', {
setFieldValue('project_flock_kandang', {
value: projectFlockKandangId,
label: projectFlockKandangLookup
? `${projectFlockKandangLookup.project_flock.flock_name} - ${projectFlockKandangLookup.kandang.name}`
@@ -1431,6 +1427,9 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
nextDayRecording,
existingRecordings,
today,
currentRecordDate,
duplicateErrorShown,
setFieldValue,
]);
useEffect(() => {
@@ -1472,11 +1471,11 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
formik.values.project_flock_kandang_id !==
projectFlockKandangDetail.id
) {
formik.setFieldValue(
setFieldValue(
'project_flock_kandang_id',
projectFlockKandangDetail.id
);
formik.setFieldValue('project_flock_kandang', {
setFieldValue('project_flock_kandang', {
value: projectFlockKandangDetail.id,
label: `${projectFlock.flock_name} - ${kandang.name}`,
});
@@ -1490,6 +1489,7 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
type,
enhancedProjectFlockOptions,
formik.values.project_flock_kandang_id,
setFieldValue,
]);
const approveHandler = async (notes: string) => {
@@ -1653,10 +1653,10 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
if (isLayingCategory && (type as 'add' | 'edit' | 'detail') !== 'detail') {
const layingValues = formik.values as RecordingLayingFormValues;
if (!layingValues.eggs || layingValues.eggs.length === 0) {
formik.setFieldValue('eggs', [{ product_warehouse_id: 0, qty: '' }]);
setFieldValue('eggs', [{ product_warehouse_id: 0, qty: '' }]);
}
}
}, [isLayingCategory, type]);
}, [isLayingCategory, type, formik.values, setFieldValue]);
useEffect(() => {
if (type !== 'add') {
@@ -3199,6 +3199,7 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
pageSize={100}
className={{
tableWrapperClassName: 'overflow-x-auto',
containerClassName: 'mb-6!',
tableClassName: 'w-full table-auto text-sm',
headerRowClassName: 'border-b border-b-gray-200',
headerColumnClassName:
@@ -19,7 +19,6 @@ import {
import { isResponseSuccess } from '@/lib/api-helper';
import { type BaseApiResponse } from '@/types/api/api-general';
import Table from '@/components/Table';
import Badge from '@/components/Badge';
import StatusBadge from '@/components/helper/StatusBadge';
import CheckboxInput from '@/components/input/CheckboxInput';
import { useModal } from '@/components/Modal';
@@ -186,7 +185,7 @@ const UniformityTable = () => {
const router = useRouter();
const searchParams = useSearchParams();
const pathname = usePathname();
const { searchValue, setSearchValue, setTableState } = useUiStore();
const { searchValue, setTableState } = useUiStore();
const isSuccess = useUniformityStore((s) => s.isSuccess);
const setIsSuccess = useUniformityStore((s) => s.setIsSuccess);
const createdUniformity = useUniformityStore((s) => s.createdUniformity);
@@ -198,7 +197,6 @@ const UniformityTable = () => {
state: tableFilterState,
updateFilter,
setPage,
setPageSize,
toQueryString: getTableFilterQueryString,
} = useTableFilter({
initial: {
@@ -251,6 +249,10 @@ const UniformityTable = () => {
const [isSubmitted, setIsSubmitted] = useState(false);
// ===== FILTER STATE =====
const [filterDateValues, setFilterDateValues] = useState({
start_date: tableFilterState.start_date,
end_date: tableFilterState.end_date,
});
const [filterLocation, setFilterLocation] = useState<OptionType | null>(null);
const [filterProjectFlock, setFilterProjectFlock] =
useState<OptionType | null>(null);
@@ -345,8 +347,8 @@ const UniformityTable = () => {
// ===== FORMIK FILTER =====
const filterFormik = useFormik<UniformityTableFilterValues>({
initialValues: {
start_date: tableFilterState.start_date,
end_date: tableFilterState.end_date,
start_date: filterDateValues.start_date,
end_date: filterDateValues.end_date,
location: filterLocation,
project_flock: filterProjectFlock,
project_flock_kandang_id: filterProjectFlockKandangId,
@@ -383,6 +385,13 @@ const UniformityTable = () => {
const { formErrorList, close, handleFormSubmit } =
useFormikErrorList(filterFormik);
useEffect(() => {
setFilterDateValues({
start_date: tableFilterState.start_date,
end_date: tableFilterState.end_date,
});
}, [tableFilterState.start_date, tableFilterState.end_date]);
// ===== BUILD SWR KEY WITH FILTERS =====
const uniformitySwrKey = useMemo(() => {
const basePath = UniformityApi.basePath;
@@ -496,6 +505,7 @@ const UniformityTable = () => {
setFilterKandang(null);
setFilterProjectFlockKandangId(undefined);
setFilterErrors({});
setFilterDateValues({ start_date: '', end_date: '' });
updateFilter('start_date', '');
updateFilter('end_date', '');
@@ -505,7 +515,7 @@ const UniformityTable = () => {
filterFormik.resetForm();
filterModal.closeModal();
}, [filterFormik, updateFilter]);
}, [filterFormik, updateFilter, filterModal]);
const selectedRowIds = useMemo(() => {
return Object.keys(rowSelection)
@@ -553,7 +563,13 @@ const UniformityTable = () => {
}
}
}
}, [searchParams, uniformities]);
}, [
searchParams,
uniformities,
router,
singleApproveModal,
singleRejectModal,
]);
useEffect(() => {
if (isSuccess) {
@@ -1289,7 +1305,7 @@ const UniformityTable = () => {
<div className='flex flex-col p-4 gap-1.5'>
{/* Rentang Waktu */}
<div>
<label className='flex text-xs items-center gap-2 py-2 font-semibold'>
<label className='flex text-xs items-center gap-1 py-2 font-semibold after:content-["*"] after:text-red-500'>
Tanggal
</label>
<div className='flex items-center gap-2'>
@@ -1301,6 +1317,10 @@ const UniformityTable = () => {
onChange={(e) => {
const value = e.target.value;
filterFormik.setFieldValue('start_date', value);
setFilterDateValues((prev) => ({
...prev,
start_date: value,
}));
if (value && filterFormik.values.end_date) {
const startDate = new Date(value);
@@ -1344,6 +1364,10 @@ const UniformityTable = () => {
onChange={(e) => {
const value = e.target.value;
filterFormik.setFieldValue('end_date', value);
setFilterDateValues((prev) => ({
...prev,
end_date: value,
}));
if (value && filterFormik.values.start_date) {
const startDateObj = new Date(
@@ -99,14 +99,22 @@ const UniformityDetail: React.FC<UniformityDetailProps> = ({
setExpandedDrawerOpen(true);
}, 0);
}
}, [shouldFetchDetails, uniformity_details, hasFetchedDetails]);
}, [
shouldFetchDetails,
uniformity_details,
hasFetchedDetails,
setExpandedDrawerContent,
setExpandedDrawerOpen,
initialValues.info_umum,
initialValues.id,
]);
useEffect(() => {
return () => {
setExpandedDrawerOpen(false);
setExpandedDrawerContent(null);
};
}, []);
}, [setExpandedDrawerOpen, setExpandedDrawerContent]);
const infoUmumTableData: DetailOptionType[] = useMemo(() => {
if (!initialValues) return [];
@@ -223,7 +231,7 @@ const UniformityDetail: React.FC<UniformityDetailProps> = ({
},
},
],
[initialValues]
[initialValues, handleViewUniformityDetails, isLoading]
);
const samplingTableData: DetailOptionType[] = useMemo(() => {
@@ -279,6 +279,8 @@ const UniformityForm = ({
},
});
const { setFieldValue, setFieldTouched } = formik;
const handleValidateForm = async () => {
const errors = await formik.validateForm();
@@ -301,10 +303,10 @@ const UniformityForm = ({
const location = val as OptionType | null;
const locationId = Number(location?.value);
formik.setFieldTouched('location', true);
formik.setFieldValue('location', location);
formik.setFieldTouched('location_id', true);
formik.setFieldValue('location_id', locationId);
setFieldTouched('location', true);
setFieldValue('location', location);
setFieldTouched('location_id', true);
setFieldValue('location_id', locationId);
setSelectedLocation(location);
setSelectedProjectFlock(null);
@@ -312,7 +314,7 @@ const UniformityForm = ({
location ? location.value.toString() : ''
);
},
[]
[setFieldTouched, setFieldValue]
);
const handleProjectFlockChange = useCallback(
@@ -320,14 +322,14 @@ const UniformityForm = ({
const projectFlock = val as OptionType | null;
const projectFlockId = Number(projectFlock?.value);
formik.setFieldTouched('project_flock', true);
formik.setFieldValue('project_flock', projectFlock);
formik.setFieldTouched('project_flock_id', true);
formik.setFieldValue('project_flock_id', projectFlockId);
setFieldTouched('project_flock', true);
setFieldValue('project_flock', projectFlock);
setFieldTouched('project_flock_id', true);
setFieldValue('project_flock_id', projectFlockId);
setSelectedProjectFlock(projectFlock);
},
[]
[setFieldTouched, setFieldValue]
);
const handleKandangChange = useCallback(
@@ -335,24 +337,24 @@ const UniformityForm = ({
const kandang = val as OptionType | null;
const kandangId = Number(kandang?.value);
formik.setFieldTouched('kandang', true);
formik.setFieldValue('kandang', kandang);
formik.setFieldTouched('kandang_id', true);
formik.setFieldValue('kandang_id', kandangId);
setFieldTouched('kandang', true);
setFieldValue('kandang', kandang);
setFieldTouched('kandang_id', true);
setFieldValue('kandang_id', kandangId);
setSelectedKandang(kandang);
},
[]
[setFieldTouched, setFieldValue]
);
const handleFileChange = useCallback(
(e: React.ChangeEvent<HTMLInputElement>) => {
const document = e.target.files?.[0];
formik.setFieldTouched('document', true);
setFieldTouched('document', true);
if (!document) {
formik.setFieldValue('document', undefined);
setFieldValue('document', undefined);
return;
}
@@ -372,24 +374,95 @@ const UniformityForm = ({
return;
}
formik.setFieldValue('document', document);
setFieldValue('document', document);
},
[]
[setFieldTouched, setFieldValue]
);
const handleDateChange = useCallback(
(e: React.ChangeEvent<HTMLInputElement>) => {
formik.setFieldValue('date', e.target.value);
setFieldValue('date', e.target.value);
},
[]
[setFieldValue]
);
const handleRemoveFile = useCallback(() => {
formik.setFieldValue('document', undefined);
setFieldValue('document', undefined);
if (fileInputRef.current) {
fileInputRef.current.value = '';
}
}, [formik]);
}, [setFieldValue]);
// ===== RESET PROJECT FLOCK & KANDANG WHEN LOCATION CHANGES =====
const prevLocationIdRef = useRef<number | string>('');
useEffect(() => {
const currentLocationId = formik.values.location_id || '';
if (currentLocationId === prevLocationIdRef.current) {
return;
}
prevLocationIdRef.current = currentLocationId;
if (formik.values.project_flock !== null) {
setFieldTouched('project_flock', false);
setFieldValue('project_flock', null);
}
if (formik.values.project_flock_id !== 0) {
setFieldTouched('project_flock_id', false);
setFieldValue('project_flock_id', 0);
}
if (formik.values.kandang !== null) {
setFieldTouched('kandang', false);
setFieldValue('kandang', null);
}
if (formik.values.kandang_id !== 0) {
setFieldTouched('kandang_id', false);
setFieldValue('kandang_id', 0);
}
setSelectedProjectFlock(null);
setSelectedKandang(null);
}, [
formik.values.location_id,
formik.values.project_flock,
formik.values.project_flock_id,
formik.values.kandang,
formik.values.kandang_id,
setFieldTouched,
setFieldValue,
]);
const prevProjectFlockIdRef = useRef<number | string>('');
useEffect(() => {
const currentProjectFlockId = formik.values.project_flock_id || '';
if (currentProjectFlockId === prevProjectFlockIdRef.current) {
return;
}
prevProjectFlockIdRef.current = currentProjectFlockId;
if (formik.values.kandang !== null) {
setFieldTouched('kandang', false);
setFieldValue('kandang', null);
}
if (formik.values.kandang_id !== 0) {
setFieldTouched('kandang_id', false);
setFieldValue('kandang_id', 0);
}
setSelectedKandang(null);
}, [
formik.values.project_flock_id,
formik.values.kandang,
formik.values.kandang_id,
setFieldTouched,
setFieldValue,
]);
const handleDownloadTemplate = useCallback(() => {
const population = projectFlockKandangLookup?.population;
@@ -442,9 +515,9 @@ const UniformityForm = ({
const weeksDiff = Math.floor(daysDiff / 7);
formik.setFieldValue('week', initialWeek + weeksDiff);
setFieldValue('week', initialWeek + weeksDiff);
} else {
formik.setFieldValue('week', initialWeek);
setFieldValue('week', initialWeek);
}
}
}, [
@@ -452,6 +525,7 @@ const UniformityForm = ({
projectFlockKandangLookup?.project_flock_kandang_id,
recordingsData,
formik.values.date,
setFieldValue,
]);
useEffect(() => {
@@ -686,7 +760,7 @@ const UniformityForm = ({
</span>
</div>
{projectFlockKandangLookup?.population && (
{!!projectFlockKandangLookup?.population && (
<>
<div className='flex items-center justify-center gap-2 py-4'>
<div className='h-px bg-[#18181B33] w-8'></div>
@@ -22,7 +22,6 @@ import { useModal } from '@/components/Modal';
import ConfirmationModal from '@/components/modal/ConfirmationModal';
import PopoverButton from '@/components/popover/PopoverButton';
import PopoverContent from '@/components/popover/PopoverContent';
import SelectInput, { OptionType } from '@/components/input/SelectInput';
import RequirePermission from '@/components/helper/RequirePermission';
import StatusBadge from '@/components/helper/StatusBadge';
import PurchaseTableSkeleton from '@/components/pages/purchase/skeleton/PurchaseTableSkeleton';
@@ -32,7 +31,6 @@ 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';
@@ -412,13 +410,13 @@ const PurchaseTable = () => {
[updateFilter, setSearchValue]
);
const pageSizeChangeHandler = useCallback(
(val: OptionType | OptionType[] | null) => {
const newVal = val as OptionType;
setPageSize(newVal.value as number);
},
[setPageSize]
);
// const pageSizeChangeHandler = useCallback(
// (val: OptionType | OptionType[] | null) => {
// const newVal = val as OptionType;
// setPageSize(newVal.value as number);
// },
// [setPageSize]
// );
return (
<>
@@ -95,62 +95,6 @@ const PurchaseOrderAcceptApprovalForm = ({
};
};
// ===== SUBMISSION HANDLERS =====
const createAcceptApprovalHandler = useCallback(
async (payload: CreateAcceptApprovalRequestPayload) => {
const purchaseRequestId = searchParams.get('purchaseId')
? parseInt(searchParams.get('purchaseId')!)
: initialValues?.id || 1;
if (!purchaseRequestId) {
setPurchaseOrderFormErrorMessage('Purchase Request ID is required');
return;
}
const res = await PurchaseApi.acceptApproval.create(
purchaseRequestId,
payload
);
if (isResponseError(res)) {
setPurchaseOrderFormErrorMessage(res.message);
return;
}
toast.success(res?.message as string);
refreshApprovals?.();
onRefetchData?.();
formik.resetForm();
onCancel?.();
onModalClose?.();
router.refresh();
},
[
initialValues?.id,
searchParams,
refreshApprovals,
onModalClose,
onRefetchData,
]
);
const updateAcceptApprovalHandler = useCallback(
async (purchaseId: number, payload: CreateAcceptApprovalRequestPayload) => {
const res = await PurchaseApi.acceptApproval.create(purchaseId, payload);
if (isResponseError(res)) {
setPurchaseOrderFormErrorMessage(res.message);
return;
}
toast.success(res?.message as string);
refreshApprovals?.();
onRefetchData?.();
formik.resetForm();
onCancel?.();
onModalClose?.();
router.refresh();
},
[refreshApprovals, onModalClose, onRefetchData]
);
// ===== SELECT INPUT DATA =====
const {
options: expeditionVendors,
@@ -244,6 +188,67 @@ const PurchaseOrderAcceptApprovalForm = ({
},
});
const { resetForm, setFieldValue } = formik;
// ===== SUBMISSION HANDLERS =====
const createAcceptApprovalHandler = useCallback(
async (payload: CreateAcceptApprovalRequestPayload) => {
const purchaseRequestId = searchParams.get('purchaseId')
? parseInt(searchParams.get('purchaseId')!)
: initialValues?.id || 1;
if (!purchaseRequestId) {
setPurchaseOrderFormErrorMessage('Purchase Request ID is required');
return;
}
const res = await PurchaseApi.acceptApproval.create(
purchaseRequestId,
payload
);
if (isResponseError(res)) {
setPurchaseOrderFormErrorMessage(res.message);
return;
}
toast.success(res?.message as string);
refreshApprovals?.();
onRefetchData?.();
resetForm();
onCancel?.();
onModalClose?.();
router.refresh();
},
[
initialValues?.id,
searchParams,
refreshApprovals,
onModalClose,
onRefetchData,
onCancel,
router,
resetForm,
]
);
const updateAcceptApprovalHandler = useCallback(
async (purchaseId: number, payload: CreateAcceptApprovalRequestPayload) => {
const res = await PurchaseApi.acceptApproval.create(purchaseId, payload);
if (isResponseError(res)) {
setPurchaseOrderFormErrorMessage(res.message);
return;
}
toast.success(res?.message as string);
refreshApprovals?.();
onRefetchData?.();
resetForm();
onCancel?.();
onModalClose?.();
router.refresh();
},
[refreshApprovals, onModalClose, onRefetchData, onCancel, router, resetForm]
);
const handleValidateForm = async () => {
const errors = await formik.validateForm();
@@ -307,9 +312,9 @@ const PurchaseOrderAcceptApprovalForm = ({
transport_per_item: item.transport_per_item || '',
};
});
formik.setFieldValue('items', updatedItems);
setFieldValue('items', updatedItems);
}
}, [purchaseItems, initialValues, key]);
}, [purchaseItems, initialValues, key, setFieldValue]);
// ===== HELPER FUNCTIONS =====
const getQuantityExceededError = useCallback(
@@ -146,120 +146,6 @@ const PurchaseOrderStaffApprovalForm = ({
return !item.purchase_item_id || item.purchase_item_id === 0;
};
// ===== SUBMISSION HANDLERS =====
const createStaffApprovalHandler = useCallback(
async (payload: CreateStaffApprovalRequestPayload) => {
const purchaseRequestId = searchParams.get('purchaseId')
? parseInt(searchParams.get('purchaseId')!)
: initialValues?.id || 1;
if (!purchaseRequestId) {
setPurchaseOrderFormErrorMessage('Purchase Request ID is required');
return;
}
const res = await PurchaseApi.staffApproval.create(
purchaseRequestId,
payload
);
if (isResponseError(res)) {
setPurchaseOrderFormErrorMessage(res.message);
return;
}
toast.success(res?.message as string);
refreshApprovals?.();
onRefetchData?.();
formik.resetForm();
onCancel?.();
onModalClose?.();
router.refresh();
},
[
initialValues?.id,
searchParams,
refreshApprovals,
onModalClose,
onRefetchData,
]
);
const updateStaffApprovalHandler = useCallback(
async (purchaseId: number, payload: UpdateStaffApprovalRequestPayload) => {
const res = await PurchaseApi.staffApproval.update(purchaseId, payload);
if (isResponseError(res)) {
setPurchaseOrderFormErrorMessage(res.message);
return;
}
toast.success(res?.message as string);
refreshApprovals?.();
onRefetchData?.();
formik.resetForm();
onCancel?.();
onModalClose?.();
router.refresh();
},
[refreshApprovals, onModalClose, onRefetchData]
);
// ===== DELETE HANDLER =====
const deleteItemsHandler = useCallback(async () => {
const purchaseRequestId = searchParams.get('purchaseId')
? parseInt(searchParams.get('purchaseId')!)
: initialValues?.id || 1;
if (!purchaseRequestId) {
toast.error('Purchase Request ID is required');
return;
}
const itemIdsToDelete = selectedItemForDelete
? [selectedItemForDelete]
: [];
if (itemIdsToDelete.length === 0) {
toast.error('Tidak ada item yang dipilih untuk dihapus');
return;
}
try {
const res = await PurchaseApi.items.delete(purchaseRequestId, {
item_ids: itemIdsToDelete,
});
if (isResponseError(res)) {
toast.error(res.message || 'Gagal menghapus item pembelian');
return;
}
const successMessage = 'Item pembelian berhasil dihapus';
toast.success(successMessage);
refreshApprovals?.();
onRefetchData?.();
deleteModal.closeModal();
setSelectedItemForDelete(null);
setSelectedItemIndex(null);
if (selectedItemIndex !== null) {
const updatedPurchaseItems = formik.values.items?.filter(
(_, i) => i !== selectedItemIndex
);
formik.setFieldValue('items', updatedPurchaseItems);
}
} catch {
toast.error('Terjadi kesalahan saat menghapus item pembelian');
}
}, [
initialValues?.id,
searchParams,
selectedItemForDelete,
selectedItemIndex,
refreshApprovals,
onRefetchData,
deleteModal,
]);
// ===== API DATA FETCHING FOR SUPPLIER PRODUCTS =====
const { data: supplierData, isLoading: isLoadingSupplierProducts } = useSWR(
initialValues?.supplier?.id
@@ -418,6 +304,127 @@ const PurchaseOrderStaffApprovalForm = ({
},
});
const { resetForm, setFieldValue } = formik;
// ===== SUBMISSION HANDLERS =====
const createStaffApprovalHandler = useCallback(
async (payload: CreateStaffApprovalRequestPayload) => {
const purchaseRequestId = searchParams.get('purchaseId')
? parseInt(searchParams.get('purchaseId')!)
: initialValues?.id || 1;
if (!purchaseRequestId) {
setPurchaseOrderFormErrorMessage('Purchase Request ID is required');
return;
}
const res = await PurchaseApi.staffApproval.create(
purchaseRequestId,
payload
);
if (isResponseError(res)) {
setPurchaseOrderFormErrorMessage(res.message);
return;
}
toast.success(res?.message as string);
refreshApprovals?.();
onRefetchData?.();
resetForm();
onCancel?.();
onModalClose?.();
router.refresh();
},
[
initialValues?.id,
searchParams,
refreshApprovals,
onModalClose,
onRefetchData,
resetForm,
onCancel,
router,
]
);
const updateStaffApprovalHandler = useCallback(
async (purchaseId: number, payload: UpdateStaffApprovalRequestPayload) => {
const res = await PurchaseApi.staffApproval.update(purchaseId, payload);
if (isResponseError(res)) {
setPurchaseOrderFormErrorMessage(res.message);
return;
}
toast.success(res?.message as string);
refreshApprovals?.();
onRefetchData?.();
resetForm();
onCancel?.();
onModalClose?.();
router.refresh();
},
[refreshApprovals, onModalClose, onRefetchData, resetForm, onCancel, router]
);
// ===== DELETE HANDLER =====
const deleteItemsHandler = useCallback(async () => {
const purchaseRequestId = searchParams.get('purchaseId')
? parseInt(searchParams.get('purchaseId')!)
: initialValues?.id || 1;
if (!purchaseRequestId) {
toast.error('Purchase Request ID is required');
return;
}
const itemIdsToDelete = selectedItemForDelete
? [selectedItemForDelete]
: [];
if (itemIdsToDelete.length === 0) {
toast.error('Tidak ada item yang dipilih untuk dihapus');
return;
}
try {
const res = await PurchaseApi.items.delete(purchaseRequestId, {
item_ids: itemIdsToDelete,
});
if (isResponseError(res)) {
toast.error(res.message || 'Gagal menghapus item pembelian');
return;
}
const successMessage = 'Item pembelian berhasil dihapus';
toast.success(successMessage);
refreshApprovals?.();
onRefetchData?.();
deleteModal.closeModal();
setSelectedItemForDelete(null);
setSelectedItemIndex(null);
if (selectedItemIndex !== null) {
const updatedPurchaseItems = formik.values.items?.filter(
(_, i) => i !== selectedItemIndex
);
setFieldValue('items', updatedPurchaseItems);
}
} catch {
toast.error('Terjadi kesalahan saat menghapus item pembelian');
}
}, [
initialValues?.id,
searchParams,
selectedItemForDelete,
selectedItemIndex,
refreshApprovals,
onRefetchData,
deleteModal,
setFieldValue,
formik.values.items,
]);
const handleValidateForm = async () => {
const errors = await formik.validateForm();
@@ -519,9 +526,9 @@ const PurchaseOrderStaffApprovalForm = ({
};
return itemData;
});
formik.setFieldValue('items', updatedItems);
setFieldValue('items', updatedItems);
}
}, [purchaseItems, type, initialValues]);
}, [purchaseItems, type, initialValues, setFieldValue]);
// ===== PURCHASE ITEM OPERATIONS =====
const addPurchaseItem = () => {
@@ -1,6 +1,6 @@
'use client';
import { useCallback, useEffect, useMemo, useState } from 'react';
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { useFormik } from 'formik';
import useSWR from 'swr';
import { useRouter } from 'next/navigation';
@@ -234,6 +234,8 @@ const PurchaseRequestForm = ({
},
});
const { setFieldValue, setFieldTouched, handleBlur } = formik;
const handleValidateForm = async () => {
const errors = await formik.validateForm();
@@ -317,51 +319,73 @@ const PurchaseRequestForm = ({
};
// ===== UTILITY FUNCTIONS =====
const updateCreditTermBasedOnSupplier = useCallback(
(supplierId: number) => {
if (supplierId > 0 && isResponseSuccess(supplierRawData)) {
const supplierData = supplierRawData.data.find(
(s: Supplier) => s.id === supplierId
);
if (supplierData?.due_date) {
formik.setFieldTouched('credit_term', false);
formik.setFieldValue('credit_term', supplierData.due_date.toString());
} else {
formik.setFieldTouched('credit_term', false);
formik.setFieldValue('credit_term', '');
}
} else {
formik.setFieldTouched('credit_term', false);
formik.setFieldValue('credit_term', '');
}
},
[supplierRawData]
);
const prevSupplierIdRef = useRef<number | string>('');
const resetPurchaseItems = useCallback(() => {
if (formik.values.items) {
formik.values.items.forEach((_, idx) => {
formik.setFieldTouched(`items.${idx}.product`, false);
formik.setFieldValue(`items.${idx}.product`, null);
formik.setFieldTouched(`items.${idx}.product_id`, false);
formik.setFieldValue(`items.${idx}.product_id`, 0);
formik.setFieldTouched(`items.${idx}.qty`, false);
formik.setFieldValue(`items.${idx}.qty`, 0);
});
}
}, []);
// ===== SIDE EFFECTS =====
useEffect(() => {
if (formik.values.supplier_id && Number(formik.values.supplier_id) > 0) {
updateCreditTermBasedOnSupplier(Number(formik.values.supplier_id));
resetPurchaseItems();
} else {
formik.setFieldTouched('credit_term', false);
formik.setFieldValue('credit_term', '');
resetPurchaseItems();
const currentSupplierId = formik.values.supplier_id || '';
if (currentSupplierId === prevSupplierIdRef.current) {
return;
}
}, [formik.values.supplier_id]);
prevSupplierIdRef.current = currentSupplierId;
if (currentSupplierId && Number(currentSupplierId) > 0) {
if (isResponseSuccess(supplierRawData)) {
const supplierData = supplierRawData.data.find(
(s: Supplier) => s.id === Number(currentSupplierId)
);
const newCreditTerm = supplierData?.due_date || 0;
if (formik.values.credit_term !== newCreditTerm) {
setFieldTouched('credit_term', false);
setFieldValue('credit_term', newCreditTerm);
}
}
const itemsNeedReset = formik.values.items?.filter(
(item) => item.product_id !== 0 || item.product !== null
);
if (itemsNeedReset && itemsNeedReset.length > 0) {
formik.values.items.forEach((item, idx) => {
if (item.product_id !== 0 || item.product !== null) {
setFieldTouched(`items.${idx}.product`, false);
setFieldValue(`items.${idx}.product`, null);
setFieldTouched(`items.${idx}.product_id`, false);
setFieldValue(`items.${idx}.product_id`, 0);
setFieldTouched(`items.${idx}.qty`, false);
setFieldValue(`items.${idx}.qty`, 0);
}
});
}
} else {
if (formik.values.credit_term !== 0) {
setFieldTouched('credit_term', false);
setFieldValue('credit_term', 0);
}
const itemsNeedReset = formik.values.items?.filter(
(item) => item.product_id !== 0 || item.product !== null
);
if (itemsNeedReset && itemsNeedReset.length > 0) {
formik.values.items.forEach((item, idx) => {
if (item.product_id !== 0 || item.product !== null) {
setFieldTouched(`items.${idx}.product`, false);
setFieldValue(`items.${idx}.product`, null);
setFieldTouched(`items.${idx}.product_id`, false);
setFieldValue(`items.${idx}.product_id`, 0);
setFieldTouched(`items.${idx}.qty`, false);
setFieldValue(`items.${idx}.qty`, 0);
}
});
}
}
}, [
formik.values.supplier_id,
formik.values.items,
formik.values.credit_term,
supplierRawData,
setFieldTouched,
setFieldValue,
]);
useEffect(() => {
if (type !== 'add' && initialValues) {
@@ -381,63 +405,63 @@ const PurchaseRequestForm = ({
const supplier = val as OptionType | null;
const supplierId = Number(supplier?.value);
formik.setFieldTouched('supplier', true);
formik.setFieldValue('supplier', supplier);
formik.setFieldTouched('supplier_id', true);
formik.setFieldValue('supplier_id', supplierId);
setFieldTouched('supplier', true);
setFieldValue('supplier', supplier);
setFieldTouched('supplier_id', true);
setFieldValue('supplier_id', supplierId);
},
[]
[setFieldTouched, setFieldValue]
);
const handleCreditTermChange = useCallback(
(e: React.ChangeEvent<HTMLInputElement>) => {
const value = e.target.value;
formik.setFieldTouched('credit_term', true);
formik.setFieldValue('credit_term', value);
setFieldTouched('credit_term', true);
setFieldValue('credit_term', value);
},
[]
[setFieldTouched, setFieldValue]
);
const handleCreditTermBlur = useCallback(
(e: React.FocusEvent<HTMLInputElement>) => {
formik.handleBlur(e);
handleBlur(e);
},
[formik]
[handleBlur]
);
const handleAreaChange = useCallback(
(val: OptionType | OptionType[] | null) => {
const area = val as OptionType | null;
formik.setFieldTouched('area_id', true);
formik.setFieldValue('area_id', (area as OptionType)?.value || 0);
formik.setFieldTouched('area', true);
formik.setFieldValue('area', area);
setFieldTouched('area_id', true);
setFieldValue('area_id', (area as OptionType)?.value || 0);
setFieldTouched('area', true);
setFieldValue('area', area);
setSelectedArea((area as OptionType)?.value as string);
setSelectedLocation('');
const disabled = (area as OptionType)?.value == null;
setDisabledLocation(disabled);
formik.setFieldTouched('location_id', false);
formik.setFieldValue('location_id', 0);
formik.setFieldTouched('location', false);
formik.setFieldValue('location', null);
setFieldTouched('location_id', false);
setFieldValue('location_id', 0);
setFieldTouched('location', false);
setFieldValue('location', null);
},
[]
[setFieldTouched, setFieldValue]
);
const handleLocationChange = useCallback(
(val: OptionType | OptionType[] | null) => {
const location = val as OptionType | null;
formik.setFieldTouched('location_id', true);
formik.setFieldValue('location_id', (location as OptionType)?.value || 0);
formik.setFieldTouched('location', true);
formik.setFieldValue('location', location);
setFieldTouched('location_id', true);
setFieldValue('location_id', (location as OptionType)?.value || 0);
setFieldTouched('location', true);
setFieldValue('location', location);
setSelectedLocation((location as OptionType)?.value as string);
},
[]
[setFieldTouched, setFieldValue]
);
const handleWarehouseChange = useCallback(
@@ -445,12 +469,12 @@ const PurchaseRequestForm = ({
const warehouse = val as OptionType | null;
const warehouseId = (warehouse as OptionType)?.value || 0;
formik.setFieldTouched(`items.${idx}.warehouse`, true);
formik.setFieldValue(`items.${idx}.warehouse`, warehouse);
formik.setFieldTouched(`items.${idx}.warehouse_id`, true);
formik.setFieldValue(`items.${idx}.warehouse_id`, warehouseId);
setFieldTouched(`items.${idx}.warehouse`, true);
setFieldValue(`items.${idx}.warehouse`, warehouse);
setFieldTouched(`items.${idx}.warehouse_id`, true);
setFieldValue(`items.${idx}.warehouse_id`, warehouseId);
},
[]
[setFieldTouched, setFieldValue]
);
// ===== PURCHASE ITEM OPERATIONS =====
@@ -351,13 +351,13 @@ const PurchaseOrderDetail = ({
refreshApprovals();
refetchData?.();
staffApprovalModal.closeModal();
}, [refreshApprovals, refetchData]);
}, [refreshApprovals, refetchData, staffApprovalModal]);
const handleEditModalClose = useCallback(() => {
refreshApprovals();
refetchData?.();
editModal.closeModal();
}, [refreshApprovals, refetchData]);
}, [refreshApprovals, refetchData, editModal]);
// ===== DELETE HANDLER =====
const deleteItemsHandler = useCallback(async () => {
@@ -399,7 +399,7 @@ const PurchaseOrderDetail = ({
deleteModal.closeModal();
setSelectedItem(null);
setRowSelection({});
} catch (error) {
} catch {
toast.error('Terjadi kesalahan saat menghapus item pembelian');
} finally {
setIsDeleteLoading(false);
@@ -410,6 +410,8 @@ const PurchaseOrderDetail = ({
selectedItem,
selectedRowIds,
refetchData,
refreshApprovals,
deleteModal,
]);
// ===== APPROVAL/REJECTION HANDLERS =====
@@ -263,6 +263,7 @@ const PurchaseOrderInvoice = ({ data }: PurchaseOrderInvoiceProps) => {
<Page size='A4' style={pdfStyles.page}>
{/* Header Section */}
<View style={pdfStyles.header}>
{/* eslint-disable-next-line jsx-a11y/alt-text */}
<Image
src={'https://placehold.co/120x30/png'}
style={pdfStyles.logo}
@@ -30,8 +30,7 @@ const getStatusColor = (action?: string): [number, number, number] => {
};
export const generateReportExpensePDF = async (
data: ReportExpense[],
params: PDFParams
data: ReportExpense[]
): Promise<void> => {
// Inisialisasi dokumen dengan tipe yang sudah diekstensi
const doc = new jsPDF('l', 'mm', 'a4') as jsPDFWithAutoTable;
@@ -1,6 +1,12 @@
'use client';
import React, { useState, useCallback, useEffect, useMemo } from 'react';
import React, {
useState,
useCallback,
useEffect,
useMemo,
useRef,
} from 'react';
import useSWR from 'swr';
import { Icon } from '@iconify/react';
import Button from '@/components/Button';
@@ -68,37 +74,10 @@ const ReportExpenseTab = ({ tabId }: ReportExpenseTabProps) => {
const [page, setPage] = useState(1);
const [pageSize, setPageSize] = useState(10);
const handleFilterModalOpenRef = useRef(() => {});
const filterModal = useModal();
// ===== OPTIONS =====
const {
setInputValue: setLocationInputValue,
options: locationOptions,
isLoadingOptions: isLoadingLocations,
loadMore: loadMoreLocations,
} = useSelect<Kandang>(LocationApi.basePath, 'id', 'name', 'search');
const {
setInputValue: setSupplierInputValue,
options: supplierOptions,
isLoadingOptions: isLoadingSuppliers,
loadMore: loadMoreSuppliers,
} = useSelect<Supplier>(SupplierApi.basePath, 'id', 'name', 'search');
const {
setInputValue: setKandangInputValue,
options: kandangOptions,
isLoadingOptions: isLoadingKandangs,
loadMore: loadMoreKandangs,
} = useSelect<Kandang>(KandangApi.basePath, 'id', 'name', 'search');
const {
setInputValue: setNonstockInputValue,
options: nonstockOptions,
isLoadingOptions: isLoadingNonstocks,
loadMore: loadMoreNonstocks,
} = useSelect<Nonstock>(NonstockApi.basePath, 'id', 'name', 'search');
const categoryOptions = useMemo(
() => [
{ value: 'BOP', label: 'BOP' },
@@ -149,6 +128,48 @@ const ReportExpenseTab = ({ tabId }: ReportExpenseTabProps) => {
},
});
handleFilterModalOpenRef.current = () => {
filterModal.openModal();
formik.validateForm();
};
// ===== OPTIONS =====
const {
setInputValue: setLocationInputValue,
options: locationOptions,
isLoadingOptions: isLoadingLocations,
loadMore: loadMoreLocations,
} = useSelect<Kandang>(LocationApi.basePath, 'id', 'name', 'search');
const {
setInputValue: setSupplierInputValue,
options: supplierOptions,
isLoadingOptions: isLoadingSuppliers,
loadMore: loadMoreSuppliers,
} = useSelect<Supplier>(SupplierApi.basePath, 'id', 'name', 'search');
const {
setInputValue: setKandangInputValue,
options: kandangOptions,
isLoadingOptions: isLoadingKandangs,
loadMore: loadMoreKandangs,
} = useSelect<Kandang>(
KandangApi.basePath,
'id',
'name',
'search',
formik.values.location_id?.value
? { location_id: String(formik.values.location_id.value) }
: undefined
);
const {
setInputValue: setNonstockInputValue,
options: nonstockOptions,
isLoadingOptions: isLoadingNonstocks,
loadMore: loadMoreNonstocks,
} = useSelect<Nonstock>(NonstockApi.basePath, 'id', 'name', 'search');
// ===== FILTER VALUES =====
const locationValue = useMemo(
() => formik.values.location_id,
@@ -268,13 +289,7 @@ const ReportExpenseTab = ({ tabId }: ReportExpenseTabProps) => {
return;
}
const pdfParams = {
location_name: locationValue?.label,
supplier_name: supplierValue?.label,
realization_date: formik.values.realization_date || undefined,
};
await generateReportExpensePDF(allData, pdfParams);
await generateReportExpensePDF(allData);
toast.success('PDF berhasil dibuat dan diunduh.');
} catch {
@@ -282,98 +297,105 @@ const ReportExpenseTab = ({ tabId }: ReportExpenseTabProps) => {
} finally {
setIsPdfExportLoading(false);
}
}, [
reportExpenseExport,
locationValue,
supplierValue,
kandangValue,
nonstockValue,
categoryValue,
formik.values.realization_date,
]);
}, [reportExpenseExport]);
// ===== REGISTER TAB ACTIONS TO STORE =====
const setTabActions = useTabActionsStore((state) => state.setTabActions);
const clearTabActions = useTabActionsStore((state) => state.clearTabActions);
// ===== TAB ACTIONS COMPONENT =====
const TabActions = useMemo(() => {
return function TabActionsComponent() {
const setTabActions = useTabActionsStore((state) => state.setTabActions);
const clearTabActions = useTabActionsStore(
(state) => state.clearTabActions
);
useEffect(() => {
setTabActions(
tabId,
<div className='flex flex-row gap-3'>
<ButtonFilter
values={formik.values}
onClick={() => filterModal.openModal()}
variant='outline'
className='px-3 py-2.5'
/>
<Dropdown
align='end'
direction='bottom'
className={{
content:
'mt-1 rounded-xl border border-base-content/5 shadow-sm overflow-hidden',
}}
trigger={
<Button
useEffect(() => {
setTabActions(
tabId,
<div className='flex flex-row gap-3'>
<ButtonFilter
values={filterParams}
onClick={() => handleFilterModalOpenRef.current()}
variant='outline'
color='none'
isLoading={isAnyExportLoading}
className='px-3 py-2.5 text-sm text-base-content/50 border border-base-content/10 rounded-xl shadow-button-soft'
className='px-3 py-2.5'
/>
<Dropdown
align='end'
direction='bottom'
className={{
content:
'mt-1 rounded-xl border border-base-content/5 shadow-sm overflow-hidden',
}}
trigger={
<Button
variant='outline'
color='none'
isLoading={isAnyExportLoading}
className='px-3 py-2.5 text-sm text-base-content/50 border border-base-content/10 rounded-xl shadow-button-soft'
>
<div className='flex flex-row items-center gap-1.5'>
<Icon
icon='heroicons:cloud-arrow-down'
width={20}
height={20}
/>
<span>Export</span>
<div className='w-px self-stretch bg-base-content/10' />
<Icon
icon='heroicons:chevron-down'
width={14}
height={14}
/>
</div>
</Button>
}
>
<div className='flex flex-row items-center gap-1.5'>
<Icon
icon='heroicons:cloud-arrow-down'
width={20}
height={20}
/>
<Button
variant='ghost'
color='none'
onClick={handleExportExcel}
isLoading={isExcelExportLoading}
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>
<Button
variant='ghost'
color='none'
onClick={handleExportPDF}
isLoading={isPdfExportLoading}
className='w-full p-3 justify-start text-sm text-base-content/50 font-semibold text-nowrap'
>
<Icon icon='heroicons:document' width={20} height={20} />
Export to PDF
</Button>
</Dropdown>
</div>
);
}, [setTabActions]);
<span>Export</span>
useEffect(() => {
return () => {
clearTabActions(tabId);
};
}, [clearTabActions]);
<div className='w-px self-stretch bg-base-content/10' />
<Icon icon='heroicons:chevron-down' width={14} height={14} />
</div>
</Button>
}
>
<Button
variant='ghost'
color='none'
onClick={handleExportExcel}
isLoading={isExcelExportLoading}
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>
<Button
variant='ghost'
color='none'
onClick={handleExportPDF}
isLoading={isPdfExportLoading}
className='w-full p-3 justify-start text-sm text-base-content/50 font-semibold text-nowrap'
>
<Icon icon='heroicons:document' width={20} height={20} />
Export to PDF
</Button>
</Dropdown>
</div>
);
return null;
};
}, [
tabId,
formik.values,
filterParams,
isAnyExportLoading,
handleExportExcel,
handleExportPDF,
setTabActions,
isExcelExportLoading,
isPdfExportLoading,
]);
useEffect(() => {
return () => {
clearTabActions(tabId);
};
}, [tabId, clearTabActions]);
const TabActionsElement = useMemo(() => <TabActions />, [TabActions]);
// ===== TABLE COLUMNS DEFINITION =====
const columns = useMemo((): ColumnDef<ReportExpense>[] => {
@@ -505,6 +527,7 @@ const ReportExpenseTab = ({ tabId }: ReportExpenseTabProps) => {
return (
<>
{TabActionsElement}
<div className='w-full p-0 sm:p-3 flex flex-col gap-3'>
{!isSubmitted ? (
<ReportExpenseSkeleton
@@ -1,4 +1,4 @@
import { useState, useMemo, useCallback, useEffect } from 'react';
import { useState, useMemo, useCallback, useEffect, useRef } from 'react';
import useSWR from 'swr';
import { Icon } from '@iconify/react';
import Card from '@/components/Card';
@@ -16,7 +16,6 @@ import {
formatDate,
formatNumber,
formatTitleCase,
cn,
} from '@/lib/helper';
import {
CustomerPaymentReport,
@@ -67,6 +66,8 @@ const CustomerPaymentTab = ({ tabId }: CustomerPaymentTabProps) => {
const [dateErrorShown, setDateErrorShown] = useState(false);
const [hasDateError, setHasDateError] = useState(false);
const handleFilterModalOpenRef = useRef(() => {});
const filterModal = useModal();
const dataTypeOptions = useMemo(
@@ -84,11 +85,6 @@ const CustomerPaymentTab = ({ tabId }: CustomerPaymentTabProps) => {
loadMore: loadMoreCustomers,
} = useSelect(CustomerApi.basePath, 'id', 'name', 'search');
const handleFilterModalOpen = () => {
filterModal.openModal();
formik.validateForm();
};
// ===== FORMIK SETUP =====
const formik = useFormik<CustomerPaymentFilterType>({
initialValues: {
@@ -123,6 +119,11 @@ const CustomerPaymentTab = ({ tabId }: CustomerPaymentTabProps) => {
},
});
handleFilterModalOpenRef.current = () => {
filterModal.openModal();
formik.validateForm();
};
const getPaymentStatusBadgeColor = (notes: string): Color => {
const normalizedValue = notes.toLowerCase();
@@ -213,7 +214,7 @@ const CustomerPaymentTab = ({ tabId }: CustomerPaymentTabProps) => {
dataTypeOptions.find((opt) => opt.value === formik.values.filter_by) ||
null
);
}, [formik.values.filter_by]);
}, [formik.values.filter_by, dataTypeOptions]);
// ===== DATA FETCHING =====
const { data: customerPayment, isLoading } = useSWR(
@@ -350,90 +351,104 @@ const CustomerPaymentTab = ({ tabId }: CustomerPaymentTabProps) => {
}
}, [customerPaymentExport, filterParams, customerOptions]);
// ===== REGISTER TAB ACTIONS TO STORE =====
const setTabActions = useTabActionsStore((state) => state.setTabActions);
const clearTabActions = useTabActionsStore((state) => state.clearTabActions);
// ===== TAB ACTIONS COMPONENT =====
const TabActions = useMemo(() => {
return function TabActionsComponent() {
const setTabActions = useTabActionsStore((state) => state.setTabActions);
const clearTabActions = useTabActionsStore(
(state) => state.clearTabActions
);
useEffect(() => {
setTabActions(
tabId,
<div className='flex flex-row gap-3'>
<ButtonFilter
values={formik.values}
fieldGroups={[['start_date', 'end_date']]}
onClick={handleFilterModalOpen}
variant='outline'
className='px-3 py-2.5'
/>
<Dropdown
align='end'
direction='bottom'
className={{
content:
'mt-1 rounded-xl border border-base-content/5 shadow-sm overflow-hidden',
}}
trigger={
<Button
useEffect(() => {
setTabActions(
tabId,
<div className='flex flex-row gap-3'>
<ButtonFilter
values={filterParams}
fieldGroups={[['start_date', 'end_date']]}
onClick={() => handleFilterModalOpenRef.current()}
variant='outline'
color='none'
isLoading={isAnyExportLoading}
className='px-3 py-2.5 text-sm text-base-content/50 border border-base-content/10 rounded-xl shadow-button-soft'
className='px-3 py-2.5'
/>
<Dropdown
align='end'
direction='bottom'
className={{
content:
'mt-1 rounded-xl border border-base-content/5 shadow-sm overflow-hidden',
}}
trigger={
<Button
variant='outline'
color='none'
isLoading={isAnyExportLoading}
className='px-3 py-2.5 text-sm text-base-content/50 border border-base-content/10 rounded-xl shadow-button-soft'
>
<div className='flex flex-row items-center gap-1.5'>
<Icon
icon='heroicons:cloud-arrow-down'
width={20}
height={20}
/>
<span>Export</span>
<div className='w-px self-stretch bg-base-content/10' />
<Icon
icon='heroicons:chevron-down'
width={14}
height={14}
/>
</div>
</Button>
}
>
<div className='flex flex-row items-center gap-1.5'>
<Icon
icon='heroicons:cloud-arrow-down'
width={20}
height={20}
/>
<Button
variant='ghost'
color='none'
onClick={handleExportExcel}
isLoading={isExcelExportLoading}
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>
<Button
variant='ghost'
color='none'
onClick={handleExportPdf}
isLoading={isPdfExportLoading}
className='w-full p-3 justify-start text-sm text-base-content/50 font-semibold text-nowrap'
>
<Icon icon='heroicons:document' width={20} height={20} />
Export to PDF
</Button>
</Dropdown>
</div>
);
}, [setTabActions]);
<span>Export</span>
useEffect(() => {
return () => {
clearTabActions(tabId);
};
}, [clearTabActions]);
<div className='w-px self-stretch bg-base-content/10' />
<Icon icon='heroicons:chevron-down' width={14} height={14} />
</div>
</Button>
}
>
<Button
variant='ghost'
color='none'
onClick={handleExportExcel}
isLoading={isExcelExportLoading}
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>
<Button
variant='ghost'
color='none'
onClick={handleExportPdf}
isLoading={isPdfExportLoading}
className='w-full p-3 justify-start text-sm text-base-content/50 font-semibold text-nowrap'
>
<Icon icon='heroicons:document' width={20} height={20} />
Export to PDF
</Button>
</Dropdown>
</div>
);
return null;
};
}, [
tabId,
formik.values,
isAnyExportLoading,
handleExportExcel,
handleExportPdf,
filterModal.open,
setTabActions,
isExcelExportLoading,
isPdfExportLoading,
filterParams,
]);
useEffect(() => {
return () => {
clearTabActions(tabId);
};
}, [tabId, clearTabActions]);
const TabActionsElement = useMemo(() => <TabActions />, [TabActions]);
const getTableColumns = (
summary: CustomerPaymentSummary
@@ -683,6 +698,7 @@ const CustomerPaymentTab = ({ tabId }: CustomerPaymentTabProps) => {
return (
<>
{TabActionsElement}
<div className='w-full p-0 sm:p-3 flex flex-col gap-3'>
{!isSubmitted ? (
<CustomerSupplierSkeleton
@@ -17,7 +17,7 @@ import { generateDebtSupplierExcel } from '@/components/pages/report/finance/exp
import { generateDebtSupplierPDF } from '@/components/pages/report/finance/export/DebtSupllierExportPDF';
import { Icon } from '@iconify/react';
import { ColumnDef } from '@tanstack/react-table';
import { useCallback, useEffect, useMemo, useState } from 'react';
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import toast from 'react-hot-toast';
import useSWR from 'swr';
import { DebtSupplierApi } from '@/services/api/report/debt-supplier';
@@ -91,6 +91,8 @@ const DebtSupplierTab = ({ tabId }: DebtSupplierTabProps) => {
const [dateErrorShown, setDateErrorShown] = useState(false);
const [hasDateError, setHasDateError] = useState(false);
const handleFilterModalOpenRef = useRef(() => {});
const filterModal = useModal();
const {
@@ -108,11 +110,6 @@ const DebtSupplierTab = ({ tabId }: DebtSupplierTabProps) => {
[]
);
const handleFilterModalOpen = () => {
filterModal.openModal();
formik.validateForm();
};
// ===== FORMIK SETUP =====
const formik = useFormik<DebtSupplierFilterType>({
initialValues: {
@@ -146,6 +143,11 @@ const DebtSupplierTab = ({ tabId }: DebtSupplierTabProps) => {
},
});
handleFilterModalOpenRef.current = () => {
filterModal.openModal();
formik.validateForm();
};
// ===== DATA FETCHING =====
const { data: debtSupplier, isLoading } = useSWR(
isSubmitted
@@ -268,92 +270,112 @@ const DebtSupplierTab = ({ tabId }: DebtSupplierTabProps) => {
} finally {
setIsPdfExportLoading(false);
}
}, [debtSupplierExport]);
}, [
debtSupplierExport,
formik.values.supplierIds,
formik.values.filterBy,
formik.values.startDate,
formik.values.endDate,
]);
// ===== REGISTER TAB ACTIONS TO STORE =====
const setTabActions = useTabActionsStore((state) => state.setTabActions);
const clearTabActions = useTabActionsStore((state) => state.clearTabActions);
// ===== TAB ACTIONS COMPONENT =====
const TabActions = useMemo(() => {
return function TabActionsComponent() {
const setTabActions = useTabActionsStore((state) => state.setTabActions);
const clearTabActions = useTabActionsStore(
(state) => state.clearTabActions
);
useEffect(() => {
setTabActions(
tabId,
<div className='flex flex-row gap-3'>
<ButtonFilter
values={formik.values}
fieldGroups={[['startDate', 'endDate']]}
onClick={handleFilterModalOpen}
variant='outline'
className='px-3 py-2.5'
/>
<Dropdown
align='end'
direction='bottom'
className={{
content:
'mt-1 rounded-xl border border-base-content/5 shadow-sm overflow-hidden',
}}
trigger={
<Button
useEffect(() => {
setTabActions(
tabId,
<div className='flex flex-row gap-3'>
<ButtonFilter
values={filterParams}
fieldGroups={[['start_date', 'end_date']]}
onClick={() => handleFilterModalOpenRef.current()}
variant='outline'
color='none'
isLoading={isAnyExportLoading}
className='px-3 py-2.5 text-sm text-base-content/50 border border-base-content/10 rounded-xl shadow-button-soft'
className='px-3 py-2.5'
/>
<Dropdown
align='end'
direction='bottom'
className={{
content:
'mt-1 rounded-xl border border-base-content/5 shadow-sm overflow-hidden',
}}
trigger={
<Button
variant='outline'
color='none'
isLoading={isAnyExportLoading}
className='px-3 py-2.5 text-sm text-base-content/50 border border-base-content/10 rounded-xl shadow-button-soft'
>
<div className='flex flex-row items-center gap-1.5'>
<Icon
icon='heroicons:cloud-arrow-down'
width={20}
height={20}
/>
<span>Export</span>
<div className='w-px self-stretch bg-base-content/10' />
<Icon
icon='heroicons:chevron-down'
width={14}
height={14}
/>
</div>
</Button>
}
>
<div className='flex flex-row items-center gap-1.5'>
<Icon
icon='heroicons:cloud-arrow-down'
width={20}
height={20}
/>
<Button
variant='ghost'
color='none'
onClick={handleExportExcel}
isLoading={isExcelExportLoading}
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>
<Button
variant='ghost'
color='none'
onClick={handleExportPdf}
isLoading={isPdfExportLoading}
className='w-full p-3 justify-start text-sm text-base-content/50 font-semibold text-nowrap'
>
<Icon icon='heroicons:document' width={20} height={20} />
Export to PDF
</Button>
</Dropdown>
</div>
);
}, [setTabActions]);
<span>Export</span>
useEffect(() => {
return () => {
clearTabActions(tabId);
};
}, [clearTabActions]);
<div className='w-px self-stretch bg-base-content/10' />
<Icon icon='heroicons:chevron-down' width={14} height={14} />
</div>
</Button>
}
>
<Button
variant='ghost'
color='none'
onClick={handleExportExcel}
isLoading={isExcelExportLoading}
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>
<Button
variant='ghost'
color='none'
onClick={handleExportPdf}
isLoading={isPdfExportLoading}
className='w-full p-3 justify-start text-sm text-base-content/50 font-semibold text-nowrap'
>
<Icon icon='heroicons:document' width={20} height={20} />
Export to PDF
</Button>
</Dropdown>
</div>
);
return null;
};
}, [
tabId,
formik.values,
filterParams,
isAnyExportLoading,
handleExportExcel,
handleExportPdf,
setTabActions,
isExcelExportLoading,
isPdfExportLoading,
]);
// Cleanup on unmount
useEffect(() => {
return () => {
clearTabActions(tabId);
};
}, [tabId, clearTabActions]);
const TabActionsElement = useMemo(() => <TabActions />, [TabActions]);
useEffect(() => {
return () => {
@@ -587,6 +609,7 @@ const DebtSupplierTab = ({ tabId }: DebtSupplierTabProps) => {
];
return (
<>
{TabActionsElement}
<div className='w-full p-0 sm:p-3 flex flex-col gap-3'>
{!isSubmitted ? (
<DebtSupplierSkeleton
@@ -6,7 +6,7 @@ import { useSelect } from '@/components/input/SelectInput';
import Modal, { useModal } from '@/components/Modal';
import Table from '@/components/Table';
import { isResponseSuccess } from '@/lib/api-helper';
import { cn, formatCurrency, formatDate, formatNumber } from '@/lib/helper';
import { formatCurrency, formatDate, formatNumber } from '@/lib/helper';
import { AreaApi } from '@/services/api/master-data';
import { SupplierApi } from '@/services/api/master-data';
import { ProductApi } from '@/services/api/master-data';
@@ -20,7 +20,7 @@ import { generatePurchasesPerSupplierExcel } from '@/components/pages/report/log
import { generatePurchasesPerSupplierPDF } from '@/components/pages/report/logistic-stock/export/PurchasesPerSupplierExportPDF';
import { Icon } from '@iconify/react';
import { ColumnDef } from '@tanstack/react-table';
import { useCallback, useEffect, useMemo, useState } from 'react';
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import toast from 'react-hot-toast';
import useSWR from 'swr';
import { useFormik } from 'formik';
@@ -65,6 +65,8 @@ const PurchasesPerSupplierTab = ({ tabId }: PurchasesPerSupplierTabProps) => {
const [dateErrorShown, setDateErrorShown] = useState(false);
const [hasDateError, setHasDateError] = useState(false);
const handleFilterModalOpenRef = useRef(() => {});
const filterModal = useModal();
// ===== OPTIONS =====
@@ -104,11 +106,6 @@ const PurchasesPerSupplierTab = ({ tabId }: PurchasesPerSupplierTabProps) => {
[]
);
const handleFilterModalOpen = () => {
filterModal.openModal();
formik.validateForm();
};
// ===== FORMIK SETUP =====
const formik = useFormik<PurchasesPerSupplierFilterType>({
initialValues: {
@@ -151,11 +148,18 @@ const PurchasesPerSupplierTab = ({ tabId }: PurchasesPerSupplierTabProps) => {
},
});
handleFilterModalOpenRef.current = () => {
filterModal.openModal();
formik.validateForm();
};
const { setFieldValue } = formik;
// ===== DATE CHANGE HANDLERS =====
const handleStartDateChange = useCallback(
(e: React.ChangeEvent<HTMLInputElement>) => {
const value = e.target.value;
formik.setFieldValue('start_date', value || null);
setFieldValue('start_date', value || null);
if (value && formik.values.end_date) {
const startDate = new Date(value);
@@ -180,13 +184,13 @@ const PurchasesPerSupplierTab = ({ tabId }: PurchasesPerSupplierTabProps) => {
setHasDateError(false);
}
},
[formik, dateErrorShown]
[setFieldValue, dateErrorShown, formik.values.end_date]
);
const handleEndDateChange = useCallback(
(e: React.ChangeEvent<HTMLInputElement>) => {
const value = e.target.value;
formik.setFieldValue('end_date', value || null);
setFieldValue('end_date', value || null);
if (value && formik.values.start_date) {
const startDateObj = new Date(formik.values.start_date);
@@ -210,7 +214,7 @@ const PurchasesPerSupplierTab = ({ tabId }: PurchasesPerSupplierTabProps) => {
setDateErrorShown(false);
}
},
[formik, dateErrorShown]
[setFieldValue, dateErrorShown, formik.values.start_date]
);
// ===== DERIVED VALUES =====
@@ -443,88 +447,104 @@ const PurchasesPerSupplierTab = ({ tabId }: PurchasesPerSupplierTabProps) => {
productCategoryOptions,
]);
// ===== REGISTER TAB ACTIONS TO STORE =====
const setTabActions = useTabActionsStore((state) => state.setTabActions);
const clearTabActions = useTabActionsStore((state) => state.clearTabActions);
// ===== TAB ACTIONS COMPONENT =====
const TabActions = useMemo(() => {
return function TabActionsComponent() {
const setTabActions = useTabActionsStore((state) => state.setTabActions);
const clearTabActions = useTabActionsStore(
(state) => state.clearTabActions
);
useEffect(() => {
setTabActions(
tabId,
<div className='flex flex-row gap-3'>
<ButtonFilter
values={formik.values}
fieldGroups={[['start_date', 'end_date']]}
onClick={handleFilterModalOpen}
variant='outline'
className='px-3 py-2.5'
/>
<Dropdown
align='end'
direction='bottom'
className={{
content:
'mt-1 rounded-xl border border-base-content/5 shadow-sm overflow-hidden',
}}
trigger={
<Button
useEffect(() => {
setTabActions(
tabId,
<div className='flex flex-row gap-3'>
<ButtonFilter
values={filterParams}
fieldGroups={[['start_date', 'end_date']]}
onClick={() => handleFilterModalOpenRef.current()}
variant='outline'
color='none'
isLoading={isAnyExportLoading}
className='px-3 py-2.5 text-sm text-base-content/50 border border-base-content/10 rounded-xl shadow-button-soft'
className='px-3 py-2.5'
/>
<Dropdown
align='end'
direction='bottom'
className={{
content:
'mt-1 rounded-xl border border-base-content/5 shadow-sm overflow-hidden',
}}
trigger={
<Button
variant='outline'
color='none'
isLoading={isAnyExportLoading}
className='px-3 py-2.5 text-sm text-base-content/50 border border-base-content/10 rounded-xl shadow-button-soft'
>
<div className='flex flex-row items-center gap-1.5'>
<Icon
icon='heroicons:cloud-arrow-down'
width={20}
height={20}
/>
<span>Export</span>
<div className='w-px self-stretch bg-base-content/10' />
<Icon
icon='heroicons:chevron-down'
width={14}
height={14}
/>
</div>
</Button>
}
>
<div className='flex flex-row items-center gap-1.5'>
<Icon
icon='heroicons:cloud-arrow-down'
width={20}
height={20}
/>
<Button
variant='ghost'
color='none'
onClick={handleExportExcel}
isLoading={isExcelExportLoading}
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>
<Button
variant='ghost'
color='none'
onClick={handleExportPdf}
isLoading={isPdfExportLoading}
className='w-full p-3 justify-start text-sm text-base-content/50 font-semibold text-nowrap'
>
<Icon icon='heroicons:document' width={20} height={20} />
Export to PDF
</Button>
</Dropdown>
</div>
);
}, [setTabActions]);
<span>Export</span>
useEffect(() => {
return () => {
clearTabActions(tabId);
};
}, [clearTabActions]);
<div className='w-px self-stretch bg-base-content/10' />
<Icon icon='heroicons:chevron-down' width={14} height={14} />
</div>
</Button>
}
>
<Button
variant='ghost'
color='none'
onClick={handleExportExcel}
isLoading={isExcelExportLoading}
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>
<Button
variant='ghost'
color='none'
onClick={handleExportPdf}
isLoading={isPdfExportLoading}
className='w-full p-3 justify-start text-sm text-base-content/50 font-semibold text-nowrap'
>
<Icon icon='heroicons:document' width={20} height={20} />
Export to PDF
</Button>
</Dropdown>
</div>
);
return null;
};
}, [
tabId,
formik.values,
filterParams,
isAnyExportLoading,
filterModal.open,
setTabActions,
handleExportExcel,
handleExportPdf,
isExcelExportLoading,
isPdfExportLoading,
]);
useEffect(() => {
return () => {
clearTabActions(tabId);
};
}, [tabId, clearTabActions]);
const TabActionsElement = useMemo(() => <TabActions />, [TabActions]);
const getTableColumns = (
summary: LogisticPurchasePerSupplierSummary
@@ -704,6 +724,7 @@ const PurchasesPerSupplierTab = ({ tabId }: PurchasesPerSupplierTabProps) => {
return (
<>
{TabActionsElement}
<div className='w-full p-0 sm:p-3 flex flex-col gap-3'>
{!isSubmitted ? (
<PurchasePerSupplierSkeleton
@@ -1,4 +1,4 @@
import { useState, useMemo, useCallback } from 'react';
import { useState, useMemo, useCallback, useRef } from 'react';
import useSWR from 'swr';
import { useSelect } from '@/components/input/SelectInput';
import DateInput from '@/components/input/DateInput';
@@ -83,6 +83,8 @@ const DailyMarketingTab = ({ tabId }: DailyMarketingTabProps) => {
const [dateErrorShown, setDateErrorShown] = useState(false);
const [hasDateError, setHasDateError] = useState(false);
const handleFilterModalOpenRef = useRef(() => {});
const filterModal = useModal();
// ===== OPTIONS =====
@@ -102,11 +104,6 @@ const DailyMarketingTab = ({ tabId }: DailyMarketingTabProps) => {
const { options: customerOptions, isLoadingOptions: isLoadingCustomers } =
useSelect(CustomerApi.basePath, 'id', 'name', 'search');
const handleFilterModalOpen = () => {
filterModal.openModal();
formik.validateForm();
};
// ===== FORMIK SETUP =====
const formik = useFormik<DailyMarketingReportFilterType>({
initialValues: {
@@ -145,6 +142,11 @@ const DailyMarketingTab = ({ tabId }: DailyMarketingTabProps) => {
},
});
handleFilterModalOpenRef.current = () => {
filterModal.openModal();
formik.validateForm();
};
// ===== SEARCH CHANGE HANDLER =====
const searchChangeHandler = useCallback(
(e: React.ChangeEvent<HTMLInputElement>) => {
@@ -353,121 +355,126 @@ const DailyMarketingTab = ({ tabId }: DailyMarketingTabProps) => {
}
}, [dailyMarketingsExport, summaryTotal]);
// ===== REGISTER TAB ACTIONS TO STORE =====
const setTabActions = useTabActionsStore((state) => state.setTabActions);
const clearTabActions = useTabActionsStore((state) => state.clearTabActions);
// ===== TAB ACTIONS COMPONENT =====
const TabActions = useMemo(() => {
return function TabActionsComponent() {
const setTabActions = useTabActionsStore((state) => state.setTabActions);
const clearTabActions = useTabActionsStore(
(state) => state.clearTabActions
);
useEffectHook(() => {
setTabActions(
tabId,
<div className='flex flex-row gap-3 items-center'>
<DebouncedTextInput
name='search'
placeholder='Search'
value={searchValue}
onChange={searchChangeHandler}
startAdornment={
<Icon icon='heroicons:magnifying-glass' width={20} height={20} />
}
className={{
wrapper: 'w-full min-w-48 max-w-3xs',
inputWrapper: 'rounded-xl! shadow-button-soft',
input: 'placeholder:font-semibold placeholder:text-base-content/50',
}}
/>
<ButtonFilter
values={formik.values}
fieldGroups={[['start_date', 'end_date']]}
onClick={handleFilterModalOpen}
variant='outline'
className='px-3 py-2.5'
/>
<Dropdown
align='end'
direction='bottom'
className={{
content:
'mt-1 rounded-xl border border-base-content/5 shadow-sm overflow-hidden',
}}
trigger={
<Button
variant='outline'
color='none'
isLoading={isAnyExportLoading}
className='px-3 py-2.5 text-sm text-base-content/50 border border-base-content/10 rounded-xl shadow-button-soft'
>
<div className='flex flex-row items-center gap-1.5'>
useEffectHook(() => {
setTabActions(
tabId,
<div className='flex flex-row gap-3 items-center'>
<DebouncedTextInput
name='search'
placeholder='Search'
value={searchValue}
onChange={searchChangeHandler}
startAdornment={
<Icon
icon='heroicons:cloud-arrow-down'
icon='heroicons:magnifying-glass'
width={20}
height={20}
/>
}
className={{
wrapper: 'w-full min-w-48 max-w-3xs',
inputWrapper: 'rounded-xl! shadow-button-soft',
input:
'placeholder:font-semibold placeholder:text-base-content/50',
}}
/>
<span>Export</span>
<ButtonFilter
values={filterParams}
fieldGroups={[['start_date', 'end_date']]}
onClick={() => handleFilterModalOpenRef.current()}
variant='outline'
className='px-3 py-2.5'
/>
<div className='w-px self-stretch bg-base-content/10' />
<Dropdown
align='end'
direction='bottom'
className={{
content:
'mt-1 rounded-xl border border-base-content/5 shadow-sm overflow-hidden',
}}
trigger={
<Button
variant='outline'
color='none'
isLoading={isAnyExportLoading}
className='px-3 py-2.5 text-sm text-base-content/50 border border-base-content/10 rounded-xl shadow-button-soft'
>
<div className='flex flex-row items-center gap-1.5'>
<Icon
icon='heroicons:cloud-arrow-down'
width={20}
height={20}
/>
<Icon icon='heroicons:chevron-down' width={14} height={14} />
</div>
</Button>
}
>
<Button
variant='ghost'
color='none'
onClick={handleExportExcel}
isLoading={isExcelExportLoading}
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>
<Button
variant='ghost'
color='none'
onClick={handleExportPDF}
isLoading={isPdfExportLoading}
className='w-full p-3 justify-start text-sm text-base-content/50 font-semibold text-nowrap'
>
<Icon icon='heroicons:document' width={20} height={20} />
Export to PDF
</Button>
</Dropdown>
</div>
);
<span>Export</span>
<div className='w-px self-stretch bg-base-content/10' />
<Icon
icon='heroicons:chevron-down'
width={14}
height={14}
/>
</div>
</Button>
}
>
<Button
variant='ghost'
color='none'
onClick={handleExportExcel}
isLoading={isExcelExportLoading}
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>
<Button
variant='ghost'
color='none'
onClick={handleExportPDF}
isLoading={isPdfExportLoading}
className='w-full p-3 justify-start text-sm text-base-content/50 font-semibold text-nowrap'
>
<Icon icon='heroicons:document' width={20} height={20} />
Export to PDF
</Button>
</Dropdown>
</div>
);
}, [setTabActions]);
useEffectHook(() => {
return () => {
clearTabActions(tabId);
};
}, [clearTabActions]);
return null;
};
}, [
tabId,
filterParams,
searchValue,
formik.values,
isAnyExportLoading,
filterModal.open,
setTabActions,
handleExportExcel,
handleExportPDF,
isExcelExportLoading,
isPdfExportLoading,
searchChangeHandler,
]);
useEffectHook(() => {
return () => {
clearTabActions(tabId);
};
}, [tabId, clearTabActions]);
useEffectHook(() => {
return () => {
if (dateErrorShown) {
toast.dismiss();
}
};
}, [dateErrorShown]);
useEffectHook(() => {
return () => {
if (dateErrorShown) {
toast.dismiss();
setDateErrorShown(false);
}
};
}, [filterModal.open, dateErrorShown]);
const TabActionsElement = useMemo(() => <TabActions />, [TabActions]);
const getTableColumns = (): ColumnDef<DailyMarketingRow>[] => {
const tableColumns: ColumnDef<DailyMarketingRow>[] = [
@@ -639,6 +646,7 @@ const DailyMarketingTab = ({ tabId }: DailyMarketingTabProps) => {
return (
<>
{TabActionsElement}
<div className='w-full p-0 sm:p-3 flex flex-col gap-3'>
{!isSubmitted ? (
<DailyMarketingReportSkeleton
@@ -1,4 +1,4 @@
import { useState, useMemo, useCallback } from 'react';
import { useState, useMemo, useCallback, useRef } from 'react';
import useSWR from 'swr';
import { useSelect } from '@/components/input/SelectInput';
import DateInput from '@/components/input/DateInput';
@@ -31,7 +31,6 @@ import {
import SelectInputCheckbox from '@/components/input/SelectInputCheckbox';
import SelectInputRadio from '@/components/input/SelectInputRadio';
import Modal, { useModal } from '@/components/Modal';
import { cn } from '@/lib/helper';
import { useTabActionsStore } from '@/stores/tab-actions/tab-actions.store';
import HppPerKandangSkeleton from '@/components/pages/report/marketing/skeleton/HppPerKandangSkeleton';
import { useEffect as useEffectHook } from 'react';
@@ -67,6 +66,8 @@ const HppPerKandangTab = ({ tabId }: HppPerKandangTabProps) => {
// ===== FILTER STATE =====
const [filterParams, setFilterParams] = useState<FilterParams>({});
const handleFilterModalOpenRef = useRef(() => {});
const filterModal = useModal();
// ===== OPTIONS =====
@@ -96,11 +97,6 @@ const HppPerKandangTab = ({ tabId }: HppPerKandangTabProps) => {
[]
);
const handleFilterModalOpen = () => {
filterModal.openModal();
formik.validateForm();
};
// ===== FORMIK SETUP =====
const formik = useFormik<HppPerKandangFilterType>({
initialValues: {
@@ -141,6 +137,11 @@ const HppPerKandangTab = ({ tabId }: HppPerKandangTabProps) => {
},
});
handleFilterModalOpenRef.current = () => {
filterModal.openModal();
formik.validateForm();
};
// ===== WEIGHT CHANGE HANDLERS =====
const handleWeightMinChange = useCallback(
(e: React.ChangeEvent<HTMLInputElement>) => {
@@ -443,87 +444,103 @@ const HppPerKandangTab = ({ tabId }: HppPerKandangTabProps) => {
allDocSuppliers,
]);
// ===== REGISTER TAB ACTIONS TO STORE =====
const setTabActions = useTabActionsStore((state) => state.setTabActions);
const clearTabActions = useTabActionsStore((state) => state.clearTabActions);
// ===== TAB ACTIONS COMPONENT =====
const TabActions = useMemo(() => {
return function TabActionsComponent() {
const setTabActions = useTabActionsStore((state) => state.setTabActions);
const clearTabActions = useTabActionsStore(
(state) => state.clearTabActions
);
useEffectHook(() => {
setTabActions(
tabId,
<div className='flex flex-row gap-3'>
<ButtonFilter
values={formik.values}
onClick={handleFilterModalOpen}
variant='outline'
className='px-3 py-2.5'
/>
<Dropdown
align='end'
direction='bottom'
className={{
content:
'mt-1 rounded-xl border border-base-content/5 shadow-sm overflow-hidden',
}}
trigger={
<Button
useEffectHook(() => {
setTabActions(
tabId,
<div className='flex flex-row gap-3'>
<ButtonFilter
values={filterParams}
onClick={() => handleFilterModalOpenRef.current()}
variant='outline'
color='none'
isLoading={isAnyExportLoading}
className='px-3 py-2.5 text-sm text-base-content/50 border border-base-content/10 rounded-xl shadow-button-soft'
className='px-3 py-2.5'
/>
<Dropdown
align='end'
direction='bottom'
className={{
content:
'mt-1 rounded-xl border border-base-content/5 shadow-sm overflow-hidden',
}}
trigger={
<Button
variant='outline'
color='none'
isLoading={isAnyExportLoading}
className='px-3 py-2.5 text-sm text-base-content/50 border border-base-content/10 rounded-xl shadow-button-soft'
>
<div className='flex flex-row items-center gap-1.5'>
<Icon
icon='heroicons:cloud-arrow-down'
width={20}
height={20}
/>
<span>Export</span>
<div className='w-px self-stretch bg-base-content/10' />
<Icon
icon='heroicons:chevron-down'
width={14}
height={14}
/>
</div>
</Button>
}
>
<div className='flex flex-row items-center gap-1.5'>
<Icon
icon='heroicons:cloud-arrow-down'
width={20}
height={20}
/>
<Button
variant='ghost'
color='none'
onClick={handleExportExcel}
isLoading={isExcelExportLoading}
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>
<Button
variant='ghost'
color='none'
onClick={handleExportPDF}
isLoading={isPdfExportLoading}
className='w-full p-3 justify-start text-sm text-base-content/50 font-semibold text-nowrap'
>
<Icon icon='heroicons:document' width={20} height={20} />
Export to PDF
</Button>
</Dropdown>
</div>
);
}, [setTabActions]);
<span>Export</span>
useEffectHook(() => {
return () => {
clearTabActions(tabId);
};
}, [clearTabActions]);
<div className='w-px self-stretch bg-base-content/10' />
<Icon icon='heroicons:chevron-down' width={14} height={14} />
</div>
</Button>
}
>
<Button
variant='ghost'
color='none'
onClick={handleExportExcel}
isLoading={isExcelExportLoading}
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>
<Button
variant='ghost'
color='none'
onClick={handleExportPDF}
isLoading={isPdfExportLoading}
className='w-full p-3 justify-start text-sm text-base-content/50 font-semibold text-nowrap'
>
<Icon icon='heroicons:document' width={20} height={20} />
Export to PDF
</Button>
</Dropdown>
</div>
);
return null;
};
}, [
tabId,
formik.values,
filterParams,
isAnyExportLoading,
filterModal.open,
setTabActions,
handleExportExcel,
handleExportPDF,
isExcelExportLoading,
isPdfExportLoading,
]);
useEffectHook(() => {
return () => {
clearTabActions(tabId);
};
}, [tabId, clearTabActions]);
const TabActionsElement = useMemo(() => <TabActions />, [TabActions]);
const getTableColumns = (): ColumnDef<HppPerKandangReport['rows'][0]>[] => {
const tableColumns: ColumnDef<HppPerKandangReport['rows'][0]>[] = [
@@ -768,6 +785,7 @@ const HppPerKandangTab = ({ tabId }: HppPerKandangTabProps) => {
return (
<>
{TabActionsElement}
<div className='w-full p-0 sm:p-3 flex flex-col gap-3'>
{!isSubmitted ? (
<HppPerKandangSkeleton
@@ -1,6 +1,12 @@
'use client';
import React, { useState, useCallback, useEffect, useMemo } from 'react';
import React, {
useState,
useCallback,
useEffect,
useMemo,
useRef,
} from 'react';
import useSWR from 'swr';
import { generateProductionResultExcel } from '../export/ProductionResultExportXLSX';
import toast from 'react-hot-toast';
@@ -37,7 +43,7 @@ import ProductionResultReportPDF from '../export/ProductionResultExportPDF';
import { pdf } from '@react-pdf/renderer';
import { useTabActionsStore } from '@/stores/tab-actions/tab-actions.store';
import Modal, { useModal } from '@/components/Modal';
import { cn, formatNumber } from '@/lib/helper';
import { formatNumber } from '@/lib/helper';
import Pagination from '@/components/Pagination';
import ProductionResultSkeleton from '@/components/pages/report/production-result/skeleton/ProductionResultSkeleton';
@@ -66,6 +72,8 @@ const ProductionResultContent = ({ tabId }: ProductionResultTabProps) => {
const [page, setPage] = useState(1);
const [pageSize, setPageSize] = useState(10);
const handleFilterModalOpenRef = useRef(() => {});
const filterModal = useModal();
// ===== TABLE COLUMNS =====
@@ -242,6 +250,11 @@ const ProductionResultContent = ({ tabId }: ProductionResultTabProps) => {
},
});
handleFilterModalOpenRef.current = () => {
filterModal.openModal();
formik.validateForm();
};
// ===== OPTIONS =====
const {
setInputValue: setAreaInputValue,
@@ -519,91 +532,108 @@ const ProductionResultContent = ({ tabId }: ProductionResultTabProps) => {
setIsPdfExportLoading(false);
}, [filterParams]);
// ===== REGISTER TAB ACTIONS TO STORE =====
const setTabActions = useTabActionsStore((state) => state.setTabActions);
const clearTabActions = useTabActionsStore((state) => state.clearTabActions);
// ===== TAB ACTIONS COMPONENT =====
const TabActions = useMemo(() => {
return function TabActionsComponent() {
const setTabActions = useTabActionsStore((state) => state.setTabActions);
const clearTabActions = useTabActionsStore(
(state) => state.clearTabActions
);
useEffect(() => {
setTabActions(
tabId,
<div className='flex flex-row gap-3'>
<ButtonFilter
values={filterParams}
onClick={() => filterModal.openModal()}
variant='outline'
className='px-3 py-2.5'
/>
<Dropdown
align='end'
direction='bottom'
className={{
content:
'mt-1 rounded-xl border border-base-content/5 shadow-sm overflow-hidden',
}}
trigger={
<Button
useEffect(() => {
setTabActions(
tabId,
<div className='flex flex-row gap-3'>
<ButtonFilter
values={filterParams}
onClick={() => handleFilterModalOpenRef.current()}
variant='outline'
color='none'
isLoading={isAnyExportLoading}
className='px-3 py-2.5 text-sm text-base-content/50 border border-base-content/10 rounded-xl shadow-button-soft'
className='px-3 py-2.5'
/>
<Dropdown
align='end'
direction='bottom'
className={{
content:
'mt-1 rounded-xl border border-base-content/5 shadow-sm overflow-hidden',
}}
trigger={
<Button
variant='outline'
color='none'
isLoading={isAnyExportLoading}
className='px-3 py-2.5 text-sm text-base-content/50 border border-base-content/10 rounded-xl shadow-button-soft'
>
<div className='flex flex-row items-center gap-1.5'>
<Icon
icon='heroicons:cloud-arrow-down'
width={20}
height={20}
/>
<span>Export</span>
<div className='w-px self-stretch bg-base-content/10' />
<Icon
icon='heroicons:chevron-down'
width={14}
height={14}
/>
</div>
</Button>
}
>
<div className='flex flex-row items-center gap-1.5'>
<Icon
icon='heroicons:cloud-arrow-down'
width={20}
height={20}
/>
<Button
variant='ghost'
color='none'
onClick={exportToExcelHandler}
isLoading={isExcelExportLoading}
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>
<Button
variant='ghost'
color='none'
onClick={exportToPdfHandler}
isLoading={isPdfExportLoading}
className='w-full p-3 justify-start text-sm text-base-content/50 font-semibold text-nowrap'
>
<Icon icon='heroicons:document' width={20} height={20} />
Export to PDF
</Button>
</Dropdown>
</div>
);
}, [setTabActions]);
<span>Export</span>
useEffect(() => {
return () => {
clearTabActions(tabId);
};
}, [clearTabActions]);
<div className='w-px self-stretch bg-base-content/10' />
<Icon icon='heroicons:chevron-down' width={14} height={14} />
</div>
</Button>
}
>
<Button
variant='ghost'
color='none'
onClick={exportToExcelHandler}
isLoading={isExcelExportLoading}
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>
<Button
variant='ghost'
color='none'
onClick={exportToPdfHandler}
isLoading={isPdfExportLoading}
className='w-full p-3 justify-start text-sm text-base-content/50 font-semibold text-nowrap'
>
<Icon icon='heroicons:document' width={20} height={20} />
Export to PDF
</Button>
</Dropdown>
</div>
);
return null;
};
}, [
tabId,
filterParams,
isAnyExportLoading,
exportToExcelHandler,
exportToPdfHandler,
setTabActions,
isExcelExportLoading,
isPdfExportLoading,
]);
useEffect(() => {
return () => {
clearTabActions(tabId);
};
}, [tabId, clearTabActions]);
// Render the TabActions component
const TabActionsElement = useMemo(() => <TabActions />, [TabActions]);
return (
<>
{TabActionsElement}
<div className='w-full p-0 sm:p-3 flex flex-col gap-3'>
{!isSubmitted ? (
<ProductionResultSkeleton
@@ -263,7 +263,7 @@ const ProductionResultProjectFlockKandangTable = ({
updateFilter('filter_by', '');
updateFilter('sort_by', '');
}
}, [sorting]);
}, [sorting, updateFilter]);
return (
<Card
+1 -1
View File
@@ -272,7 +272,7 @@ export function transformAdjustmentSubtypes(
export function transformLegacyFlagAliases(
aliases: ConstantsApiResponse['legacy_flag_aliases']
): OptionType[] {
return Object.entries(aliases).map(([key, value]) => ({
return Object.entries(aliases).map(([key]) => ({
value: key,
label: formatConstantLabel(key),
}));