bug fixed

This commit is contained in:
Malini 2025-11-10 19:21:36 +05:30
parent f5ebecdfbb
commit a136e2415f
5 changed files with 337 additions and 49 deletions

View File

@ -2,12 +2,20 @@ import React, { useState, useEffect, useMemo } from 'react';
import { useNavigate } from 'react-router-dom';
import Table from '@/components/common/Table';
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 [data, setData] = useState([]);
const [loading, setLoading] = useState(true);
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 navigate = useNavigate();
@ -118,6 +126,123 @@ const SubmissionTable = ({ selectedQuarter, selectedYear }) => {
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 = [
'Establishment',
'Emirates',
@ -195,30 +320,22 @@ const SubmissionTable = ({ selectedQuarter, selectedYear }) => {
<div className="relative group">
<CheckCircle2
size={22}
className="text-green-600 cursor-pointer hover:scale-110 transition-transform"
onClick={() =>
navigate(`/admin/validations/${item.id}`, {
state: { actionType: 'approve' },
})
}
className={`${item.status?.toLowerCase() === 'approved' ? 'text-gray-400 cursor-not-allowed' : 'text-green-600 cursor-pointer hover:scale-110'} transition-transform`}
onClick={item.status?.toLowerCase() !== 'approved' ? () => handleActionClick(item, 'approve') : undefined}
/>
<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>
</div>
<div className="relative group">
<XCircle
size={22}
className="text-red-600 cursor-pointer hover:scale-110 transition-transform"
onClick={() =>
navigate(`/admin/validations/${item.id}`, {
state: { actionType: 'reject' },
})
}
className={`${item.status?.toLowerCase() === 'rejected' ? 'text-gray-400 cursor-not-allowed' : 'text-red-600 cursor-pointer hover:scale-110'} transition-transform`}
onClick={item.status?.toLowerCase() !== 'rejected' ? () => handleActionClick(item, 'reject') : undefined}
/>
<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>
</div>
@ -259,6 +376,135 @@ const SubmissionTable = ({ selectedQuarter, selectedYear }) => {
View All Submissions
</button>
</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>
);
};

View File

@ -32,7 +32,7 @@ const AdminDashboard = () => {
const isMounted = useRef(false);
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
if (prevParams.current.quarter === quarter && prevParams.current.year === year) {
return;
@ -45,6 +45,7 @@ const AdminDashboard = () => {
setLoading(true);
const params = new URLSearchParams();
// Only add quarter and year to params if they are not 'All'
if (quarter && quarter !== 'All') {
params.append('quarter', quarter);
}
@ -58,8 +59,6 @@ const AdminDashboard = () => {
if (!isMounted.current) return;
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 || {};
console.log("Fetching data for:", { quarter, year });
@ -80,9 +79,8 @@ const AdminDashboard = () => {
grace_periods_days: quarterlyWindowsData.grace_periods_days || 0
}));
// Only update these if they're different to prevent unnecessary re-renders
setSelectedQuarter(prev => prev !== selectedQuarterFromApi ? selectedQuarterFromApi : prev);
setSelectedYear(prev => prev !== selectedYearFromApi ? selectedYearFromApi : prev);
// Keep the selected values as they are
// The dropdowns will maintain their current selection
} catch (error) {
console.error('Error fetching dashboard summary:', error);
} finally {
@ -121,11 +119,11 @@ const AdminDashboard = () => {
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]"
>
{/* <option>All</option> */}
<option>Q1</option>
<option>Q2</option>
<option>Q3</option>
<option>Q4</option>
<option value="All">All</option>
<option value="Q1">Q1</option>
<option value="Q2">Q2</option>
<option value="Q3">Q3</option>
<option value="Q4">Q4</option>
</select>
<select
@ -133,6 +131,7 @@ const AdminDashboard = () => {
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]"
>
<option value="All">All</option>
{Array.from(
{ length: new Date().getFullYear() - 2021 },
(_, i) => new Date().getFullYear() - i

View File

@ -311,21 +311,14 @@ const ValidationReview = () => {
}) : [];
return (
<div className="min-h-screen bg-[#F6F5F1]">
<div className="min-h-screen bg-[#F6F5F1] pb-24">
<AdminHeader />
<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]">
<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>
<nav className="flex items-center gap-2 text-sm text-[#8F9299] mb-6">
<Link to="/admin/validations" className="flex items-center gap-2 text-[#232528] hover:underline">
Manage Submissions
<img src={caretUpSrc} alt="Manage Submissions" className="h-3 w-3" />
Manage Submissions <img src={caretUpSrc} alt="" className="h-3 w-3 rotate-90" />
</Link>
<span className="flex items-center gap-2 text-[#92722A] font-medium">
Submission Details
</span>
<span className="text-[#7A7F87] font-medium">View Submission</span>
</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)]">
@ -587,8 +580,13 @@ const ValidationReview = () => {
<textarea
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"
placeholder="Add short note..."
placeholder="Please enter remarks here"
></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>
</section>
@ -682,6 +680,50 @@ const ValidationReview = () => {
</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>
);
};

View File

@ -15,16 +15,16 @@ const STATUS_VARIANTS = {
Pending: { background: '#E7F5FF', text: '#286CFF', border: '#E7F5FF' },
Rejected: { background: '#FEF2F2', text: '#B52520', 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 = {
total: { background: '#F9F7ED', label: '#7C5E24', 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' },
resubmitted: { background: '#F9F7ED', text: '#7C5E24', border: '#FEF2F2' },
submitted: { background: '#E7F5FF', text: '#003CFF', border: '#FEF2F2' },
};
@ -281,7 +281,7 @@ React.useEffect(() => {
}
}, [location.state, showToast]);
const fetchDashboardData = async (quarter, year, page = 1, limit = 10) => {
const fetchDashboardData = async (quarter, year, page = 1, limit = 10000) => {
try {
const params = {
quarter: quarter && quarter !== 'All' ? quarter : undefined,
@ -290,12 +290,9 @@ React.useEffect(() => {
limit
};
console.log('Fetching with params:', params);
const result = await getSubmissions(params);
console.log('API Response:', result);
// Check if result has a data property or is the data array itself
const data = Array.isArray(result) ? result : (result?.data || []);
console.log('Processed data:', data);
return data;
} catch (error) {
console.error('Error fetching dashboard data:', error);
@ -402,7 +399,6 @@ React.useEffect(() => {
return matches;
});
console.log('Filtered items count:', filteredItems.length);
return filteredItems;
}, [submissions, search, year, quarter, emirate, status]);
@ -410,8 +406,9 @@ React.useEffect(() => {
() => ({
total: submissions.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,
resubmitted: submissions.filter((i) => i.status === 'Resubmitted').length,
}),
[submissions]
);
@ -829,6 +826,10 @@ React.useEffect(() => {
{showRejectError && !rejectReason.trim() && (
<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 className="mt-4 flex justify-end gap-3">
<button

View File

@ -6,7 +6,7 @@ import apiClient from '@/services/api/apiClient';
* @param {string} [params.quarter] - Filter by quarter (Q1, Q2, Q3, Q4)
* @param {string} [params.year] - Filter by year
* @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
*/
export const getSubmissions = async (params = {}) => {
@ -16,7 +16,7 @@ export const getSubmissions = async (params = {}) => {
quarter = '',
year = '',
page = 1,
limit = 100,
limit = 10000,
...restParams
} = params;