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