bug fixed for admin submissions
This commit is contained in:
parent
776f1391d2
commit
546425a85c
3
ipi-survey-platform/public/assets/images/tooltip.svg
Normal file
3
ipi-survey-platform/public/assets/images/tooltip.svg
Normal 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 |
@ -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"
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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 || '—'}
|
||||
|
||||
@ -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"
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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>
|
||||
</>
|
||||
)}
|
||||
|
||||
@ -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 && (
|
||||
|
||||
@ -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 />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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' && (
|
||||
|
||||
@ -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>
|
||||
|
||||
|
||||
@ -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
|
||||
]}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user