manage submission - using api fetched listing data

This commit is contained in:
Senthamilselvi 2025-10-28 11:19:08 +05:30
parent dcc05fc762
commit eefb85bbde
2 changed files with 151 additions and 359 deletions

View File

@ -4,180 +4,9 @@ import AdminHeader from '../../components/admin/AdminHeader';
import Table from '../../components/common/Table'; import Table from '../../components/common/Table';
import SearchIcon from '../../assets/images/material-symbols_search-rounded.svg'; import SearchIcon from '../../assets/images/material-symbols_search-rounded.svg';
import DownloadIcon from '../../assets/images/DownloadSimple.svg'; import DownloadIcon from '../../assets/images/DownloadSimple.svg';
import ActionIcon from '../../assets/images/qlementine-icons_preview-16.svg';
import CaretUp from '../../assets/images/caret-up.svg'; import CaretUp from '../../assets/images/caret-up.svg';
import { TextField, SelectField } from '../../components/common/FormControls'; import { SelectField } from '../../components/common/FormControls';
import { getSubmissions } from '../../services/admin/submission.js';
const submissions = [
{
id: 1,
establishment: 'CASCADE MARINE FOODS LLC',
emirate: 'Sharjah',
year: '2025',
quarter: 'Q4',
submittedAt: '18/10/2025, 10:40 AM',
products: 10,
submittedBy: 'Ahmed (Owner)',
status: 'Pending',
reviewer: 'Omar',
},
{
id: 2,
establishment: 'Khazan meat factory',
emirate: 'Sharjah',
year: '2025',
quarter: 'Q3',
submittedAt: '18/10/2025, 10:40 AM',
products: 6,
submittedBy: 'Sara (Admin)',
status: 'Approved',
reviewer: 'Abdul',
},
{
id: 3,
establishment: 'SPINNEYS FRESH FOOD INDUSTRIES L.L.C',
emirate: 'Dubai',
year: '2025',
quarter: 'Q2',
submittedAt: '18/10/2025, 10:40 AM',
products: 8,
submittedBy: 'Shaik (Admin)',
status: 'Approved',
reviewer: 'Abdul',
},
{
id: 4,
establishment: 'FRESHLY FROZEN FOODS FACTORY L.L.C',
emirate: 'Dubai',
year: '2025',
quarter: 'Q1',
submittedAt: '18/10/2025, 10:40 AM',
products: 4,
submittedBy: 'Abdul (Manager)',
status: 'Rejected',
reviewer: 'Abdul',
},
{
id: 5,
establishment: 'SAHAR FOOD INDUSTRY (L.L.C)',
emirate: 'Dubai',
year: '2025',
quarter: 'Q4',
submittedAt: '18/10/2025, 10:40 AM',
products: 8,
submittedBy: 'Ahmed (Owner)',
status: 'Approved',
reviewer: 'Abdul',
},
{
id: 6,
establishment: 'CONTINENTAL FOOD PROCESSING LLC',
emirate: 'Ajman',
year: '2025',
quarter: 'Q3',
submittedAt: '18/10/2025, 10:40 AM',
products: 5,
submittedBy: 'Sara (Admin)',
status: 'Rejected',
reviewer: 'Abdul',
},
{
id: 7,
establishment: 'AL KHAZNA POULTRY FARM',
emirate: 'Abu Dhabi',
year: '2025',
quarter: 'Q2',
submittedAt: '18/10/2025, 10:40 AM',
products: 7,
submittedBy: 'Sara (Admin)',
status: 'Approved',
reviewer: 'Abdul',
},
{
id: 8,
establishment: 'DIAMOND MEAT PROCESSING L.L.C',
emirate: 'Abu Dhabi',
year: '2025',
quarter: 'Q2',
submittedAt: '18/10/2025, 10:40 AM',
products: 7,
submittedBy: 'Ahmed (Owner)',
status: 'Approved',
reviewer: 'Abdul',
},
{
id: 9,
establishment: 'ALLIANCE FOODS CO L.L.C',
emirate: 'Abu Dhabi',
year: '2025',
quarter: 'Q2',
submittedAt: '18/10/2025, 10:40 AM',
products: 7,
submittedBy: 'Ahmed (Owner)',
status: 'Approved',
reviewer: 'Abdul',
},
{
id: 10,
establishment: 'KRUSTASIA FOODS L.L.C',
emirate: 'Dubai',
year: '2025',
quarter: 'Q1',
submittedAt: '18/10/2025, 10:40 AM',
products: 8,
submittedBy: 'Sara (Admin)',
status: 'Pending',
reviewer: 'Fathima',
},
{
id: 11,
establishment: 'AL TAMEER FOOD INDUSTRIES',
emirate: 'Abu Dhabi',
year: '2025',
quarter: 'Q3',
submittedAt: '19/10/2025, 8:15 AM',
products: 11,
submittedBy: 'Ahmed (Owner)',
status: 'Pending',
reviewer: 'Omar',
},
{
id: 12,
establishment: 'AL RAWABI DAIRY COMPANY',
emirate: 'Dubai',
year: '2024',
quarter: 'Q4',
submittedAt: '12/09/2024, 4:10 PM',
products: 9,
submittedBy: 'Mariam (Admin)',
status: 'Approved',
reviewer: 'Abdul',
},
{
id: 13,
establishment: 'EMIRATES FRESH POULTRY',
emirate: 'Ajman',
year: '2024',
quarter: 'Q4',
submittedAt: '05/09/2024, 11:05 AM',
products: 6,
submittedBy: 'Haroon (Manager)',
status: 'Rejected',
reviewer: 'Omar',
},
{
id: 14,
establishment: 'GULF FISHERIES LLC',
emirate: 'Ras Al Khaimah',
year: '2024',
quarter: 'Q3',
submittedAt: '21/08/2024, 9:15 AM',
products: 12,
submittedBy: 'Owais (Owner)',
status: 'Approved',
reviewer: 'Fathima',
},
];
const STATUS_VARIANTS = { const STATUS_VARIANTS = {
Approved: { background: '#F3FAF4', text: '#2F663C', border: '#F3FAF4' }, Approved: { background: '#F3FAF4', text: '#2F663C', border: '#F3FAF4' },
@ -206,6 +35,10 @@ const Badge = ({ children, status }) => {
const ManageSubmissions = () => { const ManageSubmissions = () => {
const navigate = useNavigate(); const navigate = useNavigate();
const [submissions, setSubmissions] = React.useState([]);
const [loading, setLoading] = React.useState(true);
const [error, setError] = React.useState(null);
const [search, setSearch] = React.useState(''); const [search, setSearch] = React.useState('');
const [year, setYear] = React.useState(''); const [year, setYear] = React.useState('');
const [quarter, setQuarter] = React.useState(''); const [quarter, setQuarter] = React.useState('');
@ -214,37 +47,68 @@ const ManageSubmissions = () => {
const [currentPage, setCurrentPage] = React.useState(1); const [currentPage, setCurrentPage] = React.useState(1);
const pageSize = 10; const pageSize = 10;
const yearOptions = React.useMemo(() => ['All', ...new Set(submissions.map((item) => item.year))], []); // 🔹 Fetch data from backend
const quarterOptions = React.useMemo(() => ['All', ...new Set(submissions.map((item) => item.quarter))], []); React.useEffect(() => {
const emirateOptions = React.useMemo(() => ['All', ...new Set(submissions.map((item) => item.emirate))], []); const fetchSubmissions = async () => {
const statusOptions = React.useMemo(() => ['All', ...new Set(submissions.map((item) => item.status))], []); 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
}));
setSubmissions(mapped);
} catch (err) {
console.error('Failed to load submissions', err);
setError('Unable to load submissions. Please try again later.');
} finally {
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]);
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]);
const filtered = React.useMemo(() => { const filtered = React.useMemo(() => {
return submissions.filter((item) => { return submissions.filter((item) => {
const matchesSearch = search const matchSearch = search
? item.establishment.toLowerCase().includes(search.toLowerCase()) || ? item.establishment?.toLowerCase().includes(search.toLowerCase()) ||
item.submittedBy.toLowerCase().includes(search.toLowerCase()) item.submittedBy?.toLowerCase().includes(search.toLowerCase())
: true; : true;
const matchesYear = !year || year === 'All' || item.year === year; const matchYear = !year || year === 'All' || item.year === year;
const matchesQuarter = !quarter || quarter === 'All' || item.quarter === quarter; const matchQuarter = !quarter || quarter === 'All' || item.quarter === quarter;
const matchesEmirate = !emirate || emirate === 'All' || item.emirate === emirate; const matchEmirate = !emirate || emirate === 'All' || item.emirate === emirate;
const matchesStatus = !status || status === 'All' || item.status === status; const matchStatus = !status || status === 'All' || item.status === status;
return matchesSearch && matchesYear && matchesQuarter && matchesEmirate && matchesStatus; return matchSearch && matchYear && matchQuarter && matchEmirate && matchStatus;
}); });
}, [search, year, quarter, emirate, status]); }, [submissions, search, year, quarter, emirate, status]);
React.useEffect(() => { const summaryCounts = React.useMemo(() => ({
setCurrentPage(1); total: submissions.length,
}, [search, year, quarter, emirate, status]); approved: submissions.filter((i) => i.status === 'Approved').length,
pending: submissions.filter((i) => i.status === 'Pending').length,
const summaryCounts = React.useMemo(() => { rejected: submissions.filter((i) => i.status === 'Rejected').length,
return { }), [submissions]);
total: submissions.length,
approved: submissions.filter((item) => item.status === 'Approved').length,
pending: submissions.filter((item) => item.status === 'Pending').length,
rejected: submissions.filter((item) => item.status === 'Rejected').length,
};
}, []);
const headers = [ const headers = [
'Establishment', 'Establishment',
@ -259,116 +123,61 @@ const ManageSubmissions = () => {
'Actions', 'Actions',
]; ];
const tableRows = React.useMemo( const tableRows = filtered.map((item) => [
() => item.establishment,
filtered.map((item) => [ item.emirate,
item.establishment, item.year,
item.emirate, item.quarter,
item.year, item.products,
item.quarter, item.submittedBy,
item.products, item.submittedAt,
item.submittedBy, item.status,
item.submittedAt, item.reviewer,
item.status, item.id,
item.reviewer, ]);
item.id,
]),
[filtered]
);
const columnWidths = React.useMemo( const columnWidths = [302, 105, 83, 91, 100, 152, 208, 120, 104, 119];
() => [
302, if (loading) {
105, return (
83, <div className="flex items-center justify-center h-screen text-[#92722A]">
91, <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">
100, <circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
152, <path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8v8z"></path>
208, </svg>
120, Loading submissions...
104, </div>
119, );
], }
[]
); if (error) {
return (
<div className="flex items-center justify-center h-screen text-red-600">
{error}
</div>
);
}
const toolbar = ( const toolbar = (
<div className="px-6 pt-6 pb-5 space-y-4"> <div className="px-6 pt-6 pb-5 space-y-4">
<div className="flex flex-col gap-2 md:flex-row md:items-center md:justify-between"> <div className="flex flex-col gap-2 md:flex-row md:items-center md:justify-between">
<h2 className="text-[18px] leading-[28px] font-medium text-[#232528]"> <h2 className="text-[18px] font-medium text-[#232528]">
Manage Submissions <span className="text-[#7A7F87]">({filtered.length})</span> Manage Submissions <span className="text-[#7A7F87]">({filtered.length})</span>
</h2> </h2>
<div className="w-full md:w-auto"> <div className="flex flex-wrap gap-3">
<div className="flex flex-wrap gap-3 md:flex-nowrap"> <div className="relative w-[320px]">
<div className="relative" style={{ width: '320px', height: '40px' }}> <img src={SearchIcon} alt="Search" className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4" />
<img src={SearchIcon} alt="Search" className="pointer-events-none absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4" /> <input
<input value={search}
type="text" onChange={(e) => setSearch(e.target.value)}
value={search} placeholder="Search Establishment"
onChange={(event) => setSearch(event.target.value)} 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]"
placeholder="Search Establishment" />
className="h-full w-full rounded-md border border-[#C3C6CB] pl-9 pr-3 text-sm focus:outline-none focus:ring-1 focus:ring-[#92722A]"
/>
</div>
<div className="w-full md:w-[134px]" style={{ height: '40px' }}>
<SelectField
label={null}
value={quarter}
onChange={(event) => setQuarter(event.target.value)}
options={quarterOptions.map((value) => ({ label: value === 'All' ? 'All' : value, value }))}
placeholder="Quarter"
width="100%"
variant="toolbar"
allowClear
onClear={() => setQuarter('All')}
clearValue="All"
/>
</div>
<div className="flex w-full flex-wrap gap-3 md:w-auto md:flex-nowrap">
<div className="w-full md:w-[134px]" style={{ height: '40px' }}>
<SelectField
label={null}
value={year}
onChange={(event) => setYear(event.target.value)}
options={yearOptions.map((value) => ({ label: value === 'All' ? 'All' : value, value }))}
placeholder="Year"
width="100%"
variant="toolbar"
allowClear
onClear={() => setYear('All')}
clearValue="All"
/>
</div>
<div className="w-full md:w-[134px]" style={{ height: '40px' }}>
<SelectField
label={null}
value={status}
onChange={(event) => setStatus(event.target.value)}
options={statusOptions.map((value) => ({ label: value === 'All' ? 'All' : value, value }))}
placeholder="Status"
width="100%"
variant="toolbar"
allowClear
onClear={() => setStatus('All')}
clearValue="All"
/>
</div>
<div className="w-full md:w-[134px]" style={{ height: '40px' }}>
<SelectField
label={null}
value={emirate}
onChange={(event) => setEmirate(event.target.value)}
options={emirateOptions.map((value) => ({ label: value === 'All' ? 'All' : value, value }))}
placeholder="Emirates"
width="100%"
variant="toolbar"
allowClear
onClear={() => setEmirate('All')}
clearValue="All"
/>
</div>
</div>
</div> </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={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> </div>
</div> </div>
</div> </div>
@ -380,53 +189,37 @@ const ManageSubmissions = () => {
<div className="max-w-[1280px] mx-auto px-4 py-6 flex flex-col gap-6"> <div className="max-w-[1280px] mx-auto px-4 py-6 flex flex-col gap-6">
<nav className="flex items-center gap-2 text-sm text-[#8F9299]"> <nav className="flex items-center gap-2 text-sm text-[#8F9299]">
<Link to="/admin/dashboard" className="flex items-center gap-2 text-[#232528] hover:underline"> <Link to="/admin/dashboard" className="flex items-center gap-2 text-[#232528] hover:underline">
Dashboard Dashboard <img src={CaretUp} alt="Dashboard" className="h-3 w-3" />
<img src={CaretUp} alt="Dashboard" className="h-3 w-3" />
</Link> </Link>
<span className="flex items-center gap-2 text-[#92722A] font-medium"> <span className="flex items-center gap-2 text-[#92722A] font-medium">Manage Submissions</span>
Manage Submissions
{/* <img src={CaretUp} alt="Manage Submissions" className="h-3 w-3" /> */}
</span>
</nav> </nav>
<div className="flex flex-col gap-3 rounded-[12px] bg-white px-5 py-3 shadow-[0_12px_24px_rgba(15,23,42,0.06)] md:flex-row md:items-center md:justify-between"> {/* Summary bar */}
<div className="flex flex-wrap gap-[6px] "> <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];
{ key: 'total', label: 'Total', value: summaryCounts.total },
{ key: 'approved', label: 'Approved', value: summaryCounts.approved },
{ key: 'pending', label: 'Pending', value: summaryCounts.pending },
{ key: 'rejected', label: 'Rejected', value: summaryCounts.rejected },
]
).map((item) => {
const variant = SUMMARY_VARIANTS[item.key];
return ( return (
<div <div key={key} className="inline-flex items-center gap-[6px] rounded-[8px] px-2 py-[2px]" style={{ background: v.background }}>
key={item.key} <span className="font-medium text-[13px]" style={{ color: v.label }}>
className="inline-flex h-[28px] items-center justify-start gap-[6px] rounded-[8px] px-2 py-[2px] text-[13px] ml-5" {key.charAt(0).toUpperCase() + key.slice(1)}:
style={{ background: variant.background }}
>
<span className="font-medium" style={{ color: variant.label }}>
{item.label}:
</span> </span>
<span className="font-semibold" style={{ color: variant.value }}> <span className="font-semibold text-[13px]" style={{ color: v.value }}>
{item.value} {val}
</span> </span>
</div> </div>
); );
})} })}
</div>
<button <button
type="button" type="button"
className="inline-flex h-10 w-full items-center justify-center gap-3 rounded-[6px] border border-[#C3C6CB] bg-white px-5 text-sm font-medium text-[#5F646D] transition-colors hover:bg-[#F7F7F7] md:w-[143px]" 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]"
style={{ paddingTop: '10px', paddingBottom: '10px' }}
> >
<img src={DownloadIcon} alt="Export" className="h-4 w-4" /> <img src={DownloadIcon} alt="Export" className="h-4 w-4" />
<span>Export CSV</span> Export CSV
</button> </button>
</div> </div>
<div className="rounded-2xl border border-[#E5E7EB] bg-white shadow-[0_16px_32px_rgba(15,23,42,0.06)] overflow-hidden"> {/* Table */}
<div className="rounded-2xl border border-[#E5E7EB] bg-white shadow overflow-hidden">
<Table <Table
headers={headers} headers={headers}
rows={tableRows} rows={tableRows}
@ -440,36 +233,17 @@ const ManageSubmissions = () => {
totalItems: filtered.length, totalItems: filtered.length,
}} }}
renderCell={(value, rowIndex, columnIndex) => { renderCell={(value, rowIndex, columnIndex) => {
if (columnIndex === 7) { if (columnIndex === 7)
return <div className="flex justify-start"><Badge status={String(value)}>{value}</Badge></div>;
if (columnIndex === 9)
return ( return (
<div className="flex justify-start"> <button
<Badge status={String(value)}>{value}</Badge> onClick={() => navigate(`/admin/validations/${value}`)}
</div> className="text-sm font-medium text-[#92722A] hover:underline"
>
View Details
</button>
); );
}
if (columnIndex === 9) {
return (
<div className="flex h-12 w-full items-center justify-start">
<button
type="button"
className="whitespace-nowrap text-sm font-medium text-[#92722A]"
onClick={() => navigate(`/admin/validations/${value}`)}
>
View Details
</button>
</div>
);
}
if (columnIndex === 5) {
return <span className="text-sm text-[#232528]">{value}</span>;
}
if (columnIndex === 8) {
return <span className="text-sm text-[#232528]">{value}</span>;
}
return value; return value;
}} }}
/> />

View File

@ -0,0 +1,18 @@
import apiClient from '../api/apiClient.js';
/** Fetch all submissions for the listing page */
export const getSubmissions = async () => {
const response = await apiClient.get('/submissions');
return response.data;
};
/** 🔹 Fetch a single submission by ID */
export const getSubmissionById = async (id) => {
const response = await apiClient.get(`/submissions/${id}`);
return response.data;
};
export default {
getSubmissions,
getSubmissionById,
};