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

1431 lines
46 KiB
TypeScript

'use client';
import Button from '@/components/Button';
import CheckboxInput from '@/components/input/CheckboxInput';
import DebouncedTextInput from '@/components/input/DebouncedTextInput';
import SelectInput, {
OptionType,
useSelect,
} 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 Dropdown from '@/components/Dropdown';
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, ColumnDef, SortingState } from '@tanstack/react-table';
import { useRouter, usePathname } from 'next/navigation';
import { ChangeEventHandler, useEffect, useMemo, useState } from 'react';
import { useUiStore } from '@/stores/ui/ui.store';
import toast from 'react-hot-toast';
import useSWR from 'swr';
import { useFormik } from 'formik';
import RequirePermission from '@/components/helper/RequirePermission';
import StatusBadge from '@/components/helper/StatusBadge';
import PopoverButton from '@/components/popover/PopoverButton';
import PopoverContent from '@/components/popover/PopoverContent';
import ProjectFlockConfirmationModal from './ProjectFlockConfirmationModal';
import ProjectFlockTableSkeleton from '@/components/pages/production/project-flock/skeleton/ProjectFlockTableSkeleton';
import { useProjectFlockStore } from '@/stores/production/project-flock/project-flock.store';
import { ProjectFlockFormValues } from './form/ProjectFlockForm.schema';
import { useChickinStore } from '@/stores/production/chickin/chickin.store';
import { useProjectFlockClosingStore } from '@/stores/production/project-flock-closing/project-flock-closing.store';
import {
ProjectFlockFilterSchema,
ProjectFlockFilterType,
} from './filter/ProjectFlockFilter';
import Modal from '@/components/Modal';
import SelectInputRadio from '@/components/input/SelectInputRadio';
import ButtonFilter from '@/components/helper/ButtonFilter';
const RowOptionsMenu = ({
props,
popoverPosition = 'bottom',
editClickHandler,
detailClickHandler,
deleteClickHandler,
}: {
props: CellContext<ProjectFlock, unknown>;
popoverPosition: 'bottom' | 'top';
editClickHandler: (id: number) => void;
detailClickHandler: (id: number) => void;
deleteClickHandler: () => void;
}) => {
// TODO: change this to real condition
const showEditButton = true;
const showDeleteButton = showEditButton;
const popoverId = `projectFlock#${props.row.original.id}`;
const popoverAnchorName = `--anchor-projectFlock#${props.row.original.id}`;
const closePopover = () => {
document.getElementById(popoverId)?.hidePopover();
};
const detailClickHandlerWrapper = () => {
detailClickHandler(props.row.original.id);
closePopover();
};
const editClickHandlerWrapper = () => {
editClickHandler(props.row.original.id);
closePopover();
};
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.project_flocks.detail'>
<Button
// href={`/production/project-flock/detail/?projectFlockId=${props.row.original.id}`}
variant='ghost'
color='none'
onClick={detailClickHandlerWrapper}
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.project_flocks.update'>
<Button
// href={`/production/project-flock/detail/edit/?projectFlockId=${props.row.original.id}`}
variant='ghost'
color='none'
onClick={editClickHandlerWrapper}
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.project_flocks.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 ProjectFlockTable = ({ refresh }: { refresh?: () => void }) => {
const { searchValue, setSearchValue, setTableState } = useUiStore();
const pathname = usePathname();
const isSuccess = useProjectFlockStore((s) => s.isSuccess);
const setIsSuccess = useProjectFlockStore((s) => s.setIsSuccess);
const createdProjectFlock = useProjectFlockStore(
(s) => s.createdProjectFlock
);
const setCreatedProjectFlock = useProjectFlockStore(
(s) => s.setCreatedProjectFlock
);
const {
state: tableFilterState,
updateFilter,
setPage,
setPageSize,
toQueryString: getTableFilterQueryString,
} = useTableFilter({
initial: {
search: '',
area_id: '',
location_id: '',
kandang_id: '',
category: '',
period: '',
},
paramMap: {
page: 'page',
pageSize: 'limit',
search: 'search',
area_id: 'area_id',
location_id: 'location_id',
kandang_id: 'kandang_id',
category: 'category',
period: '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 [sorting, setSorting] = useState<SortingState>([]);
const deleteModal = useModal();
const confirmModal = useModal();
const successModal = useModal();
const chickinApproveModal = useModal();
const chickinDeleteModal = useModal();
const closingModal = useModal();
const [approvalAction, setApprovalAction] = useState<'APPROVED' | 'REJECTED'>(
'APPROVED'
);
const [isDeleteLoading, setIsDeleteLoading] = useState(false);
const [isApproveLoading, setIsApproveLoading] = useState(false);
const [isLoadingExportingToExcel, setIsLoadingExportingToExcel] =
useState(false);
const {
isChickinApproveModalOpen,
isChickinApproveLoading,
chickinApproveCallback,
closeChickinApproveModal,
setChickinApproveLoading,
isChickinDeleteModalOpen,
isChickinDeleteLoading,
chickinDeleteCallback,
closeChickinDeleteModal,
setChickinDeleteLoading,
} = useChickinStore();
const {
isClosingModalOpen,
isKandangClosed,
isClosingLoading,
closingCallback,
closeClosingModal,
setClosingLoading,
} = useProjectFlockClosingStore();
// ===== FILTER MODAL STATE =====
const filterModal = useModal();
// ===== FILTER DEPENDENCIES STATE =====
const [filterAreaId, setFilterAreaId] = useState<string | undefined>(
undefined
);
const [filterLocationId, setFilterLocationId] = useState<string | undefined>(
undefined
);
// ===== FORMIK SETUP FOR FILTER =====
const formik = useFormik<ProjectFlockFilterType>({
initialValues: {
area_id: null,
location_id: null,
kandang_id: null,
category: null,
period: null,
},
validationSchema: ProjectFlockFilterSchema,
onSubmit: (values, { setSubmitting }) => {
updateFilter('area_id', values.area_id || '');
updateFilter('location_id', values.location_id || '');
updateFilter('kandang_id', values.kandang_id || '');
updateFilter('category', values.category || '');
updateFilter('period', values.period || '');
filterModal.closeModal();
setSubmitting(false);
},
onReset: () => {
updateFilter('area_id', '');
updateFilter('location_id', '');
updateFilter('kandang_id', '');
updateFilter('category', '');
updateFilter('period', '');
setFilterAreaId(undefined);
setFilterLocationId(undefined);
filterModal.closeModal();
},
});
// ===== FILTER OPTIONS =====
const {
setInputValue: setAreaInputValue,
options: areaOptions,
isLoadingOptions: isLoadingAreaOptions,
loadMore: loadMoreAreas,
} = useSelect(AreaApi.basePath, 'id', 'name');
const {
setInputValue: setLocationInputValue,
options: locationOptions,
isLoadingOptions: isLoadingLocationOptions,
loadMore: loadMoreLocations,
} = useSelect(LocationApi.basePath, 'id', 'name', 'search', {
area_id: filterAreaId || '',
});
const {
setInputValue: setKandangInputValue,
options: kandangOptions,
isLoadingOptions: isLoadingKandangOptions,
loadMore: loadMoreKandangs,
} = useSelect(KandangApi.basePath, 'id', 'name', 'search', {
area_id: filterAreaId || '',
location_id: filterLocationId || '',
});
const categoryOptions = useMemo(
() => [
{ value: 'GROWING', label: 'Growing' },
{ value: 'LAYING', label: 'Laying' },
],
[]
);
const periodOptions = useMemo(
() => [
{ value: '1', label: 'Periode 1' },
{ value: '2', label: 'Periode 2' },
],
[]
);
// ===== FILTER HELPERS =====
const areaValue = useMemo(() => {
if (!formik.values.area_id) return null;
return (
areaOptions.find((opt) => String(opt.value) === formik.values.area_id) ||
null
);
}, [formik.values.area_id, areaOptions]);
const locationValue = useMemo(() => {
if (!formik.values.location_id) return null;
return (
locationOptions.find(
(opt) => String(opt.value) === formik.values.location_id
) || null
);
}, [formik.values.location_id, locationOptions]);
const kandangValue = useMemo(() => {
if (!formik.values.kandang_id) return null;
return (
kandangOptions.find(
(opt) => String(opt.value) === formik.values.kandang_id
) || null
);
}, [formik.values.kandang_id, kandangOptions]);
const categoryValue = useMemo(() => {
if (!formik.values.category) return null;
return (
categoryOptions.find((opt) => opt.value === formik.values.category) ||
null
);
}, [formik.values.category, categoryOptions]);
const periodValue = useMemo(() => {
if (!formik.values.period) return null;
return (
periodOptions.find((opt) => opt.value === formik.values.period) || null
);
}, [formik.values.period, periodOptions]);
// ===== FILTER DEPENDENCY HANDLERS =====
const handleFilterAreaChange = (area: OptionType | null) => {
const areaId = area?.value ? String(area.value) : undefined;
setFilterAreaId(areaId);
if (!areaId) {
setFilterLocationId(undefined);
formik.setFieldValue('location_id', null);
formik.setFieldValue('kandang_id', null);
}
};
const handleFilterLocationChange = (location: OptionType | null) => {
const locationId = location?.value ? String(location.value) : undefined;
setFilterLocationId(locationId);
if (!locationId) {
formik.setFieldValue('kandang_id', null);
}
};
// ===== HANDLE FILTER MODAL OPEN =====
const handleFilterModalOpen = () => {
const areaId = tableFilterState.area_id || null;
const locationId = tableFilterState.location_id || null;
formik.setValues({
area_id: areaId,
location_id: locationId,
kandang_id: tableFilterState.kandang_id || null,
category: tableFilterState.category || null,
period: tableFilterState.period || null,
});
setFilterAreaId(areaId || undefined);
setFilterLocationId(locationId || undefined);
filterModal.openModal();
};
// ===== Fetch Data =====
const {
data: projectFlocks,
isLoading,
mutate: refreshProjectFlocks,
} = useSWR(
`${ProjectFlockApi.basePath}${getTableFilterQueryString()}`,
ProjectFlockApi.getAllFetcher,
{ revalidateOnMount: true }
);
// ====== HANDLER ======
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({});
};
useEffect(() => {
updateFilter('search', searchValue);
}, [searchValue, updateFilter]);
useEffect(() => {
setTableState('project-flock-table', pathname);
}, [pathname, setTableState]);
const searchChangeHandler: ChangeEventHandler<HTMLInputElement> = (e) => {
setSearchValue(e.target.value);
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)) {
const successMessage =
approvalAction === 'APPROVED'
? 'Project Flock berhasil di-approve!'
: 'Project Flock berhasil di-reject!';
toast.success(successMessage);
confirmModal.closeModal();
}
if (isResponseError(approveProjectFlockRes)) {
toast.error(approveProjectFlockRes?.message as string);
confirmModal.closeModal();
}
setRowSelection({});
refreshProjectFlocks();
setIsApproveLoading(false);
};
// ====== EFFECT ======
useEffect(() => {
refreshProjectFlocks();
}, [refresh]);
useEffect(() => {
if (isChickinApproveModalOpen) {
chickinApproveModal.openModal();
} else {
chickinApproveModal.closeModal();
}
}, [isChickinApproveModalOpen, chickinApproveModal]);
useEffect(() => {
if (isChickinDeleteModalOpen) {
chickinDeleteModal.openModal();
} else {
chickinDeleteModal.closeModal();
}
}, [isChickinDeleteModalOpen, chickinDeleteModal]);
useEffect(() => {
if (isClosingModalOpen) {
closingModal.openModal();
} else {
closingModal.closeModal();
}
}, [isClosingModalOpen, closingModal]);
useEffect(() => {
if (isSuccess) {
successModal.openModal();
}
}, [isSuccess, successModal]);
const handleSuccessModalClose = () => {
successModal.closeModal();
setIsSuccess(false);
setCreatedProjectFlock(null);
};
const projectFlockFormValues = useMemo(() => {
if (!createdProjectFlock) return undefined;
return {
flock: {
value: 0,
label: createdProjectFlock.flock_name || '',
},
flock_name: createdProjectFlock.flock_name || '',
area: {
value: createdProjectFlock.area_id,
label: createdProjectFlock.area?.name || '',
},
area_id: createdProjectFlock.area_id,
category_option: {
value: createdProjectFlock.category,
label: createdProjectFlock.category,
},
category: createdProjectFlock.category,
production_standard: {
value: createdProjectFlock.production_standard_id,
label: createdProjectFlock.production_standard?.name || '',
},
production_standard_id: createdProjectFlock.production_standard_id,
location: {
value: createdProjectFlock.location_id,
label: createdProjectFlock.location?.name || '',
},
location_id: createdProjectFlock.location_id,
kandang_ids: createdProjectFlock.kandangs?.map((k) => k.id) || [],
project_budgets:
createdProjectFlock.project_budgets?.map((budget) => ({
nonstock: budget.nonstock
? {
value: budget.nonstock_id,
label: budget.nonstock.name || '',
}
: null,
nonstock_id: budget.nonstock_id,
qty: budget.qty,
price: budget.price,
total_price: budget.qty * budget.price,
})) || [],
} as ProjectFlockFormValues;
}, [createdProjectFlock]);
// ====== 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]);
const canApprove = useMemo(() => {
return selectedRowIds.every((id) => {
const projectFlock = isResponseSuccess(projectFlocks)
? projectFlocks?.data.find((row) => row.id === id)
: null;
const isProjectFlockRequesting = projectFlock?.approval?.step_number == 1;
const isProjectFlockNotRejected =
projectFlock?.approval?.action != 'REJECTED';
return isProjectFlockRequesting && isProjectFlockNotRejected;
});
}, [selectedRowIds, projectFlocks]);
// ====== COLUMNS ======
const columns = useMemo<ColumnDef<ProjectFlock>[]>(
() => [
{
id: 'select',
header: ({ table }) => {
const allRows = table.getRowModel().rows;
const selectableRows = allRows.filter((row) => {
const projectFlock = row.original;
return (
projectFlock.approval?.step_number === 1 &&
projectFlock.approval?.action !== 'REJECTED'
);
});
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));
};
const hasNoSelectableRows = selectableRows.length === 0;
return (
<div className='w-full flex flex-row justify-center'>
<CheckboxInput
name='allRow'
checked={allSelected}
indeterminate={someSelected}
onChange={toggleSelectableRows}
disabled={hasNoSelectableRows}
/>
</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: 'category',
header: 'Kategori',
},
{
accessorKey: 'approval.step_name',
header: 'Status',
cell: (props) => {
const approval = props.row.original.approval;
const isRejected = approval?.action == 'REJECTED';
const isApproved = approval?.action == 'APPROVED';
let latestApprovalStepName = approval.step_name;
const badgeColor = isRejected
? 'error'
: isApproved
? approval?.step_number == 1
? 'neutral'
: approval?.step_number == 2
? 'success'
: approval?.step_number == 3
? 'error'
: 'neutral'
: 'neutral';
switch (approval.action.toLowerCase()) {
case 'pengajuan':
latestApprovalStepName = 'Pengajuan';
break;
case 'aktif':
latestApprovalStepName = 'Aktif';
break;
case 'Selesai':
latestApprovalStepName = 'Closing';
break;
}
if (isRejected) {
latestApprovalStepName = 'Ditolak';
}
return (
<StatusBadge color={badgeColor} text={latestApprovalStepName} />
);
},
},
{
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'),
},
{
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 detailClickHandler = (id: number) => {
router.push(
`/production/project-flock/detail/?projectFlockId=${id}`
);
};
const editClickHandler = (id: number) => {
router.push(
`/production/project-flock/detail/edit/?projectFlockId=${id}`
);
};
const deleteClickHandler = () => {
// Set row selection
setRowSelection({
[String(props.row.original.id)]: true,
});
deleteModal.openModal();
};
return (
<RowOptionsMenu
props={props}
detailClickHandler={detailClickHandler}
editClickHandler={editClickHandler}
deleteClickHandler={deleteClickHandler}
popoverPosition={isLast2Rows ? 'top' : 'bottom'}
/>
);
},
},
],
[]
);
const exportToExcelHandler = async () => {
setIsLoadingExportingToExcel(true);
toast.error('Not implemented yet!');
setIsLoadingExportingToExcel(false);
};
const bulkApproveClickHandler = () => {
setApprovalAction('APPROVED');
confirmModal.openModal();
};
const bulkRejectClickHandler = () => {
setApprovalAction('REJECTED');
confirmModal.openModal();
};
return (
<>
<div className='@container min-h-screen w-full'>
<div className='flex flex-col 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'
onClick={() => {
setRowSelection({});
router.push('/production/project-flock/add');
}}
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 Flock
</Button>
</RequirePermission>
<div className='ms-auto w-full sm:w-auto'>
<DebouncedTextInput
name='search'
placeholder='Cari Project Flock'
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={isLoadingArea}
value={selectedArea}
onChange={(val) => {
setSelectedArea(val as OptionType);
updateFilter(
'areaFilter',
(val as OptionType)?.value.toString()
);
}}
onInputChange={setAreaSelectInputValue}
onMenuScrollToBottom={loadMoreArea}
isClearable
/>
<SelectInput
label='Lokasi'
options={optionsLocation}
isLoading={isLoadingLocation}
value={selectedLocation}
onChange={(val) => {
setSelectedLocation(val as OptionType);
updateFilter(
'locationFilter',
(val as OptionType)?.value.toString()
);
}}
onInputChange={setLocationSelectInputValue}
onMenuScrollToBottom={loadMoreLocation}
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}
onMenuScrollToBottom={loadMoreKandang}
isClearable
/>
<DebouncedTextInput
name='period'
type='number'
label='Periode'
placeholder='Masukan periode'
value={periodInputValue?.toString() ?? ''}
onChange={(e) => {
setPeriodInputValue(parseInt(e.target.value));
updateFilter('periodFilter', e.target.value);
}}
/>
</div>
</div> */}
<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.project_flocks.create'>
<Button
color='primary'
onClick={() => {
setRowSelection({});
router.push('/production/project-flock/add');
}}
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 Flock
</Button>
</RequirePermission>
{selectedRowIds.length > 0 && canApprove && (
<>
<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']}
onClick={handleFilterModalOpen}
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(projectFlocks) ||
projectFlocks.data?.length === 0 ? (
<div className='p-3'>
<ProjectFlockTableSkeleton
columns={columns}
icon={
<Icon
icon='heroicons:document-text'
className='text-white'
width={20}
height={20}
/>
}
/>
</div>
) : (
<Table<ProjectFlock>
data={
isResponseSuccess(projectFlocks) ? projectFlocks?.data : []
}
columns={columns}
pageSize={tableFilterState.pageSize}
page={
isResponseSuccess(projectFlocks)
? projectFlocks?.meta?.page
: 0
}
totalItems={
isResponseSuccess(projectFlocks)
? projectFlocks?.meta?.total_results
: 0
}
onPageChange={(page) => {
setPage(page);
}}
onPageSizeChange={(pageSize) => {
setPageSize(pageSize);
}}
isLoading={isLoading}
sorting={sorting}
setSorting={setSorting}
rowSelection={rowSelection}
setRowSelection={setRowSelection}
enableRowSelection={(row) => {
const projectFlock = row.original;
return (
projectFlock.approval?.step_number === 1 &&
projectFlock.approval?.action !== 'REJECTED'
);
}}
withCheckbox
className={{
containerClassName: cn('p-3 mb-0'),
headerColumnClassName: 'text-nowrap',
}}
/>
)}
</div>
</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,
}}
/>
<ProjectFlockConfirmationModal
ref={successModal.ref}
type='success'
text='Data Berhasil Ditambahkan'
subtitleText='Data project flock telah berhasil disimpan.'
projectFlockForm={projectFlockFormValues}
onClose={handleSuccessModalClose}
secondaryButton={undefined}
/>
{/* Chickin Approval Modal */}
<ConfirmationModalWithNotes
ref={chickinApproveModal.ref}
type='success'
text={`Apakah anda yakin ingin approve data Chickin yang Pending?`}
className={{
modal: 'z-9999',
}}
secondaryButton={{
text: 'Tidak',
onClick: () => {
closeChickinApproveModal();
chickinApproveModal.closeModal();
},
}}
primaryButton={{
text: 'Ya',
color: 'success',
onClick: async (notes) => {
if (chickinApproveCallback) {
setChickinApproveLoading(true);
try {
await chickinApproveCallback(notes);
} finally {
setChickinApproveLoading(false);
closeChickinApproveModal();
chickinApproveModal.closeModal();
}
}
},
isLoading: isChickinApproveLoading,
}}
/>
{/* Chickin Delete Modal */}
<ConfirmationModal
ref={chickinDeleteModal.ref}
type='error'
text='Apakah anda yakin ingin menghapus data chick in ini?'
secondaryButton={{
text: 'Tidak',
onClick: () => {
closeChickinDeleteModal();
},
}}
className={{
modal: 'z-9999',
}}
primaryButton={{
text: 'Ya',
color: 'error',
isLoading: isChickinDeleteLoading,
onClick: async () => {
if (chickinDeleteCallback) {
setChickinDeleteLoading(true);
try {
await chickinDeleteCallback();
} finally {
setChickinDeleteLoading(false);
closeChickinDeleteModal();
}
}
},
}}
/>
{/* Filter Modal */}
<Modal
ref={filterModal.ref}
className={{
modal: 'p-0',
modalBox: 'p-0 rounded-[0.875rem] xl:max-w-4/12 max-w-sm',
}}
>
{/* Modal Header */}
<div className='flex items-center justify-between gap-2 border-b border-base-content/10 p-4'>
<div className='flex items-center gap-2 text-primary'>
<Icon icon='heroicons:funnel' width={20} height={20} />
<h3 className='font-medium text-sm'>Filter Data</h3>
</div>
<Button
variant='link'
onClick={filterModal.closeModal}
className='text-base-content/50 hover:text-base-content transition-colors cursor-pointer'
>
<Icon icon='heroicons:x-mark' width={20} height={20} />
</Button>
</div>
<form onSubmit={formik.handleSubmit} onReset={formik.handleReset}>
<div className='p-4 flex flex-col gap-1.5'>
<SelectInput
label='Area'
placeholder='Pilih Area'
options={areaOptions}
value={areaValue}
onChange={(val) => {
if (!Array.isArray(val)) {
const areaValue = val?.value ? String(val.value) : null;
formik.setFieldValue('area_id', areaValue);
handleFilterAreaChange(val || null);
}
}}
onInputChange={setAreaInputValue}
isLoading={isLoadingAreaOptions}
isClearable
onMenuScrollToBottom={loadMoreAreas}
className={{ wrapper: 'w-full' }}
/>
<SelectInput
label='Lokasi'
placeholder='Pilih Lokasi'
options={locationOptions}
value={locationValue}
onChange={(val) => {
if (!Array.isArray(val)) {
const locationValue = val?.value ? String(val.value) : null;
formik.setFieldValue('location_id', locationValue);
handleFilterLocationChange(val || null);
}
}}
onInputChange={setLocationInputValue}
isLoading={isLoadingLocationOptions}
isClearable
onMenuScrollToBottom={loadMoreLocations}
className={{ wrapper: 'w-full' }}
/>
<SelectInput
label='Kandang'
placeholder='Pilih Kandang'
options={kandangOptions}
value={kandangValue}
onChange={(val) => {
if (!Array.isArray(val)) {
formik.setFieldValue(
'kandang_id',
val?.value ? String(val.value) : null
);
}
}}
onInputChange={setKandangInputValue}
isLoading={isLoadingKandangOptions}
isClearable
onMenuScrollToBottom={loadMoreKandangs}
className={{ wrapper: 'w-full' }}
/>
<SelectInputRadio
label='Kategori'
placeholder='Pilih Kategori'
options={categoryOptions}
value={categoryValue}
onChange={(val) => {
if (!Array.isArray(val)) {
formik.setFieldValue('category', val?.value || null);
}
}}
className={{ wrapper: 'w-full' }}
isClearable={true}
/>
<SelectInputRadio
label='Periode'
placeholder='Pilih Periode'
options={periodOptions}
value={periodValue}
onChange={(val) => {
if (!Array.isArray(val)) {
formik.setFieldValue('period', val?.value || null);
}
}}
className={{ wrapper: 'w-full' }}
isClearable
/>
</div>
{/* Modal Footer */}
<div className='flex justify-between items-center gap-4 p-4 border-t border-base-content/10 bg-gray-50'>
<Button
type='reset'
variant='soft'
className='rounded-lg text-base-content/65 bg-transparent border-none hover:bg-base-content/10 hover:text-base-content/65 transition-colors px-3 py-2'
>
Reset Filter
</Button>
<Button
type='submit'
className='min-w-40 text-sm rounded-lg py-3 text-white font-semibold'
disabled={!formik.isValid || formik.isSubmitting}
>
Apply Filter
</Button>
</div>
</form>
</Modal>
{/* Project Flock Closing Modal */}
<ConfirmationModal
ref={closingModal.ref}
type='error'
text={
!isKandangClosed
? 'Apakah kamu yakin ingin mengakhiri project ini ? *Pastikan persediaan produk di gudang terkait sudah kosong, dan BOP sudah selesai'
: 'Apakah kamu yakin ingin membuka kembali project ini ? *Project ini akan kembali ke status aktif'
}
className={{
modal: 'z-9999',
}}
secondaryButton={{
text: 'Tidak',
onClick: () => {
closeClosingModal();
closingModal.closeModal();
},
}}
primaryButton={{
text: 'Ya',
color: 'error',
isLoading: isClosingLoading,
onClick: async () => {
if (closingCallback) {
setClosingLoading(true);
try {
await closingCallback(!isKandangClosed ? 'close' : 'unclose');
} finally {
setClosingLoading(false);
closeClosingModal();
closingModal.closeModal();
refreshProjectFlocks();
}
}
},
}}
/>
</>
);
};
export default ProjectFlockTable;