bug fixed
This commit is contained in:
parent
f5ebecdfbb
commit
a136e2415f
@ -2,12 +2,20 @@ import React, { useState, useEffect, useMemo } from 'react';
|
|||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
import Table from '@/components/common/Table';
|
import Table from '@/components/common/Table';
|
||||||
import apiClient from '../../services/api/apiClient.js';
|
import apiClient from '../../services/api/apiClient.js';
|
||||||
import { CheckCircle2, XCircle, Eye } from 'lucide-react';
|
import { CheckCircle2, XCircle, Eye, X } from 'lucide-react';
|
||||||
|
import { toast } from 'react-toastify';
|
||||||
|
|
||||||
const SubmissionTable = ({ selectedQuarter, selectedYear }) => {
|
const SubmissionTable = ({ selectedQuarter, selectedYear }) => {
|
||||||
const [data, setData] = useState([]);
|
const [data, setData] = useState([]);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [currentPage, setCurrentPage] = React.useState(1);
|
const [currentPage, setCurrentPage] = React.useState(1);
|
||||||
|
const [showApproveModal, setShowApproveModal] = useState(false);
|
||||||
|
const [showRejectModal, setShowRejectModal] = useState(false);
|
||||||
|
const [selectedSubmission, setSelectedSubmission] = useState(null);
|
||||||
|
const [actionType, setActionType] = useState('');
|
||||||
|
const [rejectionReason, setRejectionReason] = useState('');
|
||||||
|
const [showRejectError, setShowRejectError] = useState(false);
|
||||||
|
const [isProcessing, setIsProcessing] = useState(false);
|
||||||
const pageSize = 10;
|
const pageSize = 10;
|
||||||
|
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
@ -118,6 +126,123 @@ const SubmissionTable = ({ selectedQuarter, selectedYear }) => {
|
|||||||
|
|
||||||
const userProfile = JSON.parse(sessionStorage.getItem("user_profile"));
|
const userProfile = JSON.parse(sessionStorage.getItem("user_profile"));
|
||||||
|
|
||||||
|
const handleActionClick = (submission, type) => {
|
||||||
|
setSelectedSubmission(submission);
|
||||||
|
setActionType(type);
|
||||||
|
if (type === 'approve') {
|
||||||
|
setShowApproveModal(true);
|
||||||
|
} else {
|
||||||
|
setRejectionReason('');
|
||||||
|
setShowRejectError(false);
|
||||||
|
setShowRejectModal(true);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCloseApproveModal = () => {
|
||||||
|
setShowApproveModal(false);
|
||||||
|
setSelectedSubmission(null);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCloseRejectModal = () => {
|
||||||
|
setShowRejectModal(false);
|
||||||
|
setSelectedSubmission(null);
|
||||||
|
setRejectionReason('');
|
||||||
|
setShowRejectError(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleApproveConfirm = async () => {
|
||||||
|
if (!selectedSubmission || isProcessing) return;
|
||||||
|
|
||||||
|
setIsProcessing(true);
|
||||||
|
try {
|
||||||
|
const response = await apiClient.put(`/approveOrRejectSubmission/${selectedSubmission.id}`, {
|
||||||
|
approve_reject_status: 1,
|
||||||
|
remarks: ''
|
||||||
|
});
|
||||||
|
|
||||||
|
const ok = (response && response.data) || response?.status === 200 || response?.status === 'success';
|
||||||
|
|
||||||
|
if (ok) {
|
||||||
|
toast.success('Submission approved successfully');
|
||||||
|
|
||||||
|
// Update the status of the current item in the data array
|
||||||
|
setData(prevData =>
|
||||||
|
prevData.map(item =>
|
||||||
|
item.id === selectedSubmission.id
|
||||||
|
? {
|
||||||
|
...item,
|
||||||
|
status: 'Approved',
|
||||||
|
reviewer: JSON.parse(sessionStorage.getItem("user_profile"))?.name || 'Admin',
|
||||||
|
reviewerOn: new Date().toISOString()
|
||||||
|
}
|
||||||
|
: item
|
||||||
|
)
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
throw new Error('Failed to approve submission');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error approving submission:', error);
|
||||||
|
toast.error(error.message || 'Failed to approve submission');
|
||||||
|
} finally {
|
||||||
|
setIsProcessing(false);
|
||||||
|
setShowApproveModal(false);
|
||||||
|
setSelectedSubmission(null);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleRejectConfirm = async () => {
|
||||||
|
if (!selectedSubmission || isProcessing) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate the rejection reason
|
||||||
|
if (!rejectionReason.trim()) {
|
||||||
|
setShowRejectError(true);
|
||||||
|
toast.error('Please provide a reason for rejection');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsProcessing(true);
|
||||||
|
try {
|
||||||
|
const response = await apiClient.put(`/approveOrRejectSubmission/${selectedSubmission.id}`, {
|
||||||
|
approve_reject_status: 0,
|
||||||
|
remarks: rejectionReason.trim()
|
||||||
|
});
|
||||||
|
|
||||||
|
const ok = (response && response.data) || response?.status === 200 || response?.status === 'success';
|
||||||
|
|
||||||
|
if (ok) {
|
||||||
|
toast.error('Submission has been rejected');
|
||||||
|
|
||||||
|
// Update the status of the current item in the data array
|
||||||
|
setData(prevData =>
|
||||||
|
prevData.map(item =>
|
||||||
|
item.id === selectedSubmission.id
|
||||||
|
? {
|
||||||
|
...item,
|
||||||
|
status: 'Rejected',
|
||||||
|
reviewer: JSON.parse(sessionStorage.getItem("user_profile"))?.name || 'Admin',
|
||||||
|
reviewerOn: new Date().toISOString()
|
||||||
|
}
|
||||||
|
: item
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
setRejectionReason('');
|
||||||
|
} else {
|
||||||
|
throw new Error('Failed to reject submission');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error rejecting submission:', error);
|
||||||
|
toast.error(error.message || 'Failed to reject submission');
|
||||||
|
} finally {
|
||||||
|
setIsProcessing(false);
|
||||||
|
setShowRejectModal(false);
|
||||||
|
setSelectedSubmission(null);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const headers = [
|
const headers = [
|
||||||
'Establishment',
|
'Establishment',
|
||||||
'Emirates',
|
'Emirates',
|
||||||
@ -195,30 +320,22 @@ const SubmissionTable = ({ selectedQuarter, selectedYear }) => {
|
|||||||
<div className="relative group">
|
<div className="relative group">
|
||||||
<CheckCircle2
|
<CheckCircle2
|
||||||
size={22}
|
size={22}
|
||||||
className="text-green-600 cursor-pointer hover:scale-110 transition-transform"
|
className={`${item.status?.toLowerCase() === 'approved' ? 'text-gray-400 cursor-not-allowed' : 'text-green-600 cursor-pointer hover:scale-110'} transition-transform`}
|
||||||
onClick={() =>
|
onClick={item.status?.toLowerCase() !== 'approved' ? () => handleActionClick(item, 'approve') : undefined}
|
||||||
navigate(`/admin/validations/${item.id}`, {
|
|
||||||
state: { actionType: 'approve' },
|
|
||||||
})
|
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
<span className="absolute bottom-full mb-1 left-1/2 -translate-x-1/2 bg-black text-white text-xs px-2 py-[2px] rounded opacity-0 group-hover:opacity-100 transition">
|
<span className="absolute bottom-full mb-1 left-1/2 -translate-x-1/2 bg-black text-white text-xs px-2 py-[2px] rounded opacity-0 group-hover:opacity-100 transition">
|
||||||
Approve
|
{item.status?.toLowerCase() === 'approved' ? 'Already Approved' : 'Approve'}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="relative group">
|
<div className="relative group">
|
||||||
<XCircle
|
<XCircle
|
||||||
size={22}
|
size={22}
|
||||||
className="text-red-600 cursor-pointer hover:scale-110 transition-transform"
|
className={`${item.status?.toLowerCase() === 'rejected' ? 'text-gray-400 cursor-not-allowed' : 'text-red-600 cursor-pointer hover:scale-110'} transition-transform`}
|
||||||
onClick={() =>
|
onClick={item.status?.toLowerCase() !== 'rejected' ? () => handleActionClick(item, 'reject') : undefined}
|
||||||
navigate(`/admin/validations/${item.id}`, {
|
|
||||||
state: { actionType: 'reject' },
|
|
||||||
})
|
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
<span className="absolute bottom-full mb-1 left-1/2 -translate-x-1/2 bg-black text-white text-xs px-2 py-[2px] rounded opacity-0 group-hover:opacity-100 transition">
|
<span className="absolute bottom-full mb-1 left-1/2 -translate-x-1/2 bg-black text-white text-xs px-2 py-[2px] rounded opacity-0 group-hover:opacity-100 transition">
|
||||||
Reject
|
{item.status?.toLowerCase() === 'rejected' ? 'Already Rejected' : 'Reject'}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -259,6 +376,135 @@ const SubmissionTable = ({ selectedQuarter, selectedYear }) => {
|
|||||||
View All Submissions
|
View All Submissions
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Approve Confirmation Modal */}
|
||||||
|
{showApproveModal && selectedSubmission && (
|
||||||
|
<div className="fixed inset-0 flex items-center justify-center z-50 p-4 bg-black/30">
|
||||||
|
<div className="bg-white rounded-lg w-full max-w-lg border border-gray-200 shadow-lg">
|
||||||
|
<div className="flex items-center justify-between h-12 p-4 border-b border-gray-200">
|
||||||
|
<h3 className="text-base font-medium text-gray-900">Approve this submission?</h3>
|
||||||
|
<button
|
||||||
|
onClick={handleCloseApproveModal}
|
||||||
|
className="text-gray-400 hover:text-gray-500"
|
||||||
|
>
|
||||||
|
<X size={18} />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div className="p-6">
|
||||||
|
<p className="text-sm text-gray-600 mb-4">
|
||||||
|
You're about to approve <span className="font-medium text-gray-900">{selectedSubmission.establishment?.factory_name}</span> for <span className="font-medium text-gray-900">{selectedSubmission.quarter} {selectedSubmission.year}</span>.
|
||||||
|
</p>
|
||||||
|
<p className="text-sm text-gray-600 mb-6">
|
||||||
|
This will lock the record for the establishment.
|
||||||
|
</p>
|
||||||
|
<div className="flex justify-end gap-3">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={handleCloseApproveModal}
|
||||||
|
className="px-4 py-2 text-sm font-medium text-[#92722A] bg-white border border-[#92722A] rounded-md shadow-sm hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-[#92722A]"
|
||||||
|
disabled={isProcessing}
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={handleApproveConfirm}
|
||||||
|
className="px-4 py-2 text-sm font-medium text-white bg-[#92722A] border border-transparent rounded-md shadow-sm hover:bg-[#7c5e24] focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-[#92722A] flex items-center gap-2"
|
||||||
|
disabled={isProcessing}
|
||||||
|
>
|
||||||
|
{isProcessing ? (
|
||||||
|
<>
|
||||||
|
<svg className="animate-spin h-4 w-4 text-white" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
|
||||||
|
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
|
||||||
|
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8v8z"></path>
|
||||||
|
</svg>
|
||||||
|
Approving...
|
||||||
|
</>
|
||||||
|
) : 'Approve'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Reject Confirmation Modal */}
|
||||||
|
{showRejectModal && selectedSubmission && (
|
||||||
|
<div className="fixed inset-0 flex items-center justify-center z-50 p-4 bg-black/30">
|
||||||
|
<div className="bg-white rounded-lg w-full max-w-lg border border-gray-200 shadow-lg">
|
||||||
|
<div className="flex items-center justify-between h-12 p-4 border-b border-gray-200">
|
||||||
|
<h3 className="text-base font-medium text-gray-900">Reject this submission?</h3>
|
||||||
|
<button
|
||||||
|
onClick={handleCloseRejectModal}
|
||||||
|
className="text-gray-400 hover:text-gray-500"
|
||||||
|
>
|
||||||
|
<X size={18} />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div className="p-6">
|
||||||
|
<p className="text-sm text-gray-600 leading-6 mb-2">
|
||||||
|
You're about to return <span className="font-medium text-gray-900">{selectedSubmission.establishment?.factory_name}</span>'s <span className="font-medium text-gray-900">{selectedSubmission.quarter} {selectedSubmission.year}</span> submission for correction.
|
||||||
|
</p>
|
||||||
|
<p className="text-sm text-gray-600 leading-6 mb-3">
|
||||||
|
The status will change to Rejected and the establishment can resubmit within the survey window.
|
||||||
|
</p>
|
||||||
|
<div className="mb-0">
|
||||||
|
<label htmlFor="rejectReason" className="block text-sm font-medium text-gray-700 mb-2">
|
||||||
|
Reason for rejection: <span className="text-red-500">*</span>
|
||||||
|
</label>
|
||||||
|
<textarea
|
||||||
|
id="rejectReason"
|
||||||
|
rows={4}
|
||||||
|
className={`w-full px-3 py-2 border ${showRejectError && !rejectionReason.trim() ? 'border-red-500' : 'border-gray-300'} rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-[#92722A] focus:border-[#92722A] text-sm`}
|
||||||
|
placeholder="Please provide a reason for rejection"
|
||||||
|
value={rejectionReason}
|
||||||
|
onChange={(e) => {
|
||||||
|
setRejectionReason(e.target.value);
|
||||||
|
if (e.target.value.trim() && showRejectError) {
|
||||||
|
setShowRejectError(false);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
onBlur={() => {
|
||||||
|
if (!rejectionReason.trim()) {
|
||||||
|
setShowRejectError(true);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
disabled={isProcessing}
|
||||||
|
/>
|
||||||
|
{showRejectError && !rejectionReason.trim() && (
|
||||||
|
<p className="mt-1 text-sm text-red-600">Reason for rejection is required</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="mt-4 flex justify-end gap-3">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={handleCloseRejectModal}
|
||||||
|
className="px-4 py-2 text-sm font-medium text-[#92722A] bg-white border border-[#92722A] rounded-md shadow-sm hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-[#92722A]"
|
||||||
|
disabled={isProcessing}
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={handleRejectConfirm}
|
||||||
|
className="px-4 py-2 text-sm font-medium text-white bg-[#92722A] border border-transparent rounded-md shadow-sm hover:bg-[#7c5e24] focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-[#92722A] flex items-center gap-2"
|
||||||
|
disabled={isProcessing || !rejectionReason.trim()}
|
||||||
|
>
|
||||||
|
{isProcessing ? (
|
||||||
|
<>
|
||||||
|
<svg className="animate-spin h-4 w-4 text-white" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
|
||||||
|
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
|
||||||
|
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8v8z"></path>
|
||||||
|
</svg>
|
||||||
|
Rejecting...
|
||||||
|
</>
|
||||||
|
) : 'Reject'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -32,7 +32,7 @@ const AdminDashboard = () => {
|
|||||||
const isMounted = useRef(false);
|
const isMounted = useRef(false);
|
||||||
const prevParams = useRef({ quarter: null, year: null });
|
const prevParams = useRef({ quarter: null, year: null });
|
||||||
|
|
||||||
const fetchDashboardData = React.useCallback(async (quarter, year) => {
|
const fetchDashboardData = React.useCallback(async (quarter = 'All', year = 'All') => {
|
||||||
// Skip if params haven't changed
|
// Skip if params haven't changed
|
||||||
if (prevParams.current.quarter === quarter && prevParams.current.year === year) {
|
if (prevParams.current.quarter === quarter && prevParams.current.year === year) {
|
||||||
return;
|
return;
|
||||||
@ -45,6 +45,7 @@ const AdminDashboard = () => {
|
|||||||
setLoading(true);
|
setLoading(true);
|
||||||
const params = new URLSearchParams();
|
const params = new URLSearchParams();
|
||||||
|
|
||||||
|
// Only add quarter and year to params if they are not 'All'
|
||||||
if (quarter && quarter !== 'All') {
|
if (quarter && quarter !== 'All') {
|
||||||
params.append('quarter', quarter);
|
params.append('quarter', quarter);
|
||||||
}
|
}
|
||||||
@ -58,8 +59,6 @@ const AdminDashboard = () => {
|
|||||||
if (!isMounted.current) return;
|
if (!isMounted.current) return;
|
||||||
|
|
||||||
const data = response?.data?.summary || {};
|
const data = response?.data?.summary || {};
|
||||||
const selectedQuarterFromApi = response?.data?.selected_quarter || quarter || 'All';
|
|
||||||
const selectedYearFromApi = response?.data?.selected_year || year || 'All';
|
|
||||||
const quarterlyWindowsData = response?.data?.quarterly_windows || {};
|
const quarterlyWindowsData = response?.data?.quarterly_windows || {};
|
||||||
|
|
||||||
console.log("Fetching data for:", { quarter, year });
|
console.log("Fetching data for:", { quarter, year });
|
||||||
@ -80,9 +79,8 @@ const AdminDashboard = () => {
|
|||||||
grace_periods_days: quarterlyWindowsData.grace_periods_days || 0
|
grace_periods_days: quarterlyWindowsData.grace_periods_days || 0
|
||||||
}));
|
}));
|
||||||
|
|
||||||
// Only update these if they're different to prevent unnecessary re-renders
|
// Keep the selected values as they are
|
||||||
setSelectedQuarter(prev => prev !== selectedQuarterFromApi ? selectedQuarterFromApi : prev);
|
// The dropdowns will maintain their current selection
|
||||||
setSelectedYear(prev => prev !== selectedYearFromApi ? selectedYearFromApi : prev);
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error fetching dashboard summary:', error);
|
console.error('Error fetching dashboard summary:', error);
|
||||||
} finally {
|
} finally {
|
||||||
@ -121,11 +119,11 @@ const AdminDashboard = () => {
|
|||||||
onChange={(e) => setSelectedQuarter(e.target.value)}
|
onChange={(e) => setSelectedQuarter(e.target.value)}
|
||||||
className="border border-[#D0D5DD] rounded-md h-8 px-2 text-sm focus:outline-none focus:ring-1 focus:ring-[#92722A]"
|
className="border border-[#D0D5DD] rounded-md h-8 px-2 text-sm focus:outline-none focus:ring-1 focus:ring-[#92722A]"
|
||||||
>
|
>
|
||||||
{/* <option>All</option> */}
|
<option value="All">All</option>
|
||||||
<option>Q1</option>
|
<option value="Q1">Q1</option>
|
||||||
<option>Q2</option>
|
<option value="Q2">Q2</option>
|
||||||
<option>Q3</option>
|
<option value="Q3">Q3</option>
|
||||||
<option>Q4</option>
|
<option value="Q4">Q4</option>
|
||||||
</select>
|
</select>
|
||||||
|
|
||||||
<select
|
<select
|
||||||
@ -133,6 +131,7 @@ const AdminDashboard = () => {
|
|||||||
onChange={(e) => setSelectedYear(e.target.value)}
|
onChange={(e) => setSelectedYear(e.target.value)}
|
||||||
className="border border-[#D0D5DD] rounded-md h-8 px-2 text-sm focus:outline-none focus:ring-1 focus:ring-[#92722A]"
|
className="border border-[#D0D5DD] rounded-md h-8 px-2 text-sm focus:outline-none focus:ring-1 focus:ring-[#92722A]"
|
||||||
>
|
>
|
||||||
|
<option value="All">All</option>
|
||||||
{Array.from(
|
{Array.from(
|
||||||
{ length: new Date().getFullYear() - 2021 },
|
{ length: new Date().getFullYear() - 2021 },
|
||||||
(_, i) => new Date().getFullYear() - i
|
(_, i) => new Date().getFullYear() - i
|
||||||
|
|||||||
@ -311,21 +311,14 @@ const ValidationReview = () => {
|
|||||||
}) : [];
|
}) : [];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-[#F6F5F1]">
|
<div className="min-h-screen bg-[#F6F5F1] pb-24">
|
||||||
<AdminHeader />
|
<AdminHeader />
|
||||||
<div className="max-w-[1280px] mx-auto px-4 py-6 space-y-6">
|
<div className="max-w-[1280px] mx-auto px-4 py-6 space-y-6">
|
||||||
<nav className="flex items-center gap-2 text-sm text-[#8F9299]">
|
<nav className="flex items-center gap-2 text-sm text-[#8F9299] mb-6">
|
||||||
<Link to="/admin/dashboard" className="flex items-center gap-2 text-[#232528] hover:underline">
|
|
||||||
Dashboard
|
|
||||||
<img src={caretUpSrc} alt="Dashboard" className="h-3 w-3" />
|
|
||||||
</Link>
|
|
||||||
<Link to="/admin/validations" className="flex items-center gap-2 text-[#232528] hover:underline">
|
<Link to="/admin/validations" className="flex items-center gap-2 text-[#232528] hover:underline">
|
||||||
Manage Submissions
|
Manage Submissions <img src={caretUpSrc} alt="" className="h-3 w-3 rotate-90" />
|
||||||
<img src={caretUpSrc} alt="Manage Submissions" className="h-3 w-3" />
|
|
||||||
</Link>
|
</Link>
|
||||||
<span className="flex items-center gap-2 text-[#92722A] font-medium">
|
<span className="text-[#7A7F87] font-medium">View Submission</span>
|
||||||
Submission Details
|
|
||||||
</span>
|
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
<section className="min-h-[219px] rounded-[16px] border border-[#E6EAF5] bg-white px-0 py-6 shadow-[0_16px_32px_rgba(15,23,42,0.08)]">
|
<section className="min-h-[219px] rounded-[16px] border border-[#E6EAF5] bg-white px-0 py-6 shadow-[0_16px_32px_rgba(15,23,42,0.08)]">
|
||||||
@ -587,8 +580,13 @@ const ValidationReview = () => {
|
|||||||
<textarea
|
<textarea
|
||||||
rows="2"
|
rows="2"
|
||||||
className="w-full rounded-[10px] border border-[#D1D5DB] bg-[#FFFCF2] px-4 py-2 text-sm text-[#232528] focus:ring-2 focus:ring-[#92722A] focus:outline-none"
|
className="w-full rounded-[10px] border border-[#D1D5DB] bg-[#FFFCF2] px-4 py-2 text-sm text-[#232528] focus:ring-2 focus:ring-[#92722A] focus:outline-none"
|
||||||
placeholder="Add short note..."
|
placeholder="Please enter remarks here"
|
||||||
></textarea>
|
></textarea>
|
||||||
|
<div className="mt-1 flex justify-end">
|
||||||
|
{/* <p className="text-xs text-[#6B7280] italic inline-flex items-center gap-1">
|
||||||
|
<span>Please refer to the screenshot</span>
|
||||||
|
</p> */}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
@ -682,6 +680,50 @@ const ValidationReview = () => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* Fixed bottom action bar */}
|
||||||
|
<div className="fixed bottom-0 left-0 right-0 border-t border-[#E5E7EB] bg-white py-4 px-6 shadow-[0_-4px_12px_rgba(0,0,0,0.05)]">
|
||||||
|
<div className="mx-auto flex max-w-[1280px] justify-end gap-4">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="inline-flex h-11 items-center justify-center rounded-[10px] border border-[#B52520] px-3 text-sm font-semibold text-[#B52520] transition-colors hover:bg-[#FEE2E2] disabled:opacity-50 disabled:cursor-not-allowed"
|
||||||
|
onClick={() => setIsRejectOpen(true)}
|
||||||
|
disabled={actionLoading || submissionData?.status === 'Rejected'}
|
||||||
|
>
|
||||||
|
{submissionData?.status === 'Rejected' ? 'Already Rejected' : 'Reject'}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="inline-flex h-11 items-center justify-center rounded-[10px] bg-[#92722A] px-6 text-sm font-semibold text-white transition-colors hover:bg-[#B68A35] disabled:opacity-50 disabled:cursor-not-allowed"
|
||||||
|
onClick={handleApprove}
|
||||||
|
disabled={actionLoading || submissionData?.status === 'Approved'}
|
||||||
|
>
|
||||||
|
{actionLoading ? 'Processing...' : submissionData?.status === 'Approved' ? 'Already Approved' : 'Approve'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Fixed bottom action bar */}
|
||||||
|
<div className="fixed bottom-0 left-0 right-0 border-t border-[#E5E7EB] bg-white py-4 px-6 shadow-[0_-4px_12px_rgba(0,0,0,0.05)]">
|
||||||
|
<div className="mx-auto flex max-w-[1280px] justify-end gap-4">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="inline-flex h-11 items-center justify-center rounded-[10px] border border-[#B52520] px-6 text-sm font-semibold text-[#B52520] transition-colors hover:bg-[#FEE2E2] disabled:opacity-50 disabled:cursor-not-allowed"
|
||||||
|
onClick={() => setIsRejectOpen(true)}
|
||||||
|
disabled={actionLoading || submissionData?.status === 'Rejected'}
|
||||||
|
>
|
||||||
|
{submissionData?.status === 'Rejected' ? 'Already Rejected' : 'Reject'}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="inline-flex h-11 items-center justify-center rounded-[10px] bg-[#92722A] px-6 text-sm font-semibold text-white transition-colors hover:bg-[#B68A35] disabled:opacity-50 disabled:cursor-not-allowed"
|
||||||
|
onClick={handleApprove}
|
||||||
|
disabled={actionLoading || submissionData?.status === 'Approved'}
|
||||||
|
>
|
||||||
|
{actionLoading ? 'Processing...' : submissionData?.status === 'Approved' ? 'Already Approved' : 'Approve'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -15,16 +15,16 @@ const STATUS_VARIANTS = {
|
|||||||
Pending: { background: '#E7F5FF', text: '#286CFF', border: '#E7F5FF' },
|
Pending: { background: '#E7F5FF', text: '#286CFF', border: '#E7F5FF' },
|
||||||
Rejected: { background: '#FEF2F2', text: '#B52520', border: '#FEF2F2' },
|
Rejected: { background: '#FEF2F2', text: '#B52520', border: '#FEF2F2' },
|
||||||
Resubmitted: { background: '#F9F7ED', text: '#7C5E24', border: '#FEF2F2' },
|
Resubmitted: { background: '#F9F7ED', text: '#7C5E24', border: '#FEF2F2' },
|
||||||
Submitted: { background: '#E7F5FF', text: '#003CFF', border: '#FEF2F2' },
|
Submitted: { background: '#E7F5FF', text: '#003CFF', border: '#E7F5FF' },
|
||||||
};
|
};
|
||||||
|
|
||||||
const SUMMARY_VARIANTS = {
|
const SUMMARY_VARIANTS = {
|
||||||
total: { background: '#F9F7ED', label: '#7C5E24', value: '#232528' },
|
total: { background: '#F9F7ED', label: '#7C5E24', value: '#232528' },
|
||||||
approved: { background: '#F3FAF4', label: '#2F663C', value: '#232528' },
|
approved: { background: '#F3FAF4', label: '#2F663C', value: '#232528' },
|
||||||
pending: { background: '#E7F5FF', label: '#003CFF', value: '#232528' },
|
// pending: { background: '#E7F5FF', label: '#003CFF', value: '#232528' },
|
||||||
|
submitted: { background: '#E7F5FF', text: '#003CFF', value: '#232528',border: '#FEF2F2' },
|
||||||
rejected: { background: '#FEF2F2', label: '#B52520', value: '#232528' },
|
rejected: { background: '#FEF2F2', label: '#B52520', value: '#232528' },
|
||||||
resubmitted: { background: '#F9F7ED', text: '#7C5E24', border: '#FEF2F2' },
|
resubmitted: { background: '#F9F7ED', text: '#7C5E24', border: '#FEF2F2' },
|
||||||
submitted: { background: '#E7F5FF', text: '#003CFF', border: '#FEF2F2' },
|
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -281,7 +281,7 @@ React.useEffect(() => {
|
|||||||
}
|
}
|
||||||
}, [location.state, showToast]);
|
}, [location.state, showToast]);
|
||||||
|
|
||||||
const fetchDashboardData = async (quarter, year, page = 1, limit = 10) => {
|
const fetchDashboardData = async (quarter, year, page = 1, limit = 10000) => {
|
||||||
try {
|
try {
|
||||||
const params = {
|
const params = {
|
||||||
quarter: quarter && quarter !== 'All' ? quarter : undefined,
|
quarter: quarter && quarter !== 'All' ? quarter : undefined,
|
||||||
@ -290,12 +290,9 @@ React.useEffect(() => {
|
|||||||
limit
|
limit
|
||||||
};
|
};
|
||||||
|
|
||||||
console.log('Fetching with params:', params);
|
|
||||||
const result = await getSubmissions(params);
|
const result = await getSubmissions(params);
|
||||||
console.log('API Response:', result);
|
|
||||||
// Check if result has a data property or is the data array itself
|
// Check if result has a data property or is the data array itself
|
||||||
const data = Array.isArray(result) ? result : (result?.data || []);
|
const data = Array.isArray(result) ? result : (result?.data || []);
|
||||||
console.log('Processed data:', data);
|
|
||||||
return data;
|
return data;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error fetching dashboard data:', error);
|
console.error('Error fetching dashboard data:', error);
|
||||||
@ -402,7 +399,6 @@ React.useEffect(() => {
|
|||||||
return matches;
|
return matches;
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log('Filtered items count:', filteredItems.length);
|
|
||||||
return filteredItems;
|
return filteredItems;
|
||||||
}, [submissions, search, year, quarter, emirate, status]);
|
}, [submissions, search, year, quarter, emirate, status]);
|
||||||
|
|
||||||
@ -410,8 +406,9 @@ React.useEffect(() => {
|
|||||||
() => ({
|
() => ({
|
||||||
total: submissions.length,
|
total: submissions.length,
|
||||||
approved: submissions.filter((i) => i.status === 'Approved').length,
|
approved: submissions.filter((i) => i.status === 'Approved').length,
|
||||||
pending: submissions.filter((i) => i.status === 'Pending').length,
|
submitted: submissions.filter((i) => i.status === 'Submitted').length,
|
||||||
rejected: submissions.filter((i) => i.status === 'Rejected').length,
|
rejected: submissions.filter((i) => i.status === 'Rejected').length,
|
||||||
|
resubmitted: submissions.filter((i) => i.status === 'Resubmitted').length,
|
||||||
}),
|
}),
|
||||||
[submissions]
|
[submissions]
|
||||||
);
|
);
|
||||||
@ -829,6 +826,10 @@ React.useEffect(() => {
|
|||||||
{showRejectError && !rejectReason.trim() && (
|
{showRejectError && !rejectReason.trim() && (
|
||||||
<p className="mt-1 text-sm text-red-600">Reason for rejection is required</p>
|
<p className="mt-1 text-sm text-red-600">Reason for rejection is required</p>
|
||||||
)}
|
)}
|
||||||
|
<p className="mt-1 text-xs text-right text-[#6B7280] italic">
|
||||||
|
Please refer to the screenshot
|
||||||
|
</p>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-4 flex justify-end gap-3">
|
<div className="mt-4 flex justify-end gap-3">
|
||||||
<button
|
<button
|
||||||
|
|||||||
@ -6,7 +6,7 @@ import apiClient from '@/services/api/apiClient';
|
|||||||
* @param {string} [params.quarter] - Filter by quarter (Q1, Q2, Q3, Q4)
|
* @param {string} [params.quarter] - Filter by quarter (Q1, Q2, Q3, Q4)
|
||||||
* @param {string} [params.year] - Filter by year
|
* @param {string} [params.year] - Filter by year
|
||||||
* @param {number} [params.page=1] - Page number for pagination
|
* @param {number} [params.page=1] - Page number for pagination
|
||||||
* @param {number} [params.limit=10] - Number of items per page
|
* @param {number} [params.limit=10000] - Number of items per page
|
||||||
* @returns {Promise<Object>} - Response data
|
* @returns {Promise<Object>} - Response data
|
||||||
*/
|
*/
|
||||||
export const getSubmissions = async (params = {}) => {
|
export const getSubmissions = async (params = {}) => {
|
||||||
@ -16,7 +16,7 @@ export const getSubmissions = async (params = {}) => {
|
|||||||
quarter = '',
|
quarter = '',
|
||||||
year = '',
|
year = '',
|
||||||
page = 1,
|
page = 1,
|
||||||
limit = 100,
|
limit = 10000,
|
||||||
...restParams
|
...restParams
|
||||||
} = params;
|
} = params;
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user