Files
lti-web-client/src/components/pages/production/project-flock/ProjectFlockTable.tsx
T

615 lines
21 KiB
TypeScript

'use client';
import Button from '@/components/Button';
import CheckboxInput from '@/components/input/CheckboxInput';
import DebouncedTextInput from '@/components/input/DebouncedTextInput';
import SelectInput, { OptionType } from '@/components/input/SelectInput';
import { useModal } from '@/components/Modal';
import ConfirmationModal from '@/components/modal/ConfirmationModal';
import ConfirmationModalWithNotes from '@/components/modal/ConfirmationModalWithNotes';
import Table from '@/components/Table';
import RowCollapseOptions from '@/components/table/RowCollapseOptions';
import RowDropdownOptions from '@/components/table/RowDropdownOptions';
import { ROWS_OPTIONS } from '@/config/constant';
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
import { cn } from '@/lib/helper';
import { AreaApi, KandangApi, LocationApi } from '@/services/api/master-data';
import { ProjectFlockApi } from '@/services/api/production/project-flock';
import { useTableFilter } from '@/services/hooks/useTableFilter';
import { BaseApiResponse } from '@/types/api/api-general';
import { Kandang } from '@/types/api/master-data/kandang';
import {
ProjectFlockApprovalPayload,
ProjectFlock,
} from '@/types/api/production/project-flock';
import { Icon } from '@iconify/react';
import { CellContext, SortingState } from '@tanstack/react-table';
import { ChangeEventHandler, useState } from 'react';
import toast from 'react-hot-toast';
import useSWR from 'swr';
const RowOptionsMenu = ({
type = 'dropdown',
props,
deleteClickHandler,
}: {
type: 'dropdown' | 'collapse';
props: CellContext<ProjectFlock, unknown>;
deleteClickHandler: () => void;
}) => {
return (
<div
tabIndex={type == 'dropdown' ? 0 : undefined}
className={cn(
{
'dropdown-content': type === 'dropdown',
'mt-2': type === 'collapse',
},
'p-2.5 mr-2 bg-base-100 rounded-box z-10 border border-black/10 shadow'
)}
>
<div className='flex flex-col gap-1'>
<Button
href={`/production/project-flock/detail?projectFlockId=${props.row.original.id}`}
variant='ghost'
color='primary'
className='justify-start text-sm'
>
<Icon icon='mdi:eye-outline' width={16} height={16} />
Detail
</Button>
{props.row.original.approval.step_name === 'Aktif' && (
<Button
href={`/production/project-flock/chickin/add?projectFlockId=${props.row.original.id}`}
variant='ghost'
color='success'
className='justify-start text-sm'
>
<Icon icon='mdi:home-import-outline' width={16} height={16} />
Chickin
</Button>
)}
{props.row.original.approval.step_name === 'Pengajuan' && (
<Button
href={`/production/project-flock/detail/edit?projectFlockId=${props.row.original.id}`}
variant='ghost'
color='warning'
className='justify-start text-sm'
>
<Icon icon='mdi:pencil-outline' width={16} height={16} />
Edit
</Button>
)}
<Button
onClick={deleteClickHandler}
variant='ghost'
color='error'
className='text-error hover:text-inherit justify-start text-sm'
>
<Icon
icon='material-symbols:delete-outline-rounded'
width={16}
height={16}
/>
Delete
</Button>
</div>
</div>
);
};
const ProjectFlockTable = () => {
const {
state: tableFilterState,
updateFilter,
setPage,
setPageSize,
toQueryString: getTableFilterQueryString,
} = useTableFilter({
initial: {
search: '',
areaFilter: '',
locationFilter: '',
kandangFilter: '',
periodFilter: '',
},
paramMap: {
page: 'page',
pageSize: 'limit',
search: 'search',
areaFilter: 'area_id',
locationFilter: 'location_id',
kandangFilter: 'kandang_id',
periodFilter: 'period',
},
});
// State
const [rowSelection, setRowSelection] = useState<Record<string, boolean>>({});
const selectedRowIds = Object.keys(rowSelection)
.filter((id) => rowSelection[id])
.map((id) => parseInt(id));
const [locationSelectInputValue, setLocationSelectInputValue] = useState('');
const [areaSelectInputValue, setAreaSelectInputValue] = useState('');
const [kandangSelectInputValue, setKandangSelectInputValue] = useState('');
const [selectedArea, setSelectedArea] = useState<OptionType | null>(null);
const [selectedLocation, setSelectedLocation] = useState<OptionType | null>(
null
);
const [selectedKandang, setSelectedKandang] = useState<OptionType | null>(
null
);
const [periodInputValue, setPeriodInputValue] = useState<number | null>(null);
const [sorting, setSorting] = useState<SortingState>([]);
const [selectedProjectFlock, setSelectedProjectFlock] =
useState<ProjectFlock>();
const deleteModal = useModal();
const confirmModal = useModal();
const [approvalAction, setApprovalAction] = useState<'APPROVED' | 'REJECTED'>(
'APPROVED'
);
const [isDeleteLoading, setIsDeleteLoading] = useState(false);
const [isApproveLoading, setIsApproveLoading] = useState(false);
// Fetch Data
const {
data: projectFlocks,
isLoading,
mutate: refreshProjectFlocks,
} = useSWR(
`${ProjectFlockApi.basePath}${getTableFilterQueryString()}`,
ProjectFlockApi.getAllFetcher
);
const areaUrl = `${AreaApi.basePath}?${new URLSearchParams({
search: areaSelectInputValue,
limit: '100',
}).toString()}`;
const { data: areas, isLoading: isLoadingAreas } = useSWR(
areaUrl,
AreaApi.getAllFetcher
);
const locationUrl = `${LocationApi.basePath}?${new URLSearchParams({
search: locationSelectInputValue,
area_id: selectedArea != null ? selectedArea.value.toString() : '',
limit: '100',
}).toString()}`;
const { data: locations, isLoading: isLoadingLocations } = useSWR(
locationUrl,
LocationApi.getAllFetcher
);
const kandangUrl = `${KandangApi.basePath}?${new URLSearchParams({
search: kandangSelectInputValue,
location_id:
selectedLocation != null ? selectedLocation.value.toString() : '',
limit: '100',
}).toString()}`;
const { data: kandangs, isLoading: isLoadingKandang } = useSWR(
kandangUrl,
KandangApi.getAllFetcher
);
// Data to Options Mapping
const optionsArea = isResponseSuccess(areas)
? areas?.data.map((area) => ({
value: area.id,
label: area.name,
}))
: [];
const optionsKandang = isResponseSuccess(kandangs)
? kandangs?.data.map((kandang) => ({
value: kandang.id,
label: kandang.name,
}))
: [];
const optionsLocation = isResponseSuccess(locations)
? locations?.data.map((location) => ({
value: location.id,
label: location.name,
}))
: [];
// Handler
const pageSizeChangeHandler = (val: OptionType | OptionType[] | null) => {
const newVal = val as OptionType;
setPageSize(newVal.value as number);
};
const confirmationModalDeleteClickHandler = async () => {
setIsDeleteLoading(true);
await ProjectFlockApi.delete(selectedProjectFlock?.id as number);
refreshProjectFlocks();
deleteModal.closeModal();
toast.success('Successfully delete Project Flock!');
setIsDeleteLoading(false);
};
const searchChangeHandler: ChangeEventHandler<HTMLInputElement> = (e) => {
updateFilter('search', e.target.value);
};
const confirmApprovalHandler = async (
notes: string,
approvalAction: 'APPROVED' | 'REJECTED'
) => {
setIsApproveLoading(true);
const approveProjectFlockRes =
approvalAction === 'APPROVED'
? await ProjectFlockApi.bulkApprove(
selectedRowIds.map((id) => id),
notes
)
: await ProjectFlockApi.bulkReject(
selectedRowIds.map((id) => id),
notes
);
if (isResponseSuccess(approveProjectFlockRes)) {
toast.success('Project Flock berhasil di-approve!');
confirmModal.closeModal();
}
if (isResponseError(approveProjectFlockRes)) {
toast.error(approveProjectFlockRes?.message as string);
confirmModal.closeModal();
}
setRowSelection({});
refreshProjectFlocks();
setIsApproveLoading(false);
};
return (
<>
<div className='w-full p-0 sm:p-4'>
<div className='flex flex-col gap-2 mb-4'>
<div className='w-full flex flex-col justify-between items-end gap-2'>
<div className='flex flex-col sm:flex-row gap-3 w-full'>
<Button
href='/production/project-flock/add'
variant='outline'
color='primary'
className='w-full sm:w-fit'
>
<Icon icon='ic:round-plus' width={24} height={24} />
Tambah
</Button>
<Button
variant='outline'
color='success'
onClick={() => {
setApprovalAction('APPROVED');
confirmModal.openModal();
}}
disabled={selectedRowIds.length === 0}
className='w-full sm:w-fit'
>
<Icon icon='material-symbols:check' width={24} height={24} />
Approve
</Button>
<Button
variant='outline'
color='error'
onClick={() => {
setApprovalAction('REJECTED');
confirmModal.openModal();
}}
disabled={selectedRowIds.length === 0}
className='w-full sm:w-fit'
>
<Icon icon='mdi:times' width={24} height={24} />
Reject
</Button>
<div className='ms-auto w-full sm:w-auto'>
<DebouncedTextInput
name='search'
placeholder='Cari Area'
value={tableFilterState.search}
onChange={searchChangeHandler}
className={{
wrapper: 'w-full sm:max-w-3xs',
input: 'w-full',
inputWrapper: 'w-full',
}}
/>
</div>
</div>
<div className='hidden sm:flex flex-row justify-end gap-6 w-full'>
<SelectInput
label='Area'
options={optionsArea}
isLoading={isLoadingAreas}
value={selectedArea}
onChange={(val) => {
setSelectedArea(val as OptionType);
updateFilter(
'areaFilter',
(val as OptionType)?.value.toString()
);
}}
onInputChange={setAreaSelectInputValue}
isClearable
/>
<SelectInput
label='Lokasi'
options={optionsLocation}
isLoading={isLoadingLocations}
value={selectedLocation}
onChange={(val) => {
setSelectedLocation(val as OptionType);
updateFilter(
'locationFilter',
(val as OptionType)?.value.toString()
);
}}
onInputChange={setLocationSelectInputValue}
isClearable
/>
<SelectInput
label='Kandang'
options={optionsKandang}
isLoading={isLoadingKandang}
value={selectedKandang}
onChange={(val) => {
setSelectedKandang(val as OptionType);
updateFilter(
'kandangFilter',
(val as OptionType)?.value.toString()
);
}}
onInputChange={setKandangSelectInputValue}
isClearable
/>
<DebouncedTextInput
name='period'
type='number'
label='Periode'
placeholder='Masukan periode'
value={periodInputValue ?? ''}
onChange={(e) => {
setPeriodInputValue(parseInt(e.target.value));
updateFilter('periodFilter', e.target.value);
}}
/>
<SelectInput
label='Baris'
options={ROWS_OPTIONS}
value={{
label: String(tableFilterState.pageSize),
value: tableFilterState.pageSize,
}}
onChange={pageSizeChangeHandler}
className={{ wrapper: 'max-w-28' }}
/>
</div>
</div>
<Table<ProjectFlock>
data={isResponseSuccess(projectFlocks) ? projectFlocks?.data : []}
columns={[
{
id: 'select',
header: ({ table }) => {
const allRows = table.getRowModel().rows;
const selectableRows = allRows.filter(
(row) => row.original?.approval?.step_number == 1
);
const allSelected =
selectableRows.every((row) => row.getIsSelected()) &&
selectableRows.length != 0;
const someSelected =
selectableRows.some((row) => row.getIsSelected()) &&
!allSelected;
const toggleSelectableRows = () => {
const shouldSelect = !allSelected;
selectableRows.forEach((row) =>
row.toggleSelected(shouldSelect)
);
};
return (
<div className='w-full flex flex-row justify-center'>
<CheckboxInput
name='allRow'
checked={allSelected}
indeterminate={someSelected}
onChange={toggleSelectableRows}
disabled={
isResponseSuccess(projectFlocks) &&
projectFlocks?.data?.filter(
(flock) => flock.approval.step_number == 1
).length == 0
}
/>
</div>
);
},
cell: ({ row }) => {
return (
<CheckboxInput
name='row'
checked={
row.getIsSelected() &&
row.original.approval.step_number == 1
}
disabled={
!row.getCanSelect() ||
row.original.approval.step_number != 1
}
indeterminate={row.getIsSomeSelected()}
onChange={row.getToggleSelectedHandler()}
/>
);
},
},
{
accessorKey: 'flock_name',
header: 'Flock',
},
{
accessorKey: 'area.name',
header: 'Area',
},
{
accessorKey: 'location.name',
header: 'Lokasi',
},
{
accessorKey: 'fcr.name',
header: 'FCR',
},
{
accessorKey: 'category',
header: 'Kategori',
},
{
accessorKey: 'approval.step_name',
header: 'Status',
},
{
header: 'Kandang',
cell: (props) => {
const kandang = props.row.original.kandangs;
if (kandang) {
const kandangNames = kandang.map((k: Kandang) => k.name);
return (
<div>
{kandangNames.length > 0
? kandangNames.join(', ')
: 'Tidak ada'}
</div>
);
} else {
return '-';
}
},
},
{
accessorKey: 'period',
header: 'Periode',
},
{
accessorKey: 'created_at',
header: 'Dibuat pada',
cell: (props) =>
new Date(props.row.original.created_at).toLocaleDateString(),
},
{
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 - 2;
const deleteClickHandler = () => {
setSelectedProjectFlock(props.row.original);
deleteModal.openModal();
};
return (
<>
{currentPageSize > 2 && (
<RowDropdownOptions isLast2Rows={isLast2Rows}>
<RowOptionsMenu
type='dropdown'
props={props}
deleteClickHandler={deleteClickHandler}
/>
</RowDropdownOptions>
)}
{currentPageSize <= 2 && (
<RowCollapseOptions>
<RowOptionsMenu
type='collapse'
props={props}
deleteClickHandler={deleteClickHandler}
/>
</RowCollapseOptions>
)}
</>
);
},
},
]}
pageSize={tableFilterState.pageSize}
page={
isResponseSuccess(projectFlocks) ? projectFlocks?.meta?.page : 0
}
totalItems={
isResponseSuccess(projectFlocks)
? projectFlocks?.meta?.total_results
: 0
}
onPageChange={setPage}
isLoading={isLoading}
sorting={sorting}
setSorting={setSorting}
rowSelection={rowSelection}
setRowSelection={setRowSelection}
className={{
containerClassName: cn({
'mb-20':
isResponseSuccess(projectFlocks) &&
projectFlocks?.data?.length === 0,
}),
tableWrapperClassName: 'overflow-x-auto min-h-full!',
tableClassName: 'font-inter w-full table-auto min-h-full!',
headerRowClassName: 'border-b border-b-gray-200',
headerColumnClassName:
'px-6 py-3 text-xs font-semibold text-gray-500 last:flex last:flex-row last:justify-end',
bodyRowClassName: 'border-b border-b-gray-200',
bodyColumnClassName:
'px-6 py-3 last:flex last:flex-row last:justify-end',
}}
/>
</div>
</div>
<ConfirmationModal
ref={deleteModal.ref}
type='error'
text={`Apakah anda yakin ingin menghapus data Project Flock ini (${selectedProjectFlock?.flock_name})?`}
secondaryButton={{
text: 'Tidak',
}}
primaryButton={{
text: 'Ya',
color: 'error',
isLoading: isDeleteLoading,
onClick: confirmationModalDeleteClickHandler,
}}
/>
<ConfirmationModalWithNotes
ref={confirmModal.ref}
type={approvalAction == 'APPROVED' ? 'success' : 'error'}
text={`Apakah anda yakin ingin ${approvalAction == 'APPROVED' ? 'approve' : 'reject'} data Project Flock ini (${selectedRowIds.length} data)?`}
secondaryButton={{
text: 'Tidak',
}}
primaryButton={{
text: 'Ya',
color: approvalAction == 'APPROVED' ? 'success' : 'error',
onClick: (notes) => {
confirmApprovalHandler(notes, approvalAction);
},
isLoading: isApproveLoading,
}}
/>
</>
);
};
export default ProjectFlockTable;