mirror of
https://gitlab.com/mbugroup/lti-web-client.git
synced 2026-05-20 13:32:00 +00:00
Merge branch 'fix/adjustment-status-badge' into 'development'
[FIX/FE] Adjustment Usage of Status Badge (Recording, Purchase) See merge request mbugroup/lti-web-client!297
This commit is contained in:
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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,16 +198,13 @@ 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';
|
||||
|
||||
if (status === 'REJECTED') {
|
||||
statusColor = getStatusBadgeColor(status);
|
||||
} else {
|
||||
switch (approval.step_number) {
|
||||
case 1:
|
||||
statusColor = 'neutral';
|
||||
@@ -187,21 +222,18 @@ const PurchaseTable = () => {
|
||||
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',
|
||||
|
||||
Reference in New Issue
Block a user