manage submission view design & integration done

This commit is contained in:
Senthamilselvi 2025-10-31 00:09:38 +05:30
parent 93c40e4f8c
commit 1acadd835a
5 changed files with 449 additions and 271 deletions

View File

@ -118,27 +118,32 @@ const SubmissionTable = ({ selectedQuarter, selectedYear }) => {
renderStatusBadge(item?.status),
item?.updated_by || '-',
<span className="whitespace-nowrap">{formatDate(item?.updated_at)}</span>,
<div className="flex items-center justify-center gap-[10px]">
<CheckCircle2
size={22}
className="text-green-600 cursor-pointer hover:scale-110 transition-transform"
strokeWidth={2.5}
/>
<XCircle
size={22}
className="text-red-600 cursor-pointer hover:scale-110 transition-transform"
strokeWidth={2.5}
/>
<Eye
size={22}
className={`cursor-pointer hover:scale-110 transition-transform ${
item.status?.toLowerCase() === 'pending'
? 'text-[#9E792B]'
: 'text-gray-500'
}`}
strokeWidth={2.3}
/>
</div>,
<div className="flex items-center justify-center gap-[10px]">
<div className="relative group">
<CheckCircle2 size={22} className="text-green-600 cursor-pointer hover:scale-110 transition-transform" />
<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
</span>
</div>
<div className="relative group">
<XCircle size={22} className="text-red-600 cursor-pointer hover:scale-110 transition-transform" />
<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
</span>
</div>
<div className="relative group">
<Eye
size={22}
className={`cursor-pointer hover:scale-110 transition-transform ${
item.status?.toLowerCase() === 'pending' ? 'text-[#9E792B]' : 'text-gray-500'
}`}
onClick={() => navigate(`/admin/validations/${item.id}`)}
/>
<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">
View
</span>
</div>
</div>,
]);
const columnWidths = [240, 120, 100, 100, 130, 150, 180, 150, 150, 180, 120];

View File

@ -1,6 +1,7 @@
import React from 'react';
import { Link, useNavigate, useParams } from 'react-router-dom';
import AdminHeader from '@/components/admin/AdminHeader';
import { getSubmissionById } from '@/services/admin/submission';
const caretUpSrc = '/assets/images/caret-up.svg';
const establishmentIconSrc = '/assets/images/Establishment.svg';
@ -13,21 +14,6 @@ const femaleEmployeesIconSrc = '/assets/images/femaleemployess.svg';
const uaeIconSrc = '/assets/images/uae.svg';
const nonUaeIconSrc = '/assets/images/nonuae.svg';
const establishmentDetails = [
{ label: 'Establishment Name', value: 'ABC Manufacturing LLC' },
{ label: 'Establishment ID', value: 'MSCensus-23-05-002829' },
{ label: 'Email', value: 'info@abcmg.com' },
{ label: 'Emirate', value: 'Abu Dhabi' },
];
const employmentDetails = [
{ label: 'Total Employees', value: '120' },
{ label: 'Male Employees', value: '80' },
{ label: 'Female Employees', value: '40' },
{ label: 'UAE Nationals', value: '30' },
{ label: 'Non-UAE Nationals', value: '90' },
];
const employmentIcons = {
'Total Employees': totalEmployeesIconSrc,
'Male Employees': maleEmployeesIconSrc,
@ -36,75 +22,233 @@ const employmentIcons = {
'Non-UAE Nationals': nonUaeIconSrc,
};
const quarterLegend = [
{ label: 'Previous Quarter', color: '#1D4ED8' },
{ label: 'Current Quarter', color: '#1E7C34' },
{ label: 'Next Quarter', color: '#D97706' },
];
const productDetails = [
{
code: '04012000',
name: 'Milk',
submittedOn: '28/03/2025',
unit: 'Liters',
capacity: '20000',
quarters: [
{ label: 'Q1-2025', color: '#0C64F6', cellColor: '#F4F8FF' },
{ label: 'Q2-2025', color: '#1E7C34', cellColor: '#F3FAF4' },
{ label: 'Q3-2025', color: '#D97706', cellColor: '#FFF6EC' },
],
rows: [
{ label: 'Quantity (liters)', values: ['500', '200', '50'] },
{ label: 'Cost (AED)', values: ['40,000', '20,000', '20,000'] },
{ label: 'Reason', values: ['--', 'Seasonal production surge', 'Higher demand expected'] },
],
remarks: 'NA',
},
{
code: '07028000',
name: 'Iron',
submittedOn: '18/08/2025',
unit: 'Kilogram',
capacity: '50000',
quarters: [
{ label: 'Q1-2025', color: '#0C64F6', cellColor: '#F4F8FF' },
{ label: 'Q2-2025', color: '#1E7C34', cellColor: '#F3FAF4' },
{ label: 'Q3-2025', color: '#D97706', cellColor: '#FFF6EC' },
],
rows: [
{ label: 'Quantity (kilograms)', values: ['800', '400', '80'] },
{ label: 'Cost (AED)', values: ['70,000', '40,000', '30,000'] },
{ label: 'Reason', values: ['--', 'Seasonal procurement', 'Higher demand expected'] },
],
remarks: 'NA',
},
];
const infoBadgeClass = 'inline-flex items-center rounded-[10px] border border-[#E5D7AA] bg-[#FDF9EB] px-3 py-1 text-sm font-medium text-[#92722A]';
const ValidationReview = () => {
const navigate = useNavigate();
const { id } = useParams();
const [toast, setToast] = React.useState(null);
const [isRejectOpen, setIsRejectOpen] = React.useState(false);
const [rejectReason, setRejectReason] = React.useState('');
const [loading, setLoading] = React.useState(true);
const [error, setError] = React.useState(null);
const [submissionData, setSubmissionData] = React.useState(null);
React.useEffect(() => {
const fetchSubmission = async () => {
if (!id) {
setError('No submission ID provided');
setLoading(false);
return;
}
try {
setLoading(true);
setError(null);
const result = await getSubmissionById(id);
let data = null;
if (result?.status === 'success' && result?.data) {
data = result.data;
} else if (result?.data) {
data = result.data;
} else {
data = result;
}
if (!data) {
throw new Error('No data received from API');
}
setSubmissionData(data);
} catch (err) {
console.error('Failed to load submission details:', err);
setError(err.message || 'Unable to load submission details. Please try again later.');
} finally {
setLoading(false);
}
};
fetchSubmission();
}, [id]);
const showApproveToast = () => {
setToast({ type: 'success', text: 'ABC Manufacturing LLC has been approved successfully.' });
window.clearTimeout((showApproveToast._t));
// Auto-hide after 4 seconds
const establishmentName = submissionData?.establishment?.factory_name || 'Establishment';
setToast({ type: 'success', text: `${establishmentName} has been approved successfully.` });
window.clearTimeout(showApproveToast._t);
showApproveToast._t = window.setTimeout(() => setToast(null), 4000);
};
const handleRejectSubmit = () => {
setToast({ type: 'info', text: rejectReason ? `Submission rejected. Reason: ${rejectReason}` : 'Submission rejected.' });
window.clearTimeout((showApproveToast._t));
setToast({
type: 'info',
text: rejectReason ? `Submission rejected. Reason: ${rejectReason}` : 'Submission rejected.'
});
window.clearTimeout(showApproveToast._t);
showApproveToast._t = window.setTimeout(() => setToast(null), 4000);
setIsRejectOpen(false);
setRejectReason('');
};
if (loading) {
return (
<div className="min-h-screen bg-[#F6F5F1]">
<AdminHeader />
<div className="flex items-center justify-center h-[calc(100vh-200px)] text-[#92722A]">
<svg className="animate-spin h-6 w-6 mr-3 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>
Loading submission details...
</div>
</div>
);
}
if (error || !submissionData) {
return (
<div className="min-h-screen bg-[#F6F5F1]">
<AdminHeader />
<div className="max-w-[1280px] mx-auto px-4 py-6">
<div className="rounded-lg bg-red-50 border border-red-200 p-6">
<div className="flex items-start gap-3">
<svg className="h-5 w-5 text-red-600 mt-0.5" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor">
<path fillRule="evenodd" d="M9.401 3.003c1.155-2 4.043-2 5.197 0l7.355 12.748c1.154 2-.29 4.5-2.599 4.5H4.645c-2.309 0-3.752-2.5-2.598-4.5L9.4 3.003ZM12 8.25a.75.75 0 0 1 .75.75v3.75a.75.75 0 0 1-1.5 0V9a.75.75 0 0 1 .75-.75Zm0 8.25a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5Z" clipRule="evenodd" />
</svg>
<div>
<h3 className="text-lg font-semibold text-red-800">Error Loading Submission</h3>
<p className="mt-1 text-sm text-red-600">{error || 'Submission not found'}</p>
<button
onClick={() => navigate('/admin/validations')}
className="mt-3 inline-flex items-center px-4 py-2 bg-red-600 text-white text-sm font-medium rounded-md hover:bg-red-700"
>
Back to Submissions
</button>
</div>
</div>
</div>
</div>
</div>
);
}
const establishment = submissionData?.establishment || {};
const establishmentDetails = [
{
label: 'Establishment Name',
value: establishment.factory_name || '—'
},
{
label: 'Establishment ID',
value: establishment.establishment_code || '—'
},
{
label: 'Email',
value: establishment.establishment_contact_email || '—'
},
{
label: 'Emirate',
value: establishment.establishment_address || '—'
},
];
const totalEmiratiMale = establishment.emirati_male || 0;
const totalEmiratiFemale = establishment.emirati_female || 0;
const totalNonEmiratiMale = establishment.non_emirati_male || 0;
const totalNonEmiratiFemale = establishment.non_emirati_female || 0;
const totalMale = totalEmiratiMale + totalNonEmiratiMale;
const totalFemale = totalEmiratiFemale + totalNonEmiratiFemale;
const totalEmirati = totalEmiratiMale + totalEmiratiFemale;
const totalNonEmirati = totalNonEmiratiMale + totalNonEmiratiFemale;
const employmentDetails = [
{
label: 'Total Employees',
value: establishment.total_employees?.toString() || (totalMale + totalFemale).toString() || '—'
},
{
label: 'Male Employees',
value: totalMale.toString() || '—'
},
{
label: 'Female Employees',
value: totalFemale.toString() || '—'
},
{
label: 'UAE Nationals',
value: totalEmirati.toString() || '—'
},
{
label: 'Non-UAE Nationals',
value: totalNonEmirati.toString() || '—'
},
];
const quarterPeriods = submissionData?.quarter_periods || {};
const previousQuarter = quarterPeriods.previous_quarter || 'Q1';
const currentQuarter = quarterPeriods.current_quarter || 'Q2';
const forecastQuarter = quarterPeriods.forecast_quarter || 'Q3';
const year = submissionData?.year || new Date().getFullYear();
const products = submissionData?.products || [];
const productDetails = Array.isArray(products) ? products.map((product) => {
const quarters = [
{ label: `${previousQuarter}-${year}`, color: '#0C64F6', cellColor: '#F4F8FF', type: 'previous' },
{ label: `${currentQuarter}-${year}`, color: '#1E7C34', cellColor: '#F3FAF4', type: 'current' },
{ label: `${forecastQuarter}-${year}`, color: '#D97706', cellColor: '#FFF6EC', type: 'forecast' },
];
const unitValue = product.unit?.uom || 'units';
const unit = String(unitValue);
const previousQuantity = product.previous_quantity || '—';
const previousCost = product.previous_cost || '—';
const currentQuantity = product.current_quantity || '—';
const currentCost = product.current_cost || '—';
const forecastQuantity = product.forecast_quantity || '—';
const forecastCost = product.forecast_cost || '—';
const variationReason = product.variation_reason?.reason || product.other_variation_reason || '—';
const zeroTargetReason = product.zero_target_reason?.reason || product.other_zero_target_reason || '—';
return {
code: product.product?.hs_code || '—',
name: product.product?.product_name || '—',
submittedBy: submissionData?.created_by || 'System',
submittedOn: submissionData?.created_at ? new Date(submissionData.created_at).toLocaleDateString('en-GB') : '—',
unit: unit,
capacity: product.annual_installed_capacity?.toString() || '—',
quarters,
rows: [
{
label: `Quantity (${unit.toLowerCase()})`,
values: [
previousQuantity.toString(),
currentQuantity.toString(),
forecastQuantity.toString()
]
},
{
label: 'Cost (AED)',
values: [
previousCost !== '—' ? Number(previousCost).toLocaleString() : '—',
currentCost !== '—' ? Number(currentCost).toLocaleString() : '—',
forecastCost !== '—' ? Number(forecastCost).toLocaleString() : '—'
]
},
{
label: 'Reason',
values: [
'—',
variationReason,
zeroTargetReason
]
},
],
remarks: product.remarks || 'NA',
};
}) : [];
return (
<div className="min-h-screen bg-[#F6F5F1]">
<AdminHeader />
@ -139,20 +283,6 @@ const ValidationReview = () => {
</span>
</nav>
{/* <div className="flex flex-col gap-2 md:flex-row md:items-end md:justify-between">
<div>
<h1 className="text-[24px] leading-[32px] font-semibold text-[#232528]">Submission Details</h1>
<p className="text-sm text-[#7A7F87]">Submission #{id || '—'}</p>
</div>
<button
type="button"
onClick={() => navigate('/admin/validations')}
className="inline-flex h-10 items-center justify-center rounded-[10px] border border-[#C3C6CB] bg-white px-4 text-sm font-medium text-[#5F646D] transition-colors hover:bg-[#F7F7F7]"
>
Back to Submissions
</button>
</div> */}
<section className="min-h-[219px] rounded-[16px] border border-[#E6EAF5] bg-white px-0 py-6 shadow-[0_16px_32px_rgba(15,23,42,0.08)]">
<div className="grid gap-1 lg:grid-cols-2">
<div className="space-y-6 lg:pr-6 lg:pl-15">
@ -210,79 +340,88 @@ const ValidationReview = () => {
</div>
</section>
<div className="grid grid-cols-1 gap-4 lg:grid-cols-2">
{productDetails.map((product) => (
<section key={product.code} className="rounded-[16px] bg-white p-6 shadow-[0_16px_32px_rgba(15,23,42,0.08)] ring-1 ring-[#E5E7EB] space-y-4">
<div className="flex flex-col gap-2">
<div className="text-base font-semibold text-[#232528]">Product: {product.code} - {product.name}</div>
<div className="flex flex-wrap gap-2">
<span className={infoBadgeClass}>Submitted By: <span className="ml-1 font-semibold text-[#232528]">{product.submittedOn}</span></span>
<span className={infoBadgeClass}>Unit: <span className="ml-1 font-semibold text-[#232528]">{product.unit}</span></span>
<span className={infoBadgeClass}>Capacity: <span className="ml-1 font-semibold text-[#232528]">{product.capacity}</span></span>
</div>
</div>
<div className="flex flex-wrap items-center gap-4 text-sm text-[#4B5563]">
{quarterLegend.map((item) => (
<div key={item.label} className="flex items-center gap-2">
<span className="h-2 w-2 rounded-full" style={{ backgroundColor: item.color }} />
<span>{item.label}</span>
{productDetails.length > 0 ? (
<div className="grid grid-cols-1 gap-4 lg:grid-cols-2">
{productDetails.map((product, idx) => (
<section key={product.code + idx} className="rounded-[16px] bg-white p-6 shadow-[0_16px_32px_rgba(15,23,42,0.08)] ring-1 ring-[#E5E7EB] space-y-4">
<div className="flex flex-wrap items-center justify-between w-full">
<div className="text-sm font-semibold text-[#232528]">
Product: <span className="font-normal">{product.code} - {product.name}</span>
</div>
))}
</div>
<div className="overflow-hidden rounded-[12px] border border-[#E5E7EB]">
<table className="w-full table-fixed text-sm">
<thead>
<tr>
<th className="w-[160px] bg-[#F2ECCF] px-4 py-3 text-left text-xs font-semibold uppercase tracking-wide text-[#92722A]">Description</th>
{product.quarters.map((quarter) => (
<th
key={quarter.label}
className="px-4 py-3 text-left text-xs font-semibold uppercase tracking-wide text-white"
style={{ backgroundColor: quarter.color }}
>
{quarter.label}
</th>
))}
</tr>
</thead>
<tbody>
{product.rows.map((row) => (
<tr key={row.label} className="border-t border-[#E5E7EB]">
<td className="bg-[#FFFCF2] px-4 py-3 font-medium text-[#232528]">{row.label}</td>
{row.values.map((value, index) => (
<td
key={`${row.label}-${product.quarters[index].label}`}
className="px-4 py-3 text-[#232528]"
style={{ backgroundColor: product.quarters[index].cellColor }}
<div className="flex flex-wrap items-center gap-2">
<div className="flex items-center gap-1 border border-[#D0D5DD] rounded-md px-2 py-[2px] text-xs text-[#344054]">
<span className="text-[#475467] font-medium">Submitted By:</span>
<span className="font-semibold text-[#232528]">{product.submittedBy}</span>
</div>
<div className="flex items-center gap-1 border border-[#D0D5DD] rounded-md px-2 py-[2px] text-xs text-[#344054]">
<span className="text-[#475467] font-medium">Unit:</span>
<span className="font-semibold text-[#232528]">{product.unit}</span>
</div>
<div className="flex items-center gap-1 border border-[#D0D5DD] rounded-md px-2 py-[2px] text-xs text-[#344054]">
<span className="text-[#475467] font-medium">Capacity:</span>
<span className="font-semibold text-[#232528]">{product.capacity}</span>
</div>
</div>
</div>
<div className="overflow-hidden rounded-[12px] border border-[#E5E7EB]">
<table className="w-full table-fixed text-sm">
<thead>
<tr>
<th className="w-[160px] bg-[#F2ECCF] px-4 py-3 text-left text-xs font-semibold uppercase tracking-wide text-[#92722A]">Description</th>
{product.quarters.map((quarter) => (
<th
key={quarter.label}
className="px-4 py-3 text-left text-xs font-semibold uppercase tracking-wide text-white"
style={{ backgroundColor: quarter.color }}
>
{value}
</td>
{quarter.label}
</th>
))}
</tr>
))}
</tbody>
</table>
</div>
</thead>
<tbody>
{product.rows.map((row) => (
<tr key={row.label} className="border-t border-[#E5E7EB]">
<td className="bg-[#FFFCF2] px-4 py-3 font-medium text-[#232528]">{row.label}</td>
{row.values.map((value, index) => (
<td
key={`${row.label}-${product.quarters[index].label}`}
className="px-4 py-3 text-[#232528]"
style={{ backgroundColor: product.quarters[index].cellColor }}
>
{value}
</td>
))}
</tr>
))}
</tbody>
</table>
</div>
<div className="space-y-3">
<div className="text-sm text-[#4B5563]">
<span className="font-semibold text-[#232528]">Remarks:</span> {product.remarks}
<div className="space-y-3">
<div className="text-sm text-[#4B5563]">
<span className="font-semibold text-[#232528]">Remarks:</span> {product.remarks}
</div>
<div className="flex flex-col gap-2">
<span className="text-sm font-semibold text-[#232528]">Admin Remarks</span>
<textarea
rows={3}
className="w-full rounded-[10px] border border-[#D1D5DB] bg-[#FFFCF2] px-4 py-2 text-sm text-[#232528] placeholder:text-[#9CA3AF] focus:outline-none focus:ring-2 focus:ring-[#92722A]"
placeholder="Add short note..."
defaultValue=""
/>
</div>
</div>
<div className="flex flex-col gap-2">
<span className="text-sm font-semibold text-[#232528]">Admin Remarks</span>
<textarea
rows={3}
className="w-full rounded-[10px] border border-[#D1D5DB] bg-[#FFFCF2] px-4 py-2 text-sm text-[#232528] placeholder:text-[#9CA3AF] focus:outline-none focus:ring-2 focus:ring-[#92722A]"
placeholder="Add short note..."
defaultValue=""
/>
</div>
</div>
</section>
))}
</div>
</section>
))}
</div>
) : (
<div className="rounded-lg bg-amber-50 border border-amber-200 p-6 text-center">
<p className="text-amber-800">No products found for this submission.</p>
</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">
@ -359,4 +498,4 @@ const ValidationReview = () => {
);
};
export default ValidationReview;
export default ValidationReview;

View File

@ -4,9 +4,9 @@ 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 Footer from '@/components/common/Footer';
const searchIconSrc = '/assets/images/material-symbols_search-rounded.svg';
const downloadIconSrc = '/assets/images/downloadsimple.svg';
const caretUpSrc = '/assets/images/caret-up.svg';
const STATUS_VARIANTS = {
@ -22,6 +22,21 @@ const SUMMARY_VARIANTS = {
rejected: { background: '#FEF2F2', label: '#B52520', value: '#232528' },
};
const formatDateTime = (value) => {
if (!value) return '—';
const date = new Date(value);
if (Number.isNaN(date.getTime())) return '—';
const formatted = date.toLocaleString('en-GB', {
day: '2-digit',
month: '2-digit',
year: 'numeric',
hour: '2-digit',
minute: '2-digit',
hour12: true,
});
return formatted.replace(/\s?(am|pm)$/i, (m) => m.toUpperCase());
};
const Badge = ({ children, status }) => {
const variant = STATUS_VARIANTS[status] || { background: '#F2F4F7', text: '#475467', border: '#EAECF0' };
return (
@ -39,7 +54,6 @@ const ManageSubmissions = () => {
const [submissions, setSubmissions] = React.useState([]);
const [loading, setLoading] = React.useState(true);
const [error, setError] = React.useState(null);
const [search, setSearch] = React.useState('');
const [year, setYear] = React.useState('');
const [quarter, setQuarter] = React.useState('');
@ -48,31 +62,26 @@ const ManageSubmissions = () => {
const [currentPage, setCurrentPage] = React.useState(1);
const pageSize = 10;
// 🔹 Fetch data from backend
React.useEffect(() => {
const fetchSubmissions = async () => {
try {
const result = await getSubmissions();
// Handle success or direct data array
const data = result?.status === 'success' ? result.data : result;
// Map API fields to table fields
const mapped = (data || []).map((item) => ({
id: item.id,
establishment: '', // blank
emirate: '', // blank
year: String(item.year ?? ''),
quarter: item.quarter ?? '',
products: '', // blank
submittedBy: item.created_by ?? '',
submittedAt: item.created_at
? new Date(item.created_at).toLocaleString()
: '',
status: item.status ?? '',
reviewer: '', // blank
establishment: item?.establishment?.factory_name || '-',
emirate: item?.establishment?.establishment_emirate?.name || '-',
year: String(item.year ?? '-'),
quarter: item.quarter ?? '-',
products: item?.product_count || '-',
submittedBy: item.created_by ?? '-',
submittedAt: formatDateTime(item.created_at),
status: item.status ?? '-',
reviewer: item?.updated_by || '-',
reviewerOn: formatDateTime(item?.updated_at),
}));
setSubmissions(mapped);
} catch (err) {
console.error('Failed to load submissions', err);
@ -81,9 +90,8 @@ const ManageSubmissions = () => {
setLoading(false);
}
};
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]);
@ -92,10 +100,10 @@ const ManageSubmissions = () => {
const filtered = React.useMemo(() => {
return submissions.filter((item) => {
const matchSearch = search
? item.establishment?.toLowerCase().includes(search.toLowerCase()) ||
item.submittedBy?.toLowerCase().includes(search.toLowerCase())
: true;
const matchSearch =
!search ||
item.establishment?.toLowerCase().includes(search.toLowerCase()) ||
item.submittedBy?.toLowerCase().includes(search.toLowerCase());
const matchYear = !year || year === 'All' || item.year === year;
const matchQuarter = !quarter || quarter === 'All' || item.quarter === quarter;
const matchEmirate = !emirate || emirate === 'All' || item.emirate === emirate;
@ -104,12 +112,15 @@ const ManageSubmissions = () => {
});
}, [submissions, search, year, quarter, emirate, status]);
const summaryCounts = React.useMemo(() => ({
total: submissions.length,
approved: submissions.filter((i) => i.status === 'Approved').length,
pending: submissions.filter((i) => i.status === 'Pending').length,
rejected: submissions.filter((i) => i.status === 'Rejected').length,
}), [submissions]);
const summaryCounts = React.useMemo(
() => ({
total: submissions.length,
approved: submissions.filter((i) => i.status === 'Approved').length,
pending: submissions.filter((i) => i.status === 'Pending').length,
rejected: submissions.filter((i) => i.status === 'Rejected').length,
}),
[submissions]
);
const headers = [
'Establishment',
@ -121,6 +132,7 @@ const ManageSubmissions = () => {
'Submission Date & Time',
'Status',
'Reviewer',
'Reviewer On',
'Actions',
];
@ -134,30 +146,11 @@ const ManageSubmissions = () => {
item.submittedAt,
item.status,
item.reviewer,
item.id,
item.reviewerOn,
item,
]);
const columnWidths = [302, 105, 83, 91, 100, 152, 208, 120, 104, 119];
if (loading) {
return (
<div className="flex items-center justify-center h-screen text-[#92722A]">
<svg className="animate-spin h-6 w-6 mr-3 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>
Loading submissions...
</div>
);
}
if (error) {
return (
<div className="flex items-center justify-center h-screen text-red-600">
{error}
</div>
);
}
const columnWidths = [302, 105, 83, 91, 100, 152, 208, 120, 104, 140, 130];
const toolbar = (
<div className="px-6 pt-6 pb-5 space-y-4">
@ -184,7 +177,21 @@ const ManageSubmissions = () => {
</div>
);
if (loading)
return (
<div className="flex items-center justify-center h-screen text-[#92722A]">
<svg className="animate-spin h-6 w-6 mr-3 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>
Loading submissions...
</div>
);
if (error) return <div className="flex items-center justify-center h-screen text-red-600">{error}</div>;
return (
<>
<div className="min-h-screen bg-[#F6F5F1]">
<AdminHeader />
<div className="max-w-[1280px] mx-auto px-4 py-6 flex flex-col gap-6">
@ -192,34 +199,27 @@ const ManageSubmissions = () => {
<Link to="/admin/dashboard" className="flex items-center gap-2 text-[#232528] hover:underline">
Dashboard <img src={caretUpSrc} alt="Dashboard" className="h-3 w-3" />
</Link>
<span className="flex items-center gap-2 text-[#92722A] font-medium">Manage Submissions</span>
<span className="text-[#92722A] font-medium">Manage Submissions</span>
</nav>
{/* Summary bar */}
<div className="flex flex-wrap gap-[6px] rounded-[12px] bg-white px-5 py-3 shadow">
{Object.entries(summaryCounts).map(([key, val]) => {
const v = SUMMARY_VARIANTS[key];
return (
<div key={key} className="inline-flex items-center gap-[6px] rounded-[8px] px-2 py-[2px]" style={{ background: v.background }}>
<span className="font-medium text-[13px]" style={{ color: v.label }}>
{key.charAt(0).toUpperCase() + key.slice(1)}:
</span>
<span className="font-semibold text-[13px]" style={{ color: v.value }}>
{val}
</span>
</div>
);
})}
<button
type="button"
className="ml-auto inline-flex h-10 items-center justify-center gap-3 rounded-[6px] border border-[#C3C6CB] bg-white px-5 text-sm font-medium text-[#5F646D] hover:bg-[#F7F7F7]"
>
<img src={downloadIconSrc} alt="Export" className="h-4 w-4" />
Export CSV
</button>
<div className="flex flex-wrap gap-[6px] rounded-[12px] bg-white px-5 py-3 shadow items-center justify-between">
<div className="flex flex-wrap gap-[6px]">
{Object.entries(summaryCounts).map(([key, val]) => {
const v = SUMMARY_VARIANTS[key];
return (
<div key={key} className="inline-flex items-center gap-[6px] rounded-[8px] px-2 py-[2px]" style={{ background: v.background }}>
<span className="font-medium text-[13px]" style={{ color: v.label }}>
{key.charAt(0).toUpperCase() + key.slice(1)}:
</span>
<span className="font-semibold text-[13px]" style={{ color: v.value }}>
{val}
</span>
</div>
);
})}
</div>
</div>
{/* Table */}
<div className="rounded-2xl border border-[#E5E7EB] bg-white shadow overflow-hidden">
<Table
headers={headers}
@ -233,24 +233,53 @@ const ManageSubmissions = () => {
pageSize,
totalItems: filtered.length,
}}
renderCell={(value, rowIndex, columnIndex) => {
renderCell={(value, _, columnIndex) => {
if (columnIndex === 7)
return <div className="flex justify-start"><Badge status={String(value)}>{value}</Badge></div>;
if (columnIndex === 9)
return (
<button
onClick={() => navigate(`/admin/validations/${value}`)}
className="text-sm font-medium text-[#92722A] hover:underline"
>
View Details
</button>
<div className="flex justify-start">
<Badge status={String(value)}>{value}</Badge>
</div>
);
if (columnIndex === 10) {
const item = value;
return (
<div className="flex items-center justify-center gap-[10px]">
<div className="relative group">
<CheckCircle2 size={22} className="text-green-600 cursor-pointer hover:scale-110 transition-transform" />
<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
</span>
</div>
<div className="relative group">
<XCircle size={22} className="text-red-600 cursor-pointer hover:scale-110 transition-transform" />
<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
</span>
</div>
<div className="relative group">
<Eye
size={22}
className={`cursor-pointer hover:scale-110 transition-transform ${
item.status?.toLowerCase() === 'pending' ? 'text-[#9E792B]' : 'text-gray-500'
}`}
onClick={() => navigate(`/admin/validations/${item.id}`)}
/>
<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">
View
</span>
</div>
</div>
);
}
return value;
}}
/>
</div>
</div>
</div>
<Footer />
</>
);
};

View File

@ -1,7 +1,7 @@
import React from 'react';
import { useNavigate } from 'react-router-dom';
import { login } from '@/services/auth/authService.js';
import { RefreshCw } from "lucide-react";
const logoSrc = '/assets/images/FCSCLogo.svg';
const Login = () => {
@ -277,12 +277,13 @@ const Login = () => {
{captcha}
</div>
<button
type="button"
onClick={generateCaptcha}
className="text-sm text-[#92722A] hover:underline"
>
Refresh
</button>
type="button"
onClick={generateCaptcha}
className="flex items-center gap-1 text-sm text-[#92722A] hover:underline"
>
<RefreshCw size={14} className="text-[#92722A]" />
Refresh
</button>
</div>
<p className="text-xs text-gray-500 mt-1">
If you cannot read the code, click "Refresh" or type:{" "}

View File

@ -1,7 +1,7 @@
import React, { useState, useEffect } from 'react';
import { useNavigate } from 'react-router-dom';
import apiClient from '@/services/api/apiClient';
import { RefreshCw } from "lucide-react";
const logoSrc = '/assets/images/FCSCLogo.svg';
const PublicContactForm = () => {
@ -266,14 +266,18 @@ const PublicContactForm = () => {
{captcha}
</div>
<button
type="button"
onClick={generateCaptcha}
className="text-sm text-[#92722A] hover:underline"
>
Refresh
</button>
type="button"
onClick={generateCaptcha}
className="flex items-center gap-1 text-sm text-[#92722A] hover:underline"
>
<RefreshCw size={14} className="text-[#92722A]" />
Refresh
</button>
</div>
<p className="text-xs text-gray-500 mt-1">
If you cannot read the code, click "Refresh" or type:{" "}
<span className="font-mono">{captcha}</span>
</p>
<input
type="text"
name="captcha"