Merge branch 'master' of https://bitbucket.org/jubilian/fcsc_ipi_frontend
This commit is contained in:
commit
a201bb7337
10
ipi-survey-platform/package-lock.json
generated
10
ipi-survey-platform/package-lock.json
generated
@ -9,6 +9,7 @@
|
|||||||
"version": "0.0.0",
|
"version": "0.0.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"axios": "^1.12.2",
|
"axios": "^1.12.2",
|
||||||
|
"lucide-react": "^0.548.0",
|
||||||
"react": "^19.1.1",
|
"react": "^19.1.1",
|
||||||
"react-dom": "^19.1.1",
|
"react-dom": "^19.1.1",
|
||||||
"react-google-recaptcha": "^3.1.0",
|
"react-google-recaptcha": "^3.1.0",
|
||||||
@ -3190,6 +3191,15 @@
|
|||||||
"yallist": "^3.0.2"
|
"yallist": "^3.0.2"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/lucide-react": {
|
||||||
|
"version": "0.548.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.548.0.tgz",
|
||||||
|
"integrity": "sha512-63b16z63jM9yc1MwxajHeuu0FRZFsDtljtDjYm26Kd86UQ5HQzu9ksEtoUUw4RBuewodw/tGFmvipePvRsKeDA==",
|
||||||
|
"license": "ISC",
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/magic-string": {
|
"node_modules/magic-string": {
|
||||||
"version": "0.30.19",
|
"version": "0.30.19",
|
||||||
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.19.tgz",
|
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.19.tgz",
|
||||||
|
|||||||
@ -11,6 +11,7 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"axios": "^1.12.2",
|
"axios": "^1.12.2",
|
||||||
|
"lucide-react": "^0.548.0",
|
||||||
"react": "^19.1.1",
|
"react": "^19.1.1",
|
||||||
"react-dom": "^19.1.1",
|
"react-dom": "^19.1.1",
|
||||||
"react-google-recaptcha": "^3.1.0",
|
"react-google-recaptcha": "^3.1.0",
|
||||||
|
|||||||
@ -5,14 +5,21 @@ const StatCard = ({ icon = null, label, value, width = 200.4 }) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className="flex h-14 items-center rounded-[8px] border border-[#E3E5E8] bg-white px-4 shadow-[0_8px_20px_rgba(42,47,66,0.06)]"
|
className="flex flex-col items-center justify-center rounded-[8px] border border-[#E3E5E8] bg-white px-5 py-5 shadow-[0_8px_20px_rgba(42,47,66,0.06)]"
|
||||||
style={{ width: resolvedWidth, columnGap: '4px' }}
|
style={{ width: resolvedWidth, minHeight: '120px' }}
|
||||||
>
|
>
|
||||||
<span className="inline-flex h-6 w-6 items-center justify-start shrink-0">{icon}</span>
|
<div className="flex items-center justify-center gap-2">
|
||||||
<span className="truncate text-[16px] leading-[24px] font-medium text-[#5F646D] ml-2">{label}</span>
|
{icon && <div className="flex items-center justify-center w-5 h-5">{icon}</div>}
|
||||||
<span className="text-[16px] leading-[24px] font-semibold text-[#232528] ml-2">{value}</span>
|
<span className="text-[15px] font-medium text-[#5F646D] leading-[22px] text-center">
|
||||||
|
{label}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<span className="mt-2 text-[24px] font-semibold text-[#232528] leading-[30px] text-center">
|
||||||
|
{value}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default StatCard;
|
export default StatCard;
|
||||||
@ -1,6 +1,6 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import Table from '@/components/common/Table';
|
import Table from '../common/Table';
|
||||||
import SearchIcon from '@/assets/images/material-symbols_search-rounded.svg';
|
import SearchIcon from '../../assets/images/material-symbols_search-rounded.svg';
|
||||||
|
|
||||||
const submissions = [
|
const submissions = [
|
||||||
{
|
{
|
||||||
@ -98,62 +98,107 @@ const submissions = [
|
|||||||
const SubmissionTable = () => {
|
const SubmissionTable = () => {
|
||||||
const [search, setSearch] = React.useState('');
|
const [search, setSearch] = React.useState('');
|
||||||
|
|
||||||
const filtered = React.useMemo(() => {
|
const filtered = useMemo(() => {
|
||||||
if (!search.trim()) return submissions;
|
if (!search.trim()) return filteredByQuarterYear;
|
||||||
const normalized = search.trim().toLowerCase();
|
const normalized = search.trim().toLowerCase();
|
||||||
return submissions.filter((item) =>
|
return filteredByQuarterYear.filter((item) =>
|
||||||
[
|
Object.values(item).join(' ').toLowerCase().includes(normalized)
|
||||||
item.establishment,
|
|
||||||
item.emirate,
|
|
||||||
item.productCount,
|
|
||||||
item.year,
|
|
||||||
item.quarter,
|
|
||||||
item.lastSubmission,
|
|
||||||
item.deadline,
|
|
||||||
]
|
|
||||||
.join(' ')
|
|
||||||
.toLowerCase()
|
|
||||||
.includes(normalized)
|
|
||||||
);
|
);
|
||||||
}, [search]);
|
}, [search, filteredByQuarterYear]);
|
||||||
|
|
||||||
|
const headers = [
|
||||||
|
'Establishment',
|
||||||
|
'Emirates',
|
||||||
|
'Year',
|
||||||
|
'Quarter',
|
||||||
|
'Product Count',
|
||||||
|
'Submitted By',
|
||||||
|
'Submission Date & Time',
|
||||||
|
'Status',
|
||||||
|
'Reviewer',
|
||||||
|
'Reviewer On',
|
||||||
|
'Actions',
|
||||||
|
];
|
||||||
|
|
||||||
|
const renderStatusBadge = (status) => {
|
||||||
|
const normalized = status?.toLowerCase();
|
||||||
|
if (normalized === 'approved') {
|
||||||
|
return (
|
||||||
|
<span className="px-3 py-1 rounded-md text-green-700 bg-green-50 font-medium text-sm">
|
||||||
|
Approved
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
} else if (normalized === 'rejected') {
|
||||||
|
return (
|
||||||
|
<span className="px-3 py-1 rounded-md text-red-600 bg-red-50 font-medium text-sm">
|
||||||
|
Rejected
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
} else if (normalized === 'pending') {
|
||||||
|
return (
|
||||||
|
<span className="px-3 py-1 rounded-md text-blue-600 bg-blue-50 font-medium text-sm">
|
||||||
|
Pending
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return (
|
||||||
|
<span className="px-3 py-1 rounded-md text-gray-600 bg-gray-50 font-medium text-sm">
|
||||||
|
{status || '-'}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const headers = ['Establishment', 'Emirates', 'Product Count', 'Year', 'Quarter', 'Last Submission', 'Deadline'];
|
|
||||||
const tableRows = filtered.map((item) => [
|
const tableRows = filtered.map((item) => [
|
||||||
item.establishment,
|
item.establishment,
|
||||||
item.emirate,
|
item.emirate,
|
||||||
item.productCount,
|
|
||||||
item.year,
|
item.year,
|
||||||
item.quarter,
|
item.quarter,
|
||||||
item.lastSubmission,
|
item.productCount,
|
||||||
item.deadline,
|
item.submittedBy,
|
||||||
|
item.submissionDateTime,
|
||||||
|
renderStatusBadge(item.status),
|
||||||
|
item.reviewer,
|
||||||
|
item.reviewedOn,
|
||||||
|
<div className="flex items-center justify-center gap-3">
|
||||||
|
<CheckCircle className="text-green-600 w-5 h-5 cursor-pointer hover:scale-110 transition-transform" />
|
||||||
|
<XCircle className="text-red-500 w-5 h-5 cursor-pointer hover:scale-110 transition-transform" />
|
||||||
|
<Eye className="text-gray-500 w-5 h-5 cursor-pointer hover:scale-110 transition-transform" />
|
||||||
|
</div>,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const columnWidths = React.useMemo(
|
const columnWidths = [240, 120, 100, 100, 130, 150, 180, 150, 150, 180, 120];
|
||||||
() => [
|
|
||||||
317,
|
const titleText = useMemo(() => {
|
||||||
161,
|
const count = filtered.length;
|
||||||
140,
|
if (selectedQuarter !== 'None' && selectedYear !== 'None') {
|
||||||
119,
|
return `Recent 10 Submissions for Selected Quarter ${selectedQuarter} ${selectedYear} (${count})`;
|
||||||
98,
|
} else if (selectedQuarter !== 'None') {
|
||||||
158,
|
return `Recent 10 Submissions for Selected Quarter ${selectedQuarter} (${count})`;
|
||||||
150,
|
} else if (selectedYear !== 'None') {
|
||||||
],
|
return `Recent 10 Submissions for Selected Year ${selectedYear} (${count})`;
|
||||||
[]
|
} else {
|
||||||
);
|
return `Recent 10 Submissions (${count})`;
|
||||||
|
}
|
||||||
|
}, [selectedQuarter, selectedYear, filtered]);
|
||||||
|
|
||||||
const toolbar = (
|
const toolbar = (
|
||||||
<div className="px-6 pt-6 pb-4">
|
<div className="px-6 pt-6 pb-4">
|
||||||
<div className="mx-auto flex h-10 w-full max-w-[1280px] items-center justify-between gap-4">
|
<div className="mx-auto flex h-10 w-full max-w-[1280px] items-center justify-between gap-4">
|
||||||
<h2 className="text-[18px] leading-[28px] font-medium text-[#232528]">
|
<h2 className="text-[18px] leading-[28px] font-medium text-[#232528]">
|
||||||
Recent Submissions <span className="text-[#7A7F87]">({submissions.length})</span>
|
{titleText}
|
||||||
</h2>
|
</h2>
|
||||||
<div className="relative w-[280px]">
|
<div className="relative w-[280px]">
|
||||||
<img src={SearchIcon} alt="Search" className="pointer-events-none 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
|
||||||
type="text"
|
type="text"
|
||||||
value={search}
|
value={search}
|
||||||
onChange={(event) => setSearch(event.target.value)}
|
onChange={(event) => setSearch(event.target.value)}
|
||||||
placeholder="Search Establishment"
|
placeholder="Search Establishment or Status"
|
||||||
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]"
|
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>
|
</div>
|
||||||
@ -162,18 +207,29 @@ const SubmissionTable = () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="border border-[#E5E7EB] bg-white shadow-[0_16px_32px_rgba(15,23,42,0.06)]" style={{ width: '1250px', height: '706px', borderRadius: '8px' }}>
|
<div
|
||||||
<div className="space-y-[1px]">
|
className="border border-[#E5E7EB] bg-white shadow-[0_16px_32px_rgba(15,23,42,0.06)] rounded-[8px]"
|
||||||
|
style={{ width: '1250px', height: '706px' }}
|
||||||
|
>
|
||||||
|
<div className="space-y-[1px] overflow-x-auto">
|
||||||
{toolbar}
|
{toolbar}
|
||||||
<Table
|
{loading ? (
|
||||||
headers={headers}
|
<div className="flex items-center justify-center h-[500px] text-gray-500">
|
||||||
rows={tableRows}
|
Loading submissions...
|
||||||
separated
|
</div>
|
||||||
rowGapClass="border-spacing-y-1"
|
) : (
|
||||||
columnWidths={columnWidths}
|
<div className="min-w-[1200px]">
|
||||||
disableHorizontalScroll
|
<Table
|
||||||
/>
|
headers={headers}
|
||||||
|
rows={tableRows}
|
||||||
|
separated
|
||||||
|
rowGapClass="border-spacing-y-1"
|
||||||
|
columnWidths={columnWidths}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex justify-end px-6 pb-6">
|
<div className="flex justify-end px-6 pb-6">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
@ -186,4 +242,4 @@ const SubmissionTable = () => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default SubmissionTable;
|
export default SubmissionTable
|
||||||
@ -1,34 +1,142 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import AdminHeader from '@/components/admin/AdminHeader';
|
import AdminHeader from '../../components/admin/AdminHeader';
|
||||||
import StatCard from '@/components/admin/StatCard';
|
import StatCard from '../../components/admin/StatCard';
|
||||||
import WpfSurveyIcon from '@/assets/images/wpf_survey.svg';
|
import WpfSurveyIcon from '../../assets/images/wpf_survey.svg';
|
||||||
import SubmittedIcon from '@/assets/images/check-circle-fill 1.svg';
|
import SubmittedIcon from '../../assets/images/check-circle-fill 1.svg';
|
||||||
import ApprovedIcon from '@/assets/images/check-circle-fill 1-approved.svg';
|
import ApprovedIcon from '../../assets/images/check-circle-fill 1-approved.svg';
|
||||||
import RejectedIcon from '@/assets/images/warning-circle-fill 1.svg';
|
import RejectedIcon from '../../assets/images/warning-circle-fill 1.svg';
|
||||||
import OverdueIcon from '@/assets/images/warning-fill 1.svg';
|
import OverdueIcon from '../../assets/images/warning-fill 1.svg';
|
||||||
import PendingIcon from '@/assets/images/info-fill 1.svg';
|
import PendingIcon from '../../assets/images/info-fill 1.svg';
|
||||||
import SubmissionTable from '@/components/admin/SubmissionTable';
|
import SubmissionTable from '../../components/admin/SubmissionTable';
|
||||||
|
|
||||||
const AdminDashboard = () => {
|
const AdminDashboard = () => {
|
||||||
|
const [summary, setSummary] = useState({
|
||||||
|
total_establishments: 0,
|
||||||
|
submitted: 0,
|
||||||
|
approved: 0,
|
||||||
|
rejected: 0,
|
||||||
|
not_started: 0,
|
||||||
|
pending: 0,
|
||||||
|
});
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [selectedQuarter, setSelectedQuarter] = useState('None');
|
||||||
|
const [selectedYear, setSelectedYear] = useState('None');
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const fetchDashboardData = async () => {
|
||||||
|
try {
|
||||||
|
setLoading(true);
|
||||||
|
const response = await apiClient.get('/admin_dashboard');
|
||||||
|
const data = response?.data?.summary || {};
|
||||||
|
const notStartedCombined = (data.not_started || 0) + (data.overdue || 0);
|
||||||
|
|
||||||
|
setSummary({
|
||||||
|
total_establishments: data.total_establishments || 0,
|
||||||
|
submitted: data.submitted || 0,
|
||||||
|
approved: data.approved || 0,
|
||||||
|
rejected: data.rejected || 0,
|
||||||
|
not_started: notStartedCombined,
|
||||||
|
pending: data.pending || 0,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error fetching dashboard summary:', error);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
fetchDashboardData();
|
||||||
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-[#F6F5F1]">
|
<div className="min-h-screen bg-[#F6F5F1]">
|
||||||
<AdminHeader />
|
<AdminHeader />
|
||||||
<div className="max-w-[1280px] mx-auto box-border px-4 py-8 flex flex-col gap-6">
|
<div className="max-w-[1280px] mx-auto box-border px-4 py-8 flex flex-col gap-6">
|
||||||
<div className="w-full overflow-x-auto">
|
<div className="flex items-center justify-between w-full max-w-[1280px] mx-auto mt-6 px-4">
|
||||||
<div className="mx-auto flex h-14 w-full max-w-[1280px] items-center gap-2">
|
<h1 className="text-[18px] font-semibold text-[#232528]">
|
||||||
<StatCard label="Total Establishments" value="2" width={290} icon={<img src={WpfSurveyIcon} alt="Total Establishments" className="h-5 w-5" />} />
|
Admin Dashboard{' '}
|
||||||
<StatCard label="Submitted" value="3" width={190} icon={<img src={SubmittedIcon} alt="Submitted" className="h-5 w-5" />} iconBg="#E5F0FF" iconBorder="#D0E2FF" />
|
<span className="font-normal text-[#5F646D]">– Survey Overview</span>
|
||||||
<StatCard label="Approved" value="2" width={190} icon={<img src={ApprovedIcon} alt="Approved" className="h-5 w-5" />} iconBg="#E6F4EA" iconBorder="#CBE9D5" />
|
</h1>
|
||||||
<StatCard label="Rejected" value="2" width={190} icon={<img src={RejectedIcon} alt="Rejected" className="h-5 w-5" />} iconBg="#FEEDEA" iconBorder="#FFD9D2" />
|
|
||||||
<StatCard label="Overdue" value="2" width={190} icon={<img src={OverdueIcon} alt="Overdue" className="h-5 w-5" />} iconBg="#FFF3E6" iconBorder="#FFE3C9" />
|
<div className="flex items-center gap-3">
|
||||||
<StatCard label="Pending" value="2" width={190} icon={<img src={PendingIcon} alt="Pending" className="h-5 w-5" />} iconBg="#FFF7E6" iconBorder="#FFE9B8" />
|
<span className="text-[14px] text-[#232528] font-medium">
|
||||||
|
Survey Quarter
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<select
|
||||||
|
value={selectedQuarter}
|
||||||
|
onChange={(e) => setSelectedQuarter(e.target.value)}
|
||||||
|
className="border border-[#D0D5DD] rounded-md h-8 px-2 text-sm focus:outline-none focus:ring-1 focus:ring-[#92722A]"
|
||||||
|
>
|
||||||
|
<option>None</option>
|
||||||
|
<option>Q1</option>
|
||||||
|
<option>Q2</option>
|
||||||
|
<option>Q3</option>
|
||||||
|
<option>Q4</option>
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<select
|
||||||
|
value={selectedYear}
|
||||||
|
onChange={(e) => setSelectedYear(e.target.value)}
|
||||||
|
className="border border-[#D0D5DD] rounded-md h-8 px-2 text-sm focus:outline-none focus:ring-1 focus:ring-[#92722A]"
|
||||||
|
>
|
||||||
|
<option>None</option>
|
||||||
|
<option>2025</option>
|
||||||
|
<option>2024</option>
|
||||||
|
<option>2023</option>
|
||||||
|
<option>2022</option>
|
||||||
|
</select>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<SubmissionTable />
|
<div className="w-full">
|
||||||
|
<div className="mx-auto flex w-full max-w-[1280px] items-center gap-4">
|
||||||
|
<StatCard
|
||||||
|
label="Total Establishments"
|
||||||
|
value={loading ? '--' : summary.total_establishments}
|
||||||
|
width={290}
|
||||||
|
icon={<img src={WpfSurveyIcon} alt="Total Establishments" className="h-5 w-5" />}
|
||||||
|
/>
|
||||||
|
<StatCard
|
||||||
|
label="Submitted"
|
||||||
|
value={loading ? '--' : summary.submitted}
|
||||||
|
width={190}
|
||||||
|
icon={<img src={SubmittedIcon} alt="Submitted" className="h-5 w-5" />}
|
||||||
|
/>
|
||||||
|
<StatCard
|
||||||
|
label="Approved"
|
||||||
|
value={loading ? '--' : summary.approved}
|
||||||
|
width={190}
|
||||||
|
icon={<img src={ApprovedIcon} alt="Approved" className="h-5 w-5" />}
|
||||||
|
/>
|
||||||
|
<StatCard
|
||||||
|
label="Rejected"
|
||||||
|
value={loading ? '--' : summary.rejected}
|
||||||
|
width={190}
|
||||||
|
icon={<img src={RejectedIcon} alt="Rejected" className="h-5 w-5" />}
|
||||||
|
/>
|
||||||
|
<StatCard
|
||||||
|
label="Not Started"
|
||||||
|
value={loading ? '--' : summary.not_started}
|
||||||
|
width={190}
|
||||||
|
icon={<img src={OverdueIcon} alt="Not Started" className="h-5 w-5" />}
|
||||||
|
/>
|
||||||
|
<StatCard
|
||||||
|
label="Pending"
|
||||||
|
value={loading ? '--' : summary.pending}
|
||||||
|
width={190}
|
||||||
|
icon={<img src={PendingIcon} alt="Pending" className="h-5 w-5" />}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<SubmissionTable
|
||||||
|
selectedQuarter={selectedQuarter}
|
||||||
|
selectedYear={selectedYear}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default AdminDashboard;
|
export default AdminDashboard;
|
||||||
Loading…
Reference in New Issue
Block a user