feat(FE-338): Slicing UI Halaman Reporting BOP & API integration & refactor debounce input: adding useEffect for sync value

This commit is contained in:
randy-ar
2025-12-11 18:23:55 +07:00
parent d0abc0e9ff
commit 9c09395677
11 changed files with 1795 additions and 0 deletions
@@ -0,0 +1,346 @@
import Badge from '@/components/Badge';
import Button from '@/components/Button';
import Card from '@/components/Card';
import DateInput from '@/components/input/DateInput';
import DebouncedTextInput from '@/components/input/DebouncedTextInput';
import NumberInput from '@/components/input/NumberInput';
import SelectInput, {
OptionType,
useSelect,
} from '@/components/input/SelectInput';
import ExpenseStatusBadge from '@/components/pages/expense/ExpenseStatusBadge';
import RealizationStatusBadge from '@/components/pages/expense/RealizationStatusBadge';
import Table, { TABLE_DEFAULT_STYLING } from '@/components/Table';
import { cn, formatCurrency, formatDate } from '@/lib/helper';
import { ReportExpense } from '@/types/api/report/report-expense';
import { Icon } from '@iconify/react';
import { ColumnDef } from '@tanstack/react-table';
import { useMemo, useState } from 'react';
import ReportExpenseExport from '@/components/pages/report/expense/pdf/ReportExpenseExport';
const ReportExpenseTable = ({
reportExpenses,
onSearch,
}: {
reportExpenses: ReportExpense[];
onSearch: (params: {
locationId: string | null;
supplierId: string | null;
kandangId: string | null;
startDate: string | null;
endDate: string | null;
category: string | null;
period: string | number;
search: string;
}) => void;
}) => {
const [selectedLocation, setSelectedLocation] = useState<OptionType | null>(
null
);
const [selectedSupplier, setSelectedSupplier] = useState<OptionType | null>(
null
);
const [selectedCategory, setSelectedCategory] = useState<OptionType | null>(
null
);
const [selectedKandang, setSelectedKandang] = useState<OptionType | null>(
null
);
const [search, setSearch] = useState('');
const [startDate, setStartDate] = useState<string | null>(null);
const [endDate, setEndDate] = useState<string | null>(null);
const [period, setPeriod] = useState<number | string>('');
const { options: optionsLocation, isLoadingOptions: isLoadingLocation } =
useSelect(`/master-data/locations`, 'id', 'name');
const { options: optionsSupplier, isLoadingOptions: isLoadingSupplier } =
useSelect(`/master-data/suppliers`, 'id', 'name');
const { options: optionsKandang, isLoadingOptions: isLoadingKandang } =
useSelect(`/master-data/kandangs`, 'id', 'name', '', {
location_id: selectedLocation?.value.toString() || '',
});
const columns = useMemo((): ColumnDef<ReportExpense>[] => {
return [
{
header: 'No',
accessorFn: (_, index) => index + 1,
},
{
header: 'No. PO',
accessorKey: 'po_number',
},
{
header: 'No. Referensi',
accessorKey: 'reference_number',
},
{
header: 'Tanggal Realisasi',
accessorKey: 'realization_date',
cell: ({ row }) => {
return formatDate(row.original.realization_date, 'DD MMM, YYYY');
},
},
{
header: 'Tanggal Transaksi',
accessorKey: 'transaction_date',
cell: ({ row }) => {
return formatDate(row.original.transaction_date, 'DD MMM, YYYY');
},
},
{
header: 'Kategori',
accessorKey: 'category',
},
{
header: 'Supplier',
accessorFn: (row) => row.supplier.name,
},
{
header: 'Lokasi',
accessorFn: (row) => row.location.name,
},
{
header: 'Kandang',
accessorFn: (row) => row.kandang.name,
},
{
header: 'Pengajuan',
columns: [
{
header: 'Qty',
id: 'qty_pengajuan',
accessorFn: (row) => row.pengajuan.qty,
cell: ({ row }) =>
row.original.pengajuan.qty.toLocaleString('id-ID'),
},
{
header: 'Harga',
id: 'harga_pengajuan',
accessorFn: (row) => row.pengajuan.price,
cell: ({ row }) => formatCurrency(row.original.pengajuan.price),
},
{
header: 'Total',
id: 'total_pengajuan',
accessorFn: (row) => row.pengajuan.qty * row.pengajuan.price,
cell: ({ row }) => {
const total =
row.original.pengajuan.qty * row.original.pengajuan.price;
return formatCurrency(total);
},
},
],
},
{
header: 'Realisasi',
columns: [
{
header: 'Qty',
id: 'qty_realisasi',
accessorFn: (row) => row.realisasi.qty,
cell: ({ row }) =>
row.original.realisasi.qty.toLocaleString('id-ID'),
},
{
header: 'Harga',
id: 'harga_realisasi',
accessorFn: (row) => row.realisasi.price,
cell: ({ row }) => formatCurrency(row.original.realisasi.price),
},
{
header: 'Total',
id: 'total_realisasi',
accessorFn: (row) => row.realisasi.qty * row.realisasi.price,
cell: ({ row }) => {
const total =
row.original.realisasi.qty * row.original.realisasi.price;
return formatCurrency(total);
},
},
],
},
{
header: 'Status Pencairan',
cell: (props) => (
<RealizationStatusBadge
approval={props.row.original.latest_approval}
/>
),
},
{
header: 'Status BOP',
cell: (props) => (
<ExpenseStatusBadge approval={props.row.original.latest_approval} />
),
},
];
}, []);
// Handle Search
const handleSearch = () => {
onSearch({
search,
period,
startDate,
endDate,
locationId: selectedLocation?.value.toString() ?? '',
kandangId: selectedKandang?.value.toString() ?? '',
supplierId: selectedSupplier?.value.toString() ?? '',
category: selectedCategory?.value.toString() ?? '',
});
};
const handleSearchInput = (e: React.ChangeEvent<HTMLInputElement>) => {
setSearch(e.target.value);
onSearch({
search: e.target.value,
period,
startDate,
endDate,
locationId: selectedLocation?.value.toString() ?? '',
kandangId: selectedKandang?.value.toString() ?? '',
supplierId: selectedSupplier?.value.toString() ?? '',
category: selectedCategory?.value.toString() ?? '',
});
};
const handleReset = () => {
setSearch('');
setPeriod('');
setStartDate('');
setEndDate('');
setSelectedLocation(null);
setSelectedKandang(null);
setSelectedSupplier(null);
setSelectedCategory(null);
onSearch({
search: '',
period: '',
startDate: '',
endDate: '',
locationId: '',
kandangId: '',
supplierId: '',
category: '',
});
};
return (
<div className='flex flex-col gap-4'>
<Card
title='Laporan Biaya Operasional'
variant='bordered'
className={{
wrapper: 'w-full',
}}
footer={
<div className='flex flex-row items-center justify-between gap-2'>
<div>
<ReportExpenseExport data={reportExpenses} />
</div>
<div className='flex flex-row items-center gap-2'>
<Button className='min-w-24' onClick={handleSearch}>
Cari
</Button>
<Button
className='min-w-24'
color='warning'
onClick={handleReset}
>
Reset
</Button>
</div>
</div>
}
>
<div className='grid grid-cols-2 md:grid-cols-4 gap-4'>
<SelectInput
isClearable
label='Lokasi'
options={optionsLocation}
isLoading={isLoadingLocation}
placeholder='Lokasi'
value={selectedLocation}
onChange={(option) => {
setSelectedLocation(option as OptionType);
setSelectedKandang(null);
}}
/>
<SelectInput
isClearable
label='Kandang'
options={optionsKandang}
isLoading={isLoadingKandang}
placeholder='Kandang'
value={selectedKandang}
onChange={(option) => setSelectedKandang(option as OptionType)}
/>
<SelectInput
isClearable
label='Supplier'
options={optionsSupplier}
isLoading={isLoadingSupplier}
placeholder='Supplier'
value={selectedSupplier}
onChange={(option) => setSelectedSupplier(option as OptionType)}
/>
<SelectInput
isClearable
label='Kategori'
options={[
{
value: 'BOP',
label: 'BOP',
},
{
value: 'NON BOP',
label: 'Non BOP',
},
]}
placeholder='Kategori'
value={selectedCategory}
onChange={(option) => setSelectedCategory(option as OptionType)}
/>
<NumberInput
label='Periode'
value={period}
onChange={(e) => setPeriod(e.target.value)}
name='periode'
placeholder='Periode'
/>
<DateInput
label='Tanggal Mulai'
value={startDate as string}
onChange={(e) => setStartDate(e.target.value)}
name='start_date'
placeholder='Tanggal Mulai'
/>
<DateInput
label='Tanggal Selesai'
value={endDate as string}
onChange={(e) => setEndDate(e.target.value)}
name='end_date'
placeholder='Tanggal Selesai'
/>
<DebouncedTextInput
label='Cari'
name='search'
value={search}
onChange={handleSearchInput}
placeholder='Cari'
startAdornment={<Icon icon='mdi:magnify' width={24} height={24} />}
/>
</div>
</Card>
<Table<ReportExpense>
columns={columns}
data={reportExpenses}
className={{
headerRowClassName: cn(TABLE_DEFAULT_STYLING, 'whitespace-nowrap'),
bodyRowClassName: cn(TABLE_DEFAULT_STYLING, 'whitespace-nowrap'),
}}
/>
</div>
);
};
export default ReportExpenseTable;