mirror of
https://gitlab.com/mbugroup/lti-web-client.git
synced 2026-05-20 13:32:00 +00:00
refactor(FE): Refactor PurchasesPerSupplierTab to use Formik for filters
This commit is contained in:
@@ -0,0 +1,89 @@
|
||||
import { OptionType } from '@/components/input/SelectInput';
|
||||
import * as yup from 'yup';
|
||||
|
||||
export type PurchasesPerSupplierFilterType = {
|
||||
start_date: string | null | undefined;
|
||||
end_date: string | null | undefined;
|
||||
area_ids: OptionType[] | null | undefined;
|
||||
supplier_ids: OptionType[] | null | undefined;
|
||||
product_ids: OptionType[] | null | undefined;
|
||||
product_category_ids: OptionType[] | null | undefined;
|
||||
filter_by: OptionType | null | undefined;
|
||||
sort_by: OptionType | null | undefined;
|
||||
};
|
||||
|
||||
export const PurchasesPerSupplierFilterSchema: yup.ObjectSchema<PurchasesPerSupplierFilterType> =
|
||||
yup.object({
|
||||
start_date: yup.string().optional().nullable(),
|
||||
end_date: yup
|
||||
.string()
|
||||
.optional()
|
||||
.nullable()
|
||||
.test(
|
||||
'is-greater-than-start',
|
||||
'Tanggal akhir tidak boleh masa lampau',
|
||||
function (value) {
|
||||
const { start_date } = this.parent;
|
||||
if (!start_date || !value) return true;
|
||||
return new Date(value) >= new Date(start_date);
|
||||
}
|
||||
),
|
||||
area_ids: yup
|
||||
.array()
|
||||
.of(
|
||||
yup.object({
|
||||
value: yup.mixed<string | number>().required(),
|
||||
label: yup.string().required(),
|
||||
})
|
||||
)
|
||||
.optional()
|
||||
.nullable(),
|
||||
supplier_ids: yup
|
||||
.array()
|
||||
.of(
|
||||
yup.object({
|
||||
value: yup.mixed<string | number>().required(),
|
||||
label: yup.string().required(),
|
||||
})
|
||||
)
|
||||
.optional()
|
||||
.nullable(),
|
||||
product_ids: yup
|
||||
.array()
|
||||
.of(
|
||||
yup.object({
|
||||
value: yup.mixed<string | number>().required(),
|
||||
label: yup.string().required(),
|
||||
})
|
||||
)
|
||||
.optional()
|
||||
.nullable(),
|
||||
product_category_ids: yup
|
||||
.array()
|
||||
.of(
|
||||
yup.object({
|
||||
value: yup.mixed<string | number>().required(),
|
||||
label: yup.string().required(),
|
||||
})
|
||||
)
|
||||
.optional()
|
||||
.nullable(),
|
||||
filter_by: yup
|
||||
.object({
|
||||
value: yup.mixed<string | number>().required(),
|
||||
label: yup.string().required(),
|
||||
})
|
||||
.optional()
|
||||
.nullable(),
|
||||
sort_by: yup
|
||||
.object({
|
||||
value: yup.mixed<string | number>().required(),
|
||||
label: yup.string().required(),
|
||||
})
|
||||
.optional()
|
||||
.nullable(),
|
||||
});
|
||||
|
||||
export type PurchasesPerSupplierFilterValues = yup.InferType<
|
||||
typeof PurchasesPerSupplierFilterSchema
|
||||
>;
|
||||
@@ -1,40 +1,55 @@
|
||||
import { useState, useMemo, useCallback, useEffect } from 'react';
|
||||
import useSWR from 'swr';
|
||||
import Button from '@/components/Button';
|
||||
import Card from '@/components/Card';
|
||||
import { useSelect, OptionType } from '@/components/input/SelectInput';
|
||||
import SelectInputCheckbox from '@/components/input/SelectInputCheckbox';
|
||||
import SelectInputRadio from '@/components/input/SelectInputRadio';
|
||||
import Dropdown from '@/components/Dropdown';
|
||||
import DateInput from '@/components/input/DateInput';
|
||||
import { OptionType, useSelect } from '@/components/input/SelectInput';
|
||||
import Menu from '@/components/menu/Menu';
|
||||
import MenuItem from '@/components/menu/MenuItem';
|
||||
import Modal, { useModal } from '@/components/Modal';
|
||||
import Table from '@/components/Table';
|
||||
import { isResponseSuccess } from '@/lib/api-helper';
|
||||
import { cn, formatCurrency, formatDate, formatNumber } from '@/lib/helper';
|
||||
import { AreaApi } from '@/services/api/master-data';
|
||||
import { SupplierApi } from '@/services/api/master-data';
|
||||
import { ProductApi } from '@/services/api/master-data';
|
||||
import { ProductCategoryApi } from '@/services/api/master-data';
|
||||
import { LogisticApi } from '@/services/api/report/logistic-stock';
|
||||
import Table from '@/components/Table';
|
||||
import { ColumnDef } from '@tanstack/react-table';
|
||||
import { formatCurrency, formatDate, formatNumber, cn } from '@/lib/helper';
|
||||
import {
|
||||
LogisticPurchasePerSupplierReport,
|
||||
LogisticPurchasePerSupplierSummary,
|
||||
} from '@/types/api/report/logistic-stock';
|
||||
import { isResponseSuccess } from '@/lib/api-helper';
|
||||
import Button from '@/components/Button';
|
||||
import Dropdown from '@/components/Dropdown';
|
||||
import MenuItem from '@/components/menu/MenuItem';
|
||||
import Menu from '@/components/menu/Menu';
|
||||
import Modal from '@/components/Modal';
|
||||
import { useModal } from '@/components/Modal';
|
||||
import { generatePurchasesPerSupplierPDF } from '@/components/pages/report/logistic-stock/export/PurchasesPerSupplierExportPDF';
|
||||
import { generatePurchasesPerSupplierExcel } from '@/components/pages/report/logistic-stock/export/PurchasesPerSupplierExportXLSX';
|
||||
import PurchasePerSupplierSkeleton from '@/components/pages/report/logistic-stock/skeleton/PurchasePerSupplierSkeleton';
|
||||
import toast from 'react-hot-toast';
|
||||
import { generatePurchasesPerSupplierPDF } from '@/components/pages/report/logistic-stock/export/PurchasesPerSupplierExportPDF';
|
||||
import { Icon } from '@iconify/react';
|
||||
import { ColumnDef } from '@tanstack/react-table';
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import toast from 'react-hot-toast';
|
||||
import useSWR from 'swr';
|
||||
import { useFormik } from 'formik';
|
||||
import {
|
||||
PurchasesPerSupplierFilterSchema,
|
||||
PurchasesPerSupplierFilterType,
|
||||
} from '@/components/pages/report/logistic-stock/filter/PurchasesPerSupplierFilter';
|
||||
import SelectInputCheckbox from '@/components/input/SelectInputCheckbox';
|
||||
import SelectInputRadio from '@/components/input/SelectInputRadio';
|
||||
import { useLogisticStockTabStore } from '@/stores/logistic-stock-tab/logistic-stock-tab.store';
|
||||
import PurchasePerSupplierSkeleton from '@/components/pages/report/logistic-stock/skeleton/PurchasePerSupplierSkeleton';
|
||||
|
||||
interface PurchasesPerSupplierTabProps {
|
||||
tabId: string;
|
||||
}
|
||||
|
||||
interface FilterParams {
|
||||
area_ids?: string;
|
||||
supplier_ids?: string;
|
||||
product_ids?: string;
|
||||
product_category_ids?: string;
|
||||
start_date?: string;
|
||||
end_date?: string;
|
||||
sort_by?: string;
|
||||
filter_by?: string;
|
||||
}
|
||||
|
||||
const PurchasesPerSupplierTab = ({ tabId }: PurchasesPerSupplierTabProps) => {
|
||||
// ===== STATE MANAGEMENT =====
|
||||
const [isPdfExportLoading, setIsPdfExportLoading] = useState(false);
|
||||
@@ -46,11 +61,14 @@ const PurchasesPerSupplierTab = ({ tabId }: PurchasesPerSupplierTabProps) => {
|
||||
const [pageSize] = useState(10);
|
||||
|
||||
// ===== SUBMISSION STATE =====
|
||||
const [filterParams, setFilterParams] = useState<FilterParams>({});
|
||||
const [isSubmitted, setIsSubmitted] = useState(false);
|
||||
const [dateErrorShown, setDateErrorShown] = useState(false);
|
||||
const [hasDateError, setHasDateError] = useState(false);
|
||||
|
||||
const filterModal = useModal();
|
||||
|
||||
// ===== OPTIONS (Declare before filter state) =====
|
||||
// ===== OPTIONS =====
|
||||
const { options: areaOptions, isLoadingOptions: isLoadingAreas } = useSelect(
|
||||
AreaApi.basePath,
|
||||
'id',
|
||||
@@ -87,127 +105,66 @@ const PurchasesPerSupplierTab = ({ tabId }: PurchasesPerSupplierTabProps) => {
|
||||
[]
|
||||
);
|
||||
|
||||
// ===== APPLIED FILTER STATE (Yang sudah di-apply) =====
|
||||
const [appliedFilterArea, setAppliedFilterArea] = useState<
|
||||
typeof areaOptions
|
||||
>([]);
|
||||
const [appliedFilterSupplier, setAppliedFilterSupplier] = useState<
|
||||
typeof supplierOptions
|
||||
>([]);
|
||||
const [appliedFilterProduct, setAppliedFilterProduct] = useState<
|
||||
typeof productOptions
|
||||
>([]);
|
||||
const [appliedFilterProductCategory, setAppliedFilterProductCategory] =
|
||||
useState<typeof productCategoryOptions>([]);
|
||||
const [appliedFilterByType, setAppliedFilterByType] = useState<
|
||||
(typeof dataTypeOptions)[0] | null
|
||||
>(null);
|
||||
const [appliedFilterSortBy, setAppliedFilterSortBy] = useState<
|
||||
(typeof sortByOptions)[0] | null
|
||||
>(null);
|
||||
const [appliedFilterStartDate, setAppliedFilterStartDate] = useState('');
|
||||
const [appliedFilterEndDate, setAppliedFilterEndDate] = useState('');
|
||||
const [dateErrorShown, setDateErrorShown] = useState(false);
|
||||
const [hasDateError, setHasDateError] = useState(false);
|
||||
|
||||
// ===== PENDING FILTER STATE (Yang ada di modal, belum di-apply) =====
|
||||
const [filterArea, setFilterArea] = useState<typeof areaOptions>([]);
|
||||
const [filterSupplier, setFilterSupplier] = useState<typeof supplierOptions>(
|
||||
[]
|
||||
);
|
||||
const [filterProduct, setFilterProduct] = useState<typeof productOptions>([]);
|
||||
const [filterProductCategory, setFilterProductCategory] = useState<
|
||||
typeof productCategoryOptions
|
||||
>([]);
|
||||
const [filterByType, setFilterByType] = useState<
|
||||
(typeof dataTypeOptions)[0] | null
|
||||
>(null);
|
||||
const [filterSortBy, setFilterSortBy] = useState<
|
||||
(typeof sortByOptions)[0] | null
|
||||
>(null);
|
||||
const [filterStartDate, setFilterStartDate] = useState('');
|
||||
const [filterEndDate, setFilterEndDate] = useState('');
|
||||
|
||||
// ===== FILTER HANDLERS =====
|
||||
const handleFilterModalOpen = useCallback(() => {
|
||||
setFilterArea(appliedFilterArea);
|
||||
setFilterSupplier(appliedFilterSupplier);
|
||||
setFilterProduct(appliedFilterProduct);
|
||||
setFilterProductCategory(appliedFilterProductCategory);
|
||||
setFilterByType(appliedFilterByType);
|
||||
setFilterSortBy(appliedFilterSortBy);
|
||||
setFilterStartDate(appliedFilterStartDate);
|
||||
setFilterEndDate(appliedFilterEndDate);
|
||||
const handleFilterModalOpen = () => {
|
||||
filterModal.openModal();
|
||||
}, [
|
||||
filterModal,
|
||||
appliedFilterArea,
|
||||
appliedFilterSupplier,
|
||||
appliedFilterProduct,
|
||||
appliedFilterProductCategory,
|
||||
appliedFilterByType,
|
||||
appliedFilterSortBy,
|
||||
appliedFilterStartDate,
|
||||
appliedFilterEndDate,
|
||||
]);
|
||||
};
|
||||
|
||||
const handleResetFilters = useCallback(() => {
|
||||
setIsSubmitted(false);
|
||||
setFilterArea([]);
|
||||
setFilterSupplier([]);
|
||||
setFilterProduct([]);
|
||||
setFilterProductCategory([]);
|
||||
setFilterByType(null);
|
||||
setFilterSortBy(null);
|
||||
setFilterStartDate('');
|
||||
setFilterEndDate('');
|
||||
setAppliedFilterArea([]);
|
||||
setAppliedFilterSupplier([]);
|
||||
setAppliedFilterProduct([]);
|
||||
setAppliedFilterProductCategory([]);
|
||||
setAppliedFilterByType(null);
|
||||
setAppliedFilterSortBy(null);
|
||||
setAppliedFilterStartDate('');
|
||||
setAppliedFilterEndDate('');
|
||||
setHasDateError(false);
|
||||
if (dateErrorShown) {
|
||||
toast.dismiss();
|
||||
setDateErrorShown(false);
|
||||
}
|
||||
}, [dateErrorShown]);
|
||||
|
||||
const handleApplyFilters = useCallback(() => {
|
||||
setAppliedFilterArea(filterArea);
|
||||
setAppliedFilterSupplier(filterSupplier);
|
||||
setAppliedFilterProduct(filterProduct);
|
||||
setAppliedFilterProductCategory(filterProductCategory);
|
||||
setAppliedFilterByType(filterByType);
|
||||
setAppliedFilterSortBy(filterSortBy);
|
||||
setAppliedFilterStartDate(filterStartDate);
|
||||
setAppliedFilterEndDate(filterEndDate);
|
||||
setIsSubmitted(true);
|
||||
setCurrentPage(1);
|
||||
filterModal.closeModal();
|
||||
}, [
|
||||
filterModal,
|
||||
filterArea,
|
||||
filterSupplier,
|
||||
filterProduct,
|
||||
filterProductCategory,
|
||||
filterByType,
|
||||
filterSortBy,
|
||||
filterStartDate,
|
||||
filterEndDate,
|
||||
]);
|
||||
// ===== FORMIK SETUP =====
|
||||
const formik = useFormik<PurchasesPerSupplierFilterType>({
|
||||
initialValues: {
|
||||
start_date: null,
|
||||
end_date: null,
|
||||
area_ids: null,
|
||||
supplier_ids: null,
|
||||
product_ids: null,
|
||||
product_category_ids: null,
|
||||
filter_by: null,
|
||||
sort_by: null,
|
||||
},
|
||||
validationSchema: PurchasesPerSupplierFilterSchema,
|
||||
onSubmit: (values) => {
|
||||
setFilterParams({
|
||||
start_date: values.start_date?.toString() || undefined,
|
||||
end_date: values.end_date?.toString() || undefined,
|
||||
area_ids:
|
||||
values.area_ids?.map((v) => String(v.value)).join(',') || undefined,
|
||||
supplier_ids:
|
||||
values.supplier_ids?.map((v) => String(v.value)).join(',') ||
|
||||
undefined,
|
||||
product_ids:
|
||||
values.product_ids?.map((v) => String(v.value)).join(',') ||
|
||||
undefined,
|
||||
product_category_ids:
|
||||
values.product_category_ids?.map((v) => String(v.value)).join(',') ||
|
||||
undefined,
|
||||
filter_by: values.filter_by?.value?.toString() || undefined,
|
||||
sort_by: values.sort_by?.value?.toString() || undefined,
|
||||
});
|
||||
filterModal.closeModal();
|
||||
setIsSubmitted(true);
|
||||
setCurrentPage(1);
|
||||
},
|
||||
onReset: () => {
|
||||
setFilterParams({});
|
||||
setIsSubmitted(false);
|
||||
setCurrentPage(1);
|
||||
setHasDateError(false);
|
||||
if (dateErrorShown) {
|
||||
toast.dismiss();
|
||||
setDateErrorShown(false);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
// ===== DATE CHANGE HANDLERS =====
|
||||
const handleStartDateChange = useCallback(
|
||||
(e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const value = e.target.value;
|
||||
setFilterStartDate(value);
|
||||
formik.setFieldValue('start_date', value || null);
|
||||
|
||||
if (value && filterEndDate) {
|
||||
if (value && formik.values.end_date) {
|
||||
const startDate = new Date(value);
|
||||
const endDateObj = new Date(filterEndDate);
|
||||
const endDateObj = new Date(formik.values.end_date);
|
||||
|
||||
if (endDateObj < startDate) {
|
||||
setHasDateError(true);
|
||||
@@ -228,16 +185,16 @@ const PurchasesPerSupplierTab = ({ tabId }: PurchasesPerSupplierTabProps) => {
|
||||
setHasDateError(false);
|
||||
}
|
||||
},
|
||||
[filterEndDate, dateErrorShown]
|
||||
[formik, dateErrorShown]
|
||||
);
|
||||
|
||||
const handleEndDateChange = useCallback(
|
||||
(e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const value = e.target.value;
|
||||
setFilterEndDate(value);
|
||||
formik.setFieldValue('end_date', value || null);
|
||||
|
||||
if (value && filterStartDate) {
|
||||
const startDateObj = new Date(filterStartDate);
|
||||
if (value && formik.values.start_date) {
|
||||
const startDateObj = new Date(formik.values.start_date);
|
||||
const endDate = new Date(value);
|
||||
|
||||
if (endDate < startDateObj) {
|
||||
@@ -258,59 +215,43 @@ const PurchasesPerSupplierTab = ({ tabId }: PurchasesPerSupplierTabProps) => {
|
||||
setDateErrorShown(false);
|
||||
}
|
||||
},
|
||||
[filterStartDate, dateErrorShown]
|
||||
[formik, dateErrorShown]
|
||||
);
|
||||
|
||||
// ===== ACTIVE FILTERS COUNT =====
|
||||
const activeFiltersCount = useMemo(() => {
|
||||
let count = 0;
|
||||
|
||||
// Date filter (start_date + end_date = 1 filter)
|
||||
if (appliedFilterStartDate || appliedFilterEndDate) {
|
||||
if (filterParams.start_date || filterParams.end_date) {
|
||||
count += 1;
|
||||
}
|
||||
|
||||
// Area filter
|
||||
if (appliedFilterArea.length > 0) {
|
||||
if (filterParams.area_ids) {
|
||||
count += 1;
|
||||
}
|
||||
|
||||
// Supplier filter
|
||||
if (appliedFilterSupplier.length > 0) {
|
||||
if (filterParams.supplier_ids) {
|
||||
count += 1;
|
||||
}
|
||||
|
||||
// Product filter
|
||||
if (appliedFilterProduct.length > 0) {
|
||||
if (filterParams.product_ids) {
|
||||
count += 1;
|
||||
}
|
||||
|
||||
// Product category filter
|
||||
if (appliedFilterProductCategory.length > 0) {
|
||||
if (filterParams.product_category_ids) {
|
||||
count += 1;
|
||||
}
|
||||
|
||||
// Filter by type filter
|
||||
if (appliedFilterByType) {
|
||||
if (filterParams.filter_by) {
|
||||
count += 1;
|
||||
}
|
||||
|
||||
// Sort by filter
|
||||
if (appliedFilterSortBy) {
|
||||
if (filterParams.sort_by) {
|
||||
count += 1;
|
||||
}
|
||||
|
||||
return count;
|
||||
}, [
|
||||
appliedFilterStartDate,
|
||||
appliedFilterEndDate,
|
||||
appliedFilterArea,
|
||||
appliedFilterSupplier,
|
||||
appliedFilterProduct,
|
||||
appliedFilterProductCategory,
|
||||
appliedFilterByType,
|
||||
appliedFilterSortBy,
|
||||
]);
|
||||
}, [filterParams]);
|
||||
|
||||
const hasFilters = activeFiltersCount > 0;
|
||||
|
||||
@@ -319,39 +260,14 @@ const PurchasesPerSupplierTab = ({ tabId }: PurchasesPerSupplierTabProps) => {
|
||||
isSubmitted
|
||||
? () => {
|
||||
const params = {
|
||||
area_id:
|
||||
appliedFilterArea.length > 0
|
||||
? appliedFilterArea.map((v) => String(v.value)).join(',')
|
||||
: undefined,
|
||||
supplier_id:
|
||||
appliedFilterSupplier.length > 0
|
||||
? appliedFilterSupplier.map((v) => String(v.value)).join(',')
|
||||
: undefined,
|
||||
product_id:
|
||||
appliedFilterProduct.length > 0
|
||||
? appliedFilterProduct.map((v) => String(v.value)).join(',')
|
||||
: undefined,
|
||||
product_category_id:
|
||||
appliedFilterProductCategory.length > 0
|
||||
? appliedFilterProductCategory
|
||||
.map((v) => String(v.value))
|
||||
.join(',')
|
||||
: undefined,
|
||||
received_date:
|
||||
appliedFilterByType?.value === 'received_date'
|
||||
? appliedFilterStartDate || undefined
|
||||
: undefined,
|
||||
po_date:
|
||||
appliedFilterByType?.value === 'po_date'
|
||||
? appliedFilterStartDate || undefined
|
||||
: undefined,
|
||||
start_date: appliedFilterStartDate || undefined,
|
||||
end_date: appliedFilterEndDate || undefined,
|
||||
sort_by:
|
||||
(appliedFilterSortBy?.value as 'ASC' | 'DESC') || undefined,
|
||||
filter_by:
|
||||
(appliedFilterByType?.value as 'received_date' | 'po_date') ||
|
||||
undefined,
|
||||
area_ids: filterParams.area_ids,
|
||||
supplier_ids: filterParams.supplier_ids,
|
||||
product_ids: filterParams.product_ids,
|
||||
product_category_ids: filterParams.product_category_ids,
|
||||
start_date: filterParams.start_date,
|
||||
end_date: filterParams.end_date,
|
||||
sort_by: filterParams.sort_by,
|
||||
filter_by: filterParams.filter_by,
|
||||
page: currentPage,
|
||||
limit: pageSize,
|
||||
};
|
||||
@@ -361,12 +277,12 @@ const PurchasesPerSupplierTab = ({ tabId }: PurchasesPerSupplierTabProps) => {
|
||||
: null,
|
||||
([, params]) =>
|
||||
LogisticApi.getLogisticPurchasePerSupplierReport(
|
||||
params.area_id,
|
||||
params.supplier_id,
|
||||
params.product_id,
|
||||
params.product_category_id,
|
||||
params.received_date,
|
||||
params.po_date,
|
||||
params.area_ids,
|
||||
params.supplier_ids,
|
||||
params.product_ids,
|
||||
params.product_category_ids,
|
||||
params.filter_by === 'received_date' ? params.start_date : undefined,
|
||||
params.filter_by === 'po_date' ? params.start_date : undefined,
|
||||
params.start_date,
|
||||
params.end_date,
|
||||
params.sort_by,
|
||||
@@ -395,47 +311,25 @@ const PurchasesPerSupplierTab = ({ tabId }: PurchasesPerSupplierTabProps) => {
|
||||
LogisticPurchasePerSupplierReport[] | null
|
||||
> => {
|
||||
const params = {
|
||||
area_id:
|
||||
appliedFilterArea.length > 0
|
||||
? appliedFilterArea.map((v) => String(v.value)).join(',')
|
||||
: undefined,
|
||||
supplier_id:
|
||||
appliedFilterSupplier.length > 0
|
||||
? appliedFilterSupplier.map((v) => String(v.value)).join(',')
|
||||
: undefined,
|
||||
product_id:
|
||||
appliedFilterProduct.length > 0
|
||||
? appliedFilterProduct.map((v) => String(v.value)).join(',')
|
||||
: undefined,
|
||||
product_category_id:
|
||||
appliedFilterProductCategory.length > 0
|
||||
? appliedFilterProductCategory.map((v) => String(v.value)).join(',')
|
||||
: undefined,
|
||||
received_date:
|
||||
appliedFilterByType?.value === 'received_date'
|
||||
? appliedFilterStartDate || undefined
|
||||
: undefined,
|
||||
po_date:
|
||||
appliedFilterByType?.value === 'po_date'
|
||||
? appliedFilterStartDate || undefined
|
||||
: undefined,
|
||||
start_date: appliedFilterStartDate || undefined,
|
||||
end_date: appliedFilterEndDate || undefined,
|
||||
sort_by: (appliedFilterSortBy?.value as 'ASC' | 'DESC') || undefined,
|
||||
filter_by:
|
||||
(appliedFilterByType?.value as 'received_date' | 'po_date') ||
|
||||
undefined,
|
||||
area_ids: filterParams.area_ids,
|
||||
supplier_ids: filterParams.supplier_ids,
|
||||
product_ids: filterParams.product_ids,
|
||||
product_category_ids: filterParams.product_category_ids,
|
||||
start_date: filterParams.start_date,
|
||||
end_date: filterParams.end_date,
|
||||
sort_by: filterParams.sort_by,
|
||||
filter_by: filterParams.filter_by,
|
||||
limit: 100,
|
||||
page: 1,
|
||||
};
|
||||
|
||||
const response = await LogisticApi.getLogisticPurchasePerSupplierReport(
|
||||
params.area_id,
|
||||
params.supplier_id,
|
||||
params.product_id,
|
||||
params.product_category_id,
|
||||
params.received_date,
|
||||
params.po_date,
|
||||
params.area_ids,
|
||||
params.supplier_ids,
|
||||
params.product_ids,
|
||||
params.product_category_ids,
|
||||
params.filter_by === 'received_date' ? params.start_date : undefined,
|
||||
params.filter_by === 'po_date' ? params.start_date : undefined,
|
||||
params.start_date,
|
||||
params.end_date,
|
||||
params.sort_by,
|
||||
@@ -447,16 +341,7 @@ const PurchasesPerSupplierTab = ({ tabId }: PurchasesPerSupplierTabProps) => {
|
||||
return isResponseSuccess(response)
|
||||
? (response.data as unknown as LogisticPurchasePerSupplierReport[])
|
||||
: null;
|
||||
}, [
|
||||
appliedFilterArea,
|
||||
appliedFilterSupplier,
|
||||
appliedFilterProduct,
|
||||
appliedFilterProductCategory,
|
||||
appliedFilterStartDate,
|
||||
appliedFilterEndDate,
|
||||
appliedFilterByType,
|
||||
appliedFilterSortBy,
|
||||
]);
|
||||
}, [filterParams]);
|
||||
|
||||
// ===== EXPORT HANDLERS =====
|
||||
const handleExportExcel = useCallback(async () => {
|
||||
@@ -496,39 +381,52 @@ const PurchasesPerSupplierTab = ({ tabId }: PurchasesPerSupplierTabProps) => {
|
||||
return;
|
||||
}
|
||||
|
||||
const areaName =
|
||||
appliedFilterArea.length > 0
|
||||
? appliedFilterArea.map((c) => c.label).join(', ') || 'Semua Area'
|
||||
: 'Semua Area';
|
||||
const areaName = filterParams.area_ids
|
||||
? areaOptions
|
||||
.filter((opt) =>
|
||||
filterParams.area_ids?.split(',').includes(String(opt.value))
|
||||
)
|
||||
.map((opt) => opt.label)
|
||||
.join(', ') || 'Semua Area'
|
||||
: 'Semua Area';
|
||||
|
||||
const supplierName =
|
||||
appliedFilterSupplier.length > 0
|
||||
? appliedFilterSupplier.map((c) => c.label).join(', ') ||
|
||||
'Semua Supplier'
|
||||
: 'Semua Supplier';
|
||||
const supplierName = filterParams.supplier_ids
|
||||
? supplierOptions
|
||||
.filter((opt) =>
|
||||
filterParams.supplier_ids?.split(',').includes(String(opt.value))
|
||||
)
|
||||
.map((opt) => opt.label)
|
||||
.join(', ') || 'Semua Supplier'
|
||||
: 'Semua Supplier';
|
||||
|
||||
const productName =
|
||||
appliedFilterProduct.length > 0
|
||||
? appliedFilterProduct.map((c) => c.label).join(', ') ||
|
||||
'Semua Produk'
|
||||
: 'Semua Produk';
|
||||
const productName = filterParams.product_ids
|
||||
? productOptions
|
||||
.filter((opt) =>
|
||||
filterParams.product_ids?.split(',').includes(String(opt.value))
|
||||
)
|
||||
.map((opt) => opt.label)
|
||||
.join(', ') || 'Semua Produk'
|
||||
: 'Semua Produk';
|
||||
|
||||
const productCategoryName =
|
||||
appliedFilterProductCategory.length > 0
|
||||
? appliedFilterProductCategory.map((c) => c.label).join(', ') ||
|
||||
'Semua Kategori Produk'
|
||||
: 'Semua Kategori Produk';
|
||||
const productCategoryName = filterParams.product_category_ids
|
||||
? productCategoryOptions
|
||||
.filter((opt) =>
|
||||
filterParams.product_category_ids
|
||||
?.split(',')
|
||||
.includes(String(opt.value))
|
||||
)
|
||||
.map((opt) => opt.label)
|
||||
.join(', ') || 'Semua Kategori Produk'
|
||||
: 'Semua Kategori Produk';
|
||||
|
||||
const exportParams = {
|
||||
area_name: areaName,
|
||||
supplier_name: supplierName,
|
||||
product_name: productName,
|
||||
product_category_name: productCategoryName,
|
||||
filter_by:
|
||||
(appliedFilterByType?.value as 'received_date' | 'po_date') ||
|
||||
undefined,
|
||||
start_date: appliedFilterStartDate || undefined,
|
||||
end_date: appliedFilterEndDate || undefined,
|
||||
filter_by: filterParams.filter_by,
|
||||
start_date: filterParams.start_date,
|
||||
end_date: filterParams.end_date,
|
||||
};
|
||||
|
||||
await generatePurchasesPerSupplierPDF({
|
||||
@@ -543,13 +441,11 @@ const PurchasesPerSupplierTab = ({ tabId }: PurchasesPerSupplierTabProps) => {
|
||||
}
|
||||
}, [
|
||||
logisticPurchasePerSupplierExport,
|
||||
appliedFilterArea,
|
||||
appliedFilterSupplier,
|
||||
appliedFilterProduct,
|
||||
appliedFilterProductCategory,
|
||||
appliedFilterStartDate,
|
||||
appliedFilterEndDate,
|
||||
appliedFilterByType,
|
||||
filterParams,
|
||||
areaOptions,
|
||||
supplierOptions,
|
||||
productOptions,
|
||||
productCategoryOptions,
|
||||
]);
|
||||
|
||||
// ===== REGISTER TAB ACTIONS TO STORE =====
|
||||
@@ -624,6 +520,7 @@ const PurchasesPerSupplierTab = ({ tabId }: PurchasesPerSupplierTabProps) => {
|
||||
</Dropdown>
|
||||
</div>
|
||||
);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [
|
||||
tabId,
|
||||
hasFilters,
|
||||
@@ -633,6 +530,7 @@ const PurchasesPerSupplierTab = ({ tabId }: PurchasesPerSupplierTabProps) => {
|
||||
setTabActions,
|
||||
]);
|
||||
|
||||
// Cleanup on unmount
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
clearTabActions(tabId);
|
||||
@@ -945,137 +843,184 @@ const PurchasesPerSupplierTab = ({ tabId }: PurchasesPerSupplierTabProps) => {
|
||||
<Icon icon='heroicons:x-mark' width={20} height={20} />
|
||||
</Button>
|
||||
</div>
|
||||
<div className='p-4 flex flex-col gap-3'>
|
||||
{/* Date Filter */}
|
||||
<div>
|
||||
<label className='block text-xs font-semibold text-base-content py-2'>
|
||||
Tanggal
|
||||
</label>
|
||||
<div className='flex flex-row gap-1.5 items-center justify-between'>
|
||||
<DateInput
|
||||
name='start_date'
|
||||
value={filterStartDate}
|
||||
onChange={handleStartDateChange}
|
||||
className={{ wrapper: 'w-full' }}
|
||||
isNestedModal
|
||||
/>
|
||||
<hr className='w-full max-w-3 h-px border-base-content/10' />
|
||||
<DateInput
|
||||
name='end_date'
|
||||
value={filterEndDate}
|
||||
onChange={handleEndDateChange}
|
||||
className={{ wrapper: 'w-full' }}
|
||||
isNestedModal
|
||||
/>
|
||||
<form onSubmit={formik.handleSubmit} onReset={formik.handleReset}>
|
||||
<div className='p-4 flex flex-col gap-3'>
|
||||
{/* Date Filter */}
|
||||
<div>
|
||||
<label className='block text-xs font-semibold text-base-content py-2'>
|
||||
Tanggal
|
||||
</label>
|
||||
<div className='flex flex-row gap-1.5 items-center justify-between'>
|
||||
<DateInput
|
||||
name='start_date'
|
||||
value={formik.values.start_date || ''}
|
||||
onChange={handleStartDateChange}
|
||||
className={{ wrapper: 'w-full' }}
|
||||
isNestedModal
|
||||
/>
|
||||
<hr className='w-full max-w-3 h-px border-base-content/10' />
|
||||
<DateInput
|
||||
name='end_date'
|
||||
value={formik.values.end_date || ''}
|
||||
onChange={handleEndDateChange}
|
||||
className={{ wrapper: 'w-full' }}
|
||||
isNestedModal
|
||||
isError={hasDateError}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Area Filter */}
|
||||
<SelectInputCheckbox
|
||||
label='Area'
|
||||
placeholder='Pilih Area'
|
||||
options={areaOptions}
|
||||
value={
|
||||
(formik.values.area_ids as
|
||||
| { value: number; label: string }
|
||||
| { value: number; label: string }[]
|
||||
| null
|
||||
| undefined) || []
|
||||
}
|
||||
onChange={(val) => {
|
||||
formik.setFieldValue(
|
||||
'area_ids',
|
||||
Array.isArray(val) ? val : val ? [val] : null
|
||||
);
|
||||
}}
|
||||
isLoading={isLoadingAreas}
|
||||
isClearable
|
||||
className={{ wrapper: 'w-full' }}
|
||||
/>
|
||||
|
||||
{/* Supplier Filter */}
|
||||
<SelectInputCheckbox
|
||||
label='Supplier'
|
||||
placeholder='Pilih Supplier'
|
||||
options={supplierOptions}
|
||||
value={
|
||||
(formik.values.supplier_ids as
|
||||
| { value: number; label: string }
|
||||
| { value: number; label: string }[]
|
||||
| null
|
||||
| undefined) || []
|
||||
}
|
||||
onChange={(val) => {
|
||||
formik.setFieldValue(
|
||||
'supplier_ids',
|
||||
Array.isArray(val) ? val : val ? [val] : null
|
||||
);
|
||||
}}
|
||||
isLoading={isLoadingSuppliers}
|
||||
isClearable
|
||||
className={{ wrapper: 'w-full' }}
|
||||
/>
|
||||
|
||||
{/* Product Filter */}
|
||||
<SelectInputCheckbox
|
||||
label='Produk'
|
||||
placeholder='Pilih Produk'
|
||||
options={productOptions}
|
||||
value={
|
||||
(formik.values.product_ids as
|
||||
| { value: number; label: string }
|
||||
| { value: number; label: string }[]
|
||||
| null
|
||||
| undefined) || []
|
||||
}
|
||||
onChange={(val) => {
|
||||
formik.setFieldValue(
|
||||
'product_ids',
|
||||
Array.isArray(val) ? val : val ? [val] : null
|
||||
);
|
||||
}}
|
||||
isLoading={isLoadingProducts}
|
||||
isClearable
|
||||
className={{ wrapper: 'w-full' }}
|
||||
/>
|
||||
|
||||
{/* Product Category Filter */}
|
||||
<SelectInputCheckbox
|
||||
label='Kategori Produk'
|
||||
placeholder='Pilih Kategori Produk'
|
||||
options={productCategoryOptions}
|
||||
value={
|
||||
(formik.values.product_category_ids as
|
||||
| { value: number; label: string }
|
||||
| { value: number; label: string }[]
|
||||
| null
|
||||
| undefined) || []
|
||||
}
|
||||
onChange={(val) => {
|
||||
formik.setFieldValue(
|
||||
'product_category_ids',
|
||||
Array.isArray(val) ? val : val ? [val] : null
|
||||
);
|
||||
}}
|
||||
isLoading={isLoadingProductCategories}
|
||||
isClearable
|
||||
className={{ wrapper: 'w-full' }}
|
||||
/>
|
||||
|
||||
{/* Filter By Type */}
|
||||
<SelectInputRadio
|
||||
label='Filter Berdasarkan'
|
||||
placeholder='Pilih Filter Berdasarkan'
|
||||
options={dataTypeOptions}
|
||||
value={
|
||||
(formik.values.filter_by as
|
||||
| { value: string; label: string }
|
||||
| null
|
||||
| undefined) || null
|
||||
}
|
||||
onChange={(val) => {
|
||||
if (!Array.isArray(val)) {
|
||||
formik.setFieldValue('filter_by', val);
|
||||
}
|
||||
}}
|
||||
className={{ wrapper: 'w-full' }}
|
||||
isClearable={true}
|
||||
/>
|
||||
|
||||
{/* Sort By */}
|
||||
<SelectInputRadio
|
||||
label='Urutkan Berdasarkan'
|
||||
placeholder='Pilih Urutkan Berdasarkan'
|
||||
options={sortByOptions}
|
||||
value={
|
||||
(formik.values.sort_by as
|
||||
| { value: string; label: string }
|
||||
| null
|
||||
| undefined) || null
|
||||
}
|
||||
onChange={(val) => {
|
||||
if (!Array.isArray(val)) {
|
||||
formik.setFieldValue('sort_by', val);
|
||||
}
|
||||
}}
|
||||
className={{ wrapper: 'w-full' }}
|
||||
isClearable={true}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Area Filter */}
|
||||
<SelectInputCheckbox
|
||||
label='Area'
|
||||
placeholder='Pilih Area'
|
||||
options={areaOptions}
|
||||
value={filterArea}
|
||||
onChange={(val) => {
|
||||
setFilterArea(Array.isArray(val) ? val : val ? [val] : []);
|
||||
}}
|
||||
isLoading={isLoadingAreas}
|
||||
isClearable
|
||||
className={{ wrapper: 'w-full' }}
|
||||
/>
|
||||
|
||||
{/* Supplier Filter */}
|
||||
<SelectInputCheckbox
|
||||
label='Supplier'
|
||||
placeholder='Pilih Supplier'
|
||||
options={supplierOptions}
|
||||
value={filterSupplier}
|
||||
onChange={(val) => {
|
||||
setFilterSupplier(Array.isArray(val) ? val : val ? [val] : []);
|
||||
}}
|
||||
isLoading={isLoadingSuppliers}
|
||||
isClearable
|
||||
className={{ wrapper: 'w-full' }}
|
||||
/>
|
||||
|
||||
{/* Product Filter */}
|
||||
<SelectInputCheckbox
|
||||
label='Produk'
|
||||
placeholder='Pilih Produk'
|
||||
options={productOptions}
|
||||
value={filterProduct}
|
||||
onChange={(val) => {
|
||||
setFilterProduct(Array.isArray(val) ? val : val ? [val] : []);
|
||||
}}
|
||||
isLoading={isLoadingProducts}
|
||||
isClearable
|
||||
className={{ wrapper: 'w-full' }}
|
||||
/>
|
||||
|
||||
{/* Product Category Filter */}
|
||||
<SelectInputCheckbox
|
||||
label='Kategori Produk'
|
||||
placeholder='Pilih Kategori Produk'
|
||||
options={productCategoryOptions}
|
||||
value={filterProductCategory}
|
||||
onChange={(val) => {
|
||||
setFilterProductCategory(
|
||||
Array.isArray(val) ? val : val ? [val] : []
|
||||
);
|
||||
}}
|
||||
isLoading={isLoadingProductCategories}
|
||||
isClearable
|
||||
className={{ wrapper: 'w-full' }}
|
||||
/>
|
||||
|
||||
{/* Filter By Type */}
|
||||
<SelectInputRadio
|
||||
label='Filter Berdasarkan'
|
||||
placeholder='Pilih Filter Berdasarkan'
|
||||
options={dataTypeOptions}
|
||||
value={filterByType}
|
||||
onChange={(val) => {
|
||||
if (!Array.isArray(val)) {
|
||||
setFilterByType(val);
|
||||
}
|
||||
}}
|
||||
className={{ wrapper: 'w-full' }}
|
||||
isClearable={true}
|
||||
/>
|
||||
|
||||
{/* Sort By */}
|
||||
<SelectInputRadio
|
||||
label='Urutkan Berdasarkan'
|
||||
placeholder='Pilih Urutkan Berdasarkan'
|
||||
options={sortByOptions}
|
||||
value={filterSortBy}
|
||||
onChange={(val) => {
|
||||
if (!Array.isArray(val)) {
|
||||
setFilterSortBy(val);
|
||||
}
|
||||
}}
|
||||
className={{ wrapper: 'w-full' }}
|
||||
isClearable={true}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Modal Footer */}
|
||||
<div className='flex justify-between items-center gap-4 p-4 border-t border-base-content/10 bg-gray-50'>
|
||||
<Button
|
||||
variant='soft'
|
||||
className='rounded-lg text-base-content/65 bg-transparent border-none hover:bg-base-content/10 hover:text-base-content/65 transition-colors px-3 py-2'
|
||||
onClick={handleResetFilters}
|
||||
>
|
||||
Reset Filter
|
||||
</Button>
|
||||
<Button
|
||||
className='min-w-40 text-sm rounded-lg py-3 text-white font-semibold'
|
||||
onClick={handleApplyFilters}
|
||||
disabled={hasDateError}
|
||||
>
|
||||
Apply Filter
|
||||
</Button>
|
||||
</div>
|
||||
{/* Modal Footer */}
|
||||
<div className='flex justify-between items-center gap-4 p-4 border-t border-base-content/10 bg-gray-50'>
|
||||
<Button
|
||||
type='reset'
|
||||
variant='soft'
|
||||
className='rounded-lg text-base-content/65 bg-transparent border-none hover:bg-base-content/10 hover:text-base-content/65 transition-colors px-3 py-2'
|
||||
>
|
||||
Reset Filter
|
||||
</Button>
|
||||
<Button
|
||||
type='submit'
|
||||
className='min-w-40 text-sm rounded-lg py-3 text-white font-semibold'
|
||||
disabled={hasDateError || !formik.isValid || formik.isSubmitting}
|
||||
>
|
||||
Apply Filter
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</Modal>
|
||||
</>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user