Files
lti-web-client/src/components/pages/closing/ClosingsTable.tsx
T
2025-12-06 16:52:12 +07:00

300 lines
8.5 KiB
TypeScript

'use client';
import { ChangeEventHandler, useEffect, useState } from 'react';
import useSWR from 'swr';
import { CellContext, ColumnDef, SortingState } from '@tanstack/react-table';
import { Icon } from '@iconify/react';
import Table from '@/components/Table';
import DebouncedTextInput from '@/components/input/DebouncedTextInput';
import Button from '@/components/Button';
import SelectInput, {
OptionType,
useSelect,
} from '@/components/input/SelectInput';
import RowDropdownOptions from '@/components/table/RowDropdownOptions';
import RowCollapseOptions from '@/components/table/RowCollapseOptions';
import RowOptionsMenuWrapper from '@/components/table/RowOptionsMenuWrapper';
import { cn, formatCurrency, formatDate } from '@/lib/helper';
import { isResponseSuccess } from '@/lib/api-helper';
import { useTableFilter } from '@/services/hooks/useTableFilter';
import { LocationApi } from '@/services/api/master-data';
import { Location } from '@/types/api/master-data/location';
import { ClosingApi } from '@/services/api/closing';
import { Closing } from '@/types/api/closing';
const PROJECT_STATUS_OPTIONS = [
{
value: 1,
label: 'Pengajuan',
},
{
value: 2,
label: 'Aktif',
},
];
const RowOptionsMenu = ({
type = 'dropdown',
props,
}: {
type: 'dropdown' | 'collapse';
props: CellContext<Closing, unknown>;
}) => {
return (
<RowOptionsMenuWrapper type={type}>
{/* TODO: apply RBAC */}
<div className='w-full max-h-40 overflow-auto flex flex-col gap-1'>
<Button
href={`/closing/detail/?closingId=${props.row.original.id}`}
variant='ghost'
color='primary'
className='justify-start text-sm'
>
<Icon icon='mdi:eye-outline' width={16} height={16} />
Detail
</Button>
</div>
</RowOptionsMenuWrapper>
);
};
const ClosingsTable = () => {
const {
state: tableFilterState,
updateFilter,
setPage,
setPageSize,
toQueryString: getTableFilterQueryString,
} = useTableFilter({
initial: {
search: '',
nameSort: '',
transactionDate: '',
realizationDate: '',
locationId: '',
projectStatus: '',
userId: '',
},
paramMap: {
page: 'page',
pageSize: 'limit',
nameSort: 'sort_name',
transactionDate: 'transaction_date',
realizationDate: 'realization_date',
locationId: 'location_id',
projectStatus: 'project_status',
userId: 'user_id',
},
});
const { data: closings, isLoading: isLoadingClosings } = useSWR(
`${ClosingApi.basePath}${getTableFilterQueryString()}`,
ClosingApi.getAllFetcher
);
const [sorting, setSorting] = useState<SortingState>([]);
const [rowSelection, setRowSelection] = useState<Record<string, boolean>>({});
const closingsColumns: ColumnDef<Closing>[] = [
{
header: '#',
cell: (props) => props.row.index + 1,
},
{
accessorKey: 'location_name',
header: 'Lokasi',
},
{
accessorKey: 'project_category',
header: 'Kategori',
},
{
accessorKey: 'period',
header: 'Periode',
},
{
accessorKey: 'closing_date',
header: 'Periode',
cell: (props) =>
formatDate(props.row.original.closing_date, 'DD MMM YYYY'),
},
{
accessorKey: 'shed_label',
header: 'Jumlah Kandang',
},
{
accessorKey: 'sales_paid_amount',
header: 'Jumlah Sudah Bayar',
cell: (props) => (
<span className='text-success'>
{formatCurrency(props.row.original.sales_paid_amount)}
</span>
),
},
{
accessorKey: 'sales_remaining_amount',
header: 'Jumlah Sisa Bayar',
cell: (props) => (
<span className='text-error'>
{formatCurrency(props.row.original.sales_remaining_amount)}
</span>
),
},
{
accessorKey: 'sales_payment_status',
header: 'Status Pembayaran',
},
{
accessorKey: 'project_status',
header: 'Status',
},
{
header: 'Aksi',
cell: (props) => {
const currentPageSize = props.table.getPaginationRowModel().rows.length;
const currentPageRows = props.table.getPaginationRowModel().flatRows;
const currentRowRelativeIndex =
currentPageRows.findIndex((r) => r.id === props.row.id) + 1;
const isLast2Rows = currentRowRelativeIndex > currentPageSize - 3;
return (
<>
{currentPageSize > 3 && (
<RowDropdownOptions isLast2Rows={isLast2Rows}>
<RowOptionsMenu type='dropdown' props={props} />
</RowDropdownOptions>
)}
{currentPageSize <= 3 && (
<RowCollapseOptions>
<RowOptionsMenu type='collapse' props={props} />
</RowCollapseOptions>
)}
</>
);
},
},
];
const {
setInputValue: setLocationInputValue,
options: locationOptions,
isLoadingOptions: isLoadingLocationOptions,
} = useSelect<Location>(LocationApi.basePath, 'id', 'name');
const [selectedLocation, setSelectedLocation] = useState<OptionType | null>(
null
);
const locationChangeHandler = (val: OptionType | OptionType[] | null) => {
setSelectedLocation(val as OptionType);
updateFilter(
'locationId',
val ? ((val as OptionType).value as string) : ''
);
};
const [selectedProjectStatus, setSelectedProjectStatus] =
useState<OptionType | null>(null);
const projectStatusChangeHandler = (
val: OptionType | OptionType[] | null
) => {
setSelectedProjectStatus(val as OptionType);
updateFilter(
'projectStatus',
val ? ((val as OptionType).value as string) : ''
);
};
const searchChangeHandler: ChangeEventHandler<HTMLInputElement> = (e) => {
updateFilter('search', e.target.value);
};
// track sorting
useEffect(() => {
const isNameSorted = sorting.find((sortItem) => sortItem.id === 'name');
if (!isNameSorted) {
updateFilter('nameSort', '');
} else {
updateFilter('nameSort', isNameSorted.desc ? 'desc' : 'asc');
}
}, [sorting, updateFilter]);
return (
<>
<div className='w-full p-0 sm:p-4'>
<div className='flex flex-col gap-2 mb-4'>
<div className='flex flex-col gap-2 mb-4'>
<div className='w-full flex flex-col sm:flex-row justify-end items-end sm:items-center gap-4'>
<DebouncedTextInput
name='search'
placeholder='Cari Closing'
value={tableFilterState.search}
onChange={searchChangeHandler}
className={{ wrapper: 'sm:max-w-3xs' }}
/>
</div>
<div className='grid grid-cols-12 justify-end gap-2'>
<SelectInput
label='Lokasi'
options={locationOptions}
isLoading={isLoadingLocationOptions}
value={selectedLocation}
onChange={locationChangeHandler}
onInputChange={setLocationInputValue}
isClearable
className={{
wrapper: 'col-span-12 sm:col-span-6',
}}
/>
<SelectInput
label='Status Project'
placeholder='Pilih Status'
options={PROJECT_STATUS_OPTIONS}
value={selectedProjectStatus}
onChange={projectStatusChangeHandler}
isClearable
className={{
wrapper: 'col-span-12 sm:col-span-6',
}}
/>
</div>
</div>
</div>
<Table<Closing>
data={isResponseSuccess(closings) ? closings?.data : []}
columns={closingsColumns}
pageSize={tableFilterState.pageSize}
onPageSizeChange={setPageSize}
rowOptions={[10, 20, 50, 100]}
page={isResponseSuccess(closings) ? closings?.meta?.page : 0}
totalItems={
isResponseSuccess(closings) ? closings?.meta?.total_results : 0
}
onPageChange={setPage}
isLoading={isLoadingClosings}
sorting={sorting}
setSorting={setSorting}
rowSelection={rowSelection}
setRowSelection={setRowSelection}
className={{
containerClassName: cn({
'w-full mb-20':
isResponseSuccess(closings) && closings?.data?.length === 0,
}),
}}
/>
</div>
</>
);
};
export default ClosingsTable;