Files
lti-web-client/src/components/pages/production/transfer-to-laying/TransferToLayingsTable.tsx
T

727 lines
22 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 { useEffect, useMemo, useState } from 'react';
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 ConfirmationModal from '@/components/modal/ConfirmationModal';
import CheckboxInput from '@/components/input/CheckboxInput';
import ConfirmationModalWithNotes from '@/components/modal/ConfirmationModalWithNotes';
import RequirePermission from '@/components/helper/RequirePermission';
import PopoverButton from '@/components/popover/PopoverButton';
import Badge from '@/components/Badge';
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 {
TransferToLaying,
TransferToLayingFilter,
} from '@/types/api/production/transfer-to-laying';
import { TransferToLayingApi } from '@/services/api/production/transfer-to-laying';
import { cn, formatDate } from '@/lib/helper';
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
import { useTableFilter } from '@/services/hooks/useTableFilter';
import { Color } from '@/types/theme';
const RowOptionsMenu = ({
props,
popoverPosition = 'bottom',
approveClickHandler,
rejectClickHandler,
deleteClickHandler,
}: {
props: CellContext<TransferToLaying, unknown>;
popoverPosition: 'bottom' | 'top';
approveClickHandler: () => void;
rejectClickHandler: () => void;
deleteClickHandler: () => void;
}) => {
const showEditButton =
props.row.original.approval.action !== 'APPROVED' &&
props.row.original.approval.action !== 'REJECTED';
const showDeleteButton = showEditButton;
// const showApproveButton = showEditButton;
// const showRejectButton = showEditButton;
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/detail/?transferToLayingId=${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/detail/edit/?transferToLayingId=${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>
)}
{/* {showApproveButton && (
<RequirePermission permissions='lti.production.transfer_to_laying.approve'>
<Button
variant='ghost'
color='success'
onClick={approveClickHandler}
className='p-3 justify-start text-sm font-semibold w-full'
>
<Icon icon='heroicons:check' width={20} height={20} />
Approve
</Button>
</RequirePermission>
)}
{showRejectButton && (
<RequirePermission permissions='lti.production.transfer_to_laying.approve'>
<Button
variant='ghost'
color='error'
onClick={rejectClickHandler}
className='p-3 justify-start text-sm font-semibold w-full'
>
<Icon icon='heroicons:x-mark' width={20} height={20} />
Reject
</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 {
state: tableFilterState,
updateFilter,
setPage,
setPageSize,
toQueryString: getTableFilterQueryString,
} = useTableFilter({
initial: {
search: '',
startDate: '',
endDate: '',
flockSource: '',
flockDestination: '',
status: '',
filter_by: '',
sort_by: '',
},
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',
},
});
const {
data: transferToLayings,
isLoading,
mutate: refreshTransferToLayings,
} = useSWR(
`${TransferToLayingApi.basePath}${getTableFilterQueryString()}`,
TransferToLayingApi.getAllFetcher
);
const filterCount = useMemo(() => {
let count = 0;
if (tableFilterState.startDate && tableFilterState.endDate) {
count += 1;
}
if (tableFilterState.flockSource.length > 0) {
count += 1;
}
if (tableFilterState.flockDestination.length > 0) {
count += 1;
}
if (tableFilterState.status.length > 0) {
count += 1;
}
return count;
}, [tableFilterState]);
const isFilterActive = filterCount > 0;
// 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: '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) => props.getValue() ?? props.row.original.pending_usage_qty,
},
{
accessorKey: 'notes',
header: 'Alasan Transfer',
enableSorting: false,
},
{
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 approveClickHandler = () => {
setSelectedTransferToLaying(props.row.original);
// Set row selection
setRowSelection({
[String(props.row.original.id)]: true,
});
approveModal.openModal();
};
const rejectClickHandler = () => {
setSelectedTransferToLaying(props.row.original);
// Set row selection
setRowSelection({
[String(props.row.original.id)]: true,
});
rejectModal.openModal();
};
const deleteClickHandler = () => {
setSelectedTransferToLaying(props.row.original);
deleteModal.openModal();
};
return (
<RowOptionsMenu
props={props}
approveClickHandler={approveClickHandler}
rejectClickHandler={rejectClickHandler}
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();
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);
};
const filterSubmitHandler = (values: TransferToLayingFilter) => {
updateFilter('startDate', values.startDate);
updateFilter('endDate', values.endDate);
updateFilter('flockSource', values.flockSource.join(','));
updateFilter('flockDestination', values.flockDestination.join(','));
updateFilter('status', values.status.join(','));
};
const filterResetHandler = () => {
updateFilter('startDate', '');
updateFilter('endDate', '');
updateFilter('flockSource', '');
updateFilter('flockDestination', '');
updateFilter('status', '');
};
// TODO: add export to excel functionality
const exportToExcelHandler = () => {
toast.error('Not implemented yet');
};
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='/production/transfer-to-laying/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-row justify-center items-center gap-3'>
<Button
variant='outline'
color='none'
onClick={filterModal.openModal}
className={cn(
'px-3 py-2.5 gap-1.5 text-sm text-base-content/50 border border-base-content/10 rounded-xl shadow-button-soft transition-all duration-1000',
{
'border-primary-gradient text-primary': isFilterActive,
}
)}
>
<Icon icon='heroicons:funnel' width={20} height={20} />
Filter
{isFilterActive && (
<Badge
className={{
badge:
'p-1.5 bg-[#FF3535] text-xs text-base-100 border border-base-300 rounded-lg',
}}
>
{filterCount}
</Badge>
)}
</Button>
<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}
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>
<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', {
'w-full mb-20':
isResponseSuccess(transferToLayings) &&
transferToLayings?.data?.length === 0,
}),
headerColumnClassName: 'text-nowrap',
}}
/>
</div>
<TransferToLayingFilterModal
ref={filterModal.ref}
onSubmit={filterSubmitHandler}
onReset={filterResetHandler}
/>
<ConfirmationModal
ref={deleteModal.ref}
iconPosition='left'
type='error'
text='Delete This Data?'
subtitleText='Are you sure you want to delete this data? '
secondaryButton={{
text: 'Cancel',
}}
primaryButton={{
text: 'Delete',
color: 'error',
isLoading: isDeleteLoading,
onClick: confirmationModalDeleteClickHandler,
}}
/>
<ConfirmationModalWithNotes
ref={approveModal.ref}
type='success'
iconPosition='left'
text='Approve This Submission?'
subtitleText='Are you sure you want to approve this submission?'
secondaryButton={{
text: 'Cancel',
}}
primaryButton={{
text: 'Approve',
color: 'success',
isLoading: isApproveLoading,
onClick: confirmationModalApproveClickHandler,
}}
/>
<ConfirmationModalWithNotes
ref={rejectModal.ref}
type='error'
iconPosition='left'
text='Reject This Submission?'
subtitleText='Are you sure you want to reject this submission?'
secondaryButton={{
text: 'Cancel',
}}
primaryButton={{
text: 'Reject',
color: 'error',
isLoading: isRejectLoading,
onClick: confirmationModalRejectClickHandler,
}}
/>
</>
);
};
export default TransferToLayingsTable;