mirror of
https://gitlab.com/mbugroup/lti-web-client.git
synced 2026-05-24 15:25:46 +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'> {
|
extends Omit<ConfirmationModalProps, 'children' | 'primaryButton'> {
|
||||||
rows?: number;
|
rows?: number;
|
||||||
placeholder?: string;
|
placeholder?: string;
|
||||||
|
onClose?: () => void;
|
||||||
|
|
||||||
primaryButton?: {
|
primaryButton?: {
|
||||||
text?: string;
|
text?: string;
|
||||||
@@ -32,6 +33,7 @@ const ConfirmationModalWithNotes: React.FC<ConfirmationModalWithNotesProps> = ({
|
|||||||
className,
|
className,
|
||||||
rows = 3,
|
rows = 3,
|
||||||
placeholder = 'Catatan...',
|
placeholder = 'Catatan...',
|
||||||
|
onClose,
|
||||||
...props
|
...props
|
||||||
}) => {
|
}) => {
|
||||||
const randomId = useId();
|
const randomId = useId();
|
||||||
@@ -41,6 +43,11 @@ const ConfirmationModalWithNotes: React.FC<ConfirmationModalWithNotesProps> = ({
|
|||||||
setNotes(e.target.value);
|
setNotes(e.target.value);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const closeModalHandler = () => {
|
||||||
|
onClose?.();
|
||||||
|
ref.current?.close();
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ConfirmationModal
|
<ConfirmationModal
|
||||||
ref={ref}
|
ref={ref}
|
||||||
@@ -49,12 +56,32 @@ const ConfirmationModalWithNotes: React.FC<ConfirmationModalWithNotesProps> = ({
|
|||||||
closeOnBackdrop={closeOnBackdrop}
|
closeOnBackdrop={closeOnBackdrop}
|
||||||
primaryButton={{
|
primaryButton={{
|
||||||
...primaryButton,
|
...primaryButton,
|
||||||
onClick: () => {
|
onClick: (e) => {
|
||||||
primaryButton?.onClick?.(notes);
|
if (primaryButton && primaryButton?.onClick) {
|
||||||
|
primaryButton?.onClick?.(notes);
|
||||||
|
} else {
|
||||||
|
closeModalHandler();
|
||||||
|
}
|
||||||
|
|
||||||
setNotes('');
|
setNotes('');
|
||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
secondaryButton={secondaryButton}
|
secondaryButton={
|
||||||
|
secondaryButton
|
||||||
|
? {
|
||||||
|
text: secondaryButton?.text ?? 'Tidak',
|
||||||
|
onClick: (e) => {
|
||||||
|
if (secondaryButton && secondaryButton?.onClick) {
|
||||||
|
secondaryButton.onClick?.(e);
|
||||||
|
} else {
|
||||||
|
closeModalHandler();
|
||||||
|
}
|
||||||
|
|
||||||
|
setNotes('');
|
||||||
|
},
|
||||||
|
}
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
className={className}
|
className={className}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -100,6 +100,7 @@ const ExpenseRequestContent = ({
|
|||||||
const [isCompleteLoading, setIsCompleteLoading] = useState(false);
|
const [isCompleteLoading, setIsCompleteLoading] = useState(false);
|
||||||
const [isApproveLoading, setIsApproveLoading] = useState(false);
|
const [isApproveLoading, setIsApproveLoading] = useState(false);
|
||||||
const [isRejectLoading, setIsRejectLoading] = useState(false);
|
const [isRejectLoading, setIsRejectLoading] = useState(false);
|
||||||
|
const [, setApprovalNotes] = useState('');
|
||||||
|
|
||||||
const formik = useFormik<UploadRequestDocumentsFormValues>({
|
const formik = useFormik<UploadRequestDocumentsFormValues>({
|
||||||
initialValues: {
|
initialValues: {
|
||||||
@@ -130,10 +131,12 @@ const ExpenseRequestContent = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const approveClickHandler = () => {
|
const approveClickHandler = () => {
|
||||||
|
setApprovalNotes('');
|
||||||
approveModal.openModal();
|
approveModal.openModal();
|
||||||
};
|
};
|
||||||
|
|
||||||
const rejectClickHandler = () => {
|
const rejectClickHandler = () => {
|
||||||
|
setApprovalNotes('');
|
||||||
rejectModal.openModal();
|
rejectModal.openModal();
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -200,6 +203,7 @@ const ExpenseRequestContent = ({
|
|||||||
approveModal.closeModal();
|
approveModal.closeModal();
|
||||||
|
|
||||||
toast.success(approveResponse?.message);
|
toast.success(approveResponse?.message);
|
||||||
|
setApprovalNotes('');
|
||||||
router.push('/expense');
|
router.push('/expense');
|
||||||
} else {
|
} else {
|
||||||
approveModal.closeModal();
|
approveModal.closeModal();
|
||||||
@@ -234,6 +238,7 @@ const ExpenseRequestContent = ({
|
|||||||
rejectModal.closeModal();
|
rejectModal.closeModal();
|
||||||
|
|
||||||
toast.success(rejectResponse.message);
|
toast.success(rejectResponse.message);
|
||||||
|
setApprovalNotes('');
|
||||||
router.push('/expense');
|
router.push('/expense');
|
||||||
} else {
|
} else {
|
||||||
rejectModal.closeModal();
|
rejectModal.closeModal();
|
||||||
@@ -710,6 +715,10 @@ const ExpenseRequestContent = ({
|
|||||||
text='Apakah anda yakin ingin approve data biaya operasional ini?'
|
text='Apakah anda yakin ingin approve data biaya operasional ini?'
|
||||||
secondaryButton={{
|
secondaryButton={{
|
||||||
text: 'Tidak',
|
text: 'Tidak',
|
||||||
|
onClick: () => {
|
||||||
|
setApprovalNotes('');
|
||||||
|
approveModal.closeModal();
|
||||||
|
},
|
||||||
}}
|
}}
|
||||||
primaryButton={{
|
primaryButton={{
|
||||||
text: 'Ya',
|
text: 'Ya',
|
||||||
@@ -725,6 +734,10 @@ const ExpenseRequestContent = ({
|
|||||||
text='Apakah anda yakin ingin reject data biaya operasional ini?'
|
text='Apakah anda yakin ingin reject data biaya operasional ini?'
|
||||||
secondaryButton={{
|
secondaryButton={{
|
||||||
text: 'Tidak',
|
text: 'Tidak',
|
||||||
|
onClick: () => {
|
||||||
|
setApprovalNotes('');
|
||||||
|
rejectModal.closeModal();
|
||||||
|
},
|
||||||
}}
|
}}
|
||||||
primaryButton={{
|
primaryButton={{
|
||||||
text: 'Ya',
|
text: 'Ya',
|
||||||
|
|||||||
@@ -185,6 +185,7 @@ const ExpensesTable = () => {
|
|||||||
const [isDeleteLoading, setIsDeleteLoading] = useState(false);
|
const [isDeleteLoading, setIsDeleteLoading] = useState(false);
|
||||||
const [isApproveLoading, setIsApproveLoading] = useState(false);
|
const [isApproveLoading, setIsApproveLoading] = useState(false);
|
||||||
const [isRejectLoading, setIsRejectLoading] = useState(false);
|
const [isRejectLoading, setIsRejectLoading] = useState(false);
|
||||||
|
const [, setApprovalNotes] = useState('');
|
||||||
|
|
||||||
const [sorting, setSorting] = useState<SortingState>([]);
|
const [sorting, setSorting] = useState<SortingState>([]);
|
||||||
const [rowSelection, setRowSelection] = useState<Record<string, boolean>>({});
|
const [rowSelection, setRowSelection] = useState<Record<string, boolean>>({});
|
||||||
@@ -342,6 +343,7 @@ const ExpensesTable = () => {
|
|||||||
[String(props.row.original.id)]: true,
|
[String(props.row.original.id)]: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
setApprovalNotes('');
|
||||||
approveModal.openModal();
|
approveModal.openModal();
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -353,6 +355,7 @@ const ExpensesTable = () => {
|
|||||||
[String(props.row.original.id)]: true,
|
[String(props.row.original.id)]: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
setApprovalNotes('');
|
||||||
rejectModal.openModal();
|
rejectModal.openModal();
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -412,10 +415,12 @@ const ExpensesTable = () => {
|
|||||||
// };
|
// };
|
||||||
|
|
||||||
const bulkApproveClickHandler = () => {
|
const bulkApproveClickHandler = () => {
|
||||||
|
setApprovalNotes('');
|
||||||
approveModal.openModal();
|
approveModal.openModal();
|
||||||
};
|
};
|
||||||
|
|
||||||
const bulkRejectClickHandler = () => {
|
const bulkRejectClickHandler = () => {
|
||||||
|
setApprovalNotes('');
|
||||||
rejectModal.openModal();
|
rejectModal.openModal();
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -468,6 +473,7 @@ const ExpensesTable = () => {
|
|||||||
`Berhasil approve ${selectedRowIds.length} data biaya operasional!`
|
`Berhasil approve ${selectedRowIds.length} data biaya operasional!`
|
||||||
);
|
);
|
||||||
|
|
||||||
|
setApprovalNotes('');
|
||||||
setRowSelection({});
|
setRowSelection({});
|
||||||
} else {
|
} else {
|
||||||
approveModal.closeModal();
|
approveModal.closeModal();
|
||||||
@@ -509,6 +515,7 @@ const ExpensesTable = () => {
|
|||||||
toast.success(
|
toast.success(
|
||||||
`Berhasil reject ${selectedRowIds.length} data biaya operasional!`
|
`Berhasil reject ${selectedRowIds.length} data biaya operasional!`
|
||||||
);
|
);
|
||||||
|
setApprovalNotes('');
|
||||||
setRowSelection({});
|
setRowSelection({});
|
||||||
} else {
|
} else {
|
||||||
rejectModal.closeModal();
|
rejectModal.closeModal();
|
||||||
@@ -787,6 +794,10 @@ const ExpensesTable = () => {
|
|||||||
text='Apakah anda yakin ingin approve data biaya operasional ini?'
|
text='Apakah anda yakin ingin approve data biaya operasional ini?'
|
||||||
secondaryButton={{
|
secondaryButton={{
|
||||||
text: 'Tidak',
|
text: 'Tidak',
|
||||||
|
onClick: () => {
|
||||||
|
setApprovalNotes('');
|
||||||
|
approveModal.closeModal();
|
||||||
|
},
|
||||||
}}
|
}}
|
||||||
primaryButton={{
|
primaryButton={{
|
||||||
text: 'Ya',
|
text: 'Ya',
|
||||||
@@ -802,6 +813,10 @@ const ExpensesTable = () => {
|
|||||||
text='Apakah anda yakin ingin reject data biaya operasional ini?'
|
text='Apakah anda yakin ingin reject data biaya operasional ini?'
|
||||||
secondaryButton={{
|
secondaryButton={{
|
||||||
text: 'Tidak',
|
text: 'Tidak',
|
||||||
|
onClick: () => {
|
||||||
|
setApprovalNotes('');
|
||||||
|
rejectModal.closeModal();
|
||||||
|
},
|
||||||
}}
|
}}
|
||||||
primaryButton={{
|
primaryButton={{
|
||||||
text: 'Ya',
|
text: 'Ya',
|
||||||
|
|||||||
@@ -7,14 +7,12 @@ import React, {
|
|||||||
useEffect,
|
useEffect,
|
||||||
useRef,
|
useRef,
|
||||||
} from 'react';
|
} from 'react';
|
||||||
import { RefObject } from 'react';
|
|
||||||
import useSWR from 'swr';
|
import useSWR from 'swr';
|
||||||
import { Icon } from '@iconify/react';
|
import { Icon } from '@iconify/react';
|
||||||
import { SortingState, CellContext } from '@tanstack/react-table';
|
import { SortingState, CellContext } from '@tanstack/react-table';
|
||||||
import { cn, formatDate, formatNumber } from '@/lib/helper';
|
import { cn, formatDate, formatNumber } from '@/lib/helper';
|
||||||
import RequirePermission from '@/components/helper/RequirePermission';
|
import RequirePermission from '@/components/helper/RequirePermission';
|
||||||
import { useModal } from '@/components/Modal';
|
import { useModal } from '@/components/Modal';
|
||||||
import Modal from '@/components/Modal';
|
|
||||||
import Button from '@/components/Button';
|
import Button from '@/components/Button';
|
||||||
import ConfirmationModal from '@/components/modal/ConfirmationModal';
|
import ConfirmationModal from '@/components/modal/ConfirmationModal';
|
||||||
import ConfirmationModalWithNotes from '@/components/modal/ConfirmationModalWithNotes';
|
import ConfirmationModalWithNotes from '@/components/modal/ConfirmationModalWithNotes';
|
||||||
@@ -28,14 +26,51 @@ import RowCollapseOptions from '@/components/table/RowCollapseOptions';
|
|||||||
import RowOptionsMenuWrapper from '@/components/table/RowOptionsMenuWrapper';
|
import RowOptionsMenuWrapper from '@/components/table/RowOptionsMenuWrapper';
|
||||||
import { type Recording } from '@/types/api/production/recording';
|
import { type Recording } from '@/types/api/production/recording';
|
||||||
import { RecordingApi } from '@/services/api/production';
|
import { RecordingApi } from '@/services/api/production';
|
||||||
import { ApprovalApi } from '@/services/api/approval';
|
|
||||||
import { isResponseSuccess } from '@/lib/api-helper';
|
import { isResponseSuccess } from '@/lib/api-helper';
|
||||||
import { useTableFilter } from '@/services/hooks/useTableFilter';
|
import { useTableFilter } from '@/services/hooks/useTableFilter';
|
||||||
import toast from 'react-hot-toast';
|
import toast from 'react-hot-toast';
|
||||||
import Badge from '@/components/Badge';
|
import Badge from '@/components/Badge';
|
||||||
|
import StatusBadge from '@/components/helper/StatusBadge';
|
||||||
import CheckboxInput from '@/components/input/CheckboxInput';
|
import CheckboxInput from '@/components/input/CheckboxInput';
|
||||||
import { useUiStore } from '@/stores/ui/ui.store';
|
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 = ({
|
const RowOptionsMenu = ({
|
||||||
type = 'dropdown',
|
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 RecordingTable = () => {
|
||||||
const { searchValue, setSearchValue, resetSearchValue } = useUiStore();
|
const { searchValue, setSearchValue, resetSearchValue } = useUiStore();
|
||||||
const previousPathRef = useRef<string | null>(null);
|
const previousPathRef = useRef<string | null>(null);
|
||||||
@@ -395,7 +215,6 @@ const RecordingTable = () => {
|
|||||||
const singleDeleteModal = useModal();
|
const singleDeleteModal = useModal();
|
||||||
const approveModal = useModal();
|
const approveModal = useModal();
|
||||||
const rejectModal = useModal();
|
const rejectModal = useModal();
|
||||||
const approvalHistoryModal = useModal();
|
|
||||||
|
|
||||||
const {
|
const {
|
||||||
data: recordings,
|
data: recordings,
|
||||||
@@ -1032,32 +851,19 @@ const RecordingTable = () => {
|
|||||||
const approval = props.row.original.approval;
|
const approval = props.row.original.approval;
|
||||||
if (!approval) return '-';
|
if (!approval) return '-';
|
||||||
|
|
||||||
const statusColor =
|
const status = approval.action;
|
||||||
approval.action === 'APPROVED'
|
const statusColor = getStatusBadgeColor(status);
|
||||||
? 'success'
|
|
||||||
: approval.action === 'REJECTED'
|
|
||||||
? 'error'
|
|
||||||
: approval.action === 'UPDATED'
|
|
||||||
? 'warning'
|
|
||||||
: 'info';
|
|
||||||
|
|
||||||
const openApprovalHistory = () => {
|
const statusText = approval.step_name || getStatusText(status);
|
||||||
setSelectedRecording(props.row.original);
|
|
||||||
approvalHistoryModal.openModal();
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Badge
|
<StatusBadge
|
||||||
variant='soft'
|
|
||||||
color={statusColor}
|
color={statusColor}
|
||||||
|
text={statusText}
|
||||||
className={{
|
className={{
|
||||||
badge:
|
badge: 'whitespace-nowrap',
|
||||||
'cursor-pointer hover:opacity-80 transition-opacity 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)?`}
|
text={`Apakah anda yakin ingin approve data recording ini (${eligibleRowIds.length} data dari ${selectedRowIds.length} yang dipilih)?`}
|
||||||
secondaryButton={{
|
secondaryButton={{
|
||||||
text: 'Tidak',
|
text: 'Tidak',
|
||||||
onClick: () => setApprovalNotes(''),
|
onClick: () => {
|
||||||
|
setApprovalNotes('');
|
||||||
|
approveModal.closeModal();
|
||||||
|
},
|
||||||
}}
|
}}
|
||||||
primaryButton={{
|
primaryButton={{
|
||||||
text: 'Ya',
|
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)?`}
|
text={`Apakah anda yakin ingin reject data recording ini (${eligibleRowIds.length} data dari ${selectedRowIds.length} yang dipilih)?`}
|
||||||
secondaryButton={{
|
secondaryButton={{
|
||||||
text: 'Tidak',
|
text: 'Tidak',
|
||||||
onClick: () => setApprovalNotes(''),
|
onClick: () => {
|
||||||
|
setApprovalNotes('');
|
||||||
|
rejectModal.closeModal();
|
||||||
|
},
|
||||||
}}
|
}}
|
||||||
primaryButton={{
|
primaryButton={{
|
||||||
text: 'Ya',
|
text: 'Ya',
|
||||||
@@ -1237,13 +1049,6 @@ const RecordingTable = () => {
|
|||||||
placeholder='(Opsional) Tambahkan catatan untuk reject ini...'
|
placeholder='(Opsional) Tambahkan catatan untuk reject ini...'
|
||||||
rows={3}
|
rows={3}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<ApprovalHistoryModal
|
|
||||||
ref={approvalHistoryModal.ref}
|
|
||||||
currentApproval={selectedRecording?.approval}
|
|
||||||
module_name={'RECORDINGS'}
|
|
||||||
module_id={selectedRecording?.id}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -241,6 +241,7 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
|
|||||||
new Date().toISOString().split('T')[0]
|
new Date().toISOString().split('T')[0]
|
||||||
);
|
);
|
||||||
const [duplicateErrorShown, setDuplicateErrorShown] = useState(false);
|
const [duplicateErrorShown, setDuplicateErrorShown] = useState(false);
|
||||||
|
const [nextDayErrorShown, setNextDayErrorShown] = useState(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
return () => {
|
return () => {
|
||||||
@@ -553,6 +554,7 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
|
|||||||
|
|
||||||
const nextDayRecordingUrl = useMemo(() => {
|
const nextDayRecordingUrl = useMemo(() => {
|
||||||
if (!projectFlockKandangLookup) return null;
|
if (!projectFlockKandangLookup) return null;
|
||||||
|
if (!selectedRecordDate) return null;
|
||||||
const projectFlockKandangId = projectFlockKandangLookup.id;
|
const projectFlockKandangId = projectFlockKandangLookup.id;
|
||||||
return `${RecordingApi.basePath}/next-day?project_flock_kandang_id=${projectFlockKandangId}&record_date=${selectedRecordDate}`;
|
return `${RecordingApi.basePath}/next-day?project_flock_kandang_id=${projectFlockKandangId}&record_date=${selectedRecordDate}`;
|
||||||
}, [projectFlockKandangLookup, selectedRecordDate]);
|
}, [projectFlockKandangLookup, selectedRecordDate]);
|
||||||
@@ -575,10 +577,28 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
|
|||||||
setNextDayRecording(
|
setNextDayRecording(
|
||||||
nextDayRecordingData.data as unknown as NextDayRecording
|
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 {
|
} else {
|
||||||
setNextDayRecording(null);
|
setNextDayRecording(null);
|
||||||
|
if (nextDayErrorShown) {
|
||||||
|
toast.dismiss();
|
||||||
|
setNextDayErrorShown(false);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}, [nextDayRecordingData]);
|
}, [nextDayRecordingData, nextDayErrorShown]);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
rawData: eggProductsData,
|
rawData: eggProductsData,
|
||||||
@@ -1196,6 +1216,66 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
|
|||||||
[stockProducts, depletionProductsData, eggProductsData, initialValues, type]
|
[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(() => {
|
const hasExceededStock = useMemo(() => {
|
||||||
if ((type as 'add' | 'edit' | 'detail') === 'detail') return false;
|
if ((type as 'add' | 'edit' | 'detail') === 'detail') return false;
|
||||||
return (
|
return (
|
||||||
@@ -1255,6 +1335,10 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
|
|||||||
toast.dismiss();
|
toast.dismiss();
|
||||||
setDuplicateErrorShown(false);
|
setDuplicateErrorShown(false);
|
||||||
}
|
}
|
||||||
|
if (nextDayErrorShown) {
|
||||||
|
toast.dismiss();
|
||||||
|
setNextDayErrorShown(false);
|
||||||
|
}
|
||||||
setSelectedProjectFlockLocationId(
|
setSelectedProjectFlockLocationId(
|
||||||
location ? location.value.toString() : ''
|
location ? location.value.toString() : ''
|
||||||
);
|
);
|
||||||
@@ -1275,6 +1359,10 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
|
|||||||
toast.dismiss();
|
toast.dismiss();
|
||||||
setDuplicateErrorShown(false);
|
setDuplicateErrorShown(false);
|
||||||
}
|
}
|
||||||
|
if (nextDayErrorShown) {
|
||||||
|
toast.dismiss();
|
||||||
|
setNextDayErrorShown(false);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const kandangChangeHandler = (val: OptionType | OptionType[] | null) => {
|
const kandangChangeHandler = (val: OptionType | OptionType[] | null) => {
|
||||||
@@ -1291,6 +1379,10 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
|
|||||||
toast.dismiss();
|
toast.dismiss();
|
||||||
setDuplicateErrorShown(false);
|
setDuplicateErrorShown(false);
|
||||||
}
|
}
|
||||||
|
if (nextDayErrorShown) {
|
||||||
|
toast.dismiss();
|
||||||
|
setNextDayErrorShown(false);
|
||||||
|
}
|
||||||
if (selectedLocation && kandang) {
|
if (selectedLocation && kandang) {
|
||||||
setStockProductsLocationId(selectedLocation.value.toString());
|
setStockProductsLocationId(selectedLocation.value.toString());
|
||||||
setStockProductsKandangId(kandang.value.toString());
|
setStockProductsKandangId(kandang.value.toString());
|
||||||
@@ -1320,11 +1412,15 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
|
|||||||
toast.dismiss();
|
toast.dismiss();
|
||||||
setDuplicateErrorShown(false);
|
setDuplicateErrorShown(false);
|
||||||
}
|
}
|
||||||
|
if (nextDayErrorShown) {
|
||||||
|
toast.dismiss();
|
||||||
|
setNextDayErrorShown(false);
|
||||||
|
}
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
formik.validateField('project_flock_kandang_id');
|
formik.validateField('project_flock_kandang_id');
|
||||||
}, 0);
|
}, 0);
|
||||||
},
|
},
|
||||||
[formik, duplicateErrorShown]
|
[formik, duplicateErrorShown, nextDayErrorShown]
|
||||||
);
|
);
|
||||||
|
|
||||||
const { formErrorList, handleFormSubmit, close } = useFormikErrorList(formik);
|
const { formErrorList, handleFormSubmit, close } = useFormikErrorList(formik);
|
||||||
@@ -1350,28 +1446,6 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
|
|||||||
setDuplicateErrorShown(false);
|
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) {
|
if (formik.values.project_flock_kandang_id !== projectFlockKandangId) {
|
||||||
@@ -1831,8 +1905,10 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
|
|||||||
<div>
|
<div>
|
||||||
<span className='text-sm text-gray-600'>Umur</span>
|
<span className='text-sm text-gray-600'>Umur</span>
|
||||||
<p className='font-semibold'>
|
<p className='font-semibold'>
|
||||||
{nextDayRecording
|
{type === 'add'
|
||||||
? `Hari ke-${nextDayRecording.next_day} (Minggu ke-${Math.ceil(nextDayRecording.next_day / 7)})`
|
? nextDayRecording
|
||||||
|
? `Hari ke-${nextDayRecording.next_day} (Minggu ke-${Math.ceil(nextDayRecording.next_day / 7)})`
|
||||||
|
: '-'
|
||||||
: initialValues?.day
|
: initialValues?.day
|
||||||
? `Hari ke-${initialValues.day} (Minggu ke-${Math.ceil(initialValues.day / 7)})`
|
? `Hari ke-${initialValues.day} (Minggu ke-${Math.ceil(initialValues.day / 7)})`
|
||||||
: '-'}
|
: '-'}
|
||||||
@@ -2398,7 +2474,7 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
|
|||||||
option?.value || 0
|
option?.value || 0
|
||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
options={unifiedStockProducts}
|
options={getAvailableStockProductOptions(idx)}
|
||||||
placeholder={
|
placeholder={
|
||||||
!formik.values.project_flock_kandang_id
|
!formik.values.project_flock_kandang_id
|
||||||
? 'Pilih kandang terlebih dahulu'
|
? 'Pilih kandang terlebih dahulu'
|
||||||
@@ -2619,7 +2695,7 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
|
|||||||
option?.value || 0
|
option?.value || 0
|
||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
options={depletionProducts}
|
options={getAvailableDepletionProductOptions(idx)}
|
||||||
placeholder='Pilih Kondisi'
|
placeholder='Pilih Kondisi'
|
||||||
isLoading={isLoadingDepletionProducts}
|
isLoading={isLoadingDepletionProducts}
|
||||||
onMenuScrollToBottom={loadMoreDepletionProducts}
|
onMenuScrollToBottom={loadMoreDepletionProducts}
|
||||||
@@ -2837,7 +2913,7 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
|
|||||||
option?.value || 0
|
option?.value || 0
|
||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
options={eggProducts}
|
options={getAvailableEggProductOptions(idx)}
|
||||||
placeholder='Pilih Kondisi Telur'
|
placeholder='Pilih Kondisi Telur'
|
||||||
isLoading={isLoadingEggProducts}
|
isLoading={isLoadingEggProducts}
|
||||||
onMenuScrollToBottom={loadMoreEggProducts}
|
onMenuScrollToBottom={loadMoreEggProducts}
|
||||||
@@ -3116,7 +3192,10 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
|
|||||||
text='Apakah anda yakin ingin menyetujui data Recording ini?'
|
text='Apakah anda yakin ingin menyetujui data Recording ini?'
|
||||||
secondaryButton={{
|
secondaryButton={{
|
||||||
text: 'Tidak',
|
text: 'Tidak',
|
||||||
onClick: () => setApprovalNotes(''),
|
onClick: () => {
|
||||||
|
setApprovalNotes('');
|
||||||
|
approveModal.closeModal();
|
||||||
|
},
|
||||||
}}
|
}}
|
||||||
primaryButton={{
|
primaryButton={{
|
||||||
text: 'Ya',
|
text: 'Ya',
|
||||||
@@ -3138,7 +3217,10 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
|
|||||||
text='Apakah anda yakin ingin menolak data Recording ini?'
|
text='Apakah anda yakin ingin menolak data Recording ini?'
|
||||||
secondaryButton={{
|
secondaryButton={{
|
||||||
text: 'Tidak',
|
text: 'Tidak',
|
||||||
onClick: () => setApprovalNotes(''),
|
onClick: () => {
|
||||||
|
setApprovalNotes('');
|
||||||
|
rejectModal.closeModal();
|
||||||
|
},
|
||||||
}}
|
}}
|
||||||
primaryButton={{
|
primaryButton={{
|
||||||
text: 'Ya',
|
text: 'Ya',
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ import RowDropdownOptions from '@/components/table/RowDropdownOptions';
|
|||||||
import RowCollapseOptions from '@/components/table/RowCollapseOptions';
|
import RowCollapseOptions from '@/components/table/RowCollapseOptions';
|
||||||
import RowOptionsMenuWrapper from '@/components/table/RowOptionsMenuWrapper';
|
import RowOptionsMenuWrapper from '@/components/table/RowOptionsMenuWrapper';
|
||||||
import RequirePermission from '@/components/helper/RequirePermission';
|
import RequirePermission from '@/components/helper/RequirePermission';
|
||||||
import Badge from '@/components/Badge';
|
import StatusBadge from '@/components/helper/StatusBadge';
|
||||||
|
|
||||||
import { cn, formatDate } from '@/lib/helper';
|
import { cn, formatDate } from '@/lib/helper';
|
||||||
import { isResponseSuccess } from '@/lib/api-helper';
|
import { isResponseSuccess } from '@/lib/api-helper';
|
||||||
@@ -25,6 +25,44 @@ import { useTableFilter } from '@/services/hooks/useTableFilter';
|
|||||||
import { ROWS_OPTIONS } from '@/config/constant';
|
import { ROWS_OPTIONS } from '@/config/constant';
|
||||||
import { Purchase } from '@/types/api/purchase/purchase';
|
import { Purchase } from '@/types/api/purchase/purchase';
|
||||||
import { PurchaseApi } from '@/services/api/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 =====
|
// ===== INTERFACES =====
|
||||||
interface RowOptionsMenuProps {
|
interface RowOptionsMenuProps {
|
||||||
@@ -160,48 +198,42 @@ const PurchaseTable = () => {
|
|||||||
const approval = props.row.original.latest_approval;
|
const approval = props.row.original.latest_approval;
|
||||||
if (!approval) return '-';
|
if (!approval) return '-';
|
||||||
|
|
||||||
const isRejected = approval.action === 'REJECTED';
|
const status = approval.action;
|
||||||
|
|
||||||
let statusColor:
|
let statusColor: Color = 'neutral';
|
||||||
| 'warning'
|
|
||||||
| 'success'
|
|
||||||
| 'neutral'
|
|
||||||
| 'error'
|
|
||||||
| 'primary'
|
|
||||||
| 'info' = 'neutral';
|
|
||||||
|
|
||||||
switch (approval.step_number) {
|
if (status === 'REJECTED') {
|
||||||
case 1:
|
statusColor = getStatusBadgeColor(status);
|
||||||
statusColor = 'neutral';
|
} else {
|
||||||
break;
|
switch (approval.step_number) {
|
||||||
case 2:
|
case 1:
|
||||||
statusColor = 'primary';
|
statusColor = 'neutral';
|
||||||
break;
|
break;
|
||||||
case 3:
|
case 2:
|
||||||
statusColor = 'info';
|
statusColor = 'primary';
|
||||||
break;
|
break;
|
||||||
case 4:
|
case 3:
|
||||||
statusColor = 'warning';
|
statusColor = 'info';
|
||||||
break;
|
break;
|
||||||
case 5:
|
case 4:
|
||||||
statusColor = 'success';
|
statusColor = 'warning';
|
||||||
break;
|
break;
|
||||||
|
case 5:
|
||||||
|
statusColor = 'success';
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isRejected) {
|
const statusText = approval.step_name || getStatusText(status);
|
||||||
statusColor = 'error';
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Badge
|
<StatusBadge
|
||||||
variant='soft'
|
|
||||||
color={statusColor}
|
color={statusColor}
|
||||||
|
text={statusText}
|
||||||
className={{
|
className={{
|
||||||
badge: 'whitespace-nowrap',
|
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?`}
|
text={`Apakah anda yakin ingin menghapus data permintaan pembelian ini?`}
|
||||||
secondaryButton={{
|
secondaryButton={{
|
||||||
text: 'Tidak',
|
text: 'Tidak',
|
||||||
|
onClick: () => deleteModal.closeModal(),
|
||||||
}}
|
}}
|
||||||
primaryButton={{
|
primaryButton={{
|
||||||
text: 'Ya',
|
text: 'Ya',
|
||||||
|
|||||||
@@ -105,6 +105,7 @@ const PurchaseOrderDetail = ({
|
|||||||
const [rowSelection, setRowSelection] = useState<Record<string, boolean>>({});
|
const [rowSelection, setRowSelection] = useState<Record<string, boolean>>({});
|
||||||
const [isDeleteLoading, setIsDeleteLoading] = useState(false);
|
const [isDeleteLoading, setIsDeleteLoading] = useState(false);
|
||||||
const [selectedItem, setSelectedItem] = useState<PurchaseItem | null>(null);
|
const [selectedItem, setSelectedItem] = useState<PurchaseItem | null>(null);
|
||||||
|
const [, setApprovalNotes] = useState('');
|
||||||
|
|
||||||
const selectedRowIds = Object.keys(rowSelection).map((item) =>
|
const selectedRowIds = Object.keys(rowSelection).map((item) =>
|
||||||
parseInt(item)
|
parseInt(item)
|
||||||
@@ -207,12 +208,15 @@ const PurchaseOrderDetail = ({
|
|||||||
|
|
||||||
switch (approvalStep) {
|
switch (approvalStep) {
|
||||||
case 1:
|
case 1:
|
||||||
|
setApprovalNotes('');
|
||||||
staffApprovalModal.openModal();
|
staffApprovalModal.openModal();
|
||||||
break;
|
break;
|
||||||
case 2:
|
case 2:
|
||||||
|
setApprovalNotes('');
|
||||||
confirmationModalWithNotes.openModal();
|
confirmationModalWithNotes.openModal();
|
||||||
break;
|
break;
|
||||||
case 3:
|
case 3:
|
||||||
|
setApprovalNotes('');
|
||||||
acceptApprovalModal.openModal();
|
acceptApprovalModal.openModal();
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
@@ -225,12 +229,15 @@ const PurchaseOrderDetail = ({
|
|||||||
|
|
||||||
switch (approvalStep) {
|
switch (approvalStep) {
|
||||||
case 1:
|
case 1:
|
||||||
|
setApprovalNotes('');
|
||||||
staffRejectionModal.openModal();
|
staffRejectionModal.openModal();
|
||||||
break;
|
break;
|
||||||
case 2:
|
case 2:
|
||||||
|
setApprovalNotes('');
|
||||||
managerRejectionModal.openModal();
|
managerRejectionModal.openModal();
|
||||||
break;
|
break;
|
||||||
case 3:
|
case 3:
|
||||||
|
setApprovalNotes('');
|
||||||
acceptRejectionModal.openModal();
|
acceptRejectionModal.openModal();
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
@@ -406,6 +413,56 @@ const PurchaseOrderDetail = ({
|
|||||||
refetchData,
|
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) {
|
if (!initialValues) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -969,20 +1026,14 @@ const PurchaseOrderDetail = ({
|
|||||||
primaryButton={{
|
primaryButton={{
|
||||||
text: 'Ya, Lanjutkan',
|
text: 'Ya, Lanjutkan',
|
||||||
color: 'success',
|
color: 'success',
|
||||||
onClick: async (notes) => {
|
onClick: managerApprovalHandler,
|
||||||
const payload: CreateManagerApprovalRequestPayload = {
|
|
||||||
action: 'APPROVED',
|
|
||||||
notes: notes || null,
|
|
||||||
};
|
|
||||||
|
|
||||||
await createManagerApprovalHandler(payload);
|
|
||||||
await refreshApprovals();
|
|
||||||
await refetchData?.();
|
|
||||||
confirmationModalWithNotes.closeModal();
|
|
||||||
},
|
|
||||||
}}
|
}}
|
||||||
secondaryButton={{
|
secondaryButton={{
|
||||||
text: 'Batal',
|
text: 'Batal',
|
||||||
|
onClick: () => {
|
||||||
|
setApprovalNotes('');
|
||||||
|
confirmationModalWithNotes.closeModal();
|
||||||
|
},
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
@@ -1071,19 +1122,14 @@ const PurchaseOrderDetail = ({
|
|||||||
primaryButton={{
|
primaryButton={{
|
||||||
text: 'Ya, Tolak',
|
text: 'Ya, Tolak',
|
||||||
color: 'error',
|
color: 'error',
|
||||||
onClick: async (notes) => {
|
onClick: staffRejectionHandler,
|
||||||
const payload: CreateStaffApprovalRequestPayload = {
|
|
||||||
action: 'REJECTED',
|
|
||||||
notes: notes || null,
|
|
||||||
};
|
|
||||||
|
|
||||||
await createStaffApprovalHandler(payload);
|
|
||||||
await refetchData?.();
|
|
||||||
staffRejectionModal.closeModal();
|
|
||||||
},
|
|
||||||
}}
|
}}
|
||||||
secondaryButton={{
|
secondaryButton={{
|
||||||
text: 'Batal',
|
text: 'Batal',
|
||||||
|
onClick: () => {
|
||||||
|
setApprovalNotes('');
|
||||||
|
staffRejectionModal.closeModal();
|
||||||
|
},
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
@@ -1098,19 +1144,14 @@ const PurchaseOrderDetail = ({
|
|||||||
primaryButton={{
|
primaryButton={{
|
||||||
text: 'Ya, Tolak',
|
text: 'Ya, Tolak',
|
||||||
color: 'error',
|
color: 'error',
|
||||||
onClick: async (notes) => {
|
onClick: acceptRejectionHandler,
|
||||||
const payload: CreateAcceptApprovalRequestPayload = {
|
|
||||||
action: 'REJECTED',
|
|
||||||
notes: notes || null,
|
|
||||||
};
|
|
||||||
|
|
||||||
await createAcceptApprovalHandler(payload);
|
|
||||||
await refetchData?.();
|
|
||||||
acceptRejectionModal.closeModal();
|
|
||||||
},
|
|
||||||
}}
|
}}
|
||||||
secondaryButton={{
|
secondaryButton={{
|
||||||
text: 'Batal',
|
text: 'Batal',
|
||||||
|
onClick: () => {
|
||||||
|
setApprovalNotes('');
|
||||||
|
acceptRejectionModal.closeModal();
|
||||||
|
},
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
@@ -1125,19 +1166,14 @@ const PurchaseOrderDetail = ({
|
|||||||
primaryButton={{
|
primaryButton={{
|
||||||
text: 'Ya, Tolak',
|
text: 'Ya, Tolak',
|
||||||
color: 'error',
|
color: 'error',
|
||||||
onClick: async (notes) => {
|
onClick: managerRejectionHandler,
|
||||||
const payload: CreateManagerApprovalRequestPayload = {
|
|
||||||
action: 'REJECTED',
|
|
||||||
notes: notes || null,
|
|
||||||
};
|
|
||||||
|
|
||||||
await createManagerApprovalHandler(payload);
|
|
||||||
await refetchData?.();
|
|
||||||
managerRejectionModal.closeModal();
|
|
||||||
},
|
|
||||||
}}
|
}}
|
||||||
secondaryButton={{
|
secondaryButton={{
|
||||||
text: 'Batal',
|
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 { useSelect } from '@/components/input/SelectInput';
|
||||||
import { KandangApi } from '@/services/api/master-data';
|
import { KandangApi } from '@/services/api/master-data';
|
||||||
import DebouncedTextInput from '@/components/input/DebouncedTextInput';
|
import DebouncedTextInput from '@/components/input/DebouncedTextInput';
|
||||||
|
import RequirePermission from '@/components/helper/RequirePermission';
|
||||||
|
|
||||||
interface Kandang {
|
interface Kandang {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -389,19 +390,21 @@ export function ListDailyChecklistContent() {
|
|||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
{row.original.status === 'DRAFT' && (
|
{row.original.status === 'DRAFT' && (
|
||||||
<Button
|
<RequirePermission permissions='lti.daily_checklist.create'>
|
||||||
size='sm'
|
<Button
|
||||||
variant='outline'
|
size='sm'
|
||||||
onClick={() => handleEdit(row.original)}
|
variant='outline'
|
||||||
className='border-gray-200 text-gray-700 hover:bg-gray-50'
|
onClick={() => handleEdit(row.original)}
|
||||||
>
|
className='border-gray-200 text-gray-700 hover:bg-gray-50'
|
||||||
<Edit className='w-4 h-4 mr-1' />
|
>
|
||||||
Edit
|
<Edit className='w-4 h-4 mr-1' />
|
||||||
</Button>
|
Edit
|
||||||
|
</Button>
|
||||||
|
</RequirePermission>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{row.original.status === 'SUBMITTED' && (
|
{row.original.status === 'SUBMITTED' && (
|
||||||
<>
|
<RequirePermission permissions='lti.daily_checklist.create'>
|
||||||
<Button
|
<Button
|
||||||
size='sm'
|
size='sm'
|
||||||
onClick={() => handleApprove(row.original)}
|
onClick={() => handleApprove(row.original)}
|
||||||
@@ -419,19 +422,21 @@ export function ListDailyChecklistContent() {
|
|||||||
<XCircle className='w-4 h-4 mr-1' />
|
<XCircle className='w-4 h-4 mr-1' />
|
||||||
Reject
|
Reject
|
||||||
</Button>
|
</Button>
|
||||||
</>
|
</RequirePermission>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{row.original.status === 'DRAFT' && (
|
{row.original.status === 'DRAFT' && (
|
||||||
<Button
|
<RequirePermission permissions='lti.daily_checklist.create'>
|
||||||
size='sm'
|
<Button
|
||||||
variant='destructive'
|
size='sm'
|
||||||
onClick={() => handleDelete(row.original)}
|
variant='destructive'
|
||||||
className='bg-red-600 hover:bg-red-700 text-white'
|
onClick={() => handleDelete(row.original)}
|
||||||
>
|
className='bg-red-600 hover:bg-red-700 text-white'
|
||||||
<Trash2 className='w-4 h-4 mr-1' />
|
>
|
||||||
Hapus
|
<Trash2 className='w-4 h-4 mr-1' />
|
||||||
</Button>
|
Hapus
|
||||||
|
</Button>
|
||||||
|
</RequirePermission>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
),
|
),
|
||||||
|
|||||||
+22
-19
@@ -23,6 +23,7 @@ import { isResponseError } from '@/lib/api-helper';
|
|||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
import { Icon } from '@iconify/react';
|
import { Icon } from '@iconify/react';
|
||||||
import { Document } from '@/types/api/api-general';
|
import { Document } from '@/types/api/api-general';
|
||||||
|
import RequirePermission from '@/components/helper/RequirePermission';
|
||||||
|
|
||||||
interface ChecklistDetailRow {
|
interface ChecklistDetailRow {
|
||||||
checklist_id: string;
|
checklist_id: string;
|
||||||
@@ -593,25 +594,27 @@ export function DetailDailyChecklistContent() {
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
{header.status === 'SUBMITTED' && (
|
{header.status === 'SUBMITTED' && (
|
||||||
<div className='flex gap-2'>
|
<RequirePermission permissions='lti.daily_checklist.create'>
|
||||||
<Button
|
<div className='flex gap-2'>
|
||||||
onClick={handleApprove}
|
<Button
|
||||||
disabled={actionLoading}
|
onClick={handleApprove}
|
||||||
className='bg-green-600 hover:bg-green-700 text-white'
|
disabled={actionLoading}
|
||||||
>
|
className='bg-green-600 hover:bg-green-700 text-white'
|
||||||
<CheckCircle className='w-4 h-4 mr-2' />
|
>
|
||||||
Approve
|
<CheckCircle className='w-4 h-4 mr-2' />
|
||||||
</Button>
|
Approve
|
||||||
<Button
|
</Button>
|
||||||
onClick={handleReject}
|
<Button
|
||||||
disabled={actionLoading}
|
onClick={handleReject}
|
||||||
variant='destructive'
|
disabled={actionLoading}
|
||||||
className='bg-red-600 hover:bg-red-700 text-white'
|
variant='destructive'
|
||||||
>
|
className='bg-red-600 hover:bg-red-700 text-white'
|
||||||
<XCircle className='w-4 h-4 mr-2' />
|
>
|
||||||
Reject
|
<XCircle className='w-4 h-4 mr-2' />
|
||||||
</Button>
|
Reject
|
||||||
</div>
|
</Button>
|
||||||
|
</div>
|
||||||
|
</RequirePermission>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user