bug fixed for admin submissions

This commit is contained in:
Malini 2025-11-09 17:55:29 +05:30
parent 776f1391d2
commit 546425a85c
13 changed files with 575 additions and 165 deletions

View File

@ -0,0 +1,3 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M7.99968 11.3333C8.18856 11.3333 8.34701 11.2693 8.47501 11.1413C8.60301 11.0133 8.66679 10.855 8.66634 10.6666V7.99992C8.66634 7.81103 8.60234 7.65281 8.47434 7.52525C8.34634 7.3977 8.18812 7.3337 7.99968 7.33325C7.81123 7.33281 7.65301 7.39681 7.52501 7.52525C7.39701 7.6537 7.33301 7.81192 7.33301 7.99992V10.6666C7.33301 10.8555 7.39701 11.0139 7.52501 11.1419C7.65301 11.2699 7.81123 11.3337 7.99968 11.3333ZM7.99968 5.99992C8.18856 5.99992 8.34701 5.93592 8.47501 5.80792C8.60301 5.67992 8.66679 5.5217 8.66634 5.33325C8.6659 5.14481 8.6019 4.98659 8.47434 4.85859C8.34679 4.73059 8.18856 4.66658 7.99968 4.66658C7.81079 4.66658 7.65256 4.73059 7.52501 4.85859C7.39745 4.98659 7.33345 5.14481 7.33301 5.33325C7.33256 5.5217 7.39656 5.68014 7.52501 5.80859C7.65345 5.93703 7.81168 6.00081 7.99968 5.99992ZM7.99968 14.6666C7.07745 14.6666 6.21079 14.4915 5.39968 14.1413C4.58856 13.791 3.88301 13.3161 3.28301 12.7166C2.68301 12.117 2.20812 11.4115 1.85834 10.5999C1.50856 9.78836 1.33345 8.9217 1.33301 7.99992C1.33256 7.07814 1.50768 6.21147 1.85834 5.39992C2.20901 4.58836 2.6839 3.88281 3.28301 3.28325C3.88212 2.6837 4.58768 2.20881 5.39968 1.85859C6.21168 1.50836 7.07834 1.33325 7.99968 1.33325C8.92101 1.33325 9.78767 1.50836 10.5997 1.85859C11.4117 2.20881 12.1172 2.6837 12.7163 3.28325C13.3155 3.88281 13.7906 4.58836 14.1417 5.39992C14.4928 6.21147 14.6677 7.07814 14.6663 7.99992C14.665 8.9217 14.4899 9.78836 14.141 10.5999C13.7921 11.4115 13.3172 12.117 12.7163 12.7166C12.1155 13.3161 11.4099 13.7913 10.5997 14.1419C9.78945 14.4926 8.92279 14.6675 7.99968 14.6666ZM7.99968 13.3333C9.48856 13.3333 10.7497 12.8166 11.783 11.7833C12.8163 10.7499 13.333 9.48881 13.333 7.99992C13.333 6.51103 12.8163 5.24992 11.783 4.21658C10.7497 3.18325 9.48856 2.66659 7.99968 2.66659C6.51079 2.66659 5.24968 3.18325 4.21634 4.21658C3.18301 5.24992 2.66634 6.51103 2.66634 7.99992C2.66634 9.48881 3.18301 10.7499 4.21634 11.7833C5.24968 12.8166 6.51079 13.3333 7.99968 13.3333Z" fill="#5F646D"/>
</svg>

After

Width:  |  Height:  |  Size: 2.1 KiB

View File

@ -80,16 +80,16 @@ const AdminHeader = () => {
<NavItem to="/admin/dashboard" end label="Dashboard" iconActive={dashboardIconActiveSrc} iconInactive={dashboardIconInactiveSrc} />
<NavItem to="/admin/validations" label="Submissions" iconActive={checkCircleActiveSrc} iconInactive={manageSubmissionInactiveSrc} />
<NavItem to="/admin/configuration" label="Configuration" iconActive={configActiveSrc} iconInactive={configInactiveSrc} />
<NavItem to="/admin/users" label="Users" iconActive={userActiveSrc} iconInactive={userInactiveSrc} />
{/* <NavItem to="admin/configuration/admin-users"label="Users" iconActive={userActiveSrc} iconInactive={userInactiveSrc} /> */}
</nav>
<div className="hidden lg:flex items-center gap-3 ml-auto">
<button
{/* <button
type="button"
aria-label="Notifications"
className="inline-flex h-9 w-9 items-center justify-center rounded-full border border-transparent hover:border-[#E5E7EB]"
>
<img src={bellIconSrc} alt="Notifications" className="h-[18px] w-[18px]" />
</button>
</button> */}
<div className="relative">
<button
type="button"

View File

@ -85,7 +85,7 @@ const downloadCsv = (data) => {
item.quarter || '—',
statusText,
item.submission_on || '—',
`${item.products || 0} ${item.products === 1 ? 'product' : 'products'}`,
`${item.products || 0}`,
];
csvRows.push(row.map((cell) => `"${String(cell).replace(/"/g, '""')}"`).join(','));
@ -118,7 +118,7 @@ export const SubmissionHistory = ({ history = [], loading = false, error = '' })
return history.map((entry, index) => {
const quarter = entry?.quarter || '';
const year = entry?.year || '';
const surveyName = `IPI ${quarter} ${year} Survey`;
const surveyName = entry?.survey_name || 'IPI Survey Platform';
return {
id: entry?.id ?? index,
@ -153,7 +153,7 @@ export const SubmissionHistory = ({ history = [], loading = false, error = '' })
];
const tableRows = filtered.map((item) => {
const productsLabel = `${item.products} ${item.products === 1 ? 'product' : 'products'}`;
const productsLabel = `${item.products}`;
return [
item.survey_name,
item.year,
@ -176,7 +176,7 @@ export const SubmissionHistory = ({ history = [], loading = false, error = '' })
<div className="max-w-[1280px] mx-auto mb-5 bg-white border border-[#E5E7EB] shadow-[0_16px_32px_rgba(15,23,42,0.06)] rounded-[8px] mt-8 w-full overflow-hidden">
<div className="flex items-center justify-between px-6 pt-6 pb-4">
<h2 className="text-[18px] leading-[28px] font-medium text-[#232528]">
Submission History
Submission History ({filtered.length})
</h2>
<div className="flex items-center gap-3">
@ -234,7 +234,7 @@ export const SubmissionHistory = ({ history = [], loading = false, error = '' })
</div>
) : (
<div className="overflow-x-auto">
<div className="min-w-[1200px] ">
<div className="min-w-[1240px] ">
<Table
headers={tableHeaders}
rows={tableRows}
@ -246,7 +246,7 @@ export const SubmissionHistory = ({ history = [], loading = false, error = '' })
pageSize,
totalItems: filtered.length,
}}
columnWidths={[250, 100, 100, 160, 180, 150, 120]}
columnWidths={[150, 100, 100, 150, 170, 140, 110]}
/>
</div>
</div>

View File

@ -30,8 +30,8 @@ const SurveyCarousel = ({
setCurrentIndex((prev) => (prev === surveys.length - 1 ? 0 : prev + 1));
return (
<div className="bg-black rounded-lg shadow-sm ring-1 w-[1275px] ring-gray-200 h-[148px] mb-8 ml-2 ">
<div className="rounded-none bg-[#F2ECCF] px-8 py-10 min-h-[148px] flex items-center justify-between transition-all duration-500">
<div className="bg-black rounded-lg shadow-sm ring-1 w-[1275px] ring-gray-200 h-[148px] mb-10 ml-2 ">
<div className="rounded-none bg-[#F2ECCF] px-8 py-10 flex items-center justify-between transition-all duration-500">
<div className="flex-1">
<h3 className="text-[18px] leading-[28px] font-medium text-[#232528]">
{current?.title || '—'}

View File

@ -116,7 +116,7 @@ const HeaderBar = () => {
</>
)}
</NavLink>
<NavLink
{/* <NavLink
to="/history"
className={({ isActive }) => `inline-flex items-center gap-2 pb-1 border-b-2 ${isActive ? 'text-[#92722A] font-medium border-[#92722A]' : 'text-[#232528] hover:text-gray-900 border-transparent'}`}
>
@ -126,7 +126,7 @@ const HeaderBar = () => {
<span>History</span>
</>
)}
</NavLink>
</NavLink> */}
<div className="relative">
<button
type="button"

View File

@ -49,7 +49,8 @@ const AdminDashboard = () => {
const selectedQuarterFromApi = response?.data?.selected_quarter || quarter || 'All';
const selectedYearFromApi = response?.data?.selected_year || year || 'All';
const quarterlyWindowsData = response?.data?.quarterly_windows || {};
console.log("selectedQuarter",selectedQuarterFromApi)
console.log("selectedYear",selectedYearFromApi)
setSummary({
total_establishments: data.total_establishments || 0,
submitted: data.submitted || 0,

View File

@ -59,22 +59,24 @@ const Configuration = () => {
<nav className="mb-6 text-sm text-[#8F9299] flex items-center gap-2">
<Link
to="/admin/dashboard"
className="text-[#232528] hover:underline font-medium"
className="text-[#232528] hover:underline font-medium flex items-center gap-1"
>
Dashboard
<img src="/assets/images/Breadcrumb-vector.svg" alt="" className=" ml-1 w-3 h-3" />
</Link>
<span className="text-[#8F9299]">{">"}</span>
{/* <span className="text-[#8F9299]">{">"}</span> */}
<Link
to="/admin/configuration"
className="text-[#232528] hover:underline font-medium"
>
Configuration
</Link>
{active && (
<>
<span className="text-[#8F9299]">{">"}</span>
<img src="/assets/images/Breadcrumb-vector.svg" alt="" className=" ml-1 w-3 h-3" />
<span className="text-[#92722A] font-medium">{active}</span>
</>
)}

View File

@ -600,30 +600,31 @@ const ValidationReview = () => {
</div>
)}
<section className="rounded-[16px] bg-white p-6 shadow-[0_16px_32px_rgba(15,23,42,0.08)] ring-1 ring-[#E5E7EB]">
<div className="flex flex-col gap-4 md:flex-row md:items-center md:justify-between">
<p className="text-sm text-[#4B5563]"><span className="font-semibold text-[#232528]">Note:</span> Please review the details carefully before taking action.</p>
<div className="flex flex-col gap-3 md:flex-row md:items-center">
<button
type="button"
className="inline-flex h-11 w-full items-center justify-center rounded-[10px] border border-[#B52520] text-sm font-semibold text-[#B52520] transition-colors hover:bg-[#FEE2E2] md:w-[132px] disabled:opacity-50 disabled:cursor-not-allowed"
onClick={() => setIsRejectOpen(true)}
disabled={actionLoading || actionType === 'approve'}
>
Reject
</button>
<button
type="button"
className="inline-flex h-11 w-full items-center justify-center rounded-[10px] bg-[#92722A] text-sm font-semibold text-white transition-colors hover:bg-[#B68A35] md:w-[132px] disabled:opacity-50 disabled:cursor-not-allowed"
onClick={handleApprove}
disabled={actionLoading || actionType === 'reject'}
>
{actionLoading ? 'Processing...' : 'Approve'}
</button>
{actionType !== 'view' && (
<section className="rounded-[16px] bg-white p-6 shadow-[0_16px_32px_rgba(15,23,42,0.08)] ring-1 ring-[#E5E7EB]">
<div className="flex flex-col gap-4 md:flex-row md:items-center md:justify-between">
<p className="text-sm text-[#4B5563]"><span className="font-semibold text-[#232528]">Note:</span> Please review the details carefully before taking action.</p>
<div className="flex flex-col gap-3 md:flex-row md:items-center">
<button
type="button"
className="inline-flex h-11 w-full items-center justify-center rounded-[10px] border border-[#B52520] text-sm font-semibold text-[#B52520] transition-colors hover:bg-[#FEE2E2] md:w-[132px] disabled:opacity-50 disabled:cursor-not-allowed"
onClick={() => setIsRejectOpen(true)}
disabled={actionLoading || actionType === 'approve'}
>
Reject
</button>
<button
type="button"
className="inline-flex h-11 w-full items-center justify-center rounded-[10px] bg-[#92722A] text-sm font-semibold text-white transition-colors hover:bg-[#B68A35] md:w-[132px] disabled:opacity-50 disabled:cursor-not-allowed"
onClick={handleApprove}
disabled={actionLoading || actionType === 'reject'}
>
{actionLoading ? 'Processing...' : 'Approve'}
</button>
</div>
</div>
</div>
</section>
</section>
)}
</div>
{isRejectOpen && (

View File

@ -4,10 +4,11 @@ import AdminHeader from '@/components/admin/AdminHeader';
import Table from '@/components/common/Table';
import { SelectField } from '@/components/common/FormControls';
import { getSubmissions } from '@/services/admin/submission';
import { CheckCircle2, XCircle, Eye } from 'lucide-react';
import { CheckCircle2, XCircle, Eye, X } from 'lucide-react';
import Footer from '@/components/common/Footer';
const searchIconSrc = '/assets/images/material-symbols_search-rounded.svg';
const caretUpSrc = '/assets/images/caret-up.svg';
import apiClient from '@/services/api/apiClient';
const STATUS_VARIANTS = {
Approved: { background: '#F3FAF4', text: '#2F663C', border: '#F3FAF4' },
@ -66,8 +67,17 @@ const ManageSubmissions = () => {
const [quarter, setQuarter] = React.useState('');
const [emirate, setEmirate] = React.useState('');
const [status, setStatus] = React.useState('');
const [selectedQuarter, setSelectedQuarter] = React.useState('Q1');
const [selectedYear, setSelectedYear] = React.useState(new Date().getFullYear().toString());
const [currentPage, setCurrentPage] = React.useState(1);
const [toast, setToast] = React.useState(null);
const [showApproveModal, setShowApproveModal] = React.useState(false);
const [showRejectModal, setShowRejectModal] = React.useState(false);
const [selectedSubmission, setSelectedSubmission] = React.useState(null);
const [isApproving, setIsApproving] = React.useState(false);
const [isRejecting, setIsRejecting] = React.useState(false);
const [rejectReason, setRejectReason] = React.useState('');
const [showRejectError, setShowRejectError] = React.useState(false);
const pageSize = 10;
const toastTimeoutRef = React.useRef(null);
@ -93,6 +103,121 @@ const ManageSubmissions = () => {
setToast(null);
}, []);
const handleApproveClick = (submission) => {
setSelectedSubmission(submission);
setShowApproveModal(true);
};
const handleCloseApproveModal = () => {
setShowApproveModal(false);
setSelectedSubmission(null);
};
const handleRejectClick = (submission) => {
setSelectedSubmission(submission);
setShowRejectModal(true);
};
const handleCloseRejectModal = () => {
setShowRejectModal(false);
setSelectedSubmission(null);
setRejectReason('');
};
const handleApproveConfirm = async () => {
if (!selectedSubmission || isApproving) return;
setIsApproving(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) {
showToast('success', 'Submission approved successfully');
// Refresh the submissions list
const data = await fetchDashboardData(selectedQuarter, selectedYear);
const userProfile = JSON.parse(sessionStorage.getItem("user_profile"));
const mapped = (data || []).map((item) => ({
id: item.id,
establishment: item?.establishment?.factory_name || '-',
emirate: item?.establishment?.establishment_emirate?.name || '-',
year: String(item.year ?? '-'),
quarter: item.quarter ?? '-',
products: item?.product_count || '-',
submittedBy: userProfile?.name || '-',
submittedAt: formatDateTime(item.created_at),
status: item.status ?? '-',
reviewer: userProfile?.name || '-',
reviewerOn: formatDateTime(item.updated_at || item.created_at),
}));
setSubmissions(mapped);
} else {
throw new Error('Failed to approve submission');
}
} catch (error) {
console.error('Error approving submission:', error);
showToast('error', error.message || 'Failed to approve submission');
} finally {
setIsApproving(false);
setShowApproveModal(false);
setSelectedSubmission(null);
}
};
const handleRejectConfirm = async () => {
if (!selectedSubmission || isRejecting || !rejectReason.trim()) {
if (!rejectReason.trim()) {
showToast('error', 'Please provide a reason for rejection');
}
return;
}
setIsRejecting(true);
try {
const response = await apiClient.put(`/approveOrRejectSubmission/${selectedSubmission.id}`, {
approve_reject_status: 0,
remarks: rejectReason.trim()
});
const ok = (response && response.data) || response?.status === 200 || response?.status === 'success';
if (ok) {
showToast('success', 'Submission has been rejected');
// Refresh the submissions list
const data = await fetchDashboardData(selectedQuarter, selectedYear);
const userProfile = JSON.parse(sessionStorage.getItem("user_profile"));
const mapped = (data || []).map((item) => ({
id: item.id,
establishment: item?.establishment?.factory_name || '-',
emirate: item?.establishment?.establishment_emirate?.name || '-',
year: String(item.year ?? '-'),
quarter: item.quarter ?? '-',
products: item?.product_count || '-',
submittedBy: userProfile?.name || '-',
submittedAt: formatDateTime(item.created_at),
status: item.status ?? '-',
reviewer: userProfile?.name || '-',
reviewerOn: formatDateTime(item.updated_at || item.created_at),
}));
setSubmissions(mapped);
setRejectReason('');
} else {
throw new Error('Failed to reject submission');
}
} catch (error) {
console.error('Error rejecting submission:', error);
showToast('error', error.message || 'Failed to reject submission');
} finally {
setIsRejecting(false);
setShowRejectModal(false);
setSelectedSubmission(null);
}
};
React.useEffect(() => {
return () => {
if (toastTimeoutRef.current) {
@ -108,11 +233,46 @@ const ManageSubmissions = () => {
}
}, [location.state, showToast]);
const fetchDashboardData = async (quarter, year) => {
try {
// Add quarter and year to the API call to filter submissions
const params = new URLSearchParams();
if (quarter && quarter !== 'All') params.append('quarter', quarter);
if (year && year !== 'All') params.append('year', year);
const result = await getSubmissions(params.toString());
return result?.status === 'success' ? result.data : [];
} catch (error) {
console.error('Error fetching dashboard data:', error);
return [];
}
};
// Fetch current quarter and year from admin_dashboard endpoint
React.useEffect(() => {
const fetchCurrentQuarterAndYear = async () => {
try {
const response = await apiClient.get('/admin_dashboard');
const currentQuarter = response?.data?.selected_quarter;
const currentYear = response?.data?.selected_year || new Date().getFullYear().toString();
console.log("currentQuarter",currentQuarter)
console.log("currentYear",currentYear)
setSelectedQuarter(currentQuarter);
setSelectedYear(currentYear);
setQuarter(currentQuarter);
setYear(currentYear);
} catch (error) {
console.error('Error fetching current quarter and year:', error);
}
};
fetchCurrentQuarterAndYear();
}, []);
React.useEffect(() => {
const fetchSubmissions = async () => {
try {
const result = await getSubmissions();
const data = result?.status === 'success' ? result.data : result;
const data = await fetchDashboardData(selectedQuarter, selectedYear);
const userProfile = JSON.parse(sessionStorage.getItem("user_profile"));
const mapped = (data || []).map((item) => ({
id: item.id,
@ -142,8 +302,16 @@ const ManageSubmissions = () => {
fetchSubmissions();
}, []);
const yearOptions = React.useMemo(() => ['All', ...new Set(submissions.map((i) => i.year))], [submissions]);
const quarterOptions = React.useMemo(() => ['All', ...new Set(submissions.map((i) => i.quarter))], [submissions]);
const yearOptions = React.useMemo(() => {
const years = ['All', ...new Set(submissions.map((i) => i.year))];
return years.sort((a, b) => {
if (a === 'All') return -1;
if (b === 'All') return 1;
return b.localeCompare(a);
});
}, [submissions]);
const quarterOptions = React.useMemo(() => ['All', 'Q1', 'Q2', 'Q3', 'Q4'], []);
const emirateOptions = React.useMemo(() => ['All', ...new Set(submissions.map((i) => i.emirate))], [submissions]);
const statusOptions = React.useMemo(() => ['All', ...new Set(submissions.map((i) => i.status))], [submissions]);
@ -221,8 +389,28 @@ const ManageSubmissions = () => {
className="h-10 w-full rounded-md border border-[#C3C6CB] pl-9 pr-3 text-sm focus:outline-none focus:ring-1 focus:ring-[#92722A]"
/>
</div>
<SelectField value={quarter} onChange={(e) => setQuarter(e.target.value)} options={quarterOptions.map((v) => ({ label: v, value: v }))} placeholder="Quarter" width="134px" variant="toolbar" />
<SelectField value={year} onChange={(e) => setYear(e.target.value)} options={yearOptions.map((v) => ({ label: v, value: v }))} placeholder="Year" width="134px" variant="toolbar" />
<SelectField
value={selectedQuarter}
onChange={(e) => {
setSelectedQuarter(e.target.value);
setQuarter(e.target.value);
}}
options={quarterOptions.map((v) => ({ label: v, value: v }))}
placeholder="Quarter"
width="134px"
variant="toolbar"
/>
<SelectField
value={selectedYear}
onChange={(e) => {
setSelectedYear(e.target.value);
setYear(e.target.value);
}}
options={yearOptions.map((v) => ({ label: v, value: v }))}
placeholder="Year"
width="100px"
variant="toolbar"
/>
<SelectField value={status} onChange={(e) => setStatus(e.target.value)} options={statusOptions.map((v) => ({ label: v, value: v }))} placeholder="Status" width="134px" variant="toolbar" />
<SelectField value={emirate} onChange={(e) => setEmirate(e.target.value)} options={emirateOptions.map((v) => ({ label: v, value: v }))} placeholder="Emirates" width="134px" variant="toolbar" />
</div>
@ -418,30 +606,22 @@ const ManageSubmissions = () => {
<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' ? () => handleApproveClick(item) : 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' ? () => handleRejectClick(item) : 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>
@ -475,8 +655,127 @@ const ManageSubmissions = () => {
</div>
</div>
<Footer />
</>
{/* 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}</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-indigo-500"
disabled={isApproving}
>
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={isApproving}
>
{isApproving ? (
<>
<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}</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 ${!rejectReason.trim() && showRejectError ? '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={rejectReason}
onChange={(e) => setRejectReason(e.target.value)}
onBlur={() => setShowRejectError(!rejectReason.trim())}
disabled={isRejecting}
/>
{!rejectReason.trim() && showRejectError && (
<p className="mt-1 text-sm text-red-600">Reason for rejection is required</p>
)}
</div>
<div className="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={isRejecting}
>
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={isRejecting || !rejectReason.trim()}
>
{isRejecting ? (
<>
<svg className="animate-spin h-4 w-4 text-[#92722A]" 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>
)}
<Footer />
</>
);
};

View File

@ -477,12 +477,12 @@ const CompanyProfile = () => {
/>
<TextField
label="Industry Code (Current Production)"
value={form.industryCodeCurrent}
onChange={handleFormChange('industryCodeCurrent')}
value={form.industryCodeProduction}
onChange={handleFormChange('industryCodeProduction')}
placeholder="Enter Code"
width="100%"
required
error={fieldErrors.industryCodeCurrent}
error={fieldErrors.industryCodeProduction}
/>
</div>
<div className="mt-4">
@ -1300,11 +1300,12 @@ const corporateFieldKeys = Object.values(contactToCorporateMap);
return {
...base,
apiId: item?.id ?? null,
establishmentName: item?.factory_name ?? '',
establishmentName: item?.factory_name,
emirate: establishmentEmirateName,
isicCode: item?.isic_code ?? '',
industryCodeBusiness: item?.industry_code ?? base.industryCodeBusiness,
industryCodeCurrent: item?.isic_code ?? base.industryCodeCurrent,
industryCodeBusiness: item?.industry_code,
industryCodeProduction: item?.industry_code_production,
industryCodeCurrent: item?.industryCodeCurrent,
industryDescription: item?.description ?? base.industryDescription,
establishmentId: item?.establishment_code ?? '',
contactAddress: item?.establishment_address ?? base.contactAddress,
@ -1320,7 +1321,7 @@ const corporateFieldKeys = Object.values(contactToCorporateMap);
createdById: item?.created_by ?? base.createdById,
createdOn: formatApiDate(item?.created_at),
lastUpdated: item?.updated_by ?? formatApiDate(item?.updated_at),
contactName: item?.factory_name ?? '',
contactName: item?.factory_name || '',
// contactEmail: item?.email ?? '',
contactEmail: item?.establishment_contact_email ?? base.contactEmail,
contactPostalCode: item?.establishment_postal_code ?? base.contactPostalCode,
@ -1376,14 +1377,29 @@ const loadProfiles = async () => {
try {
const response = await fetchEstablishments({ params, signal: controller.signal });
const payload = response?.data ?? response;
console.log("payloadtest",payload)
const records = Array.isArray(payload?.data) ? payload.data : Array.isArray(payload) ? payload : [];
if (!cancelled) {
setProfiles(records.map(mapApiEstablishmentToProfile));
// Update total items from API response or use records length as fallback
const apiTotal = payload?.total || payload?.count || records.length;
setTotalItems(apiTotal);
// Log the complete response structure for debugging
console.group('API Response Details');
console.log('Full response:', response);
console.log('Response data:', payload);
console.log('Pagination object:', payload?.pagination);
console.log('Total records from pagination:', payload?.pagination?.total_records);
console.groupEnd();
const records = Array.isArray(payload?.data) ? payload.data : Array.isArray(payload) ? payload : [];
if (cancelled) return;
setProfiles(records.map(mapApiEstablishmentToProfile));
// Get total records from pagination object in the API response
const totalRecords = payload?.pagination?.total_records;
if (totalRecords !== undefined) {
console.log(`Setting total items from pagination.total_records: ${totalRecords}`);
setTotalItems(totalRecords);
} else {
// Fallback to other possible locations if pagination is not available
const fallbackTotal = payload?.total || payload?.count || records.length;
console.warn('pagination.total_records not found, using fallback value:', fallbackTotal);
setTotalItems(fallbackTotal);
}
} catch (error) {
if (cancelled) return;
@ -1945,12 +1961,12 @@ const requiredFields = [
if (modalMode !== 'edit') {
const apiPayload = {
establishment_code: form.establishmentId || '',
factory_name: form.establishmentName || form.contactName || '',
factory_name: form.contactName || '',
permanent_factory_code: form.permanentFactoryCode || '',
industry_code: form.industryCodeBusiness || form.industryCodeCurrent || '',
industry_code_production: form.industryCodeBusiness || form.industryCodeCurrent || '',
industry_code: form.industryCodeBusiness || '',
industry_code_production: form.industryCodeProduction || form.industryCodeCurrent || '',
license_number: form.uniqueLicenseNumber || '',
isic_code: form.isicCode || form.industryCodeBusiness || '',
isic_code: form.industryCodeProduction || '',
description: form.industryDescription || '',
establishment_address: form.contactAddress || '',
establishment_city_town_id: Number(form.contactCityTownId) || 0,
@ -2024,12 +2040,12 @@ const requiredFields = [
}
const updatePayload = {
establishment_code: form.establishmentId || '',
factory_name: form.establishmentName || form.contactName || '',
factory_name: form.contactName || '',
permanent_factory_code: form.permanentFactoryCode || '',
industry_code: form.industryCodeBusiness || form.industryCodeCurrent || '',
industry_code_production: form.industryCodeProduction || '',
industry_code: form.industryCodeBusiness || '',
industry_code_production: form.industryCodeProduction || form.industryCodeCurrent || '',
license_number: form.uniqueLicenseNumber || '',
isic_code: form.isicCode || form.industryCodeBusiness || '',
isic_code: form.industryCodeProduction || '',
description: form.industryDescription || '',
establishment_address: form.contactAddress || '',
establishment_city_town_id: Number(form.contactCityTownId) || 0,
@ -2202,7 +2218,12 @@ const requiredFields = [
</div>
)}
<div className="px-6 py-3 flex items-center justify-between">
<h3 className="text-[16px] font-medium text-[#232528]">Company Profile</h3>
<div className="flex items-center gap-2">
<h3 className="text-[16px] font-medium text-[#232528]">Company Profile</h3>
<span className="text-sm bg-gray-100 text-gray-600 px-2 py-0.5 rounded-full">
{totalItems} {totalItems === 1 ? 'record' : 'records'}
</span>
</div>
<div className="flex items-center gap-3">
<div className="relative min-w-[260px]">
<input

View File

@ -1055,7 +1055,7 @@ const IsicHsCodes = () => {
{/* Header */}
<div className="flex items-center justify-between p-6 border-b border-[#E5E7EB]">
<h3 className="text-lg font-semibold text-[#111827]">
{modalMode === 'add' ? 'Add New HS Code' : modalMode === 'edit' ? 'Edit HS Code' : 'View HS Code'}
{modalMode === 'add' ? 'Add HS Code' : modalMode === 'edit' ? 'Edit HS Code' : 'View HS Code'}
</h3>
<button
type="button"
@ -1099,8 +1099,7 @@ const IsicHsCodes = () => {
/>
</div>
</div>
<div className="grid grid-cols-2 gap-4 mt-4">
<div className="grid grid-cols-2 gap-4 mt-4">
<div>
<label className="block text-sm font-medium text-[#374151] mb-1">
Unit <span className="text-[#EF4444]">*</span>
@ -1130,7 +1129,7 @@ const IsicHsCodes = () => {
)}
</div>
<div>
{/* <div>
<label className="block text-sm font-medium text-[#374151] mb-1">
Status <span className="text-[#EF4444]">*</span>
</label>
@ -1144,9 +1143,11 @@ const IsicHsCodes = () => {
<option value="Active">Active</option>
<option value="Inactive">Inactive</option>
</select>
</div>
</div> */}
</div>
</div>
{/* Footer */}
{modalMode !== 'view' && (

View File

@ -20,8 +20,8 @@ const createEmptyQuarterForm = () => ({
quarter: '',
startDate: '',
endDate: '',
gracePeriod: '0 days',
submissionCount: '0',
gracePeriod: '',
submissionCount: '',
status: 'Active', // Default to Active
});
@ -71,6 +71,9 @@ const QuarterlyWindows = () => {
endDate: item.end_date || '',
gracePeriod: item.grace_periods_days ? `${item.grace_periods_days} days` : '0 days',
submissionCount: item.submission_count?.toString() || '-',
assigned: item.assigned?.toString() || '0',
responded: item.responded?.toString() || '0',
not_responded: item.not_responded?.toString() || '0',
status: item.is_active ? 'Active' : 'Inactive'
}));
setQuarterData(formattedData);
@ -89,8 +92,36 @@ const QuarterlyWindows = () => {
fetchQuarterlyWindows();
}, []);
const headers = ['Survey Name', 'Year', 'Quarter', 'Start Date', 'End Date', 'Grace Period', 'Submission Count', 'Status', 'Actions'];
const columnwidth = [300, 100, 100, 100, 100, 100, 100, 100, 100];
// Table headers with whitespace-nowrap to prevent line breaks
const headers = [
{ name: 'Survey Name', className: 'whitespace-nowrap' },
{ name: 'Year', className: 'whitespace-nowrap' },
{ name: 'Quarter', className: 'whitespace-nowrap' },
{ name: 'Start Date', className: 'whitespace-nowrap' },
{ name: 'End Date', className: 'whitespace-nowrap' },
{ name: 'Grace Period', className: 'whitespace-nowrap' },
{ name: 'Assigned', className: 'whitespace-nowrap' },
{ name: 'Responded', className: 'whitespace-nowrap' },
{ name: 'Not Responded', className: 'whitespace-nowrap' },
{ name: 'Submission Count', className: 'whitespace-nowrap' },
{ name: 'Status', className: 'whitespace-nowrap' },
{ name: 'Actions', className: 'whitespace-nowrap' },
];
// Column widths significantly increased for better content visibility
const columnwidth = [
550, // Survey Name
150, // Year
180, // Quarter
220, // Start Date
220, // End Date
250, // Grace Period (increased from 200 to 250)
180, // Assigned
200, // Responded
220, // Not Responded
240, // Submission Count
180, // Status
160 // Actions
];
const filteredRows = React.useMemo(() => {
const term = searchTerm.trim().toLowerCase();
const statusValue = statusFilter.toLowerCase();
@ -107,6 +138,9 @@ const QuarterlyWindows = () => {
item.startDate,
item.endDate,
item.gracePeriod,
item.assigned,
item.responded,
item.not_responded,
item.submissionCount,
item.status,
]
@ -116,6 +150,20 @@ const QuarterlyWindows = () => {
});
}, [quarterData, searchTerm, statusFilter]);
const Tooltip = ({ children, text }) => (
<div className="flex items-center gap-1 group relative">
<span>{children}</span>
<div className="relative group cursor-pointer" title={text}>
<img
src="/assets/images/tooltip.svg"
alt="info"
className="w-4 h-4"
style={{ filter: 'invert(45%) sepia(6%) saturate(323%) hue-rotate(175deg) brightness(93%) contrast(82%)' }}
/>
</div>
</div>
);
const rows = filteredRows.map((item) => [
item.survey_name || '-',
item.year,
@ -123,11 +171,21 @@ const QuarterlyWindows = () => {
item.startDate ? formatDateForDisplay(item.startDate) : '-',
item.endDate ? formatDateForDisplay(item.endDate) : '-',
item.gracePeriod,
<Tooltip text="Number of establishments assigned this survey">
{item.assigned || '0'}
</Tooltip>,
<Tooltip text="Number of establishments that have responded">
{item.responded || '0'}
</Tooltip>,
<Tooltip text="Number of establishments yet to respond">
{item.not_responded || '0'}
</Tooltip>,
item.submissionCount,
<StatusBadge tone={String(item.status).toLowerCase() === 'active' ? 'green' : 'gray'} status={item.status} />,
<StatusBadge status={item.status} />,
'actions',
]);
const statusOptions = React.useMemo(
() => [
{ label: 'All', value: 'all' },
@ -227,6 +285,10 @@ const QuarterlyWindows = () => {
start_date: startDate.toISOString().split('T')[0],
end_date: endDate.toISOString().split('T')[0],
grace_periods_days: form.gracePeriod ? parseInt(form.gracePeriod.split(' ')[0], 10) || 0 : 0,
assigned: form.assigned || 0,
responded: form.responded || 0,
not_responded: form.not_responded || 0,
// submission_count: form.submissionCount || 0,
is_active: form.status === 'Active',
establishment: form.establishment || '-',
};
@ -296,7 +358,7 @@ const QuarterlyWindows = () => {
<div className="w-full max-w-[1280px] rounded-lg border border-[#C3C6CB] bg-white min-h-[348px] pb-6">
<div className="px-6 py-4">
<div className="flex flex-col gap-3 md:h-12 md:flex-row md:items-center md:justify-between">
<h3 className="text-[18px] leading-[28px] font-medium text-[#232528]">Quarterly Windows</h3>
<h3 className="text-[18px] leading-[28px] font-medium text-[#232528]">Manage Surveys ({filteredRows.length})</h3>
<div className="flex flex-col gap-4 md:flex-row md:items-center md:gap-4 md:flex-1 md:justify-end">
<div className="relative w-full md:w-[320px]">
<input
@ -324,26 +386,40 @@ const QuarterlyWindows = () => {
</div>
<button
type="button"
onClick={() => {
if (!filteredRows.length) return;
const csvHeader = headers.slice(0, headers.length - 1).join(',');
const csvRows = filteredRows.map((item) =>
[item.year, item.quarter, item.startDate, item.endDate, item.gracePeriod, item.submissionCount, item.status]
.map((value) => `"${String(value ?? '').replace(/"/g, '""')}"`)
.join(',')
);
const blob = new Blob([csvHeader + '\n' + csvRows.join('\n')], {
type: 'text/csv;charset=utf-8;',
});
const url = URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = url;
link.setAttribute('download', 'quarterly-windows.csv');
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
URL.revokeObjectURL(url);
}}
onClick={() => {
if (!filteredRows.length) return;
const csvHeader = headers
.filter((h) => h.name !== 'Actions')
.map((h) => `"${h.name}"`)
.join(',');
const csvRows = filteredRows.map((item) => {
const rowData = [
item.survey_name || '',
item.year || '',
item.quarter || '',
item.startDate || '',
item.endDate || '',
item.gracePeriod || '',
item.assigned || '',
item.responded || '',
item.not_responded || '',
item.submissionCount || '',
item.status || ''
];
return rowData.map((val) => `"${String(val).replace(/"/g, '""')}"`).join(',');
});
const blob = new Blob([csvHeader + '\n' + csvRows.join('\n')], {
type: 'text/csv;charset=utf-8;',
});
const url = URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = url;
link.setAttribute('download', 'quarterly-windows.csv');
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
URL.revokeObjectURL(url);
}}
className={`h-10 px-5 rounded-[6px] border text-sm inline-flex items-center gap-3 ${
filteredRows.length
? 'bg-[#F7F7F7] border-[#C3C6CB] text-[#232528]'
@ -361,47 +437,50 @@ const QuarterlyWindows = () => {
style={{ minWidth: '148px' }}
>
<img src={addIconSrc} alt="Add" className="h-5 w-5" />
<span className="font-medium">Add Quarter</span>
<span className="font-medium">Create Survey</span>
</button>
</div>
</div>
</div>
<Table
headers={headers}
columnWidth={columnwidth}
rows={rows}
renderCell={(value, rowIndex, colIndex) => {
if (colIndex === headers.length - 1) {
return (
<div className="flex items-center gap-2">
<button
type="button"
className="h-8 w-8 grid place-items-center rounded hover:bg-gray-50 cursor-pointer"
title="Edit"
aria-label="Edit Quarterly Survey"
onClick={() => openEditModal(rowIndex)}
onMouseEnter={() => setHoveredEdit(rowIndex)}
onMouseLeave={() => setHoveredEdit(null)}
>
<img
src={hoveredEdit === rowIndex || editingRow === rowIndex ? pencilActiveSrc : pencilInactiveSrc}
alt="Edit"
className="h-5 w-5"
/>
</button>
</div>
);
}
return value;
}}
pagination={{
currentPage,
onPageChange: setCurrentPage,
pageSize,
totalItems: filteredRows.length,
}}
/>
<div className="overflow-x-auto w-full">
<Table
headers={headers}
columnWidth={columnwidth}
rows={rows}
disableHorizontalScroll={false}
renderCell={(value, rowIndex, colIndex) => {
if (colIndex === headers.length - 1) {
return (
<div className="flex items-center gap-2">
<button
type="button"
className="h-8 w-8 grid place-items-center rounded hover:bg-gray-50 cursor-pointer"
title="Edit"
aria-label="Edit Quarterly Survey"
onClick={() => openEditModal(rowIndex)}
onMouseEnter={() => setHoveredEdit(rowIndex)}
onMouseLeave={() => setHoveredEdit(null)}
>
<img
src={hoveredEdit === rowIndex || editingRow === rowIndex ? pencilActiveSrc : pencilInactiveSrc}
alt="Edit"
className="h-5 w-5"
/>
</button>
</div>
);
}
return value;
}}
pagination={{
currentPage,
onPageChange: setCurrentPage,
pageSize,
totalItems: filteredRows.length,
}}
/>
</div>
{modalMode && (
<div className="fixed inset-0 z-50">
@ -514,17 +593,9 @@ const QuarterlyWindows = () => {
onChange={(e) => setForm({ ...form, endDate: e.target.value })}
placeholder="Select Date"
/>
<SelectField
label="Grace Period (Days)"
value={form.gracePeriod}
onChange={(e) => setForm({ ...form, gracePeriod: e.target.value })}
options={['5 days', '10 days', '15 days', '20 days', '30 days'].map((item) => ({
label: item,
value: item,
}))}
placeholder="Select Grace Period"
/>
<SelectField
</div>
{/* <SelectField
label="Status"
value={form.status}
required
@ -534,6 +605,18 @@ const QuarterlyWindows = () => {
{ label: 'Inactive', value: 'Inactive' },
]}
placeholder="Select Status"
/> */}
{/* </div> */}
<div className="w-full">
<SelectField
label="Grace Period (Days)"
value={form.gracePeriod}
onChange={(e) => setForm({ ...form, gracePeriod: e.target.value })}
options={['5 days', '10 days', '15 days', '20 days', '30 days'].map((item) => ({
label: item,
value: item,
}))}
placeholder="Select Grace Period"
/>
</div>

View File

@ -885,7 +885,6 @@ const tableRows = auditHistory.map((entry, index) => {
{ width: 120, style: { whiteSpace: 'normal', wordWrap: 'break-word' } }, // HS Code
{ width: 200, style: { whiteSpace: 'normal', wordWrap: 'break-word' } }, // Product
{ width: 150, align: 'center', style: { whiteSpace: 'normal', wordWrap: 'break-word' } }, // Status
{ width: 100, style: { padding: 10, margin: 0 } }, // Gap between Status and Actors
{ width: 200, style: { whiteSpace: 'normal', wordWrap: 'break-word' } }, // Actors
{ width: 270, style: { whiteSpace: 'normal', wordWrap: 'break-word' } } // Details
]}