mirror of
https://gitlab.com/mbugroup/lti-web-client.git
synced 2026-05-20 13:32:00 +00:00
Merge branch 'development' of https://gitlab.com/mbugroup/lti-web-client into fix/marketing
This commit is contained in:
@@ -13,6 +13,7 @@ interface ConfirmationModalWithNotesProps
|
||||
extends Omit<ConfirmationModalProps, 'children' | 'primaryButton'> {
|
||||
rows?: number;
|
||||
placeholder?: string;
|
||||
onClose?: () => void;
|
||||
|
||||
primaryButton?: {
|
||||
text?: string;
|
||||
@@ -32,6 +33,7 @@ const ConfirmationModalWithNotes: React.FC<ConfirmationModalWithNotesProps> = ({
|
||||
className,
|
||||
rows = 3,
|
||||
placeholder = 'Catatan...',
|
||||
onClose,
|
||||
...props
|
||||
}) => {
|
||||
const randomId = useId();
|
||||
@@ -41,6 +43,11 @@ const ConfirmationModalWithNotes: React.FC<ConfirmationModalWithNotesProps> = ({
|
||||
setNotes(e.target.value);
|
||||
};
|
||||
|
||||
const closeModalHandler = () => {
|
||||
onClose?.();
|
||||
ref.current?.close();
|
||||
};
|
||||
|
||||
return (
|
||||
<ConfirmationModal
|
||||
ref={ref}
|
||||
@@ -49,12 +56,32 @@ const ConfirmationModalWithNotes: React.FC<ConfirmationModalWithNotesProps> = ({
|
||||
closeOnBackdrop={closeOnBackdrop}
|
||||
primaryButton={{
|
||||
...primaryButton,
|
||||
onClick: () => {
|
||||
primaryButton?.onClick?.(notes);
|
||||
onClick: (e) => {
|
||||
if (primaryButton && primaryButton?.onClick) {
|
||||
primaryButton?.onClick?.(notes);
|
||||
} else {
|
||||
closeModalHandler();
|
||||
}
|
||||
|
||||
setNotes('');
|
||||
},
|
||||
}}
|
||||
secondaryButton={secondaryButton}
|
||||
secondaryButton={
|
||||
secondaryButton
|
||||
? {
|
||||
text: secondaryButton?.text ?? 'Tidak',
|
||||
onClick: (e) => {
|
||||
if (secondaryButton && secondaryButton?.onClick) {
|
||||
secondaryButton.onClick?.(e);
|
||||
} else {
|
||||
closeModalHandler();
|
||||
}
|
||||
|
||||
setNotes('');
|
||||
},
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
className={className}
|
||||
{...props}
|
||||
>
|
||||
|
||||
@@ -100,6 +100,7 @@ const ExpenseRequestContent = ({
|
||||
const [isCompleteLoading, setIsCompleteLoading] = useState(false);
|
||||
const [isApproveLoading, setIsApproveLoading] = useState(false);
|
||||
const [isRejectLoading, setIsRejectLoading] = useState(false);
|
||||
const [, setApprovalNotes] = useState('');
|
||||
|
||||
const formik = useFormik<UploadRequestDocumentsFormValues>({
|
||||
initialValues: {
|
||||
@@ -130,10 +131,12 @@ const ExpenseRequestContent = ({
|
||||
};
|
||||
|
||||
const approveClickHandler = () => {
|
||||
setApprovalNotes('');
|
||||
approveModal.openModal();
|
||||
};
|
||||
|
||||
const rejectClickHandler = () => {
|
||||
setApprovalNotes('');
|
||||
rejectModal.openModal();
|
||||
};
|
||||
|
||||
@@ -200,6 +203,7 @@ const ExpenseRequestContent = ({
|
||||
approveModal.closeModal();
|
||||
|
||||
toast.success(approveResponse?.message);
|
||||
setApprovalNotes('');
|
||||
router.push('/expense');
|
||||
} else {
|
||||
approveModal.closeModal();
|
||||
@@ -234,6 +238,7 @@ const ExpenseRequestContent = ({
|
||||
rejectModal.closeModal();
|
||||
|
||||
toast.success(rejectResponse.message);
|
||||
setApprovalNotes('');
|
||||
router.push('/expense');
|
||||
} else {
|
||||
rejectModal.closeModal();
|
||||
@@ -710,6 +715,10 @@ const ExpenseRequestContent = ({
|
||||
text='Apakah anda yakin ingin approve data biaya operasional ini?'
|
||||
secondaryButton={{
|
||||
text: 'Tidak',
|
||||
onClick: () => {
|
||||
setApprovalNotes('');
|
||||
approveModal.closeModal();
|
||||
},
|
||||
}}
|
||||
primaryButton={{
|
||||
text: 'Ya',
|
||||
@@ -725,6 +734,10 @@ const ExpenseRequestContent = ({
|
||||
text='Apakah anda yakin ingin reject data biaya operasional ini?'
|
||||
secondaryButton={{
|
||||
text: 'Tidak',
|
||||
onClick: () => {
|
||||
setApprovalNotes('');
|
||||
rejectModal.closeModal();
|
||||
},
|
||||
}}
|
||||
primaryButton={{
|
||||
text: 'Ya',
|
||||
|
||||
@@ -185,6 +185,7 @@ const ExpensesTable = () => {
|
||||
const [isDeleteLoading, setIsDeleteLoading] = useState(false);
|
||||
const [isApproveLoading, setIsApproveLoading] = useState(false);
|
||||
const [isRejectLoading, setIsRejectLoading] = useState(false);
|
||||
const [, setApprovalNotes] = useState('');
|
||||
|
||||
const [sorting, setSorting] = useState<SortingState>([]);
|
||||
const [rowSelection, setRowSelection] = useState<Record<string, boolean>>({});
|
||||
@@ -342,6 +343,7 @@ const ExpensesTable = () => {
|
||||
[String(props.row.original.id)]: true,
|
||||
});
|
||||
|
||||
setApprovalNotes('');
|
||||
approveModal.openModal();
|
||||
};
|
||||
|
||||
@@ -353,6 +355,7 @@ const ExpensesTable = () => {
|
||||
[String(props.row.original.id)]: true,
|
||||
});
|
||||
|
||||
setApprovalNotes('');
|
||||
rejectModal.openModal();
|
||||
};
|
||||
|
||||
@@ -412,10 +415,12 @@ const ExpensesTable = () => {
|
||||
// };
|
||||
|
||||
const bulkApproveClickHandler = () => {
|
||||
setApprovalNotes('');
|
||||
approveModal.openModal();
|
||||
};
|
||||
|
||||
const bulkRejectClickHandler = () => {
|
||||
setApprovalNotes('');
|
||||
rejectModal.openModal();
|
||||
};
|
||||
|
||||
@@ -468,6 +473,7 @@ const ExpensesTable = () => {
|
||||
`Berhasil approve ${selectedRowIds.length} data biaya operasional!`
|
||||
);
|
||||
|
||||
setApprovalNotes('');
|
||||
setRowSelection({});
|
||||
} else {
|
||||
approveModal.closeModal();
|
||||
@@ -509,6 +515,7 @@ const ExpensesTable = () => {
|
||||
toast.success(
|
||||
`Berhasil reject ${selectedRowIds.length} data biaya operasional!`
|
||||
);
|
||||
setApprovalNotes('');
|
||||
setRowSelection({});
|
||||
} else {
|
||||
rejectModal.closeModal();
|
||||
@@ -787,6 +794,10 @@ const ExpensesTable = () => {
|
||||
text='Apakah anda yakin ingin approve data biaya operasional ini?'
|
||||
secondaryButton={{
|
||||
text: 'Tidak',
|
||||
onClick: () => {
|
||||
setApprovalNotes('');
|
||||
approveModal.closeModal();
|
||||
},
|
||||
}}
|
||||
primaryButton={{
|
||||
text: 'Ya',
|
||||
@@ -802,6 +813,10 @@ const ExpensesTable = () => {
|
||||
text='Apakah anda yakin ingin reject data biaya operasional ini?'
|
||||
secondaryButton={{
|
||||
text: 'Tidak',
|
||||
onClick: () => {
|
||||
setApprovalNotes('');
|
||||
rejectModal.closeModal();
|
||||
},
|
||||
}}
|
||||
primaryButton={{
|
||||
text: 'Ya',
|
||||
|
||||
@@ -7,14 +7,12 @@ import React, {
|
||||
useEffect,
|
||||
useRef,
|
||||
} from 'react';
|
||||
import { RefObject } from 'react';
|
||||
import useSWR from 'swr';
|
||||
import { Icon } from '@iconify/react';
|
||||
import { SortingState, CellContext } from '@tanstack/react-table';
|
||||
import { cn, formatDate, formatNumber } from '@/lib/helper';
|
||||
import RequirePermission from '@/components/helper/RequirePermission';
|
||||
import { useModal } from '@/components/Modal';
|
||||
import Modal from '@/components/Modal';
|
||||
import Button from '@/components/Button';
|
||||
import ConfirmationModal from '@/components/modal/ConfirmationModal';
|
||||
import ConfirmationModalWithNotes from '@/components/modal/ConfirmationModalWithNotes';
|
||||
@@ -28,14 +26,51 @@ import RowCollapseOptions from '@/components/table/RowCollapseOptions';
|
||||
import RowOptionsMenuWrapper from '@/components/table/RowOptionsMenuWrapper';
|
||||
import { type Recording } from '@/types/api/production/recording';
|
||||
import { RecordingApi } from '@/services/api/production';
|
||||
import { ApprovalApi } from '@/services/api/approval';
|
||||
import { isResponseSuccess } from '@/lib/api-helper';
|
||||
import { useTableFilter } from '@/services/hooks/useTableFilter';
|
||||
import toast from 'react-hot-toast';
|
||||
import Badge from '@/components/Badge';
|
||||
import StatusBadge from '@/components/helper/StatusBadge';
|
||||
import CheckboxInput from '@/components/input/CheckboxInput';
|
||||
import { useUiStore } from '@/stores/ui/ui.store';
|
||||
import { BaseApproval, BaseApiResponse } from '@/types/api/api-general';
|
||||
import { Color } from '@/types/theme';
|
||||
|
||||
// ===== STATUS BADGE UTILITIES =====
|
||||
const statusTextMap: Record<string, string> = {
|
||||
APPROVED: 'Disetujui',
|
||||
Disetujui: 'Disetujui',
|
||||
REJECTED: 'Ditolak',
|
||||
Ditolak: 'Ditolak',
|
||||
CREATED: 'Dibuat',
|
||||
UPDATED: 'Diperbarui',
|
||||
};
|
||||
|
||||
const getStatusText = (status: string): string => {
|
||||
return statusTextMap[status] || status;
|
||||
};
|
||||
|
||||
const statusBadgeColorMap: Record<string, Color> = {
|
||||
APPROVED: 'success',
|
||||
Disetujui: 'success',
|
||||
approved: 'success',
|
||||
disetujui: 'success',
|
||||
REJECTED: 'error',
|
||||
Ditolak: 'error',
|
||||
rejected: 'error',
|
||||
ditolak: 'error',
|
||||
CREATED: 'neutral',
|
||||
Dibuat: 'neutral',
|
||||
created: 'neutral',
|
||||
dibuat: 'neutral',
|
||||
UPDATED: 'warning',
|
||||
Diperbarui: 'warning',
|
||||
updated: 'warning',
|
||||
diperbarui: 'warning',
|
||||
};
|
||||
|
||||
const getStatusBadgeColor = (status: string): Color => {
|
||||
return statusBadgeColorMap[status] || 'neutral';
|
||||
};
|
||||
|
||||
const RowOptionsMenu = ({
|
||||
type = 'dropdown',
|
||||
@@ -135,221 +170,6 @@ const RowOptionsMenu = ({
|
||||
);
|
||||
};
|
||||
|
||||
const ApprovalHistoryModal = ({
|
||||
ref,
|
||||
currentApproval,
|
||||
module_name = 'RECORDINGS',
|
||||
module_id,
|
||||
}: {
|
||||
ref: RefObject<HTMLDialogElement | null>;
|
||||
currentApproval?: BaseApproval;
|
||||
module_name?: string;
|
||||
module_id?: number | undefined;
|
||||
}) => {
|
||||
const [isModalOpen, setIsModalOpen] = useState(false);
|
||||
|
||||
const approvalHistoryUrl = useMemo(() => {
|
||||
if (!isModalOpen) return null;
|
||||
const params = new URLSearchParams({
|
||||
module_name: module_name,
|
||||
group_step_number: 'true',
|
||||
});
|
||||
|
||||
if (module_id) {
|
||||
params.append('module_id', module_id.toString());
|
||||
}
|
||||
|
||||
return `${ApprovalApi.basePath}?${params.toString()}`;
|
||||
}, [module_name, module_id, isModalOpen]);
|
||||
|
||||
type GroupedApprovalResponse = {
|
||||
step_number: number;
|
||||
step_name: string;
|
||||
approvals: BaseApproval[];
|
||||
};
|
||||
|
||||
const fetchGroupedApprovals = async (
|
||||
url: string
|
||||
): Promise<BaseApiResponse<GroupedApprovalResponse[]>> => {
|
||||
return (await ApprovalApi.getAllFetcher(url)) as BaseApiResponse<
|
||||
GroupedApprovalResponse[]
|
||||
>;
|
||||
};
|
||||
|
||||
const { data: approvalHistoryData, isLoading } = useSWR<
|
||||
BaseApiResponse<GroupedApprovalResponse[]>
|
||||
>(approvalHistoryUrl, fetchGroupedApprovals);
|
||||
|
||||
useEffect(() => {
|
||||
const checkModalOpen = () => {
|
||||
const isOpen = ref.current?.open || false;
|
||||
setIsModalOpen(isOpen);
|
||||
};
|
||||
|
||||
checkModalOpen();
|
||||
|
||||
const observer = new MutationObserver(checkModalOpen);
|
||||
if (ref.current) {
|
||||
observer.observe(ref.current, {
|
||||
attributes: true,
|
||||
attributeFilter: ['open'],
|
||||
});
|
||||
}
|
||||
|
||||
return () => observer.disconnect();
|
||||
}, [ref]);
|
||||
|
||||
const approvalHistory = useMemo(() => {
|
||||
if (!approvalHistoryData || approvalHistoryData.status !== 'success')
|
||||
return [];
|
||||
|
||||
const groupedData = approvalHistoryData.data || [];
|
||||
const flattenedApprovals: BaseApproval[] = [];
|
||||
|
||||
groupedData.forEach((group) => {
|
||||
group.approvals.forEach((approval) => {
|
||||
flattenedApprovals.push(approval);
|
||||
});
|
||||
});
|
||||
|
||||
return flattenedApprovals;
|
||||
}, [approvalHistoryData]);
|
||||
|
||||
const closeModalHandler = () => {
|
||||
ref.current?.close();
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal ref={ref} className={{ modalBox: 'max-w-2xl' }}>
|
||||
<div className='w-full flex flex-col gap-6'>
|
||||
{/* Header */}
|
||||
<div className='flex items-center justify-between'>
|
||||
<h2 className='text-xl font-bold'>Riwayat Approval</h2>
|
||||
<Button
|
||||
onClick={closeModalHandler}
|
||||
variant='ghost'
|
||||
className='btn-circle btn-sm p-0'
|
||||
>
|
||||
<Icon icon='mdi:close' width={20} height={20} />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{isLoading ? (
|
||||
<div className='flex justify-center py-8'>
|
||||
<span className='loading loading-spinner loading-lg'></span>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
{/* Current Status */}
|
||||
{currentApproval && (
|
||||
<div className='bg-base-200 rounded-lg p-4'>
|
||||
<h3 className='font-semibold mb-2'>Status Saat Ini</h3>
|
||||
<div className='flex items-center gap-3'>
|
||||
<Badge
|
||||
variant='soft'
|
||||
color={
|
||||
currentApproval.action === 'APPROVED'
|
||||
? 'success'
|
||||
: currentApproval.action === 'REJECTED'
|
||||
? 'error'
|
||||
: currentApproval.action === 'UPDATED'
|
||||
? 'warning'
|
||||
: 'info'
|
||||
}
|
||||
>
|
||||
{currentApproval.step_name}
|
||||
</Badge>
|
||||
<span className='text-sm text-gray-600'>
|
||||
{currentApproval.action === 'APPROVED' && 'Disetujui'}
|
||||
{currentApproval.action === 'REJECTED' && 'Ditolak'}
|
||||
{currentApproval.action === 'CREATED' && 'Dibuat'}
|
||||
{currentApproval.action === 'UPDATED' && 'Diperbarui'}
|
||||
</span>
|
||||
</div>
|
||||
{currentApproval.notes && (
|
||||
<p className='mt-2 text-sm text-gray-600'>
|
||||
<span className='font-medium'>Catatan:</span>{' '}
|
||||
{currentApproval.notes}
|
||||
</p>
|
||||
)}
|
||||
<p className='mt-1 text-xs text-gray-500'>
|
||||
Oleh: {currentApproval.action_by.name} •{' '}
|
||||
{formatDate(currentApproval.action_at, 'DD MMMM YYYY HH:mm')}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Full History */}
|
||||
{approvalHistory.length > 0 && (
|
||||
<div className='space-y-4'>
|
||||
<h3 className='font-semibold'>Riwayat Lengkap</h3>
|
||||
<div className='overflow-x-auto'>
|
||||
<table className='table table-sm'>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Tahap</th>
|
||||
<th>Aksi</th>
|
||||
<th>Catatan</th>
|
||||
<th>Oleh</th>
|
||||
<th>Waktu</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{approvalHistory
|
||||
.sort(
|
||||
(a: BaseApproval, b: BaseApproval) =>
|
||||
new Date(b.action_at).getTime() -
|
||||
new Date(a.action_at).getTime()
|
||||
)
|
||||
.map((approval: BaseApproval, index: number) => (
|
||||
<tr key={index}>
|
||||
<td>{approval.step_name}</td>
|
||||
<td>
|
||||
<Badge
|
||||
variant='soft'
|
||||
color={
|
||||
approval.action === 'APPROVED'
|
||||
? 'success'
|
||||
: approval.action === 'REJECTED'
|
||||
? 'error'
|
||||
: approval.action === 'UPDATED'
|
||||
? 'warning'
|
||||
: 'info'
|
||||
}
|
||||
size='sm'
|
||||
>
|
||||
{approval.action === 'APPROVED' && 'Disetujui'}
|
||||
{approval.action === 'REJECTED' && 'Ditolak'}
|
||||
{approval.action === 'CREATED' && 'Dibuat'}
|
||||
{approval.action === 'UPDATED' && 'Diperbarui'}
|
||||
</Badge>
|
||||
</td>
|
||||
<td className='max-w-xs'>
|
||||
<div className='truncate'>
|
||||
{approval.notes || '-'}
|
||||
</div>
|
||||
</td>
|
||||
<td>{approval.action_by.name}</td>
|
||||
<td className='text-xs'>
|
||||
{formatDate(
|
||||
approval.action_at,
|
||||
'DD MMMM YYYY HH:mm'
|
||||
)}
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
const RecordingTable = () => {
|
||||
const { searchValue, setSearchValue, resetSearchValue } = useUiStore();
|
||||
const previousPathRef = useRef<string | null>(null);
|
||||
@@ -395,7 +215,6 @@ const RecordingTable = () => {
|
||||
const singleDeleteModal = useModal();
|
||||
const approveModal = useModal();
|
||||
const rejectModal = useModal();
|
||||
const approvalHistoryModal = useModal();
|
||||
|
||||
const {
|
||||
data: recordings,
|
||||
@@ -1032,32 +851,19 @@ const RecordingTable = () => {
|
||||
const approval = props.row.original.approval;
|
||||
if (!approval) return '-';
|
||||
|
||||
const statusColor =
|
||||
approval.action === 'APPROVED'
|
||||
? 'success'
|
||||
: approval.action === 'REJECTED'
|
||||
? 'error'
|
||||
: approval.action === 'UPDATED'
|
||||
? 'warning'
|
||||
: 'info';
|
||||
const status = approval.action;
|
||||
const statusColor = getStatusBadgeColor(status);
|
||||
|
||||
const openApprovalHistory = () => {
|
||||
setSelectedRecording(props.row.original);
|
||||
approvalHistoryModal.openModal();
|
||||
};
|
||||
const statusText = approval.step_name || getStatusText(status);
|
||||
|
||||
return (
|
||||
<Badge
|
||||
variant='soft'
|
||||
<StatusBadge
|
||||
color={statusColor}
|
||||
text={statusText}
|
||||
className={{
|
||||
badge:
|
||||
'cursor-pointer hover:opacity-80 transition-opacity whitespace-nowrap',
|
||||
badge: 'whitespace-nowrap',
|
||||
}}
|
||||
onClick={openApprovalHistory}
|
||||
>
|
||||
{approval.step_name || approval.action}
|
||||
</Badge>
|
||||
/>
|
||||
);
|
||||
},
|
||||
},
|
||||
@@ -1208,7 +1014,10 @@ const RecordingTable = () => {
|
||||
text={`Apakah anda yakin ingin approve data recording ini (${eligibleRowIds.length} data dari ${selectedRowIds.length} yang dipilih)?`}
|
||||
secondaryButton={{
|
||||
text: 'Tidak',
|
||||
onClick: () => setApprovalNotes(''),
|
||||
onClick: () => {
|
||||
setApprovalNotes('');
|
||||
approveModal.closeModal();
|
||||
},
|
||||
}}
|
||||
primaryButton={{
|
||||
text: 'Ya',
|
||||
@@ -1226,7 +1035,10 @@ const RecordingTable = () => {
|
||||
text={`Apakah anda yakin ingin reject data recording ini (${eligibleRowIds.length} data dari ${selectedRowIds.length} yang dipilih)?`}
|
||||
secondaryButton={{
|
||||
text: 'Tidak',
|
||||
onClick: () => setApprovalNotes(''),
|
||||
onClick: () => {
|
||||
setApprovalNotes('');
|
||||
rejectModal.closeModal();
|
||||
},
|
||||
}}
|
||||
primaryButton={{
|
||||
text: 'Ya',
|
||||
@@ -1237,13 +1049,6 @@ const RecordingTable = () => {
|
||||
placeholder='(Opsional) Tambahkan catatan untuk reject ini...'
|
||||
rows={3}
|
||||
/>
|
||||
|
||||
<ApprovalHistoryModal
|
||||
ref={approvalHistoryModal.ref}
|
||||
currentApproval={selectedRecording?.approval}
|
||||
module_name={'RECORDINGS'}
|
||||
module_id={selectedRecording?.id}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -241,6 +241,7 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
|
||||
new Date().toISOString().split('T')[0]
|
||||
);
|
||||
const [duplicateErrorShown, setDuplicateErrorShown] = useState(false);
|
||||
const [nextDayErrorShown, setNextDayErrorShown] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
@@ -553,6 +554,7 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
|
||||
|
||||
const nextDayRecordingUrl = useMemo(() => {
|
||||
if (!projectFlockKandangLookup) return null;
|
||||
if (!selectedRecordDate) return null;
|
||||
const projectFlockKandangId = projectFlockKandangLookup.id;
|
||||
return `${RecordingApi.basePath}/next-day?project_flock_kandang_id=${projectFlockKandangId}&record_date=${selectedRecordDate}`;
|
||||
}, [projectFlockKandangLookup, selectedRecordDate]);
|
||||
@@ -575,10 +577,28 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
|
||||
setNextDayRecording(
|
||||
nextDayRecordingData.data as unknown as NextDayRecording
|
||||
);
|
||||
if (nextDayErrorShown) {
|
||||
toast.dismiss();
|
||||
setNextDayErrorShown(false);
|
||||
}
|
||||
} else if (nextDayRecordingData?.status === 'error') {
|
||||
setNextDayRecording(null);
|
||||
if (!nextDayErrorShown) {
|
||||
toast.error(
|
||||
nextDayRecordingData.message ||
|
||||
'Terjadi kesalahan saat memuat data hari berikutnya',
|
||||
{ duration: Infinity }
|
||||
);
|
||||
setNextDayErrorShown(true);
|
||||
}
|
||||
} else {
|
||||
setNextDayRecording(null);
|
||||
if (nextDayErrorShown) {
|
||||
toast.dismiss();
|
||||
setNextDayErrorShown(false);
|
||||
}
|
||||
}
|
||||
}, [nextDayRecordingData]);
|
||||
}, [nextDayRecordingData, nextDayErrorShown]);
|
||||
|
||||
const {
|
||||
rawData: eggProductsData,
|
||||
@@ -1196,6 +1216,66 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
|
||||
[stockProducts, depletionProductsData, eggProductsData, initialValues, type]
|
||||
);
|
||||
|
||||
const getAvailableStockProductOptions = useCallback(
|
||||
(currentIdx: number) => {
|
||||
const selectedProductIds =
|
||||
formik.values.stocks
|
||||
?.filter((s, idx) => {
|
||||
return (
|
||||
idx !== currentIdx &&
|
||||
s.product_warehouse_id &&
|
||||
s.product_warehouse_id !== 0
|
||||
);
|
||||
})
|
||||
.map((s) => s.product_warehouse_id) || [];
|
||||
|
||||
return unifiedStockProducts.filter(
|
||||
(opt) => !selectedProductIds.includes(Number(opt.value))
|
||||
);
|
||||
},
|
||||
[formik.values.stocks, unifiedStockProducts]
|
||||
);
|
||||
|
||||
const getAvailableDepletionProductOptions = useCallback(
|
||||
(currentIdx: number) => {
|
||||
const selectedProductIds =
|
||||
formik.values.depletions
|
||||
?.filter((d, idx) => {
|
||||
return (
|
||||
idx !== currentIdx &&
|
||||
d.product_warehouse_id &&
|
||||
d.product_warehouse_id !== 0
|
||||
);
|
||||
})
|
||||
.map((d) => d.product_warehouse_id) || [];
|
||||
|
||||
return depletionProducts.filter(
|
||||
(opt) => !selectedProductIds.includes(Number(opt.value))
|
||||
);
|
||||
},
|
||||
[formik.values.depletions, depletionProducts]
|
||||
);
|
||||
|
||||
const getAvailableEggProductOptions = useCallback(
|
||||
(currentIdx: number) => {
|
||||
const selectedProductIds =
|
||||
(formik.values as RecordingLayingFormValues).eggs
|
||||
?.filter((e, idx) => {
|
||||
return (
|
||||
idx !== currentIdx &&
|
||||
e.product_warehouse_id &&
|
||||
e.product_warehouse_id !== 0
|
||||
);
|
||||
})
|
||||
.map((e) => e.product_warehouse_id) || [];
|
||||
|
||||
return eggProducts.filter(
|
||||
(opt) => !selectedProductIds.includes(Number(opt.value))
|
||||
);
|
||||
},
|
||||
[formik.values, eggProducts]
|
||||
);
|
||||
|
||||
const hasExceededStock = useMemo(() => {
|
||||
if ((type as 'add' | 'edit' | 'detail') === 'detail') return false;
|
||||
return (
|
||||
@@ -1255,6 +1335,10 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
|
||||
toast.dismiss();
|
||||
setDuplicateErrorShown(false);
|
||||
}
|
||||
if (nextDayErrorShown) {
|
||||
toast.dismiss();
|
||||
setNextDayErrorShown(false);
|
||||
}
|
||||
setSelectedProjectFlockLocationId(
|
||||
location ? location.value.toString() : ''
|
||||
);
|
||||
@@ -1275,6 +1359,10 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
|
||||
toast.dismiss();
|
||||
setDuplicateErrorShown(false);
|
||||
}
|
||||
if (nextDayErrorShown) {
|
||||
toast.dismiss();
|
||||
setNextDayErrorShown(false);
|
||||
}
|
||||
};
|
||||
|
||||
const kandangChangeHandler = (val: OptionType | OptionType[] | null) => {
|
||||
@@ -1291,6 +1379,10 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
|
||||
toast.dismiss();
|
||||
setDuplicateErrorShown(false);
|
||||
}
|
||||
if (nextDayErrorShown) {
|
||||
toast.dismiss();
|
||||
setNextDayErrorShown(false);
|
||||
}
|
||||
if (selectedLocation && kandang) {
|
||||
setStockProductsLocationId(selectedLocation.value.toString());
|
||||
setStockProductsKandangId(kandang.value.toString());
|
||||
@@ -1320,11 +1412,15 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
|
||||
toast.dismiss();
|
||||
setDuplicateErrorShown(false);
|
||||
}
|
||||
if (nextDayErrorShown) {
|
||||
toast.dismiss();
|
||||
setNextDayErrorShown(false);
|
||||
}
|
||||
setTimeout(() => {
|
||||
formik.validateField('project_flock_kandang_id');
|
||||
}, 0);
|
||||
},
|
||||
[formik, duplicateErrorShown]
|
||||
[formik, duplicateErrorShown, nextDayErrorShown]
|
||||
);
|
||||
|
||||
const { formErrorList, handleFormSubmit, close } = useFormikErrorList(formik);
|
||||
@@ -1350,28 +1446,6 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
|
||||
setDuplicateErrorShown(false);
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
nextDayRecording &&
|
||||
nextDayRecording.project_flock_kandang_id === projectFlockKandangId
|
||||
) {
|
||||
const hasSameDayRecording = isResponseSuccess(existingRecordings)
|
||||
? existingRecordings.data?.some(
|
||||
(recording: Recording) =>
|
||||
recording.project_flock.project_flock_kandang_id ===
|
||||
projectFlockKandangId &&
|
||||
recording.day === nextDayRecording.next_day
|
||||
)
|
||||
: false;
|
||||
|
||||
if (hasSameDayRecording) {
|
||||
toast.error(
|
||||
`Recording untuk hari ke-${nextDayRecording.next_day} sudah ada datanya.
|
||||
Tidak bisa membuat recording di hari yang sama dengan project flock yang sama, mohon perbarui recording yang sudah ada terlebih dahulu.`
|
||||
);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (formik.values.project_flock_kandang_id !== projectFlockKandangId) {
|
||||
@@ -1831,8 +1905,10 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
|
||||
<div>
|
||||
<span className='text-sm text-gray-600'>Umur</span>
|
||||
<p className='font-semibold'>
|
||||
{nextDayRecording
|
||||
? `Hari ke-${nextDayRecording.next_day} (Minggu ke-${Math.ceil(nextDayRecording.next_day / 7)})`
|
||||
{type === 'add'
|
||||
? nextDayRecording
|
||||
? `Hari ke-${nextDayRecording.next_day} (Minggu ke-${Math.ceil(nextDayRecording.next_day / 7)})`
|
||||
: '-'
|
||||
: initialValues?.day
|
||||
? `Hari ke-${initialValues.day} (Minggu ke-${Math.ceil(initialValues.day / 7)})`
|
||||
: '-'}
|
||||
@@ -2398,7 +2474,7 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
|
||||
option?.value || 0
|
||||
);
|
||||
}}
|
||||
options={unifiedStockProducts}
|
||||
options={getAvailableStockProductOptions(idx)}
|
||||
placeholder={
|
||||
!formik.values.project_flock_kandang_id
|
||||
? 'Pilih kandang terlebih dahulu'
|
||||
@@ -2619,7 +2695,7 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
|
||||
option?.value || 0
|
||||
);
|
||||
}}
|
||||
options={depletionProducts}
|
||||
options={getAvailableDepletionProductOptions(idx)}
|
||||
placeholder='Pilih Kondisi'
|
||||
isLoading={isLoadingDepletionProducts}
|
||||
onMenuScrollToBottom={loadMoreDepletionProducts}
|
||||
@@ -2837,7 +2913,7 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
|
||||
option?.value || 0
|
||||
);
|
||||
}}
|
||||
options={eggProducts}
|
||||
options={getAvailableEggProductOptions(idx)}
|
||||
placeholder='Pilih Kondisi Telur'
|
||||
isLoading={isLoadingEggProducts}
|
||||
onMenuScrollToBottom={loadMoreEggProducts}
|
||||
@@ -3116,7 +3192,10 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
|
||||
text='Apakah anda yakin ingin menyetujui data Recording ini?'
|
||||
secondaryButton={{
|
||||
text: 'Tidak',
|
||||
onClick: () => setApprovalNotes(''),
|
||||
onClick: () => {
|
||||
setApprovalNotes('');
|
||||
approveModal.closeModal();
|
||||
},
|
||||
}}
|
||||
primaryButton={{
|
||||
text: 'Ya',
|
||||
@@ -3138,7 +3217,10 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
|
||||
text='Apakah anda yakin ingin menolak data Recording ini?'
|
||||
secondaryButton={{
|
||||
text: 'Tidak',
|
||||
onClick: () => setApprovalNotes(''),
|
||||
onClick: () => {
|
||||
setApprovalNotes('');
|
||||
rejectModal.closeModal();
|
||||
},
|
||||
}}
|
||||
primaryButton={{
|
||||
text: 'Ya',
|
||||
|
||||
@@ -16,7 +16,7 @@ import RowDropdownOptions from '@/components/table/RowDropdownOptions';
|
||||
import RowCollapseOptions from '@/components/table/RowCollapseOptions';
|
||||
import RowOptionsMenuWrapper from '@/components/table/RowOptionsMenuWrapper';
|
||||
import RequirePermission from '@/components/helper/RequirePermission';
|
||||
import Badge from '@/components/Badge';
|
||||
import StatusBadge from '@/components/helper/StatusBadge';
|
||||
|
||||
import { cn, formatDate } from '@/lib/helper';
|
||||
import { isResponseSuccess } from '@/lib/api-helper';
|
||||
@@ -25,6 +25,44 @@ import { useTableFilter } from '@/services/hooks/useTableFilter';
|
||||
import { ROWS_OPTIONS } from '@/config/constant';
|
||||
import { Purchase } from '@/types/api/purchase/purchase';
|
||||
import { PurchaseApi } from '@/services/api/purchase';
|
||||
import { Color } from '@/types/theme';
|
||||
|
||||
// ===== STATUS BADGE UTILITIES =====
|
||||
const statusTextMap: Record<string, string> = {
|
||||
APPROVED: 'Disetujui',
|
||||
Disetujui: 'Disetujui',
|
||||
REJECTED: 'Ditolak',
|
||||
Ditolak: 'Ditolak',
|
||||
CREATED: 'Dibuat',
|
||||
UPDATED: 'Diperbarui',
|
||||
};
|
||||
|
||||
const getStatusText = (status: string): string => {
|
||||
return statusTextMap[status] || status;
|
||||
};
|
||||
|
||||
const statusBadgeColorMap: Record<string, Color> = {
|
||||
APPROVED: 'success',
|
||||
Disetujui: 'success',
|
||||
approved: 'success',
|
||||
disetujui: 'success',
|
||||
REJECTED: 'error',
|
||||
Ditolak: 'error',
|
||||
rejected: 'error',
|
||||
ditolak: 'error',
|
||||
CREATED: 'neutral',
|
||||
Dibuat: 'neutral',
|
||||
created: 'neutral',
|
||||
dibuat: 'neutral',
|
||||
UPDATED: 'warning',
|
||||
Diperbarui: 'warning',
|
||||
updated: 'warning',
|
||||
diperbarui: 'warning',
|
||||
};
|
||||
|
||||
const getStatusBadgeColor = (status: string): Color => {
|
||||
return statusBadgeColorMap[status] || 'neutral';
|
||||
};
|
||||
|
||||
// ===== INTERFACES =====
|
||||
interface RowOptionsMenuProps {
|
||||
@@ -160,48 +198,42 @@ const PurchaseTable = () => {
|
||||
const approval = props.row.original.latest_approval;
|
||||
if (!approval) return '-';
|
||||
|
||||
const isRejected = approval.action === 'REJECTED';
|
||||
const status = approval.action;
|
||||
|
||||
let statusColor:
|
||||
| 'warning'
|
||||
| 'success'
|
||||
| 'neutral'
|
||||
| 'error'
|
||||
| 'primary'
|
||||
| 'info' = 'neutral';
|
||||
let statusColor: Color = 'neutral';
|
||||
|
||||
switch (approval.step_number) {
|
||||
case 1:
|
||||
statusColor = 'neutral';
|
||||
break;
|
||||
case 2:
|
||||
statusColor = 'primary';
|
||||
break;
|
||||
case 3:
|
||||
statusColor = 'info';
|
||||
break;
|
||||
case 4:
|
||||
statusColor = 'warning';
|
||||
break;
|
||||
case 5:
|
||||
statusColor = 'success';
|
||||
break;
|
||||
if (status === 'REJECTED') {
|
||||
statusColor = getStatusBadgeColor(status);
|
||||
} else {
|
||||
switch (approval.step_number) {
|
||||
case 1:
|
||||
statusColor = 'neutral';
|
||||
break;
|
||||
case 2:
|
||||
statusColor = 'primary';
|
||||
break;
|
||||
case 3:
|
||||
statusColor = 'info';
|
||||
break;
|
||||
case 4:
|
||||
statusColor = 'warning';
|
||||
break;
|
||||
case 5:
|
||||
statusColor = 'success';
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (isRejected) {
|
||||
statusColor = 'error';
|
||||
}
|
||||
const statusText = approval.step_name || getStatusText(status);
|
||||
|
||||
return (
|
||||
<Badge
|
||||
variant='soft'
|
||||
<StatusBadge
|
||||
color={statusColor}
|
||||
text={statusText}
|
||||
className={{
|
||||
badge: 'whitespace-nowrap',
|
||||
}}
|
||||
>
|
||||
{isRejected ? 'Ditolak' : approval.step_name}
|
||||
</Badge>
|
||||
/>
|
||||
);
|
||||
},
|
||||
},
|
||||
@@ -369,6 +401,7 @@ const PurchaseTable = () => {
|
||||
text={`Apakah anda yakin ingin menghapus data permintaan pembelian ini?`}
|
||||
secondaryButton={{
|
||||
text: 'Tidak',
|
||||
onClick: () => deleteModal.closeModal(),
|
||||
}}
|
||||
primaryButton={{
|
||||
text: 'Ya',
|
||||
|
||||
@@ -105,6 +105,7 @@ const PurchaseOrderDetail = ({
|
||||
const [rowSelection, setRowSelection] = useState<Record<string, boolean>>({});
|
||||
const [isDeleteLoading, setIsDeleteLoading] = useState(false);
|
||||
const [selectedItem, setSelectedItem] = useState<PurchaseItem | null>(null);
|
||||
const [, setApprovalNotes] = useState('');
|
||||
|
||||
const selectedRowIds = Object.keys(rowSelection).map((item) =>
|
||||
parseInt(item)
|
||||
@@ -207,12 +208,15 @@ const PurchaseOrderDetail = ({
|
||||
|
||||
switch (approvalStep) {
|
||||
case 1:
|
||||
setApprovalNotes('');
|
||||
staffApprovalModal.openModal();
|
||||
break;
|
||||
case 2:
|
||||
setApprovalNotes('');
|
||||
confirmationModalWithNotes.openModal();
|
||||
break;
|
||||
case 3:
|
||||
setApprovalNotes('');
|
||||
acceptApprovalModal.openModal();
|
||||
break;
|
||||
default:
|
||||
@@ -225,12 +229,15 @@ const PurchaseOrderDetail = ({
|
||||
|
||||
switch (approvalStep) {
|
||||
case 1:
|
||||
setApprovalNotes('');
|
||||
staffRejectionModal.openModal();
|
||||
break;
|
||||
case 2:
|
||||
setApprovalNotes('');
|
||||
managerRejectionModal.openModal();
|
||||
break;
|
||||
case 3:
|
||||
setApprovalNotes('');
|
||||
acceptRejectionModal.openModal();
|
||||
break;
|
||||
default:
|
||||
@@ -406,6 +413,56 @@ const PurchaseOrderDetail = ({
|
||||
refetchData,
|
||||
]);
|
||||
|
||||
// ===== APPROVAL/REJECTION HANDLERS =====
|
||||
const managerApprovalHandler = async (notes: string) => {
|
||||
const payload: CreateManagerApprovalRequestPayload = {
|
||||
action: 'APPROVED',
|
||||
notes: notes || null,
|
||||
};
|
||||
|
||||
await createManagerApprovalHandler(payload);
|
||||
await refreshApprovals();
|
||||
await refetchData?.();
|
||||
setApprovalNotes('');
|
||||
confirmationModalWithNotes.closeModal();
|
||||
};
|
||||
|
||||
const staffRejectionHandler = async (notes: string) => {
|
||||
const payload: CreateStaffApprovalRequestPayload = {
|
||||
action: 'REJECTED',
|
||||
notes: notes || null,
|
||||
};
|
||||
|
||||
await createStaffApprovalHandler(payload);
|
||||
await refetchData?.();
|
||||
setApprovalNotes('');
|
||||
staffRejectionModal.closeModal();
|
||||
};
|
||||
|
||||
const acceptRejectionHandler = async (notes: string) => {
|
||||
const payload: CreateAcceptApprovalRequestPayload = {
|
||||
action: 'REJECTED',
|
||||
notes: notes || null,
|
||||
};
|
||||
|
||||
await createAcceptApprovalHandler(payload);
|
||||
await refetchData?.();
|
||||
setApprovalNotes('');
|
||||
acceptRejectionModal.closeModal();
|
||||
};
|
||||
|
||||
const managerRejectionHandler = async (notes: string) => {
|
||||
const payload: CreateManagerApprovalRequestPayload = {
|
||||
action: 'REJECTED',
|
||||
notes: notes || null,
|
||||
};
|
||||
|
||||
await createManagerApprovalHandler(payload);
|
||||
await refetchData?.();
|
||||
setApprovalNotes('');
|
||||
managerRejectionModal.closeModal();
|
||||
};
|
||||
|
||||
if (!initialValues) {
|
||||
return null;
|
||||
}
|
||||
@@ -969,20 +1026,14 @@ const PurchaseOrderDetail = ({
|
||||
primaryButton={{
|
||||
text: 'Ya, Lanjutkan',
|
||||
color: 'success',
|
||||
onClick: async (notes) => {
|
||||
const payload: CreateManagerApprovalRequestPayload = {
|
||||
action: 'APPROVED',
|
||||
notes: notes || null,
|
||||
};
|
||||
|
||||
await createManagerApprovalHandler(payload);
|
||||
await refreshApprovals();
|
||||
await refetchData?.();
|
||||
confirmationModalWithNotes.closeModal();
|
||||
},
|
||||
onClick: managerApprovalHandler,
|
||||
}}
|
||||
secondaryButton={{
|
||||
text: 'Batal',
|
||||
onClick: () => {
|
||||
setApprovalNotes('');
|
||||
confirmationModalWithNotes.closeModal();
|
||||
},
|
||||
}}
|
||||
/>
|
||||
|
||||
@@ -1071,19 +1122,14 @@ const PurchaseOrderDetail = ({
|
||||
primaryButton={{
|
||||
text: 'Ya, Tolak',
|
||||
color: 'error',
|
||||
onClick: async (notes) => {
|
||||
const payload: CreateStaffApprovalRequestPayload = {
|
||||
action: 'REJECTED',
|
||||
notes: notes || null,
|
||||
};
|
||||
|
||||
await createStaffApprovalHandler(payload);
|
||||
await refetchData?.();
|
||||
staffRejectionModal.closeModal();
|
||||
},
|
||||
onClick: staffRejectionHandler,
|
||||
}}
|
||||
secondaryButton={{
|
||||
text: 'Batal',
|
||||
onClick: () => {
|
||||
setApprovalNotes('');
|
||||
staffRejectionModal.closeModal();
|
||||
},
|
||||
}}
|
||||
/>
|
||||
|
||||
@@ -1098,19 +1144,14 @@ const PurchaseOrderDetail = ({
|
||||
primaryButton={{
|
||||
text: 'Ya, Tolak',
|
||||
color: 'error',
|
||||
onClick: async (notes) => {
|
||||
const payload: CreateAcceptApprovalRequestPayload = {
|
||||
action: 'REJECTED',
|
||||
notes: notes || null,
|
||||
};
|
||||
|
||||
await createAcceptApprovalHandler(payload);
|
||||
await refetchData?.();
|
||||
acceptRejectionModal.closeModal();
|
||||
},
|
||||
onClick: acceptRejectionHandler,
|
||||
}}
|
||||
secondaryButton={{
|
||||
text: 'Batal',
|
||||
onClick: () => {
|
||||
setApprovalNotes('');
|
||||
acceptRejectionModal.closeModal();
|
||||
},
|
||||
}}
|
||||
/>
|
||||
|
||||
@@ -1125,19 +1166,14 @@ const PurchaseOrderDetail = ({
|
||||
primaryButton={{
|
||||
text: 'Ya, Tolak',
|
||||
color: 'error',
|
||||
onClick: async (notes) => {
|
||||
const payload: CreateManagerApprovalRequestPayload = {
|
||||
action: 'REJECTED',
|
||||
notes: notes || null,
|
||||
};
|
||||
|
||||
await createManagerApprovalHandler(payload);
|
||||
await refetchData?.();
|
||||
managerRejectionModal.closeModal();
|
||||
},
|
||||
onClick: managerRejectionHandler,
|
||||
}}
|
||||
secondaryButton={{
|
||||
text: 'Batal',
|
||||
onClick: () => {
|
||||
setApprovalNotes('');
|
||||
managerRejectionModal.closeModal();
|
||||
},
|
||||
}}
|
||||
/>
|
||||
|
||||
|
||||
+25
-20
@@ -36,6 +36,7 @@ import { ColumnDef } from '@tanstack/react-table';
|
||||
import { useSelect } from '@/components/input/SelectInput';
|
||||
import { KandangApi } from '@/services/api/master-data';
|
||||
import DebouncedTextInput from '@/components/input/DebouncedTextInput';
|
||||
import RequirePermission from '@/components/helper/RequirePermission';
|
||||
|
||||
interface Kandang {
|
||||
id: string;
|
||||
@@ -389,19 +390,21 @@ export function ListDailyChecklistContent() {
|
||||
</Button>
|
||||
|
||||
{row.original.status === 'DRAFT' && (
|
||||
<Button
|
||||
size='sm'
|
||||
variant='outline'
|
||||
onClick={() => handleEdit(row.original)}
|
||||
className='border-gray-200 text-gray-700 hover:bg-gray-50'
|
||||
>
|
||||
<Edit className='w-4 h-4 mr-1' />
|
||||
Edit
|
||||
</Button>
|
||||
<RequirePermission permissions='lti.daily_checklist.create'>
|
||||
<Button
|
||||
size='sm'
|
||||
variant='outline'
|
||||
onClick={() => handleEdit(row.original)}
|
||||
className='border-gray-200 text-gray-700 hover:bg-gray-50'
|
||||
>
|
||||
<Edit className='w-4 h-4 mr-1' />
|
||||
Edit
|
||||
</Button>
|
||||
</RequirePermission>
|
||||
)}
|
||||
|
||||
{row.original.status === 'SUBMITTED' && (
|
||||
<>
|
||||
<RequirePermission permissions='lti.daily_checklist.create'>
|
||||
<Button
|
||||
size='sm'
|
||||
onClick={() => handleApprove(row.original)}
|
||||
@@ -419,19 +422,21 @@ export function ListDailyChecklistContent() {
|
||||
<XCircle className='w-4 h-4 mr-1' />
|
||||
Reject
|
||||
</Button>
|
||||
</>
|
||||
</RequirePermission>
|
||||
)}
|
||||
|
||||
{row.original.status === 'DRAFT' && (
|
||||
<Button
|
||||
size='sm'
|
||||
variant='destructive'
|
||||
onClick={() => handleDelete(row.original)}
|
||||
className='bg-red-600 hover:bg-red-700 text-white'
|
||||
>
|
||||
<Trash2 className='w-4 h-4 mr-1' />
|
||||
Hapus
|
||||
</Button>
|
||||
<RequirePermission permissions='lti.daily_checklist.create'>
|
||||
<Button
|
||||
size='sm'
|
||||
variant='destructive'
|
||||
onClick={() => handleDelete(row.original)}
|
||||
className='bg-red-600 hover:bg-red-700 text-white'
|
||||
>
|
||||
<Trash2 className='w-4 h-4 mr-1' />
|
||||
Hapus
|
||||
</Button>
|
||||
</RequirePermission>
|
||||
)}
|
||||
</div>
|
||||
),
|
||||
|
||||
+22
-19
@@ -23,6 +23,7 @@ import { isResponseError } from '@/lib/api-helper';
|
||||
import Link from 'next/link';
|
||||
import { Icon } from '@iconify/react';
|
||||
import { Document } from '@/types/api/api-general';
|
||||
import RequirePermission from '@/components/helper/RequirePermission';
|
||||
|
||||
interface ChecklistDetailRow {
|
||||
checklist_id: string;
|
||||
@@ -593,25 +594,27 @@ export function DetailDailyChecklistContent() {
|
||||
</p>
|
||||
</div>
|
||||
{header.status === 'SUBMITTED' && (
|
||||
<div className='flex gap-2'>
|
||||
<Button
|
||||
onClick={handleApprove}
|
||||
disabled={actionLoading}
|
||||
className='bg-green-600 hover:bg-green-700 text-white'
|
||||
>
|
||||
<CheckCircle className='w-4 h-4 mr-2' />
|
||||
Approve
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleReject}
|
||||
disabled={actionLoading}
|
||||
variant='destructive'
|
||||
className='bg-red-600 hover:bg-red-700 text-white'
|
||||
>
|
||||
<XCircle className='w-4 h-4 mr-2' />
|
||||
Reject
|
||||
</Button>
|
||||
</div>
|
||||
<RequirePermission permissions='lti.daily_checklist.create'>
|
||||
<div className='flex gap-2'>
|
||||
<Button
|
||||
onClick={handleApprove}
|
||||
disabled={actionLoading}
|
||||
className='bg-green-600 hover:bg-green-700 text-white'
|
||||
>
|
||||
<CheckCircle className='w-4 h-4 mr-2' />
|
||||
Approve
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleReject}
|
||||
disabled={actionLoading}
|
||||
variant='destructive'
|
||||
className='bg-red-600 hover:bg-red-700 text-white'
|
||||
>
|
||||
<XCircle className='w-4 h-4 mr-2' />
|
||||
Reject
|
||||
</Button>
|
||||
</div>
|
||||
</RequirePermission>
|
||||
)}
|
||||
</div>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user