mirror of
https://gitlab.com/mbugroup/lti-web-client.git
synced 2026-05-20 13:32:00 +00:00
664 lines
22 KiB
TypeScript
664 lines
22 KiB
TypeScript
'use client';
|
|
|
|
import Badge from '@/components/Badge';
|
|
import Button from '@/components/Button';
|
|
import FloatingActionsButton from '@/components/FloatingActionsButton';
|
|
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 { ROWS_OPTIONS } from '@/config/constant';
|
|
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
|
|
import { cn, formatDate } 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 { Kandang } from '@/types/api/master-data/kandang';
|
|
import { ProjectFlock } from '@/types/api/production/project-flock';
|
|
import { Icon } from '@iconify/react';
|
|
import { CellContext, SortingState } from '@tanstack/react-table';
|
|
import { useRouter } from 'next/navigation';
|
|
import { ChangeEventHandler, useEffect, useMemo, useState } from 'react';
|
|
import toast from 'react-hot-toast';
|
|
import useSWR from 'swr';
|
|
|
|
import RequirePermission from '@/components/helper/RequirePermission';
|
|
|
|
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'>
|
|
<RequirePermission permissions='lti.production.project_flocks.detail'>
|
|
<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>
|
|
</RequirePermission>
|
|
{props.row.original.approval.step_name === 'Aktif' && (
|
|
<RequirePermission permissions='lti.production.chickins.create'>
|
|
<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>
|
|
</RequirePermission>
|
|
)}
|
|
{props.row.original.approval.step_name === 'Pengajuan' && (
|
|
<RequirePermission permissions='lti.production.project_flocks.update'>
|
|
<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>
|
|
</RequirePermission>
|
|
)}
|
|
<RequirePermission permissions='lti.production.project_flocks.delete'>
|
|
<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>
|
|
</RequirePermission>
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
const ProjectFlockTable = ({ refresh }: { refresh?: () => void }) => {
|
|
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',
|
|
},
|
|
});
|
|
const router = useRouter();
|
|
|
|
// ===== 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,
|
|
{ revalidateOnMount: true }
|
|
);
|
|
|
|
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);
|
|
|
|
const response = await ProjectFlockApi.delete(
|
|
selectedSingleRow?.id as number
|
|
);
|
|
if (isResponseSuccess(response)) {
|
|
toast.success(response?.message as string);
|
|
}
|
|
if (isResponseError(response)) {
|
|
toast.error(response?.message as string);
|
|
}
|
|
refreshProjectFlocks();
|
|
|
|
deleteModal.closeModal();
|
|
setIsDeleteLoading(false);
|
|
setRowSelection({});
|
|
};
|
|
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);
|
|
};
|
|
|
|
// ====== EFFECT ======
|
|
useEffect(() => {
|
|
refreshProjectFlocks();
|
|
}, [refresh]);
|
|
|
|
// ====== MEMO ======
|
|
const selectedSingleRow: ProjectFlock | null | undefined = useMemo(() => {
|
|
return selectedRowIds.length === 1
|
|
? isResponseSuccess(projectFlocks)
|
|
? projectFlocks?.data.find((row) => row.id === selectedRowIds[0])
|
|
: null
|
|
: null;
|
|
}, [rowSelection]);
|
|
|
|
const canApprove = useMemo(() => {
|
|
if (!selectedSingleRow || isApproveLoading) return false;
|
|
|
|
const isPengajuan = selectedSingleRow.approval.step_number == 1;
|
|
const isNotRejected = selectedSingleRow.approval.action != 'REJECTED';
|
|
|
|
return isPengajuan && isNotRejected;
|
|
}, [selectedSingleRow, isApproveLoading]);
|
|
|
|
return (
|
|
<>
|
|
<div className='min-h-screen w-full 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'>
|
|
<RequirePermission permissions='lti.production.project_flocks.create'>
|
|
<Button
|
|
color='primary'
|
|
className='w-full sm:w-fit'
|
|
onClick={() => {
|
|
setRowSelection({});
|
|
router.push('/production/project-flock/add');
|
|
}}
|
|
>
|
|
<Icon icon='ic:round-plus' width={24} height={24} />
|
|
Tambah
|
|
</Button>
|
|
</RequirePermission>
|
|
<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;
|
|
|
|
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}
|
|
/>
|
|
</div>
|
|
);
|
|
},
|
|
cell: ({ row }) => {
|
|
return (
|
|
<CheckboxInput
|
|
name='row'
|
|
checked={row.getIsSelected()}
|
|
disabled={!row.getCanSelect()}
|
|
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',
|
|
cell: (props) => {
|
|
const approval = props.row.original.approval;
|
|
|
|
return (
|
|
<Badge
|
|
variant='soft'
|
|
className={{
|
|
badge:
|
|
'rounded-lg px-2 w-full flex flex-row justify-start',
|
|
}}
|
|
color={
|
|
approval.step_number == 1
|
|
? 'neutral'
|
|
: approval.step_number == 2
|
|
? 'success'
|
|
: 'error'
|
|
}
|
|
>
|
|
<Icon
|
|
icon='mdi:circle'
|
|
width={12}
|
|
height={12}
|
|
color={
|
|
approval.step_number == 1
|
|
? 'neutral'
|
|
: approval.step_number == 2
|
|
? 'success'
|
|
: 'error'
|
|
}
|
|
/>
|
|
{approval.step_name}
|
|
</Badge>
|
|
);
|
|
},
|
|
},
|
|
{
|
|
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) =>
|
|
formatDate(props.row.original.created_at, 'MMM DD, YYYY'),
|
|
},
|
|
]}
|
|
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>
|
|
|
|
<FloatingActionsButton
|
|
actions={[
|
|
{
|
|
action: 'DETAIL',
|
|
icon: 'mdi:eye-outline',
|
|
label: 'Lihat Detail',
|
|
hidden: selectedRowIds.length !== 1,
|
|
onClick() {
|
|
router.push(
|
|
`/production/project-flock/detail?projectFlockId=${selectedRowIds[0]}`
|
|
);
|
|
setRowSelection({});
|
|
},
|
|
permissions: 'lti.production.project_flocks.detail',
|
|
},
|
|
{
|
|
action: 'DELETE',
|
|
icon: 'material-symbols:delete-outline-rounded',
|
|
label: `Hapus data`,
|
|
hidden: selectedRowIds.length !== 1,
|
|
onClick: () => {
|
|
deleteModal.openModal();
|
|
},
|
|
permissions: 'lti.production.project_flocks.delete',
|
|
},
|
|
]}
|
|
approvals={[
|
|
{
|
|
icon: 'material-symbols:check',
|
|
label: 'Approve',
|
|
action: 'APPROVED',
|
|
onClick: () => {
|
|
setApprovalAction('APPROVED');
|
|
confirmModal.openModal();
|
|
},
|
|
disabled: !canApprove,
|
|
permissions: 'lti.production.project_flocks.approve',
|
|
},
|
|
{
|
|
icon: 'mdi:times',
|
|
label: 'Reject',
|
|
action: 'REJECTED',
|
|
onClick: () => {
|
|
setApprovalAction('REJECTED');
|
|
confirmModal.openModal();
|
|
},
|
|
permissions: 'lti.production.project_flocks.approve',
|
|
},
|
|
]}
|
|
selectedRowIds={selectedRowIds}
|
|
onClose={() => {
|
|
setRowSelection({});
|
|
}}
|
|
/>
|
|
|
|
<ConfirmationModal
|
|
ref={deleteModal.ref}
|
|
type='error'
|
|
text={`Apakah anda yakin ingin menghapus data Project Flock ini (${selectedRowIds?.length} data)?`}
|
|
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;
|