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 SearchIcon from '../../assets/images/material-symbols_search-rounded.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 { TextField, SelectField } from '../../components/common/FormControls';
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',
},
];
import { SelectField } from '../../components/common/FormControls';
import { getSubmissions } from '../../services/admin/submission.js';
const STATUS_VARIANTS = {
Approved: { background: '#F3FAF4', text: '#2F663C', border: '#F3FAF4' },
@ -206,6 +35,10 @@ const Badge = ({ children, status }) => {
const ManageSubmissions = () => {
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 [year, setYear] = React.useState('');
const [quarter, setQuarter] = React.useState('');
@ -214,37 +47,68 @@ const ManageSubmissions = () => {
const [currentPage, setCurrentPage] = React.useState(1);
const pageSize = 10;
const yearOptions = React.useMemo(() => ['All', ...new Set(submissions.map((item) => item.year))], []);
const quarterOptions = React.useMemo(() => ['All', ...new Set(submissions.map((item) => item.quarter))], []);
const emirateOptions = React.useMemo(() => ['All', ...new Set(submissions.map((item) => item.emirate))], []);
const statusOptions = React.useMemo(() => ['All', ...new Set(submissions.map((item) => item.status))], []);
// 🔹 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
}));
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(() => {
return submissions.filter((item) => {
const matchesSearch = search
? item.establishment.toLowerCase().includes(search.toLowerCase()) ||
item.submittedBy.toLowerCase().includes(search.toLowerCase())
const matchSearch = search
? item.establishment?.toLowerCase().includes(search.toLowerCase()) ||
item.submittedBy?.toLowerCase().includes(search.toLowerCase())
: true;
const matchesYear = !year || year === 'All' || item.year === year;
const matchesQuarter = !quarter || quarter === 'All' || item.quarter === quarter;
const matchesEmirate = !emirate || emirate === 'All' || item.emirate === emirate;
const matchesStatus = !status || status === 'All' || item.status === status;
return matchesSearch && matchesYear && matchesQuarter && matchesEmirate && matchesStatus;
const matchYear = !year || year === 'All' || item.year === year;
const matchQuarter = !quarter || quarter === 'All' || item.quarter === quarter;
const matchEmirate = !emirate || emirate === 'All' || item.emirate === emirate;
const matchStatus = !status || status === 'All' || item.status === status;
return matchSearch && matchYear && matchQuarter && matchEmirate && matchStatus;
});
}, [search, year, quarter, emirate, status]);
}, [submissions, search, year, quarter, emirate, status]);
React.useEffect(() => {
setCurrentPage(1);
}, [search, year, quarter, emirate, status]);
const summaryCounts = React.useMemo(() => {
return {
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 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',
@ -259,116 +123,61 @@ const ManageSubmissions = () => {
'Actions',
];
const tableRows = React.useMemo(
() =>
filtered.map((item) => [
item.establishment,
item.emirate,
item.year,
item.quarter,
item.products,
item.submittedBy,
item.submittedAt,
item.status,
item.reviewer,
item.id,
]),
[filtered]
);
const tableRows = filtered.map((item) => [
item.establishment,
item.emirate,
item.year,
item.quarter,
item.products,
item.submittedBy,
item.submittedAt,
item.status,
item.reviewer,
item.id,
]);
const columnWidths = React.useMemo(
() => [
302,
105,
83,
91,
100,
152,
208,
120,
104,
119,
],
[]
);
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 toolbar = (
<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">
<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>
</h2>
<div className="w-full md:w-auto">
<div className="flex flex-wrap gap-3 md:flex-nowrap">
<div className="relative" style={{ width: '320px', height: '40px' }}>
<img src={SearchIcon} alt="Search" className="pointer-events-none absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4" />
<input
type="text"
value={search}
onChange={(event) => setSearch(event.target.value)}
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 className="flex flex-wrap gap-3">
<div className="relative w-[320px]">
<img src={SearchIcon} alt="Search" className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4" />
<input
value={search}
onChange={(e) => setSearch(e.target.value)}
placeholder="Search Establishment"
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={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>
@ -380,53 +189,37 @@ const ManageSubmissions = () => {
<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]">
<Link to="/admin/dashboard" className="flex items-center gap-2 text-[#232528] hover:underline">
Dashboard
<img src={CaretUp} alt="Dashboard" className="h-3 w-3" />
Dashboard <img src={CaretUp} alt="Dashboard" className="h-3 w-3" />
</Link>
<span className="flex items-center gap-2 text-[#92722A] font-medium">
Manage Submissions
{/* <img src={CaretUp} alt="Manage Submissions" className="h-3 w-3" /> */}
</span>
<span className="flex items-center gap-2 text-[#92722A] font-medium">Manage Submissions</span>
</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">
<div className="flex flex-wrap gap-[6px] ">
{(
[
{ 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];
{/* 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={item.key}
className="inline-flex h-[28px] items-center justify-start gap-[6px] rounded-[8px] px-2 py-[2px] text-[13px] ml-5"
style={{ background: variant.background }}
>
<span className="font-medium" style={{ color: variant.label }}>
{item.label}:
<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" style={{ color: variant.value }}>
{item.value}
<span className="font-semibold text-[13px]" style={{ color: v.value }}>
{val}
</span>
</div>
);
})}
</div>
<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]"
style={{ paddingTop: '10px', paddingBottom: '10px' }}
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={DownloadIcon} alt="Export" className="h-4 w-4" />
<span>Export CSV</span>
Export CSV
</button>
</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
headers={headers}
rows={tableRows}
@ -440,36 +233,17 @@ const ManageSubmissions = () => {
totalItems: filtered.length,
}}
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 (
<div className="flex justify-start">
<Badge status={String(value)}>{value}</Badge>
</div>
<button
onClick={() => navigate(`/admin/validations/${value}`)}
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;
}}
/>

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,
};