mirror of
https://gitlab.com/mbugroup/lti-web-client.git
synced 2026-05-20 13:32:00 +00:00
1041 lines
34 KiB
TypeScript
1041 lines
34 KiB
TypeScript
import Button from '@/components/Button';
|
|
import Card from '@/components/Card';
|
|
import Dropdown from '@/components/Dropdown';
|
|
import DateInput from '@/components/input/DateInput';
|
|
import { useSelect } from '@/components/input/SelectInput';
|
|
import Modal, { useModal } from '@/components/Modal';
|
|
import Table from '@/components/Table';
|
|
import { isResponseSuccess } from '@/lib/api-helper';
|
|
import { formatCurrency, formatDate, formatNumber } from '@/lib/helper';
|
|
import { AreaApi } from '@/services/api/master-data';
|
|
import { SupplierApi } from '@/services/api/master-data';
|
|
import { ProductApi } from '@/services/api/master-data';
|
|
import { ProductCategoryApi } from '@/services/api/master-data';
|
|
import { LogisticApi } from '@/services/api/report/logistic-stock';
|
|
import {
|
|
LogisticPurchasePerSupplierReport,
|
|
LogisticPurchasePerSupplierSummary,
|
|
} from '@/types/api/report/logistic-stock';
|
|
import { generatePurchasesPerSupplierExcel } from '@/components/pages/report/logistic-stock/export/PurchasesPerSupplierExportXLSX';
|
|
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, useRef, 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 { useTabActionsStore } from '@/stores/tab-actions/tab-actions.store';
|
|
import PurchasePerSupplierSkeleton from '@/components/pages/report/logistic-stock/skeleton/PurchasePerSupplierSkeleton';
|
|
import ButtonFilter from '@/components/helper/ButtonFilter';
|
|
|
|
interface PurchasesPerSupplierTabProps {
|
|
tabId: string;
|
|
}
|
|
|
|
interface FilterParams {
|
|
area_id?: string;
|
|
supplier_id?: string;
|
|
product_id?: string;
|
|
product_category_id?: string;
|
|
start_date?: string;
|
|
end_date?: string;
|
|
sort_by?: string;
|
|
filter_by?: string;
|
|
}
|
|
|
|
const PurchasesPerSupplierTab = ({ tabId }: PurchasesPerSupplierTabProps) => {
|
|
// ===== STATE MANAGEMENT =====
|
|
const [isPdfExportLoading, setIsPdfExportLoading] = useState(false);
|
|
const [isExcelExportLoading, setIsExcelExportLoading] = useState(false);
|
|
const isAnyExportLoading = isPdfExportLoading || isExcelExportLoading;
|
|
|
|
// ===== PAGINATION STATE =====
|
|
const [currentPage, setCurrentPage] = useState(1);
|
|
const [pageSize] = useState(10);
|
|
|
|
// ===== SUBMISSION STATE =====
|
|
const [filterParams, setFilterParams] = useState<FilterParams>({});
|
|
const [dateErrorShown, setDateErrorShown] = useState(false);
|
|
const [hasDateError, setHasDateError] = useState(false);
|
|
|
|
const handleFilterModalOpenRef = useRef(() => {});
|
|
|
|
const filterModal = useModal();
|
|
|
|
// ===== OPTIONS =====
|
|
const {
|
|
options: areaOptions,
|
|
isLoadingOptions: isLoadingAreas,
|
|
setInputValue: setAreaInputValue,
|
|
loadMore: loadMoreArea,
|
|
} = useSelect(AreaApi.basePath, 'id', 'name', 'search');
|
|
|
|
const {
|
|
options: supplierOptions,
|
|
isLoadingOptions: isLoadingSuppliers,
|
|
setInputValue: setSupplierInputValue,
|
|
loadMore: loadMoreSupplier,
|
|
} = useSelect(SupplierApi.basePath, 'id', 'name', 'search', {
|
|
category: 'SAPRONAK',
|
|
});
|
|
|
|
const {
|
|
options: productOptions,
|
|
isLoadingOptions: isLoadingProducts,
|
|
setInputValue: setProductInputValue,
|
|
loadMore: loadMoreProduct,
|
|
} = useSelect(ProductApi.basePath, 'id', 'name', 'search');
|
|
|
|
const {
|
|
options: productCategoryOptions,
|
|
isLoadingOptions: isLoadingProductCategories,
|
|
setInputValue: setProductCategoryInputValue,
|
|
loadMore: loadMoreProductCategory,
|
|
} = useSelect(ProductCategoryApi.basePath, 'id', 'name', 'search');
|
|
|
|
const dataTypeOptions = useMemo(
|
|
() => [
|
|
{ value: 'received_date', label: 'Tanggal Terima' },
|
|
{ value: 'po_date', label: 'Tanggal PO' },
|
|
],
|
|
[]
|
|
);
|
|
|
|
const sortByOptions = useMemo(
|
|
() => [
|
|
{ value: 'ASC', label: 'Ascending' },
|
|
{ value: 'DESC', label: 'Descending' },
|
|
],
|
|
[]
|
|
);
|
|
|
|
// ===== 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, { setSubmitting }) => {
|
|
setFilterParams({
|
|
start_date: values.start_date || undefined,
|
|
end_date: values.end_date || undefined,
|
|
area_id: values.area_ids || undefined,
|
|
supplier_id: values.supplier_ids || undefined,
|
|
product_id: values.product_ids || undefined,
|
|
product_category_id: values.product_category_ids || undefined,
|
|
filter_by: values.filter_by || undefined,
|
|
sort_by: values.sort_by || undefined,
|
|
});
|
|
filterModal.closeModal();
|
|
setCurrentPage(1);
|
|
setSubmitting(false);
|
|
},
|
|
onReset: () => {
|
|
setFilterParams({});
|
|
setCurrentPage(1);
|
|
setHasDateError(false);
|
|
if (dateErrorShown) {
|
|
toast.dismiss();
|
|
setDateErrorShown(false);
|
|
}
|
|
filterModal.closeModal();
|
|
},
|
|
});
|
|
|
|
handleFilterModalOpenRef.current = () => {
|
|
formik.setValues({
|
|
start_date: filterParams.start_date || null,
|
|
end_date: filterParams.end_date || null,
|
|
area_ids: filterParams.area_id || null,
|
|
supplier_ids: filterParams.supplier_id || null,
|
|
product_ids: filterParams.product_id || null,
|
|
product_category_ids: filterParams.product_category_id || null,
|
|
filter_by: filterParams.filter_by || null,
|
|
sort_by: filterParams.sort_by || null,
|
|
});
|
|
filterModal.openModal();
|
|
};
|
|
|
|
const { setFieldValue } = formik;
|
|
|
|
// ===== DATE CHANGE HANDLERS =====
|
|
const handleStartDateChange = useCallback(
|
|
(e: React.ChangeEvent<HTMLInputElement>) => {
|
|
const value = e.target.value;
|
|
setFieldValue('start_date', value || null);
|
|
|
|
if (value && formik.values.end_date) {
|
|
const startDate = new Date(value);
|
|
const endDateObj = new Date(formik.values.end_date);
|
|
|
|
if (endDateObj < startDate) {
|
|
setHasDateError(true);
|
|
if (!dateErrorShown) {
|
|
toast.error('Tanggal akhir tidak boleh masa lampau', {
|
|
duration: Infinity,
|
|
});
|
|
setDateErrorShown(true);
|
|
}
|
|
} else {
|
|
setHasDateError(false);
|
|
if (dateErrorShown) {
|
|
toast.dismiss();
|
|
setDateErrorShown(false);
|
|
}
|
|
}
|
|
} else {
|
|
setHasDateError(false);
|
|
}
|
|
},
|
|
[setFieldValue, dateErrorShown, formik.values.end_date]
|
|
);
|
|
|
|
const handleEndDateChange = useCallback(
|
|
(e: React.ChangeEvent<HTMLInputElement>) => {
|
|
const value = e.target.value;
|
|
setFieldValue('end_date', value || null);
|
|
|
|
if (value && formik.values.start_date) {
|
|
const startDateObj = new Date(formik.values.start_date);
|
|
const endDate = new Date(value);
|
|
|
|
if (endDate < startDateObj) {
|
|
setHasDateError(true);
|
|
if (!dateErrorShown) {
|
|
toast.error('Tanggal akhir tidak boleh masa lampau', {
|
|
duration: Infinity,
|
|
});
|
|
setDateErrorShown(true);
|
|
}
|
|
return;
|
|
}
|
|
}
|
|
|
|
setHasDateError(false);
|
|
if (dateErrorShown) {
|
|
toast.dismiss();
|
|
setDateErrorShown(false);
|
|
}
|
|
},
|
|
[setFieldValue, dateErrorShown, formik.values.start_date]
|
|
);
|
|
|
|
// ===== DERIVED VALUES =====
|
|
const areaIdsValue = useMemo(() => {
|
|
if (!formik.values.area_ids) return [];
|
|
const ids = formik.values.area_ids.split(',');
|
|
return areaOptions.filter((opt) => ids.includes(String(opt.value)));
|
|
}, [formik.values.area_ids, areaOptions]);
|
|
|
|
const supplierIdsValue = useMemo(() => {
|
|
if (!formik.values.supplier_ids) return [];
|
|
const ids = formik.values.supplier_ids.split(',');
|
|
return supplierOptions.filter((opt) => ids.includes(String(opt.value)));
|
|
}, [formik.values.supplier_ids, supplierOptions]);
|
|
|
|
const productIdsValue = useMemo(() => {
|
|
if (!formik.values.product_ids) return [];
|
|
const ids = formik.values.product_ids.split(',');
|
|
return productOptions.filter((opt) => ids.includes(String(opt.value)));
|
|
}, [formik.values.product_ids, productOptions]);
|
|
|
|
const productCategoryIdsValue = useMemo(() => {
|
|
if (!formik.values.product_category_ids) return [];
|
|
const ids = formik.values.product_category_ids.split(',');
|
|
return productCategoryOptions.filter((opt) =>
|
|
ids.includes(String(opt.value))
|
|
);
|
|
}, [formik.values.product_category_ids, productCategoryOptions]);
|
|
|
|
const filterByValue = useMemo(() => {
|
|
if (!formik.values.filter_by) return null;
|
|
return (
|
|
dataTypeOptions.find((opt) => opt.value === formik.values.filter_by) ||
|
|
null
|
|
);
|
|
}, [formik.values.filter_by, dataTypeOptions]);
|
|
|
|
const sortByValue = useMemo(() => {
|
|
if (!formik.values.sort_by) return null;
|
|
return (
|
|
sortByOptions.find((opt) => opt.value === formik.values.sort_by) || null
|
|
);
|
|
}, [formik.values.sort_by, sortByOptions]);
|
|
|
|
// ===== DATA FETCHING =====
|
|
const { data: purchasePerSupplier, isLoading } = useSWR(
|
|
() => {
|
|
const params = {
|
|
area_id: filterParams.area_id,
|
|
supplier_id: filterParams.supplier_id,
|
|
product_id: filterParams.product_id,
|
|
product_category_id: filterParams.product_category_id,
|
|
start_date: filterParams.start_date,
|
|
end_date: filterParams.end_date,
|
|
sort_by: filterParams.sort_by,
|
|
filter_by: filterParams.filter_by,
|
|
page: currentPage,
|
|
limit: pageSize,
|
|
};
|
|
|
|
return ['logistic-purchase-report', params];
|
|
},
|
|
([, params]) =>
|
|
LogisticApi.getLogisticPurchasePerSupplierReport(
|
|
params.area_id,
|
|
params.supplier_id,
|
|
params.product_id,
|
|
params.product_category_id,
|
|
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,
|
|
params.filter_by,
|
|
params.page,
|
|
params.limit
|
|
)
|
|
);
|
|
|
|
const data: LogisticPurchasePerSupplierReport[] = useMemo(
|
|
() =>
|
|
isResponseSuccess(purchasePerSupplier)
|
|
? (purchasePerSupplier?.data as unknown as LogisticPurchasePerSupplierReport[]) ||
|
|
[]
|
|
: [],
|
|
[purchasePerSupplier]
|
|
);
|
|
|
|
// ===== EXPORT DATA FETCHER =====
|
|
const logisticPurchasePerSupplierExport = useCallback(async (): Promise<
|
|
LogisticPurchasePerSupplierReport[] | null
|
|
> => {
|
|
const params = {
|
|
area_id: filterParams.area_id,
|
|
supplier_id: filterParams.supplier_id,
|
|
product_id: filterParams.product_id,
|
|
product_category_id: filterParams.product_category_id,
|
|
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.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,
|
|
params.filter_by,
|
|
params.page,
|
|
params.limit
|
|
);
|
|
|
|
return isResponseSuccess(response)
|
|
? (response.data as unknown as LogisticPurchasePerSupplierReport[])
|
|
: null;
|
|
}, [filterParams]);
|
|
|
|
// ===== EXPORT HANDLERS =====
|
|
const handleExportExcel = useCallback(async () => {
|
|
setIsExcelExportLoading(true);
|
|
try {
|
|
const allDataForExport = await logisticPurchasePerSupplierExport();
|
|
|
|
if (
|
|
!allDataForExport ||
|
|
!Array.isArray(allDataForExport) ||
|
|
allDataForExport.length === 0
|
|
) {
|
|
toast.error('Tidak ada data untuk diekspor.');
|
|
return;
|
|
}
|
|
|
|
await generatePurchasesPerSupplierExcel({ data: allDataForExport });
|
|
toast.success('Excel berhasil dibuat dan diunduh.');
|
|
} catch {
|
|
toast.error('Gagal membuat Excel. Silakan coba lagi.');
|
|
} finally {
|
|
setIsExcelExportLoading(false);
|
|
}
|
|
}, [logisticPurchasePerSupplierExport]);
|
|
|
|
const handleExportPdf = useCallback(async () => {
|
|
setIsPdfExportLoading(true);
|
|
try {
|
|
const allDataForExport = await logisticPurchasePerSupplierExport();
|
|
|
|
if (
|
|
!allDataForExport ||
|
|
!Array.isArray(allDataForExport) ||
|
|
allDataForExport.length === 0
|
|
) {
|
|
toast.error('Tidak ada data untuk diekspor.');
|
|
return;
|
|
}
|
|
|
|
const areaName = filterParams.area_id
|
|
? areaOptions
|
|
.filter((opt) =>
|
|
filterParams.area_id?.split(',').includes(String(opt.value))
|
|
)
|
|
.map((opt) => opt.label)
|
|
.join(', ') || 'Semua Area'
|
|
: 'Semua Area';
|
|
|
|
const supplierName = filterParams.supplier_id
|
|
? supplierOptions
|
|
.filter((opt) =>
|
|
filterParams.supplier_id?.split(',').includes(String(opt.value))
|
|
)
|
|
.map((opt) => opt.label)
|
|
.join(', ') || 'Semua Supplier'
|
|
: 'Semua Supplier';
|
|
|
|
const productName = filterParams.product_id
|
|
? productOptions
|
|
.filter((opt) =>
|
|
filterParams.product_id?.split(',').includes(String(opt.value))
|
|
)
|
|
.map((opt) => opt.label)
|
|
.join(', ') || 'Semua Produk'
|
|
: 'Semua Produk';
|
|
|
|
const productCategoryName = filterParams.product_category_id
|
|
? productCategoryOptions
|
|
.filter((opt) =>
|
|
filterParams.product_category_id
|
|
?.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: filterParams.filter_by,
|
|
start_date: filterParams.start_date,
|
|
end_date: filterParams.end_date,
|
|
};
|
|
|
|
await generatePurchasesPerSupplierPDF({
|
|
data: allDataForExport,
|
|
params: exportParams,
|
|
});
|
|
toast.success('PDF berhasil dibuat dan diunduh.');
|
|
} catch {
|
|
toast.error('Gagal membuat PDF. Silakan coba lagi.');
|
|
} finally {
|
|
setIsPdfExportLoading(false);
|
|
}
|
|
}, [
|
|
logisticPurchasePerSupplierExport,
|
|
filterParams,
|
|
areaOptions,
|
|
supplierOptions,
|
|
productOptions,
|
|
productCategoryOptions,
|
|
]);
|
|
|
|
// ===== TAB ACTIONS COMPONENT =====
|
|
const TabActions = useMemo(() => {
|
|
return function TabActionsComponent() {
|
|
const setTabActions = useTabActionsStore((state) => state.setTabActions);
|
|
const clearTabActions = useTabActionsStore(
|
|
(state) => state.clearTabActions
|
|
);
|
|
|
|
useEffect(() => {
|
|
setTabActions(
|
|
tabId,
|
|
<div className='flex flex-row gap-3'>
|
|
<ButtonFilter
|
|
values={filterParams}
|
|
fieldGroups={[['start_date', 'end_date']]}
|
|
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'
|
|
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>
|
|
}
|
|
>
|
|
<Button
|
|
variant='ghost'
|
|
color='none'
|
|
onClick={handleExportExcel}
|
|
isLoading={isExcelExportLoading}
|
|
className='w-full p-3 justify-start text-sm text-base-content/50 font-semibold text-nowrap'
|
|
>
|
|
<Icon icon='heroicons:table-cells' width={20} height={20} />
|
|
Export to Excel
|
|
</Button>
|
|
<Button
|
|
variant='ghost'
|
|
color='none'
|
|
onClick={handleExportPdf}
|
|
isLoading={isPdfExportLoading}
|
|
className='w-full p-3 justify-start text-sm text-base-content/50 font-semibold text-nowrap'
|
|
>
|
|
<Icon icon='heroicons:document' width={20} height={20} />
|
|
Export to PDF
|
|
</Button>
|
|
</Dropdown>
|
|
</div>
|
|
);
|
|
}, [setTabActions]);
|
|
|
|
useEffect(() => {
|
|
return () => {
|
|
clearTabActions(tabId);
|
|
};
|
|
}, [clearTabActions]);
|
|
|
|
return null;
|
|
};
|
|
}, [
|
|
tabId,
|
|
filterParams,
|
|
isAnyExportLoading,
|
|
handleExportExcel,
|
|
handleExportPdf,
|
|
isExcelExportLoading,
|
|
isPdfExportLoading,
|
|
]);
|
|
|
|
const TabActionsElement = useMemo(() => <TabActions />, [TabActions]);
|
|
|
|
const getTableColumns = (
|
|
summary: LogisticPurchasePerSupplierSummary
|
|
): ColumnDef<LogisticPurchasePerSupplierReport['rows'][0]>[] => {
|
|
const tableColumns: ColumnDef<
|
|
LogisticPurchasePerSupplierReport['rows'][0]
|
|
>[] = [
|
|
{
|
|
id: 'no',
|
|
header: 'No',
|
|
cell: (props) => props.row.index + 1,
|
|
footer: () => <div className='font-semibold text-gray-900'>Total</div>,
|
|
},
|
|
{
|
|
id: 'received_date',
|
|
header: 'Tanggal Terima',
|
|
accessorKey: 'receive_date',
|
|
enableSorting: false,
|
|
cell: (props) => {
|
|
const value = props.row.original.receive_date;
|
|
return formatDate(value, 'DD MMM YYYY');
|
|
},
|
|
},
|
|
{
|
|
id: 'po_date',
|
|
header: 'Tanggal PO',
|
|
accessorKey: 'po_date',
|
|
enableSorting: false,
|
|
cell: (props) => {
|
|
const value = props.row.original.po_date;
|
|
return formatDate(value, 'DD MMM YYYY');
|
|
},
|
|
},
|
|
{
|
|
id: 'po_number',
|
|
header: 'No. Referensi',
|
|
accessorKey: 'po_number',
|
|
enableSorting: false,
|
|
cell: (props) => {
|
|
const value = props.row.original.po_number;
|
|
return value || '-';
|
|
},
|
|
},
|
|
{
|
|
id: 'product_name',
|
|
header: 'Nama Produk',
|
|
accessorKey: 'product.name',
|
|
enableSorting: false,
|
|
cell: (props) => {
|
|
const product = props.row.original.product;
|
|
return product?.name || '-';
|
|
},
|
|
},
|
|
{
|
|
id: 'destination_warehouse',
|
|
header: 'Tujuan',
|
|
accessorKey: 'warehouse.name',
|
|
enableSorting: false,
|
|
cell: (props) => {
|
|
const warehouse = props.row.original.warehouse;
|
|
return warehouse?.name || '-';
|
|
},
|
|
},
|
|
{
|
|
id: 'qty',
|
|
header: 'QTY',
|
|
accessorKey: 'qty',
|
|
enableSorting: false,
|
|
cell: (props) => {
|
|
const value = props.row.original.qty;
|
|
return <div className='text-right'>{formatNumber(value)}</div>;
|
|
},
|
|
footer: () => (
|
|
<div className='text-right font-semibold text-gray-900'>
|
|
{formatNumber(summary.total_qty) || '-'}
|
|
</div>
|
|
),
|
|
},
|
|
{
|
|
id: 'price',
|
|
header: 'Harga Beli (Rp)',
|
|
accessorKey: 'unit_price',
|
|
enableSorting: false,
|
|
cell: (props) => {
|
|
const value = props.row.original.unit_price;
|
|
return <div className='text-right'>{formatCurrency(value)}</div>;
|
|
},
|
|
footer: () => (
|
|
<div className='text-right font-semibold text-gray-900'>
|
|
{formatCurrency(summary.total_unit_price) || '-'}
|
|
</div>
|
|
),
|
|
},
|
|
{
|
|
id: 'purchase_amount',
|
|
header: 'Value Harga Beli (Rp)',
|
|
accessorKey: 'purchase_value',
|
|
enableSorting: false,
|
|
cell: (props) => {
|
|
const value = props.row.original.purchase_value;
|
|
return <div className='text-right'>{formatCurrency(value)}</div>;
|
|
},
|
|
footer: () => (
|
|
<div className='text-right font-semibold text-gray-900'>
|
|
{formatCurrency(summary.total_purchase_value) || '-'}
|
|
</div>
|
|
),
|
|
},
|
|
{
|
|
id: 'transport',
|
|
header: 'Transport (Rp)',
|
|
accessorKey: 'transport_unit_price',
|
|
enableSorting: false,
|
|
cell: (props) => {
|
|
const value = props.row.original.transport_unit_price;
|
|
return <div className='text-right'>{formatCurrency(value)}</div>;
|
|
},
|
|
footer: () => (
|
|
<div className='text-right font-semibold text-gray-900'>
|
|
{formatCurrency(summary.total_transport_unit_price) || '-'}
|
|
</div>
|
|
),
|
|
},
|
|
{
|
|
id: 'value_transport',
|
|
header: 'Value Transport (Rp)',
|
|
accessorKey: 'transport_value',
|
|
enableSorting: false,
|
|
cell: (props) => {
|
|
const value = props.row.original.transport_value;
|
|
return <div className='text-right'>{formatCurrency(value)}</div>;
|
|
},
|
|
footer: () => (
|
|
<div className='text-right font-semibold text-gray-900'>
|
|
{formatCurrency(summary.total_transport_value) || '-'}
|
|
</div>
|
|
),
|
|
},
|
|
{
|
|
id: 'total',
|
|
header: 'Jumlah (Rp)',
|
|
accessorKey: 'total_amount',
|
|
enableSorting: false,
|
|
cell: (props) => {
|
|
const value = props.row.original.total_amount;
|
|
return <div className='text-right'>{formatCurrency(value)}</div>;
|
|
},
|
|
footer: () => (
|
|
<div className='text-right font-semibold text-gray-900'>
|
|
{formatCurrency(summary.total_amount) || '-'}
|
|
</div>
|
|
),
|
|
},
|
|
{
|
|
id: 'expedition_vendor_name',
|
|
header: 'Ekspedisi',
|
|
accessorKey: 'expedition',
|
|
enableSorting: false,
|
|
cell: (props) => {
|
|
const value = props.row.original.expedition;
|
|
return value || '-';
|
|
},
|
|
},
|
|
{
|
|
id: 'travel_number',
|
|
header: 'Surat Jalan',
|
|
accessorKey: 'delivery_number',
|
|
enableSorting: false,
|
|
cell: (props) => {
|
|
const value = props.row.original.delivery_number;
|
|
return value || '-';
|
|
},
|
|
},
|
|
];
|
|
return tableColumns;
|
|
};
|
|
|
|
return (
|
|
<>
|
|
{TabActionsElement}
|
|
<div className='w-full p-0 sm:p-3 flex flex-col gap-3'>
|
|
{isLoading && (
|
|
<PurchasePerSupplierSkeleton
|
|
columns={getTableColumns({} as LogisticPurchasePerSupplierSummary)}
|
|
icon={
|
|
<Icon
|
|
icon='heroicons:document-report'
|
|
className='text-white'
|
|
width={20}
|
|
height={20}
|
|
/>
|
|
}
|
|
title='Memuat Data Pembelian Per Supplier'
|
|
subtitle='Silakan tunggu sebentar...'
|
|
/>
|
|
)}
|
|
|
|
{!isLoading && data.length === 0 && (
|
|
<PurchasePerSupplierSkeleton
|
|
columns={getTableColumns({} as LogisticPurchasePerSupplierSummary)}
|
|
icon={
|
|
<Icon
|
|
icon='heroicons:chart-bar'
|
|
className='text-white'
|
|
width={20}
|
|
height={20}
|
|
/>
|
|
}
|
|
title='Data Not Yet Available'
|
|
subtitle='Please change your filters to get the data.'
|
|
/>
|
|
)}
|
|
|
|
{!isLoading &&
|
|
data.length > 0 &&
|
|
data.map((supplierReport) => {
|
|
const summary = supplierReport.summary || {
|
|
total_qty: 0,
|
|
total_unit_price: 0,
|
|
total_purchase_value: 0,
|
|
total_transport_unit_price: 0,
|
|
total_transport_value: 0,
|
|
total_amount: 0,
|
|
};
|
|
|
|
const totalPurchase = summary.total_amount;
|
|
const tableColumns = getTableColumns(summary);
|
|
|
|
return (
|
|
<Card
|
|
key={supplierReport.supplier.id}
|
|
title={supplierReport.supplier.name}
|
|
subtitle={`Total Pembelian: ${formatCurrency(totalPurchase)}`}
|
|
className={{
|
|
wrapper: 'w-full rounded-lg border-none',
|
|
body: 'p-0',
|
|
title:
|
|
'px-2 py-1.5 font-normal text-sm bg-primary text-white',
|
|
subtitle:
|
|
'px-2 pb-1.5 bg-primary text-white text-xs font-normal',
|
|
collapsible: 'rounded-lg',
|
|
}}
|
|
variant='bordered'
|
|
collapsible={true}
|
|
>
|
|
<Table
|
|
data={supplierReport.rows}
|
|
columns={tableColumns}
|
|
pageSize={supplierReport.rows.length}
|
|
renderFooter={supplierReport.rows.length > 0}
|
|
className={{
|
|
containerClassName: 'w-full mb-0!',
|
|
tableWrapperClassName:
|
|
'overflow-x-auto rounded-tr-none rounded-tl-none',
|
|
tableClassName: 'w-full table-auto text-sm',
|
|
headerRowClassName: 'border-b border-b-gray-200 bg-gray-50',
|
|
headerColumnClassName:
|
|
'px-4 py-3 text-xs font-semibold text-gray-700 text-left border border-gray-200',
|
|
bodyRowClassName:
|
|
'hover:bg-gray-50 transition-colors border-b border-l border-r border-b-gray-200 border-l-gray-200 border-r-gray-200',
|
|
bodyColumnClassName:
|
|
'px-4 py-3 text-xs text-gray-900 whitespace-nowrap',
|
|
tableFooterClassName:
|
|
'bg-gray-100 font-semibold border border-gray-200',
|
|
footerRowClassName: 'border-t-2 border-gray-300',
|
|
footerColumnClassName:
|
|
'px-4 py-3 text-xs text-gray-900 whitespace-nowrap',
|
|
paginationClassName: 'hidden',
|
|
}}
|
|
/>
|
|
</Card>
|
|
);
|
|
})}
|
|
</div>
|
|
|
|
{/* Filter Modal */}
|
|
<Modal
|
|
ref={filterModal.ref}
|
|
className={{
|
|
modal: 'p-0',
|
|
modalBox: 'p-0 rounded-[0.875rem] xl:max-w-4/12 max-w-sm',
|
|
}}
|
|
>
|
|
{/* Modal Header */}
|
|
<div className='flex items-center justify-between gap-2 border-b border-base-content/10 p-4'>
|
|
<div className='flex items-center gap-2 text-primary'>
|
|
<Icon icon='heroicons:funnel' width={20} height={20} />
|
|
<h3 className='font-medium text-sm'>Filter Data</h3>
|
|
</div>
|
|
<Button
|
|
variant='link'
|
|
onClick={filterModal.closeModal}
|
|
className='text-base-content/50 hover:text-base-content transition-colors cursor-pointer'
|
|
>
|
|
<Icon icon='heroicons:x-mark' width={20} height={20} />
|
|
</Button>
|
|
</div>
|
|
<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 || ''}
|
|
errorMessage={formik.errors.start_date}
|
|
onChange={handleStartDateChange}
|
|
className={{ wrapper: 'w-full' }}
|
|
isNestedModal
|
|
isError={
|
|
formik.touched.start_date &&
|
|
Boolean(formik.errors.start_date)
|
|
}
|
|
/>
|
|
<hr className='w-full max-w-3 h-px border-base-content/10' />
|
|
<DateInput
|
|
name='end_date'
|
|
value={formik.values.end_date || ''}
|
|
errorMessage={formik.errors.end_date}
|
|
onChange={handleEndDateChange}
|
|
className={{ wrapper: 'w-full' }}
|
|
isNestedModal
|
|
isError={
|
|
(formik.touched.end_date &&
|
|
Boolean(formik.errors.end_date)) ||
|
|
hasDateError
|
|
}
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Area Filter */}
|
|
<SelectInputCheckbox
|
|
label='Area'
|
|
placeholder='Pilih Area'
|
|
options={areaOptions}
|
|
value={areaIdsValue}
|
|
onChange={(val) => {
|
|
formik.setFieldValue(
|
|
'area_ids',
|
|
Array.isArray(val) && val.length > 0
|
|
? val.map((v) => String(v.value)).join(',')
|
|
: null
|
|
);
|
|
}}
|
|
isLoading={isLoadingAreas}
|
|
isClearable
|
|
className={{ wrapper: 'w-full' }}
|
|
onInputChange={setAreaInputValue}
|
|
onMenuScrollToBottom={loadMoreArea}
|
|
/>
|
|
|
|
{/* Supplier Filter */}
|
|
<SelectInputCheckbox
|
|
label='Supplier'
|
|
placeholder='Pilih Supplier'
|
|
options={supplierOptions}
|
|
value={supplierIdsValue}
|
|
onChange={(val) => {
|
|
formik.setFieldValue(
|
|
'supplier_ids',
|
|
Array.isArray(val) && val.length > 0
|
|
? val.map((v) => String(v.value)).join(',')
|
|
: null
|
|
);
|
|
}}
|
|
isLoading={isLoadingSuppliers}
|
|
isClearable
|
|
className={{ wrapper: 'w-full' }}
|
|
onInputChange={setSupplierInputValue}
|
|
onMenuScrollToBottom={loadMoreSupplier}
|
|
/>
|
|
|
|
{/* Product Filter */}
|
|
<SelectInputCheckbox
|
|
label='Produk'
|
|
placeholder='Pilih Produk'
|
|
options={productOptions}
|
|
value={productIdsValue}
|
|
onChange={(val) => {
|
|
formik.setFieldValue(
|
|
'product_ids',
|
|
Array.isArray(val) && val.length > 0
|
|
? val.map((v) => String(v.value)).join(',')
|
|
: null
|
|
);
|
|
}}
|
|
isLoading={isLoadingProducts}
|
|
isClearable
|
|
className={{ wrapper: 'w-full' }}
|
|
onInputChange={setProductInputValue}
|
|
onMenuScrollToBottom={loadMoreProduct}
|
|
/>
|
|
|
|
{/* Product Category Filter */}
|
|
<SelectInputCheckbox
|
|
label='Kategori Produk'
|
|
placeholder='Pilih Kategori Produk'
|
|
options={productCategoryOptions}
|
|
value={productCategoryIdsValue}
|
|
onChange={(val) => {
|
|
formik.setFieldValue(
|
|
'product_category_ids',
|
|
Array.isArray(val) && val.length > 0
|
|
? val.map((v) => String(v.value)).join(',')
|
|
: null
|
|
);
|
|
}}
|
|
isLoading={isLoadingProductCategories}
|
|
isClearable
|
|
className={{ wrapper: 'w-full' }}
|
|
onInputChange={setProductCategoryInputValue}
|
|
onMenuScrollToBottom={loadMoreProductCategory}
|
|
/>
|
|
|
|
{/* Filter By Type */}
|
|
<SelectInputRadio
|
|
label='Filter Berdasarkan'
|
|
placeholder='Pilih Filter Berdasarkan'
|
|
options={dataTypeOptions}
|
|
value={filterByValue}
|
|
onChange={(val) => {
|
|
if (!Array.isArray(val)) {
|
|
formik.setFieldValue(
|
|
'filter_by',
|
|
val?.value?.toString() || null
|
|
);
|
|
}
|
|
}}
|
|
className={{ wrapper: 'w-full' }}
|
|
isClearable={true}
|
|
/>
|
|
|
|
{/* Sort By */}
|
|
<SelectInputRadio
|
|
label='Urutkan Berdasarkan'
|
|
placeholder='Pilih Urutkan Berdasarkan'
|
|
options={sortByOptions}
|
|
value={sortByValue}
|
|
onChange={(val) => {
|
|
if (!Array.isArray(val)) {
|
|
formik.setFieldValue(
|
|
'sort_by',
|
|
val?.value?.toString() || null
|
|
);
|
|
}
|
|
}}
|
|
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
|
|
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>
|
|
</>
|
|
);
|
|
};
|
|
|
|
export default PurchasesPerSupplierTab;
|