Files
lti-web-client/src/components/pages/production/transfer-to-laying/TransferToLayingsTable.tsx
T
2026-04-27 10:49:07 +07:00

833 lines
26 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
'use client';
import { ChangeEventHandler, useEffect, useMemo, useState } from 'react';
import { usePathname } from 'next/navigation';
import { useUiStore } from '@/stores/ui/ui.store';
import useSWR from 'swr';
import {
CellContext,
ColumnDef,
Row,
SortingState,
} from '@tanstack/react-table';
import toast from 'react-hot-toast';
import { Icon } from '@iconify/react';
import Table from '@/components/Table';
import Button from '@/components/Button';
import { useModal } from '@/components/Modal';
import CheckboxInput from '@/components/input/CheckboxInput';
import RequirePermission from '@/components/helper/RequirePermission';
import PopoverButton from '@/components/popover/PopoverButton';
import PopoverContent from '@/components/popover/PopoverContent';
import Dropdown from '@/components/Dropdown';
import StatusBadge from '@/components/helper/StatusBadge';
import TransferToLayingFilterModal from '@/components/pages/production/transfer-to-laying/TransferToLayingFilterModal';
import TransferToLayingConfirmationModal from '@/components/pages/production/transfer-to-laying/TransferToLayingConfirmationModal';
import TransferToLayingTableSkeleton from '@/components/pages/production/transfer-to-laying/skeleton/TransferToLayingTableSkeleton';
import { TransferToLaying } from '@/types/api/production/transfer-to-laying';
import { TransferToLayingFilterValues } from '@/components/pages/production/transfer-to-laying/filter/TransferToLayingFilter';
import { OptionType } from '@/components/input/SelectInput';
import { TransferToLayingApi } from '@/services/api/production/transfer-to-laying';
import { cn, formatDate, formatNumber } from '@/lib/helper';
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
import { useTableFilter } from '@/services/hooks/useTableFilter';
import { Color } from '@/types/theme';
import DebouncedTextInput from '@/components/input/DebouncedTextInput';
import ButtonFilter from '@/components/helper/ButtonFilter';
const RowOptionsMenu = ({
props,
popoverPosition = 'bottom',
deleteClickHandler,
}: {
props: CellContext<TransferToLaying, unknown>;
popoverPosition: 'bottom' | 'top';
deleteClickHandler: () => void;
}) => {
const showEditButton = props.row.original.approval.action !== 'APPROVED';
const showDeleteButton =
props.row.original.approval.action === 'APPROVED' ||
props.row.original.approval.step_name.toLowerCase() === 'pengajuan';
const popoverId = `transferToLaying#${props.row.original.id}`;
const popoverAnchorName = `--anchor-transferToLaying#${props.row.original.id}`;
return (
<div className='relative'>
<PopoverButton
tabIndex={0}
variant='ghost'
color='none'
popoverTarget={popoverId}
anchorName={popoverAnchorName}
>
<Icon icon='material-symbols:more-vert' width={16} height={16} />
</PopoverButton>
<PopoverContent
id={popoverId}
anchorName={popoverAnchorName}
position={popoverPosition === 'bottom' ? 'bottom-start' : 'left'}
className='w-full max-w-40 rounded-xl border border-base-content/5 shadow-sm'
>
<div className='flex flex-col bg-base-100 rounded-xl'>
<RequirePermission permissions='lti.production.transfer_to_laying.detail'>
<Button
href={`/production/transfer-to-laying/?action=detail&id=${props.row.original.id}`}
variant='ghost'
color='none'
className='p-3 justify-start text-sm font-semibold w-full'
>
<Icon icon='heroicons:eye' width={20} height={20} />
View Details
</Button>
</RequirePermission>
{showEditButton && (
<RequirePermission permissions='lti.production.transfer_to_laying.update'>
<Button
href={`/production/transfer-to-laying/?action=edit&id=${props.row.original.id}`}
variant='ghost'
color='none'
className='p-3 justify-start text-sm font-semibold w-full'
>
<Icon icon='heroicons:pencil-square' width={20} height={20} />
Edit
</Button>
</RequirePermission>
)}
{showDeleteButton && (
<RequirePermission permissions='lti.production.transfer_to_laying.delete'>
<hr className='mx-3 border-base-content/10 h-px' />
<Button
onClick={deleteClickHandler}
variant='ghost'
color='error'
className='p-3 justify-start text-sm font-semibold w-full'
>
<Icon icon='heroicons:trash' width={20} height={20} />
Delete
</Button>
</RequirePermission>
)}
</div>
</PopoverContent>
</div>
);
};
const TransferToLayingsTable = () => {
const { searchValue, setSearchValue, setTableState } = useUiStore();
const pathname = usePathname();
const {
state: tableFilterState,
updateFilter,
setPage,
setPageSize,
toQueryString: getTableFilterQueryString,
} = useTableFilter({
initial: {
search: '',
startDate: '',
endDate: '',
flockSource: '',
flockDestination: '',
status: '',
filter_by: '',
sort_by: '',
flockSourceNames: '',
flockDestinationNames: '',
},
paramMap: {
page: 'page',
pageSize: 'limit',
startDate: 'start_date',
endDate: 'end_date',
flockSource: 'flock_source',
flockDestination: 'flock_destination',
status: 'status',
filter_by: 'filter_by',
sort_by: 'sort_by',
},
excludeKeysFromUrl: ['flockSourceNames', 'flockDestinationNames'],
persist: true,
storeName: 'transfer-to-laying-table',
});
const {
data: transferToLayings,
isLoading,
mutate: refreshTransferToLayings,
} = useSWR(
`${TransferToLayingApi.basePath}${getTableFilterQueryString()}`,
TransferToLayingApi.getAllFetcher
);
const [isLoadingExportingToExcel, setIsLoadingExportingToExcel] =
useState(false);
// Modal hooks
const filterModal = useModal();
const deleteModal = useModal();
const approveModal = useModal();
const rejectModal = useModal();
const [selectedTransferToLaying, setSelectedTransferToLaying] = useState<
TransferToLaying | undefined
>(undefined);
// Modal loading state
const [isDeleteLoading, setIsDeleteLoading] = useState(false);
const [isApproveLoading, setIsApproveLoading] = useState(false);
const [isRejectLoading, setIsRejectLoading] = useState(false);
const [sorting, setSorting] = useState<SortingState>([]);
const [rowSelection, setRowSelection] = useState<Record<string, boolean>>({});
const selectedRowIds = Object.keys(rowSelection).map((item) =>
parseInt(item)
);
const transferToLayingsColumns: ColumnDef<TransferToLaying>[] = [
{
id: 'select',
header: ({ table }) => (
<div className='w-full flex flex-row justify-center'>
<CheckboxInput
name='allRow'
checked={table.getIsAllRowsSelected()}
indeterminate={table.getIsSomeRowsSelected()}
onChange={table.getToggleAllRowsSelectedHandler()}
/>
</div>
),
cell: ({ row }) => {
const isCheckboxDisabled =
!row.getCanSelect() ||
row.original.approval.action === 'APPROVED' ||
row.original.approval.action === 'REJECTED';
return (
<div>
<CheckboxInput
name='row'
checked={row.getIsSelected()}
disabled={isCheckboxDisabled}
indeterminate={row.getIsSomeSelected()}
onChange={row.getToggleSelectedHandler()}
/>
</div>
);
},
},
{
accessorKey: 'transfer_date',
header: 'Tanggal Transfer',
cell: (props) => formatDate(props.getValue() as string, 'DD MMM YYYY'),
},
{
accessorKey: 'transfer_number',
header: 'No. Transfer',
},
{
accessorKey: 'flock_source',
header: 'Flock Asal',
cell: (props) => props.row.original.from_project_flock.flock_name,
},
{
accessorKey: 'flock_destination',
header: 'Flock Tujuan',
cell: (props) => props.row.original.to_project_flock.flock_name,
},
{
accessorKey: 'usage_qty',
header: 'Kuantitas',
cell: (props) => {
const totalQuantity = props.row.original.targets.reduce(
(total, target) => total + target.qty,
0
);
return formatNumber(totalQuantity, 'en-US');
},
},
{
accessorKey: 'notes',
header: 'Alasan Transfer',
enableSorting: false,
cell: (props) => {
return (
<span title={props.row.original.notes} className='line-clamp-1'>
{props.row.original.notes}
</span>
);
},
},
{
header: 'Status',
cell: (props) => {
const isLatestApprovalRejected =
props.row.original.approval.action === 'REJECTED';
let latestApprovalStepName = props.row.original.approval.step_name;
let badgeColor: Color = 'neutral';
switch (latestApprovalStepName.toLowerCase()) {
case 'pengajuan':
badgeColor = 'neutral';
break;
case 'disetujui':
badgeColor = 'success';
break;
}
if (isLatestApprovalRejected) {
badgeColor = 'error';
latestApprovalStepName = 'Ditolak';
}
return <StatusBadge color={badgeColor} text={latestApprovalStepName} />;
},
},
{
id: 'actions',
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 - 2;
const deleteClickHandler = () => {
setSelectedTransferToLaying(props.row.original);
// Set row selection
setRowSelection({
[String(props.row.original.id)]: true,
});
deleteModal.openModal();
};
return (
<RowOptionsMenu
props={props}
deleteClickHandler={deleteClickHandler}
popoverPosition={isLast2Rows ? 'top' : 'bottom'}
/>
);
},
},
];
const tableEnableRowSelectionHandler: (
row: Row<TransferToLaying>
) => boolean = (row) => {
return (
row.original.approval.action !== 'APPROVED' &&
row.original.approval.action !== 'REJECTED'
);
};
const bulkApproveClickHandler = () => {
approveModal.openModal();
};
const bulkRejectClickHandler = () => {
rejectModal.openModal();
};
// Modal confirm click handler
const confirmationModalDeleteClickHandler = async () => {
setIsDeleteLoading(true);
const deleteResponse = await TransferToLayingApi.delete(
selectedTransferToLaying?.id as number
);
if (isResponseError(deleteResponse)) {
toast.error(deleteResponse.message);
setIsDeleteLoading(false);
return;
}
refreshTransferToLayings();
setRowSelection({});
setSelectedTransferToLaying(undefined);
deleteModal.closeModal();
toast.success('Berhasil menghapus data transfer ke laying!');
setIsDeleteLoading(false);
};
const confirmationModalApproveClickHandler = async (notes: string) => {
setIsApproveLoading(true);
const bulkApproveResponse = await TransferToLayingApi.bulkApprove(
selectedRowIds,
notes
);
if (isResponseSuccess(bulkApproveResponse)) {
refreshTransferToLayings();
approveModal.closeModal();
toast.success(
`Berhasil approve ${selectedRowIds.length} data transfer ke laying!`
);
setRowSelection({});
} else {
approveModal.closeModal();
toast.error(
`Gagal approve ${selectedRowIds.length} data transfer ke laying!`
);
}
setIsApproveLoading(false);
};
const confirmationModalRejectClickHandler = async (notes: string) => {
setIsRejectLoading(true);
const bulkRejectResponse = await TransferToLayingApi.bulkReject(
selectedRowIds,
notes
);
if (isResponseSuccess(bulkRejectResponse)) {
refreshTransferToLayings();
rejectModal.closeModal();
toast.success(
`Berhasil reject ${selectedRowIds.length} data transfer ke laying!`
);
setRowSelection({});
} else {
rejectModal.closeModal();
toast.error(
`Gagal reject ${selectedRowIds.length} data transfer ke laying!`
);
}
setIsRejectLoading(false);
};
useEffect(() => {
updateFilter('search', searchValue);
}, [searchValue, updateFilter]);
useEffect(() => {
setTableState('transfer-to-laying-table', pathname);
}, [pathname, setTableState]);
const searchChangeHandler: ChangeEventHandler<HTMLInputElement> = (e) => {
setSearchValue(e.target.value);
updateFilter('search', e.target.value);
};
const STATUS_FILTER_OPTIONS = [
{ value: 'PENDING', label: 'Pengajuan' },
{ value: 'APPROVED', label: 'Disetujui' },
{ value: 'REJECTED', label: 'Ditolak' },
];
const filterModalInitialValues = useMemo(() => {
const flockSourceIds = tableFilterState.flockSource
? tableFilterState.flockSource.split(',')
: [];
const flockSourceNameList = tableFilterState.flockSourceNames
? tableFilterState.flockSourceNames.split(',')
: [];
const flockSourceOptions = flockSourceIds.filter(Boolean).map((id, i) => ({
value: parseInt(id),
label: flockSourceNameList[i] || id,
}));
const flockDestIds = tableFilterState.flockDestination
? tableFilterState.flockDestination.split(',')
: [];
const flockDestNameList = tableFilterState.flockDestinationNames
? tableFilterState.flockDestinationNames.split(',')
: [];
const flockDestOptions = flockDestIds.filter(Boolean).map((id, i) => ({
value: parseInt(id),
label: flockDestNameList[i] || id,
}));
const statusIds = tableFilterState.status
? tableFilterState.status.split(',')
: [];
const statusOptions = statusIds.filter(Boolean).map((id) => {
const found = STATUS_FILTER_OPTIONS.find((opt) => opt.value === id);
return found || { value: id, label: id };
});
return {
startDate: tableFilterState.startDate || '',
endDate: tableFilterState.endDate || '',
flockSource: flockSourceOptions,
flockDestination: flockDestOptions,
status: statusOptions,
};
}, [
tableFilterState.startDate,
tableFilterState.endDate,
tableFilterState.flockSource,
tableFilterState.flockDestination,
tableFilterState.status,
tableFilterState.flockSourceNames,
tableFilterState.flockDestinationNames,
]);
const filterSubmitHandler = (values: TransferToLayingFilterValues) => {
const flockSourceOpts = (values.flockSource as OptionType[]) || [];
const flockDestOpts = (values.flockDestination as OptionType[]) || [];
const statusOpts = (values.status as OptionType[]) || [];
updateFilter('startDate', values.startDate || '');
updateFilter('endDate', values.endDate || '');
updateFilter(
'flockSource',
flockSourceOpts.map((o) => String(o.value)).join(',')
);
updateFilter(
'flockDestination',
flockDestOpts.map((o) => String(o.value)).join(',')
);
updateFilter('status', statusOpts.map((o) => String(o.value)).join(','));
updateFilter(
'flockSourceNames',
flockSourceOpts.map((o) => String(o.label)).join(',')
);
updateFilter(
'flockDestinationNames',
flockDestOpts.map((o) => String(o.label)).join(',')
);
};
const filterResetHandler = () => {
updateFilter('startDate', '');
updateFilter('endDate', '');
updateFilter('flockSource', '');
updateFilter('flockDestination', '');
updateFilter('status', '');
updateFilter('flockSourceNames', '');
updateFilter('flockDestinationNames', '');
};
const exportToExcelHandler = async () => {
setIsLoadingExportingToExcel(true);
await TransferToLayingApi.exportToExcel(getTableFilterQueryString());
setIsLoadingExportingToExcel(false);
};
useEffect(() => {
if (sorting.length === 1) {
updateFilter('filter_by', sorting[0].id);
updateFilter('sort_by', sorting[0].desc ? 'desc' : 'asc');
} else {
updateFilter('filter_by', '');
updateFilter('sort_by', '');
}
}, [sorting]);
return (
<>
<div className='@container w-full'>
<div className='w-full p-3 flex flex-row justify-between gap-3 flex-wrap border-b border-base-content/10'>
<div className='w-fit flex flex-row gap-3 flex-wrap'>
<RequirePermission permissions='lti.production.transfer_to_laying.create'>
<Button
href={{
pathname: '/production/transfer-to-laying',
query: {
action: 'add',
},
}}
color='primary'
className='px-3 py-2.5 w-fit text-sm text-base-100 rounded-lg shadow-sm'
>
<Icon icon='heroicons:plus' width={20} height={20} />
Add Transfer to Laying
</Button>
</RequirePermission>
{selectedRowIds.length > 0 && (
<>
<hr className='w-px h-full border-none bg-base-content/10 hidden @sm:block' />
<RequirePermission permissions='lti.production.transfer_to_laying.approve'>
<Button
variant='outline'
color='none'
onClick={bulkRejectClickHandler}
disabled={selectedRowIds.length === 0}
className='px-3 py-2.5 gap-1.5 text-sm text-base-content/50 border border-base-content/10 rounded-xl shadow-button-soft'
>
<Icon
icon='heroicons:x-mark'
width={20}
height={20}
className='text-error'
/>
Reject
</Button>
</RequirePermission>
<RequirePermission permissions='lti.production.transfer_to_laying.approve'>
<Button
variant='outline'
color='none'
onClick={bulkApproveClickHandler}
disabled={selectedRowIds.length === 0}
className='px-3 py-2.5 gap-1.5 text-sm text-base-content/50 border border-base-content/10 rounded-xl shadow-button-soft'
>
<Icon
icon='heroicons:check'
width={20}
height={20}
className='text-success'
/>
Approve
</Button>
</RequirePermission>
</>
)}
</div>
<div className='flex flex-1 flex-row justify-start sm:justify-end items-center gap-3 flex-wrap'>
<DebouncedTextInput
name='search'
placeholder='Search'
value={tableFilterState.search ?? ''}
onChange={searchChangeHandler}
startAdornment={
<Icon
icon='heroicons:magnifying-glass'
width={20}
height={20}
/>
}
className={{
wrapper: 'w-full min-w-24 max-w-3xs',
inputWrapper: 'rounded-xl! shadow-button-soft',
input:
'placeholder:font-semibold placeholder:text-base-content/50',
}}
/>
<ButtonFilter
values={tableFilterState}
excludeFields={[
'page',
'pageSize',
'search',
'filter_by',
'sort_by',
'flockSourceNames',
'flockDestinationNames',
]}
fieldGroups={[['startDate', 'endDate']]}
onClick={filterModal.openModal}
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'
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={exportToExcelHandler}
isLoading={isLoadingExportingToExcel}
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>
</Dropdown>
</div>
</div>
<div className='flex flex-col mb-4'>
{isLoading ? (
<div className='w-full flex flex-row justify-center items-center p-4'>
<span className='loading loading-spinner loading-xl' />
</div>
) : !isResponseSuccess(transferToLayings) ||
transferToLayings.data?.length === 0 ? (
<div className='p-3'>
<TransferToLayingTableSkeleton
columns={transferToLayingsColumns}
icon={
<Icon
icon='heroicons:document-text'
className='text-white'
width={20}
height={20}
/>
}
/>
</div>
) : (
<Table<TransferToLaying>
data={
isResponseSuccess(transferToLayings)
? transferToLayings?.data
: []
}
columns={transferToLayingsColumns}
pageSize={tableFilterState.pageSize}
page={
isResponseSuccess(transferToLayings)
? transferToLayings?.meta?.page
: 0
}
totalItems={
isResponseSuccess(transferToLayings)
? transferToLayings?.meta?.total_results
: 0
}
onPageChange={setPage}
onPageSizeChange={setPageSize}
isLoading={isLoading}
sorting={sorting}
setSorting={setSorting}
rowSelection={rowSelection}
setRowSelection={setRowSelection}
enableRowSelection={tableEnableRowSelectionHandler}
withCheckbox
className={{
containerClassName: cn('p-3 mb-0'),
headerColumnClassName: 'text-nowrap',
}}
/>
)}
</div>
</div>
<TransferToLayingFilterModal
ref={filterModal.ref}
initialValues={filterModalInitialValues}
onSubmit={filterSubmitHandler}
onReset={filterResetHandler}
/>
<TransferToLayingConfirmationModal
ref={deleteModal.ref}
type='error'
text='Delete This Data?'
subtitleText='Are you sure you want to delete this data? '
transferToLayingIds={selectedRowIds}
primaryButton={{
text: 'Delete',
isLoading: isDeleteLoading,
color: 'error',
onClick: confirmationModalDeleteClickHandler,
}}
secondaryButton={{
text: 'Cancel',
color: 'none',
onClick: () => {
setRowSelection({});
deleteModal.closeModal();
},
}}
/>
{/* Approve Modal */}
<TransferToLayingConfirmationModal
ref={approveModal.ref}
text='Approve This Submission?'
subtitleText='Are you sure you want to approve this submission?'
type='success'
transferToLayingIds={selectedRowIds}
withNote
noteLabel='Notes Approval'
primaryButton={{
text: 'Approve',
isLoading: isApproveLoading,
onClick: confirmationModalApproveClickHandler,
}}
secondaryButton={{
text: 'Cancel',
color: 'none',
onClick: () => {
setRowSelection({});
approveModal.closeModal();
},
}}
/>
{/* Reject Modal */}
<TransferToLayingConfirmationModal
ref={rejectModal.ref}
type='error'
text='Reject This Submission?'
subtitleText='Are you sure you want to reject this submission?'
transferToLayingIds={selectedRowIds}
withNote
noteLabel='Notes Reject'
secondaryButton={{
text: 'Cancel',
color: 'none',
onClick: () => {
setRowSelection({});
rejectModal.closeModal();
},
}}
primaryButton={{
text: 'Reject',
isLoading: isRejectLoading,
color: 'error',
onClick: confirmationModalRejectClickHandler,
}}
/>
</>
);
};
export default TransferToLayingsTable;