Added submission history for establishedment user
This commit is contained in:
parent
3c0665a909
commit
e0362de08c
3
ipi-survey-platform/public/assets/images/Duedate.svg
Normal file
3
ipi-survey-platform/public/assets/images/Duedate.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="M12.6667 2.66634H11.3334V1.99967C11.3334 1.82286 11.2631 1.65329 11.1381 1.52827C11.0131 1.40325 10.8435 1.33301 10.6667 1.33301C10.4899 1.33301 10.3203 1.40325 10.1953 1.52827C10.0703 1.65329 10 1.82286 10 1.99967V2.66634H6.00004V1.99967C6.00004 1.82286 5.9298 1.65329 5.80478 1.52827C5.67975 1.40325 5.51018 1.33301 5.33337 1.33301C5.15656 1.33301 4.98699 1.40325 4.86197 1.52827C4.73695 1.65329 4.66671 1.82286 4.66671 1.99967V2.66634H3.33337C2.80294 2.66634 2.29423 2.87705 1.91916 3.25213C1.54409 3.6272 1.33337 4.13591 1.33337 4.66634V12.6663C1.33337 13.1968 1.54409 13.7055 1.91916 14.0806C2.29423 14.4556 2.80294 14.6663 3.33337 14.6663H12.6667C13.1971 14.6663 13.7058 14.4556 14.0809 14.0806C14.456 13.7055 14.6667 13.1968 14.6667 12.6663V4.66634C14.6667 4.13591 14.456 3.6272 14.0809 3.25213C13.7058 2.87705 13.1971 2.66634 12.6667 2.66634ZM13.3334 12.6663C13.3334 12.8432 13.2631 13.0127 13.1381 13.1377C13.0131 13.2628 12.8435 13.333 12.6667 13.333H3.33337C3.15656 13.333 2.98699 13.2628 2.86197 13.1377C2.73695 13.0127 2.66671 12.8432 2.66671 12.6663V7.99967H13.3334V12.6663ZM13.3334 6.66634H2.66671V4.66634C2.66671 4.48953 2.73695 4.31996 2.86197 4.19494C2.98699 4.06991 3.15656 3.99967 3.33337 3.99967H4.66671V4.66634C4.66671 4.84315 4.73695 5.01272 4.86197 5.13775C4.98699 5.26277 5.15656 5.33301 5.33337 5.33301C5.51018 5.33301 5.67975 5.26277 5.80478 5.13775C5.9298 5.01272 6.00004 4.84315 6.00004 4.66634V3.99967H10V4.66634C10 4.84315 10.0703 5.01272 10.1953 5.13775C10.3203 5.26277 10.4899 5.33301 10.6667 5.33301C10.8435 5.33301 11.0131 5.26277 11.1381 5.13775C11.2631 5.01272 11.3334 4.84315 11.3334 4.66634V3.99967H12.6667C12.8435 3.99967 13.0131 4.06991 13.1381 4.19494C13.2631 4.31996 13.3334 4.48953 13.3334 4.66634V6.66634Z" fill="#6C4527"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.8 KiB |
@ -0,0 +1,3 @@
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M14 2H6C5.46957 2 4.96086 2.21071 4.58579 2.58579C4.21071 2.96086 4 3.46957 4 4V20C4 20.5304 4.21071 21.0391 4.58579 21.4142C4.96086 21.7893 5.46957 22 6 22H18C18.5304 22 19.0391 21.7893 19.4142 21.4142C19.7893 21.0391 20 20.5304 20 20V8L14 2ZM6 4H13L18 9V17.58L16.16 15.74C16.7947 14.7789 17.0759 13.6272 16.9555 12.4817C16.8351 11.3362 16.3207 10.2682 15.5 9.46C15.0422 8.9972 14.4971 8.62985 13.8963 8.37923C13.2955 8.12861 12.651 7.99971 12 8C10.72 8 9.45 8.5 8.47 9.46C6.5 11.41 6.5 14.57 8.47 16.5C9.44 17.5 10.72 17.97 12 17.97C12.96 17.97 13.92 17.69 14.75 17.14L17.6 20H6V4ZM14.11 15.1C13.55 15.66 12.8 16 12 16C11.2 16 10.45 15.67 9.89 15.1C9.33 14.54 9 13.79 9 13C9 12.19 9.32 11.44 9.89 10.88C10.45 10.31 11.2 10 12 10C12.8 10 13.55 10.31 14.11 10.88C14.67 11.44 15 12.19 15 13C15 13.79 14.68 14.54 14.11 15.1Z" fill="#232528"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 953 B |
@ -0,0 +1,3 @@
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M14 2H6C5.46957 2 4.96086 2.21071 4.58579 2.58579C4.21071 2.96086 4 3.46957 4 4V20C4 20.5304 4.21071 21.0391 4.58579 21.4142C4.96086 21.7893 5.46957 22 6 22H18C18.5304 22 19.0391 21.7893 19.4142 21.4142C19.7893 21.0391 20 20.5304 20 20V8L14 2ZM6 4H13L18 9V17.58L16.16 15.74C16.7947 14.7789 17.0759 13.6272 16.9555 12.4817C16.8351 11.3362 16.3207 10.2682 15.5 9.46C15.0422 8.9972 14.4971 8.62985 13.8963 8.37923C13.2955 8.12861 12.651 7.99971 12 8C10.72 8 9.45 8.5 8.47 9.46C6.5 11.41 6.5 14.57 8.47 16.5C9.44 17.5 10.72 17.97 12 17.97C12.96 17.97 13.92 17.69 14.75 17.14L17.6 20H6V4ZM14.11 15.1C13.55 15.66 12.8 16 12 16C11.2 16 10.45 15.67 9.89 15.1C9.33 14.54 9 13.79 9 13C9 12.19 9.32 11.44 9.89 10.88C10.45 10.31 11.2 10 12 10C12.8 10 13.55 10.31 14.11 10.88C14.67 11.44 15 12.19 15 13C15 13.79 14.68 14.54 14.11 15.1Z" fill="#B68A35"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 953 B |
@ -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="M14.6667 2H3.33333C2.97971 2 2.64057 2.14048 2.39052 2.39052C2.14048 2.64057 2 2.97971 2 3.33333V6H3.33333V3.33333H14.6667V12.6667H3.33333V10H2V12.6667C2 13.0203 2.14048 13.3594 2.39052 13.6095C2.64057 13.8595 2.97971 14 3.33333 14H14.6667C15.0203 14 15.3594 13.8595 15.6095 13.6095C15.8595 13.3594 16 13.0203 16 12.6667V3.33333C16 2.97971 15.8595 2.64057 15.6095 2.39052C15.3594 2.14048 15.0203 2 14.6667 2ZM4.66667 10V8.66667H0V7.33333H4.66667V6L7.33333 8L4.66667 10ZM13.3333 8.66667H8.66667V7.33333H13.3333V8.66667ZM13.3333 6H8.66667V4.66667H13.3333V6ZM11.3333 11.3333H8.66667V10H11.3333V11.3333Z" fill="#6C4527"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 730 B |
@ -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="M8.00004 1.33301C11.682 1.33301 14.6667 4.31767 14.6667 7.99967C14.6667 11.6817 11.682 14.6663 8.00004 14.6663C4.31804 14.6663 1.33337 11.6817 1.33337 7.99967C1.33337 4.31767 4.31804 1.33301 8.00004 1.33301ZM8.00004 2.66634C6.58555 2.66634 5.229 3.22824 4.2288 4.22844C3.22861 5.22863 2.66671 6.58519 2.66671 7.99967C2.66671 9.41416 3.22861 10.7707 4.2288 11.7709C5.229 12.7711 6.58555 13.333 8.00004 13.333C9.41453 13.333 10.7711 12.7711 11.7713 11.7709C12.7715 10.7707 13.3334 9.41416 13.3334 7.99967C13.3334 6.58519 12.7715 5.22863 11.7713 4.22844C10.7711 3.22824 9.41453 2.66634 8.00004 2.66634ZM8.00004 3.99967C8.16333 3.9997 8.32093 4.05964 8.44296 4.16815C8.56498 4.27666 8.64294 4.42617 8.66204 4.58834L8.66671 4.66634V7.72367L10.4714 9.52834C10.5909 9.64831 10.6604 9.8093 10.6655 9.9786C10.6707 10.1479 10.6112 10.3128 10.4992 10.4399C10.3872 10.5669 10.231 10.6465 10.0624 10.6626C9.89378 10.6787 9.72537 10.6299 9.59137 10.5263L9.52871 10.471L7.52871 8.47101C7.42509 8.36731 7.35855 8.23234 7.33937 8.08701L7.33337 7.99967V4.66634C7.33337 4.48953 7.40361 4.31996 7.52864 4.19494C7.65366 4.06991 7.82323 3.99967 8.00004 3.99967Z" fill="#6C4527"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.2 KiB |
@ -7,6 +7,7 @@ import Validations from '@/pages/Admin/Validations';
|
||||
import ValidationReview from '@/pages/Admin/ValidationReview';
|
||||
import Configuration from '@/pages/Admin/Configuration';
|
||||
import Survey from '@/pages/Survey/Survey';
|
||||
import Overview from '@/pages/Overview/Overview';
|
||||
import Login from '@/pages/Login/Login';
|
||||
import History from '@/pages/History/History';
|
||||
import ChangePassword from '@/pages/ChangePassword/ChangePassword';
|
||||
@ -101,6 +102,14 @@ function App() {
|
||||
</RequireRole>
|
||||
)}
|
||||
/>
|
||||
<Route
|
||||
path="/overview"
|
||||
element={(
|
||||
<RequireRole allowedRoles={['EstablishmentUser']}>
|
||||
<Overview />
|
||||
</RequireRole>
|
||||
)}
|
||||
/>
|
||||
<Route path="/change-password" element={<ChangePassword />} />
|
||||
<Route path="/forgot-password" element={<ForgotPassword />} />
|
||||
<Route path="/reset-password" element={<PublicContactForm />} />
|
||||
|
||||
@ -106,6 +106,16 @@ const AdminHeader = () => {
|
||||
<p className="text-sm font-semibold text-[#232528] truncate" title={userName}>{userName}</p>
|
||||
<p className="text-xs text-[#6B7280] truncate" title={userEmail}>{userEmail}</p>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
className="w-full px-4 py-2.5 text-center text-sm text-[#232528] hover:bg-[#F2ECCF]"
|
||||
onClick={() => {
|
||||
setProfileOpen(false);
|
||||
navigate('/admin/profile');
|
||||
}}
|
||||
>
|
||||
Profile
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="w-full px-4 py-2.5 text-center text-sm text-[#232528] hover:bg-[#F2ECCF]"
|
||||
|
||||
109
ipi-survey-platform/src/components/common/ProductTable.jsx
Normal file
109
ipi-survey-platform/src/components/common/ProductTable.jsx
Normal file
@ -0,0 +1,109 @@
|
||||
import React from 'react';
|
||||
|
||||
const statusStyles = {
|
||||
approved: 'bg-[#F3FAF4] text-[#2F663C]',
|
||||
'pending approval': 'bg-[#E7F5FF] text-[#003CFF]',
|
||||
'changes requested': 'bg-[#FEF2F2] text-[#B45309]',
|
||||
};
|
||||
|
||||
const statusDots = {
|
||||
approved: 'bg-[#2F663C]',
|
||||
'pending approval': 'bg-[#003CFF]',
|
||||
'changes requested': 'bg-[#B45309]',
|
||||
};
|
||||
|
||||
const ProductTable = ({ products, onViewDetails }) => {
|
||||
const columns = [
|
||||
{
|
||||
key: 'name',
|
||||
header: 'Product Name',
|
||||
render: (product) => (
|
||||
<span className="font-medium text-[#232528]">{product.name}</span>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: 'status',
|
||||
header: 'Status',
|
||||
render: (product) => (
|
||||
<span className={`px-2.5 py-1 inline-flex items-center justify-center rounded-full text-xs font-medium ${
|
||||
statusStyles[product.status.toLowerCase()] || ''
|
||||
}`}>
|
||||
<span
|
||||
className={`w-1.5 h-1.5 rounded-full mr-1.5 ${
|
||||
statusDots[product.status.toLowerCase()] || ''
|
||||
}`}
|
||||
></span>
|
||||
{product.status}
|
||||
</span>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: 'price',
|
||||
header: 'Price (AED)',
|
||||
render: (product) => (
|
||||
<span className="text-[#4B5563]">
|
||||
{new Intl.NumberFormat('en-AE', { minimumFractionDigits: 2 }).format(product.price)}
|
||||
</span>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: 'lastUpdated',
|
||||
header: 'Last Updated',
|
||||
render: (product) => (
|
||||
<span className="text-[#4B5563]">{product.lastUpdated}</span>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: 'action',
|
||||
header: 'Action',
|
||||
align: 'right',
|
||||
render: (product) => (
|
||||
<button
|
||||
onClick={() => onViewDetails && onViewDetails(product)}
|
||||
className="text-[#92722A] hover:underline text-sm font-medium"
|
||||
>
|
||||
View Details
|
||||
</button>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="overflow-x-auto">
|
||||
<table className="min-w-full divide-y divide-[#E2E8F0]">
|
||||
<thead className="bg-[#F8FAFC]">
|
||||
<tr>
|
||||
{columns.map((column) => (
|
||||
<th
|
||||
key={column.key}
|
||||
className={`px-6 py-3 text-xs font-medium text-[#6B7280] uppercase tracking-wider ${
|
||||
column.align === 'right' ? 'text-right' : 'text-left'
|
||||
}`}
|
||||
>
|
||||
{column.header}
|
||||
</th>
|
||||
))}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="bg-white divide-y divide-[#E2E8F0] text-sm">
|
||||
{products.map((product, index) => (
|
||||
<tr key={index}>
|
||||
{columns.map((column) => (
|
||||
<td
|
||||
key={`${column.key}-${index}`}
|
||||
className={`px-6 py-4 whitespace-nowrap ${
|
||||
column.align === 'right' ? 'text-right' : 'text-left'
|
||||
}`}
|
||||
>
|
||||
{column.render ? column.render(product) : product[column.key]}
|
||||
</td>
|
||||
))}
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ProductTable;
|
||||
@ -148,7 +148,9 @@ const Table = ({
|
||||
>
|
||||
<thead className={headerClassName || 'bg-[#F2ECCF] rounded-2xl'}>
|
||||
<tr>
|
||||
{headers.map((h, i) => {
|
||||
{headers.map((header, i) => {
|
||||
const headerText = typeof header === 'object' ? header.name : header;
|
||||
const headerClass = typeof header === 'object' ? header.className : 'text-left';
|
||||
const widthValue = columnWidths[i];
|
||||
const widthStyle = widthValue
|
||||
? {
|
||||
@ -159,12 +161,12 @@ const Table = ({
|
||||
return (
|
||||
<th
|
||||
key={i}
|
||||
className={`px-6 h-12 text-left text-xs font-medium text-[#1B1D21] align-middle ${
|
||||
className={`px-6 h-12 text-xs font-medium text-[#1B1D21] align-middle ${headerClass} ${
|
||||
i === 0 ? 'first:rounded-tl-md' : ''
|
||||
} ${i === headers.length - 1 ? 'last:rounded-tr-md' : ''}`}
|
||||
style={widthStyle}
|
||||
>
|
||||
{h}
|
||||
{headerText}
|
||||
</th>
|
||||
);
|
||||
})}
|
||||
|
||||
@ -8,6 +8,8 @@ const dashboardIconActiveSrc = '/assets/images/cuida_dashboard-outline.svg';
|
||||
const dashboardIconInactiveSrc = '/assets/images/cuida_dashboard-outline-inactive.svg';
|
||||
const historyIconActiveSrc = '/assets/images/mdi_history.svg';
|
||||
const historyIconInactiveSrc = '/assets/images/history.svg';
|
||||
const overviewIconActiveSrc = '/assets/images/mdi_file-find-outline.svg';
|
||||
const overviewIconInactiveSrc = '/assets/images/mdi_file-find-outline-active.svg';
|
||||
|
||||
const HeaderBar = () => {
|
||||
const [open, setOpen] = React.useState(false);
|
||||
@ -88,6 +90,17 @@ const HeaderBar = () => {
|
||||
</>
|
||||
)}
|
||||
</NavLink>
|
||||
<NavLink
|
||||
to="/overview"
|
||||
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'}`}
|
||||
>
|
||||
{({ isActive }) => (
|
||||
<>
|
||||
<img src={isActive ? overviewIconActiveSrc : overviewIconInactiveSrc} alt="Overview" className="h-[18px] w-[18px]" />
|
||||
<span>Overview</span>
|
||||
</>
|
||||
)}
|
||||
</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'}`}
|
||||
@ -114,6 +127,16 @@ const HeaderBar = () => {
|
||||
<p className="text-sm font-semibold text-[#232528] truncate" title={userName}>{userName}</p>
|
||||
{userEmail && <p className="text-xs text-[#64748B] truncate" title={userEmail}>{userEmail}</p>}
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
setUserOpen(false);
|
||||
navigate('/profile');
|
||||
}}
|
||||
className="w-full px-4 py-2.5 text-center text-sm text-[#232528] hover:bg-[#F2ECCF] transition-colors"
|
||||
>
|
||||
Edit Profile
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={onChangePassword}
|
||||
@ -154,6 +177,10 @@ const HeaderBar = () => {
|
||||
<img src={surveyIconInactiveSrc} alt="Survey" className="h-[18px] w-[18px]" />
|
||||
<span>Survey</span>
|
||||
</NavLink>
|
||||
<NavLink to="/overview" onClick={() => setOpen(false)} className={({ isActive }) => `inline-flex items-center gap-2 px-2 py-2 rounded border-b-2 ${isActive ? 'text-[#92722A] bg-[#F2ECCF] border-[#92722A]' : 'text-[#232528] hover:bg-gray-100 border-transparent'}`}>
|
||||
<img src={overviewIconInactiveSrc} alt="Overview" className="h-[18px] w-[18px]" />
|
||||
<span>Overview</span>
|
||||
</NavLink>
|
||||
<NavLink to="/history" onClick={() => setOpen(false)} className={({ isActive }) => `inline-flex items-center gap-2 px-2 py-2 rounded border-b-2 ${isActive ? 'text-[#92722A] bg-[#F2ECCF] border-[#92722A]' : 'text-[#232528] hover:bg-gray-100 border-transparent'}`}>
|
||||
<img src={historyIconInactiveSrc} alt="History" className="h-[18px] w-[18px]" />
|
||||
<span>History</span>
|
||||
@ -170,8 +197,18 @@ const HeaderBar = () => {
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={onLogout}
|
||||
onClick={() => {
|
||||
setOpen(false);
|
||||
navigate('/profile');
|
||||
}}
|
||||
className="inline-flex items-center gap-2 px-2 py-2 rounded text-[#232528] hover:bg-gray-100 text-left"
|
||||
>
|
||||
<span>Edit Profile</span>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={onLogout}
|
||||
className="inline-flex items-center gap-2 px-2 py-2 rounded text-[#B91C1C] hover:bg-[#FEE2E2] text-left"
|
||||
>
|
||||
<span>Logout</span>
|
||||
</button>
|
||||
|
||||
262
ipi-survey-platform/src/components/overview/DetailedOverview.jsx
Normal file
262
ipi-survey-platform/src/components/overview/DetailedOverview.jsx
Normal file
@ -0,0 +1,262 @@
|
||||
import React, { useState, useCallback, useMemo } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import Table from '@/components/common/Table';
|
||||
import ProductDetails from './ProductDetails';
|
||||
|
||||
const searchIconSrc = '/assets/images/material-symbols_search-rounded.svg';
|
||||
const caretDownSrc = '/assets/images/CaretDown.svg';
|
||||
const downloadIconSrc = '/assets/images/DownloadSimple.svg';
|
||||
export const DetailedOverview = ({ submission, onBack }) => {
|
||||
const navigate = useNavigate();
|
||||
const [searchTerm, setSearchTerm] = useState('');
|
||||
const [selectedStatus, setSelectedStatus] = useState('');
|
||||
const [selectedProduct, setSelectedProduct] = useState(null);
|
||||
|
||||
// Mock data - replace with actual data from props
|
||||
const productData = [
|
||||
{
|
||||
id: 'PRD-2023-001',
|
||||
hsCode: '1234.56.78',
|
||||
name: 'Product 1',
|
||||
unit: 'PCS',
|
||||
quantity: 1,
|
||||
cost: '1,000.00',
|
||||
status: 'Active',
|
||||
submissionDate: '30/10/2023 14:30',
|
||||
},
|
||||
{
|
||||
id: 'PRD-2023-002',
|
||||
hsCode: '8765.43.21',
|
||||
name: 'Product 2',
|
||||
unit: 'KG',
|
||||
quantity: 5,
|
||||
cost: '2,000.00',
|
||||
status: 'Pending',
|
||||
submissionDate: '29/10/2023 10:15',
|
||||
},
|
||||
];
|
||||
|
||||
const filteredProducts = useMemo(() => {
|
||||
const normalizedTerm = searchTerm.trim().toLowerCase();
|
||||
return productData.filter((product) => {
|
||||
const matchesSearch =
|
||||
!normalizedTerm ||
|
||||
product.name.toLowerCase().includes(normalizedTerm) ||
|
||||
product.hsCode.toLowerCase().includes(normalizedTerm) ||
|
||||
product.status.toLowerCase().includes(normalizedTerm);
|
||||
const matchesStatus =
|
||||
selectedStatus === '' ||
|
||||
product.status.toLowerCase() === selectedStatus.toLowerCase();
|
||||
return matchesSearch && matchesStatus;
|
||||
});
|
||||
}, [searchTerm, selectedStatus, productData]);
|
||||
|
||||
const downloadCsv = useCallback(() => {
|
||||
if (!filteredProducts.length) return;
|
||||
|
||||
const headers = [
|
||||
'HS Code',
|
||||
'Product',
|
||||
'Unit',
|
||||
'Quantity',
|
||||
'Cost (AED)',
|
||||
'Status',
|
||||
'Submission Date & Time'
|
||||
];
|
||||
|
||||
const csvRows = [headers.join(',')];
|
||||
|
||||
filteredProducts.forEach((product) => {
|
||||
csvRows.push([
|
||||
product.hsCode,
|
||||
product.name,
|
||||
product.unit,
|
||||
product.quantity,
|
||||
product.cost,
|
||||
product.status,
|
||||
product.submissionDate
|
||||
].map((value) => `"${String(value).replace(/"/g, '""')}"`).join(','));
|
||||
});
|
||||
|
||||
const blob = new Blob([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', 'product-list.csv');
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
URL.revokeObjectURL(url);
|
||||
}, [filteredProducts]);
|
||||
|
||||
const rows = filteredProducts.map(product => [
|
||||
product.hsCode,
|
||||
product.name,
|
||||
product.unit,
|
||||
product.quantity,
|
||||
`AED ${product.cost}`,
|
||||
<span className={`inline-flex items-center justify-center rounded-md px-3 py-1 text-xs font-medium ${
|
||||
product.status === 'Active' ? 'text-[#2F663C] bg-[#F3FAF4]' :
|
||||
'text-[#B45309] bg-[#FEF2F2]'
|
||||
}`}>
|
||||
{product.status}
|
||||
</span>,
|
||||
product.submissionDate,
|
||||
<button
|
||||
onClick={() => setSelectedProduct(product)}
|
||||
className="text-[#92722A] hover:underline text-sm font-medium"
|
||||
>
|
||||
View Details
|
||||
</button>
|
||||
]);
|
||||
|
||||
const toolbar = (
|
||||
|
||||
<div className="px-6 pt-6 pb-4 flex flex-col gap-4 md:flex-row md:items-center md:justify-between">
|
||||
<div>
|
||||
<h2 className="text-[18px] font-medium text-[#232528]">Selected Quarter: {submission.quarter} </h2>
|
||||
</div>
|
||||
<div className="flex flex-col md:flex-row md:items-center gap-3">
|
||||
<div className="relative w-full md:w-72">
|
||||
<input
|
||||
type="text"
|
||||
value={searchTerm}
|
||||
onChange={(event) => setSearchTerm(event.target.value)}
|
||||
placeholder="Search establishment"
|
||||
className="w-full h-10 rounded-md border border-[#E2E8F0] pl-10 pr-3 text-sm text-[#232528] focus:outline-none"
|
||||
/>
|
||||
<img
|
||||
src={searchIconSrc}
|
||||
alt="Search"
|
||||
className="pointer-events-none absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4"
|
||||
/>
|
||||
</div>
|
||||
<div className="relative w-full md:w-40">
|
||||
<select
|
||||
value={selectedStatus}
|
||||
onChange={(event) => setSelectedStatus(event.target.value)}
|
||||
className="h-10 w-full appearance-none rounded-md border border-[#E2E8F0] bg-white pl-3 pr-9 text-sm text-[#232528] focus:outline-none cursor-pointer"
|
||||
>
|
||||
<option value="">All Status</option>
|
||||
<option value="approved">Approved</option>
|
||||
<option value="submitted">Submitted</option>
|
||||
<option value="rejected">Rejected</option>
|
||||
</select>
|
||||
<img
|
||||
src={caretDownSrc}
|
||||
alt="Open"
|
||||
className="pointer-events-none absolute right-3 top-1/2 -translate-y-1/2 h-4 w-4"
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={downloadCsv}
|
||||
disabled={!filteredProducts.length}
|
||||
className={`inline-flex items-center gap-2 h-10 rounded-md border px-4 text-sm font-medium ${filteredProducts.length ? 'border-[#92722A] text-[#92722A] hover:bg-[#F2ECCF]' : 'border-gray-200 text-gray-400 cursor-not-allowed'}`}
|
||||
>
|
||||
<img src={downloadIconSrc} alt="Export" className="h-4 w-4" />
|
||||
<span>Export CSV</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
const columns = [
|
||||
'HS Code',
|
||||
'Product',
|
||||
'Unit',
|
||||
'Quantity',
|
||||
'Cost (AED)',
|
||||
'Status',
|
||||
'Submission Date & Time',
|
||||
'Action'
|
||||
];
|
||||
|
||||
// If a product is selected, show the product details view
|
||||
if (selectedProduct) {
|
||||
return (
|
||||
<ProductDetails
|
||||
product={selectedProduct}
|
||||
onBack={() => setSelectedProduct(null)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
// Otherwise show the main overview
|
||||
return (
|
||||
<div className="space-y-6 w-full">
|
||||
{/* ----- EXISTING CARD SECTION ----- */}
|
||||
<div className="bg-white p-4 rounded-t-lg border-b border-[#E2E8F0]">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center space-x-4">
|
||||
<button
|
||||
onClick={onBack}
|
||||
className="text-[#6B7280] hover:text-[#374151] p-1 -ml-1"
|
||||
>
|
||||
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M12.5 15L7.5 10L12.5 5" stroke="#6B7280" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round"/>
|
||||
</svg>
|
||||
</button>
|
||||
<div className="flex items-center space-x-2 pr-6">
|
||||
<p className="text-sm text-[#232528] font-medium">Establishment ID:</p>
|
||||
<p className="text-sm text-[#232528]">Al Khazna Investment</p>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2 px-6">
|
||||
<p className="text-sm text-[#232528] font-medium">Total Submitted Products:</p>
|
||||
<p className="text-sm text-[#232528]">678</p>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2 pl-6">
|
||||
<p className="text-sm text-[#232528] font-medium">Average Cost:</p>
|
||||
<p className="text-sm text-[#232528]">7889</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center space-x-4">
|
||||
<div className="relative">
|
||||
<select className="h-10 w-32 rounded-md border border-[#E2E8F0] bg-white pl-3 pr-8 text-sm text-[#232528] focus:outline-none cursor-pointer appearance-none">
|
||||
<option>2023</option>
|
||||
<option>2024</option>
|
||||
<option>2025</option>
|
||||
</select>
|
||||
<svg className="pointer-events-none absolute right-3 top-1/2 -translate-y-1/2 h-4 w-4 text-[#6B7280]" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
||||
<polyline points="6 9 12 15 18 9" />
|
||||
</svg>
|
||||
</div>
|
||||
<div className="relative">
|
||||
<select className="h-10 w-32 rounded-md border border-[#E2E8F0] bg-white pl-3 pr-8 text-sm text-[#232528] focus:outline-none cursor-pointer appearance-none">
|
||||
<option>January</option>
|
||||
<option>February</option>
|
||||
<option>March</option>
|
||||
<option>April</option>
|
||||
<option>May</option>
|
||||
<option>June</option>
|
||||
<option>July</option>
|
||||
<option>August</option>
|
||||
<option>September</option>
|
||||
<option>October</option>
|
||||
<option>November</option>
|
||||
<option>December</option>
|
||||
</select>
|
||||
<svg className="pointer-events-none absolute right-3 top-1/2 -translate-y-1/2 h-4 w-4 text-[#6B7280]" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
||||
<polyline points="6 9 12 15 18 9" />
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* ----- ADDED TABLE SECTION BELOW CARD ----- */}
|
||||
<section className="bg-white rounded-lg border border-[#E2E8F0] shadow-sm">
|
||||
<Table
|
||||
headers={columns}
|
||||
rows={rows}
|
||||
beforeHeader={toolbar}
|
||||
separated
|
||||
rowGapClass="border-spacing-y-2"
|
||||
className="w-full min-w-[1200px] [&_td]:whitespace-nowrap [&_th]:whitespace-nowrap"
|
||||
/>
|
||||
</section>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default DetailedOverview;
|
||||
228
ipi-survey-platform/src/components/overview/ProductDetails.jsx
Normal file
228
ipi-survey-platform/src/components/overview/ProductDetails.jsx
Normal file
@ -0,0 +1,228 @@
|
||||
import React from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
|
||||
// Mock data for the quarterly details - replace with actual data from props
|
||||
const quarterlyData = {
|
||||
q4_oct_quantity: '1,000',
|
||||
q4_nov_quantity: '1,200',
|
||||
q4_dec_quantity: '1,500',
|
||||
q1_jan_quantity: '1,300',
|
||||
q1_feb_quantity: '1,400',
|
||||
q1_mar_quantity: '1,600',
|
||||
q2_apr_quantity: '1,700',
|
||||
q2_may_quantity: '1,800',
|
||||
q2_jun_quantity: '2,000',
|
||||
q4_oct_cost: '10,000',
|
||||
q4_nov_cost: '12,000',
|
||||
q4_dec_cost: '15,000',
|
||||
q1_jan_cost: '13,000',
|
||||
q1_feb_cost: '14,000',
|
||||
q1_mar_cost: '16,000',
|
||||
q2_apr_cost: '17,000',
|
||||
q2_may_cost: '18,000',
|
||||
q2_jun_cost: '20,000',
|
||||
variationReason: 'Seasonal demand increase',
|
||||
zeroTargetReason: 'Expected market expansion',
|
||||
remarks: 'Product shows steady growth with seasonal variations.'
|
||||
};
|
||||
|
||||
const ProductDetails = ({ product, onBack }) => {
|
||||
const navigate = useNavigate();
|
||||
|
||||
if (!product) return null;
|
||||
|
||||
// Merge product data with quarterly data
|
||||
const productWithDetails = { ...product, ...quarterlyData };
|
||||
|
||||
return (
|
||||
<div className="space-y-6 w-full">
|
||||
<div className="bg-white p-4 rounded-t-lg border-b border-[#E2E8F0]">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center space-x-4">
|
||||
<button
|
||||
onClick={onBack}
|
||||
className="text-[#6B7280] hover:text-[#374151] p-1 -ml-1"
|
||||
>
|
||||
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M12.5 15L7.5 10L12.5 5" stroke="#6B7280" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round"/>
|
||||
</svg>
|
||||
</button>
|
||||
<h1 className="text-lg font-medium text-[#232528]">Product Details</h1>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-white rounded-lg border border-[#E2E8F0] p-6">
|
||||
{/* Basic Information */}
|
||||
<div className="mb-6">
|
||||
<div className="grid grid-cols-1 md:grid-cols-4 gap-6 mb-4">
|
||||
<div className="space-y-1">
|
||||
<label className="block text-sm font-medium text-[#6B7280]">Product Name</label>
|
||||
<div className="mt-1">
|
||||
<p className="block w-full rounded-md border-0 py-1.5 text-gray-900 bg-white">
|
||||
{product.name}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<label className="block text-sm font-medium text-[#6B7280]">HS Code</label>
|
||||
<div className="mt-1">
|
||||
<p className="block w-full rounded-md border-0 py-1.5 text-gray-900 bg-white">
|
||||
{product.hsCode || '—'}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<label className="block text-sm font-medium text-[#6B7280]">Unit</label>
|
||||
<div className="mt-1">
|
||||
<p className="block w-full rounded-md border-0 py-1.5 text-gray-900 bg-white">
|
||||
{product.unit || 'PCS'}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<label className="block text-sm font-medium text-[#6B7280]">Status</label>
|
||||
<div className="mt-1">
|
||||
<span className={`inline-flex items-center rounded-md px-3 py-1.5 text-sm font-medium ${
|
||||
product.status === 'Active'
|
||||
? 'bg-green-50 text-green-700 ring-1 ring-inset ring-green-600/20'
|
||||
: 'bg-yellow-50 text-yellow-700 ring-1 ring-inset ring-yellow-600/20'
|
||||
}`}>
|
||||
{product.status || '—'}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Quarterly Data Table */}
|
||||
<div className="overflow-x-auto">
|
||||
<table className="min-w-full border border-gray-300 text-sm text-center">
|
||||
<thead>
|
||||
<tr className="font-semibold text-[#232528] text-center">
|
||||
<th rowSpan="2" className="border border-gray-300 py-2 px-3 text-left bg-[#F2ECCF] w-[140px]">
|
||||
Metric
|
||||
</th>
|
||||
<th colSpan="3" className="border border-gray-300 py-2 px-3 bg-[#F2ECCF]">
|
||||
Q4-2024 (Previous Quarter)
|
||||
</th>
|
||||
<th colSpan="3" className="border border-gray-300 py-2 px-3 bg-[#F2ECCF]">
|
||||
Q1-2025 (Current Quarter)
|
||||
</th>
|
||||
<th colSpan="3" className="border border-gray-300 py-2 px-3 bg-[#F2ECCF]">
|
||||
Q2-2025 (Next Quarter)
|
||||
</th>
|
||||
</tr>
|
||||
|
||||
{/* Second header row */}
|
||||
<tr className="bg-white text-[13px] font-medium">
|
||||
<th className="border border-gray-300 py-2 px-3 bg-[#E9F6FF] text-[#286CFF]">Oct</th>
|
||||
<th className="border border-gray-300 py-2 px-3 bg-[#E9F6FF] text-[#286CFF]">Nov</th>
|
||||
<th className="border border-gray-300 py-2 px-3 bg-[#E9F6FF] text-[#286CFF]">Dec</th>
|
||||
<th className="border border-gray-300 py-2 px-3 bg-[#F3FAF4] text-[#2F663C]">Jan</th>
|
||||
<th className="border border-gray-300 py-2 px-3 bg-[#F3FAF4] text-[#2F663C]">Feb</th>
|
||||
<th className="border border-gray-300 py-2 px-3 bg-[#F3FAF4] text-[#2F663C]">Mar</th>
|
||||
<th className="border border-gray-300 py-2 px-3 bg-[#FFF7E9] text-[#F29F0E]">Apr</th>
|
||||
<th className="border border-gray-300 py-2 px-3 bg-[#FFF7E9] text-[#F29F0E]">May</th>
|
||||
<th className="border border-gray-300 py-2 px-3 bg-[#FFF7E9] text-[#F29F0E]">Jun</th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
<tbody>
|
||||
{/* Quantity Row */}
|
||||
<tr>
|
||||
<td className="border border-gray-300 py-2 px-3 text-left font-medium">
|
||||
Quantity ({product.unit || 'units'})
|
||||
</td>
|
||||
<td className="border border-gray-300 py-2">{product.q4_oct_quantity || '—'}</td>
|
||||
<td className="border border-gray-300 py-2">{product.q4_nov_quantity || '—'}</td>
|
||||
<td className="border border-gray-300 py-2">{product.q4_dec_quantity || '—'}</td>
|
||||
<td className="border border-gray-300 py-2">{product.q1_jan_quantity || '—'}</td>
|
||||
<td className="border border-gray-300 py-2">{product.q1_feb_quantity || '—'}</td>
|
||||
<td className="border border-gray-300 py-2">{product.q1_mar_quantity || '—'}</td>
|
||||
<td className="border border-gray-300 py-2">{product.q2_apr_quantity || '—'}</td>
|
||||
<td className="border border-gray-300 py-2">{product.q2_may_quantity || '—'}</td>
|
||||
<td className="border border-gray-300 py-2">{product.q2_jun_quantity || '—'}</td>
|
||||
</tr>
|
||||
|
||||
{/* Cost Row */}
|
||||
<tr>
|
||||
<td className="border border-gray-300 py-2 px-3 text-left font-medium">
|
||||
Cost (AED)
|
||||
</td>
|
||||
<td className="border border-gray-300 py-2">{product.q4_oct_cost || '—'}</td>
|
||||
<td className="border border-gray-300 py-2">{product.q4_nov_cost || '—'}</td>
|
||||
<td className="border border-gray-300 py-2">{product.q4_dec_cost || '—'}</td>
|
||||
<td className="border border-gray-300 py-2">{product.q1_jan_cost || '—'}</td>
|
||||
<td className="border border-gray-300 py-2">{product.q1_feb_cost || '—'}</td>
|
||||
<td className="border border-gray-300 py-2">{product.q1_mar_cost || '—'}</td>
|
||||
<td className="border border-gray-300 py-2">{product.q2_apr_cost || '—'}</td>
|
||||
<td className="border border-gray-300 py-2">{product.q2_may_cost || '—'}</td>
|
||||
<td className="border border-gray-300 py-2">{product.q2_jun_cost || '—'}</td>
|
||||
</tr>
|
||||
|
||||
{/* Reason Current */}
|
||||
<tr>
|
||||
<td className="border border-gray-300 py-2 px-3 text-left font-medium">
|
||||
Reason (Current)
|
||||
</td>
|
||||
<td colSpan="3" className="border border-gray-300 py-2 text-center">
|
||||
-- --
|
||||
</td>
|
||||
<td colSpan="3" className="border border-gray-300 py-2 text-center bg-[#F3FAF4] text-[#2F663C]">
|
||||
{product.variationReason || '—'}
|
||||
</td>
|
||||
<td colSpan="3" className="border border-gray-300 py-2 text-center">
|
||||
-- --
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
{/* Reason Forecast */}
|
||||
<tr>
|
||||
<td className="border border-gray-300 py-2 px-3 text-left font-medium">
|
||||
Reason (Forecast)
|
||||
</td>
|
||||
<td colSpan="6" className="border border-gray-300 py-2 text-center">
|
||||
-- --
|
||||
</td>
|
||||
<td colSpan="3" className="border border-gray-300 py-2 text-center bg-[#FFF7E9] text-[#F29F0E]">
|
||||
{product.zeroTargetReason || '—'}
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
{/* Remarks */}
|
||||
<div className="mt-6">
|
||||
<h3 className="text-sm font-medium text-[#232528] mb-2">Remarks</h3>
|
||||
<div className="border border-[#F3B340] rounded-md p-3 text-sm bg-white">
|
||||
{product.remarks || 'No remarks provided for this product.'}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Submission Info */}
|
||||
<div className="mt-6 pt-4 border-t border-[#E2E8F0] text-sm">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<p className="text-[#6B7280]">Submitted by: <span className="text-[#232528] font-medium">User Name</span></p>
|
||||
<p className="text-[#6B7280]">Submission Date: <span className="text-[#232528] font-medium">{product.submissionDate || '—'}</span></p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-[#6B7280]">Last Updated: <span className="text-[#232528] font-medium">{product.lastUpdated || product.submissionDate || '—'}</span></p>
|
||||
<p className="text-[#6B7280]">Status:
|
||||
<span className={`inline-flex items-center justify-center rounded-md px-2 py-0.5 text-xs font-medium ml-1 ${
|
||||
product.status === 'Active' ? 'text-[#2F663C] bg-[#F3FAF4]' : 'text-[#B45309] bg-[#FEF2F2]'
|
||||
}`}>
|
||||
{product.status}
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ProductDetails;
|
||||
@ -3,6 +3,9 @@ const caretDownSrc = '/assets/images/CaretDown.svg';
|
||||
const backVectorSrc = '/assets/images/BackVector.svg';
|
||||
const mailIconSrc = '/assets/images/material-symbols_mail-outline.svg';
|
||||
const phoneIconSrc = '/assets/images/line-md_phone.svg';
|
||||
const calendarIcon = '/assets/images/Duedate.svg';
|
||||
const clockIcon = '/assets/images/mingcute_time-line.svg';
|
||||
const nextIcon = '/assets/images/mdi_page-next-outline.svg';
|
||||
|
||||
const Field = ({ label, placeholder = '', type = 'text', value = '', onChange = () => {}, disabled = true }) => (
|
||||
<div>
|
||||
@ -71,7 +74,13 @@ const EstablishmentInfo = ({
|
||||
onBack = () => {},
|
||||
loading = false,
|
||||
error = '',
|
||||
isComplete = false,
|
||||
}) => {
|
||||
const [showInfoCard, setShowInfoCard] = React.useState(true);
|
||||
|
||||
const handleCloseInfo = () => {
|
||||
setShowInfoCard(false);
|
||||
};
|
||||
const info = React.useMemo(
|
||||
() => ({
|
||||
quarter: '',
|
||||
@ -108,67 +117,137 @@ const EstablishmentInfo = ({
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
return (
|
||||
<div className="space-y-6 relative">
|
||||
{loading && (
|
||||
<div className="absolute inset-0 z-40 flex items-center justify-center bg-white/80">
|
||||
<div className="h-12 w-12 animate-spin rounded-full border-[3px] border-[#92722A] border-t-transparent" />
|
||||
</div>
|
||||
)}
|
||||
<h2 className="text-xl font-semibold text-[#232528] mb-2">IPI Quarterly Survey — Establishment Information (Step 1 of 3)</h2>
|
||||
<h2 className="text-xl font-semibold text-[#232528] mb-2">Industrial Production Index (IPI) Survey: {data.quarter} {data.year}</h2>
|
||||
|
||||
{/* Survey Information */}
|
||||
<div className="bg-[#F2ECCF] p-5 rounded-md shadow-sm ring-1 ring-gray-200">
|
||||
<h2 className="text-lg font-semibold text-[#6C4527] mb-2">Industrial Production Index (IPI) survey: {data.quarter} {data.year}</h2>
|
||||
<p className="text-sm text-[#6C4527] mb-4">
|
||||
You're completing the <b>IPI Quarterly Survey</b> for <b>{data.quarter} {data.year}</b>. This step shows establishment details already registered with us. <b>Step 1 is read-only.</b> To make corrections, update them in <b>Profile → Establishment</b>, then return to this survey.
|
||||
</p>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<div>
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-baseline justify-between flex-wrap md:flex-nowrap gap-6">
|
||||
<div className="flex items-baseline gap-1">
|
||||
<span className="text-sm font-medium text-[#6C4527] whitespace-nowrap">Due:</span>
|
||||
<span className="text-sm text-[#6C4527] whitespace-nowrap">Jan 31, 2025</span>
|
||||
</div>
|
||||
<div className="flex items-baseline gap-1">
|
||||
<span className="text-sm font-medium text-[#6C4527] whitespace-nowrap">Estimated time:</span>
|
||||
<span className="text-sm text-[#6C4527] whitespace-nowrap">15-20 minutes (step 1: 1-2 mins)</span>
|
||||
</div>
|
||||
<div className="flex items-baseline gap-1">
|
||||
<span className="text-sm font-medium text-[#6C4527] whitespace-nowrap">Next:</span>
|
||||
<span className="text-sm text-[#6C4527] whitespace-nowrap">Product Data, then Review & Submit</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<ul className="text-sm text-[#6C4527] list-disc pl-5 space-y-1">
|
||||
<li>After submission, responses are locked.A receipt appears in History after submission</li>
|
||||
<li className="whitespace-nowrap">
|
||||
After submission, responses are Data is used for statistical reporting and handled per our privacy policy locked. A receipt appears in History.
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div className="flex items-center gap-6 mt-2 text-sm text-[#6C4527]">
|
||||
{/* Email */}
|
||||
<div className="flex items-center gap-2">
|
||||
<img src={mailIconSrc} alt="email" className="w-4 h-4" />
|
||||
<span>support@ipi.gov.example</span>
|
||||
</div>
|
||||
|
||||
{/* Phone */}
|
||||
<div className="flex items-center gap-2">
|
||||
<img src={phoneIconSrc} alt="phone" className="w-4 h-4" />
|
||||
<span>+971-2-000-0000</span>
|
||||
{!showInfoCard && (
|
||||
<button
|
||||
onClick={() => setShowInfoCard(true)}
|
||||
className="flex items-center gap-1 text-[#6C4527] text-sm font-medium hover:underline mb-4"
|
||||
>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" className="h-4 w-4" viewBox="0 0 20 20" fill="currentColor">
|
||||
<path fillRule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a1 1 0 000 2v3a1 1 0 001 1h.01a1 1 0 100-2H10V9a1 1 0 00-1-1z" clipRule="evenodd" />
|
||||
</svg>
|
||||
Show information
|
||||
</button>
|
||||
)}
|
||||
{showInfoCard && (
|
||||
<div className="bg-[#F2ECCF] p-5 rounded-md shadow-sm ring-1 ring-gray-200 relative mb-6">
|
||||
<button
|
||||
onClick={handleCloseInfo}
|
||||
className="absolute right-4 top-4 text-gray-500 hover:text-gray-700 focus:outline-none"
|
||||
aria-label="Close information"
|
||||
>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" className="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
|
||||
<path fillRule="evenodd" d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z" clipRule="evenodd" />
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
<div className="space-y-4">
|
||||
<p className="text-sm text-[#6C4527] mb-4">
|
||||
You're completing the <b>IPI Quarterly Survey</b> for <b>{data.quarter} {data.year}</b>. This step shows establishment details already registered with us. <b>Step 1 is read-only.</b> To make corrections, update them in <b>Profile → Edit Profile</b>, then return to this survey.
|
||||
</p>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<div>
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-baseline justify-between flex-wrap md:flex-nowrap gap-6">
|
||||
<div className="flex items-center gap-1">
|
||||
<img src={calendarIcon} alt="Calendar" className="w-4 h-4 flex-shrink-0" />
|
||||
<span className="text-sm font-medium text-[#6C4527] whitespace-nowrap">Due:</span>
|
||||
<span className="text-sm text-[#6C4527] whitespace-nowrap">Jan 31, 2025</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-1">
|
||||
<img src={clockIcon} alt="Time" className="w-4 h-4 flex-shrink-0" />
|
||||
<span className="text-sm font-medium text-[#6C4527] whitespace-nowrap">Estimated time:</span>
|
||||
<span className="text-sm text-[#6C4527] whitespace-nowrap">15-20 minutes (step 1: 1-2 minutes)</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-1">
|
||||
<img src={nextIcon} alt="Next" className="w-4 h-4 flex-shrink-0" />
|
||||
<span className="text-sm font-medium text-[#6C4527] whitespace-nowrap">Next:</span>
|
||||
<span className="text-sm text-[#6C4527] whitespace-nowrap"> Add Product Data → Review & Submit</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<ul className="text-sm text-[#6C4527] list-disc pl-5 space-y-1">
|
||||
<li><b>After submission</b>: responses are locked. A receipt appears in History after submission</li>
|
||||
<li><b>After submission</b>: responses are used for statistical reporting and handled per our privacy policy</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div className="flex items-center gap-6 mt-2 text-sm text-[#6C4527]">
|
||||
<div className="flex items-center gap-2">
|
||||
<img src={mailIconSrc} alt="email" className="w-4 h-4" />
|
||||
<span>support@ipi.gov.example</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<img src={phoneIconSrc} alt="phone" className="w-4 h-4" />
|
||||
<span>+971-2-000-0000</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{/* Blue Info Box */}
|
||||
<div className="bg-[#E8F3FF] border border-[#2F80ED] text-[#0B4DA2] px-4 py-2 rounded-md mt-3 flex items-center gap-2 text-sm">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
strokeWidth={2}
|
||||
stroke="#0B4DA2"
|
||||
className="w-5 h-5 flex-shrink-0"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
|
||||
/>
|
||||
</svg>
|
||||
{/* <p>
|
||||
<span className="font-medium">Need to update details?</span> Go to{' '}
|
||||
<b>Profile → Edit Profile</b>. Changes saved there will appear{' '}
|
||||
<a href="#" className="underline font-medium text-[#0B4DA2] hover:text-[#063B82]">
|
||||
here
|
||||
</a>.
|
||||
</p> */}
|
||||
<p className="text-sm text-[#043DFF] bg-[#E7F5FF]">
|
||||
<span className="font-medium">Need to update details?</span> Go to Profile →{' '}
|
||||
<a
|
||||
href={`/admin/configuration/${data.id || ''}`}
|
||||
className="underline font-medium text-[#043DFF] hover:text-[#063B82]"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
// Store current URL to return after login
|
||||
localStorage.setItem('returnTo', window.location.pathname);
|
||||
window.location.href = e.currentTarget.href;
|
||||
}}
|
||||
>
|
||||
Edit Profile
|
||||
</a>
|
||||
. Changes saved there will appear here.
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<h2 className="text-lg font-semibold text-[#232528]">Step 1: Review Establishment Information</h2>
|
||||
{isComplete && (
|
||||
<img
|
||||
src="/assets/images/CircleFrame.svg"
|
||||
alt="Completed"
|
||||
className="w-5 h-5"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex justify-end gap-1 mt-1 text-sm">
|
||||
<a href="#" className="text-[#92722A] hover:underline">You can return to Step 1 after updating details in Profile.</a>
|
||||
</div>
|
||||
<div className="space-y-1" aria-busy={loading ? "true" : "false"}>
|
||||
<div className="mt-2" aria-busy={loading ? "true" : "false"}>
|
||||
{/* Reporting Period */}
|
||||
<Card>
|
||||
<h3 className="text-sm font-semibold text-gray-900 mb-4">Reporting Period</h3>
|
||||
@ -180,6 +259,7 @@ return (
|
||||
value={info.quarter}
|
||||
onChange={handleFieldChange('quarter')}
|
||||
disabled={true}
|
||||
required
|
||||
className="block w-full h-10 rounded-md border-2 border-[#E6D7A2] focus:border-[#92722A] focus:ring-0 px-4 pr-10 text-sm bg-gray-50 appearance-none cursor-not-allowed"
|
||||
>
|
||||
<option value="" disabled>Select quarter</option>
|
||||
|
||||
@ -7,6 +7,9 @@ import {
|
||||
} from '@/services/masters/masterService.js';
|
||||
|
||||
const caretDownSrc = '/assets/images/CaretDown-black.svg';
|
||||
const clockIcon = '/assets/images/mingcute_time-line.svg';
|
||||
const calendarIcon = '/assets/images/Duedate.svg';
|
||||
const nextIcon = '/assets/images/mdi_page-next-outline.svg';
|
||||
|
||||
const Input = ({ placeholder = '', type = 'text', value = '', onChange = () => {}, required = false }) => {
|
||||
const [isFocused, setIsFocused] = React.useState(false);
|
||||
@ -28,37 +31,63 @@ const Input = ({ placeholder = '', type = 'text', value = '', onChange = () => {
|
||||
);
|
||||
};
|
||||
|
||||
const Select = ({ placeholder = '', options = [], value = '', onChange }) => {
|
||||
const Select = ({
|
||||
placeholder = '',
|
||||
options = [],
|
||||
value = '',
|
||||
onChange,
|
||||
loading = false,
|
||||
disabled = false
|
||||
}) => {
|
||||
const [isFocused, setIsFocused] = React.useState(false);
|
||||
|
||||
return (
|
||||
<div className="relative">
|
||||
<select
|
||||
className={`block w-full h-10 rounded-md border-2 ${value ? 'border-[#92722A]' : 'border-[#CBA344]'} ${isFocused ? 'border-[#92722A]' : ''} focus:ring-0 px-4 pr-10 text-sm bg-white appearance-none`}
|
||||
className={`block w-full h-10 rounded-md border-2 ${
|
||||
disabled ? 'bg-gray-50 cursor-not-allowed' : 'bg-white'
|
||||
} ${
|
||||
value ? 'border-[#92722A]' : 'border-[#CBA344]'
|
||||
} ${
|
||||
isFocused && !disabled ? 'border-[#92722A]' : ''
|
||||
} focus:ring-0 px-4 pr-10 text-sm appearance-none`}
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
onFocus={() => setIsFocused(true)}
|
||||
onBlur={() => setIsFocused(false)}
|
||||
onFocus={() => !disabled && setIsFocused(true)}
|
||||
onBlur={() => !disabled && setIsFocused(false)}
|
||||
disabled={disabled || loading}
|
||||
>
|
||||
<option value="" disabled>{placeholder}</option>
|
||||
{options.map((opt) => {
|
||||
if (opt && typeof opt === 'object') {
|
||||
const optionValue = opt.value ?? '';
|
||||
const optionLabel = opt.label ?? optionValue;
|
||||
if (!optionValue && !optionLabel) return null;
|
||||
return (
|
||||
<option key={optionValue || optionLabel} value={optionValue}>
|
||||
{optionLabel}
|
||||
</option>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<option key={opt} value={opt}>{opt}</option>
|
||||
);
|
||||
})}
|
||||
</select>
|
||||
<img src={caretDownSrc} alt="open" className="pointer-events-none absolute right-3 top-1/2 -translate-y-1/2 h-4 w-4" />
|
||||
</div>
|
||||
<option value="" disabled>{loading ? 'Loading...' : placeholder}</option>
|
||||
{loading ? (
|
||||
<option disabled>Loading options...</option>
|
||||
) : (
|
||||
options.map((opt) => {
|
||||
if (opt && typeof opt === 'object') {
|
||||
const optionValue = opt.value ?? '';
|
||||
const optionLabel = opt.label ?? optionValue;
|
||||
if (!optionValue && !optionLabel) return null;
|
||||
return (
|
||||
<option key={optionValue || optionLabel} value={optionValue}>
|
||||
{optionLabel}
|
||||
</option>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<option key={opt} value={opt}>
|
||||
{opt}
|
||||
</option>
|
||||
);
|
||||
})
|
||||
)}
|
||||
</select>
|
||||
<div className="pointer-events-none absolute right-3 top-1/2 -translate-y-1/2 flex items-center">
|
||||
{loading ? (
|
||||
<div className="h-4 w-4 animate-spin rounded-full border-2 border-[#92722A] border-t-transparent" />
|
||||
) : (
|
||||
<img src={caretDownSrc} alt="open" className="h-4 w-4" />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@ -107,7 +136,12 @@ const ProductData = ({
|
||||
onProductsChange = () => {},
|
||||
onNext = () => {},
|
||||
onBack = () => {},
|
||||
loading = false,
|
||||
error = '',
|
||||
}) => {
|
||||
const [showDataDefinitions, setShowDataDefinitions] = React.useState(true);
|
||||
const [showEntryRules, setShowEntryRules] = React.useState(true);
|
||||
const [showInfoCard, setShowInfoCard] = React.useState(true);
|
||||
const MAX_PRODUCTS = 10;
|
||||
const nextId = React.useRef(products.length + 1);
|
||||
const [variationReasons, setVariationReasons] = React.useState([]);
|
||||
@ -122,6 +156,7 @@ const ProductData = ({
|
||||
const [unitOptions, setUnitOptions] = React.useState([]);
|
||||
const [isLoadingUnits, setIsLoadingUnits] = React.useState(false);
|
||||
const [unitsError, setUnitsError] = React.useState('');
|
||||
const [isInitialLoad, setIsInitialLoad] = React.useState(true);
|
||||
|
||||
React.useEffect(() => {
|
||||
const variationController = new AbortController();
|
||||
@ -139,8 +174,8 @@ const ProductData = ({
|
||||
setReasonsError('');
|
||||
} catch (error) {
|
||||
if (error.name !== 'AbortError') {
|
||||
console.error('Failed to fetch variation reasons', error);
|
||||
setReasonsError('Failed to load variation reasons.');
|
||||
// console.error('Failed to fetch variation reasons', error);
|
||||
// setReasonsError('Failed to load variation reasons.');
|
||||
}
|
||||
} finally {
|
||||
setIsLoadingReasons(false);
|
||||
@ -156,8 +191,8 @@ const ProductData = ({
|
||||
setZeroReasonsError('');
|
||||
} catch (error) {
|
||||
if (error.name !== 'AbortError') {
|
||||
console.error('Failed to fetch zero target reasons', error);
|
||||
setZeroReasonsError('Failed to load zero target reasons.');
|
||||
// console.error('Failed to fetch zero target reasons', error);
|
||||
// setZeroReasonsError('Failed to load zero target reasons.');
|
||||
}
|
||||
} finally {
|
||||
setIsLoadingZeroReasons(false);
|
||||
@ -174,8 +209,8 @@ const ProductData = ({
|
||||
setProductsError('');
|
||||
} catch (error) {
|
||||
if (error.name !== 'AbortError') {
|
||||
console.error('Failed to fetch products', error);
|
||||
setProductsError('Failed to load products.');
|
||||
// console.error('Failed to fetch products', error);
|
||||
// setProductsError('Failed to load products.');
|
||||
}
|
||||
} finally {
|
||||
setIsLoadingProducts(false);
|
||||
@ -191,18 +226,28 @@ const ProductData = ({
|
||||
setUnitsError('');
|
||||
} catch (error) {
|
||||
if (error.name !== 'AbortError') {
|
||||
console.error('Failed to fetch units', error);
|
||||
setUnitsError('Failed to load units.');
|
||||
// console.error('Failed to fetch units', error);
|
||||
// setUnitsError('Failed to load units.');
|
||||
}
|
||||
} finally {
|
||||
setIsLoadingUnits(false);
|
||||
}
|
||||
};
|
||||
|
||||
loadReasons();
|
||||
loadZeroReasons();
|
||||
loadProducts();
|
||||
loadUnits();
|
||||
const loadAllData = async () => {
|
||||
try {
|
||||
await Promise.all([
|
||||
loadReasons(),
|
||||
loadZeroReasons(),
|
||||
loadProducts(),
|
||||
loadUnits()
|
||||
]);
|
||||
} finally {
|
||||
setIsInitialLoad(false);
|
||||
}
|
||||
};
|
||||
|
||||
loadAllData();
|
||||
return () => {
|
||||
variationController.abort();
|
||||
zeroController.abort();
|
||||
@ -255,93 +300,143 @@ const ProductData = ({
|
||||
|
||||
const canAddMoreProducts = products.length < MAX_PRODUCTS;
|
||||
|
||||
const isLoading = loading || isLoadingReasons || isLoadingZeroReasons || isLoadingProducts || isLoadingUnits;
|
||||
const hasError = error || reasonsError || zeroReasonsError || productsError || unitsError;
|
||||
|
||||
return (
|
||||
<div className="space-y-5">
|
||||
<div className="space-y-6 relative">
|
||||
{isLoading && (
|
||||
<div className="absolute inset-0 z-40 flex items-center justify-center bg-white/80">
|
||||
<div className="h-12 w-12 animate-spin rounded-full border-[3px] border-[#92722A] border-t-transparent" />
|
||||
</div>
|
||||
)}
|
||||
<h2 className="text-xl font-semibold text-[#232528] mb-2">Step 2: Product Data — Monthly Output & Cost</h2>
|
||||
{!showInfoCard && (
|
||||
<button
|
||||
onClick={() => setShowInfoCard(true)}
|
||||
className="flex items-center gap-1 text-[#6C4527] text-sm font-medium hover:underline mb-4"
|
||||
>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" className="h-4 w-4" viewBox="0 0 20 20" fill="currentColor">
|
||||
<path fillRule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a1 1 0 000 2v3a1 1 0 001 1h.01a1 1 0 100-2H10V9a1 1 0 00-1-1z" clipRule="evenodd" />
|
||||
</svg>
|
||||
Show information
|
||||
</button>
|
||||
)}
|
||||
|
||||
{/* Informational Section */}
|
||||
<div className="bg-[#F2ECCF] rounded-md p-4 mb-6">
|
||||
<h3 className="text-base font-semibold mb-2 text-[#6C4527]">
|
||||
Why we collect monthly product data
|
||||
</h3>
|
||||
<p className="text-sm text-[#6C4527] mb-4">
|
||||
This step collects <strong className="text-[#6C4527]">monthly output quantity and </strong><strong className="text-[#6C4527]">production cost</strong> for each product you manufacture or process. The data inform the
|
||||
<strong className="text-[#6C4527]"> Industrial Production Index (IPI)</strong> and capacity-utilization statistics.
|
||||
Results are published <strong className="text-[#6C4527]">only in aggregated form</strong> and your information is protected
|
||||
by national statistics confidentiality provisions.
|
||||
</p>
|
||||
{showInfoCard && (
|
||||
<div className="bg-[#F2ECCF] rounded-md p-4 mb-6 relative">
|
||||
<button
|
||||
onClick={() => setShowInfoCard(false)}
|
||||
className="absolute top-2 right-2 text-gray-600 hover:text-gray-800"
|
||||
aria-label="Close info"
|
||||
>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" className="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
</button>
|
||||
<div className="text-[#6C4527] space-y-2">
|
||||
<h3 className="text-base font-semibold mb-2">Why we collect monthly product data</h3>
|
||||
<p className="text-sm mb-4">
|
||||
This step collects <strong>monthly output quantity and production cost</strong> for each product you manufacture or process. The data inform the <strong>Industrial Production Index (IPI)</strong> and capacity-utilization statistics. Results are published <strong>only in aggregated form</strong> and your information is protected by national statistics confidentiality provisions.
|
||||
</p>
|
||||
|
||||
<div className="flex gap-2 text-sm">
|
||||
<div className="flex items-center space-x-1">
|
||||
<img src={calendarIcon} alt="" className="h-4 w-4" />
|
||||
<span className="font-medium">Due:</span>
|
||||
<span>Jan 31, 2025</span>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 sm:grid-cols-3 gap-4 text-sm text-[#6C4527] mt-4">
|
||||
<div className="flex items-baseline justify-between flex-wrap md:flex-nowrap gap-6">
|
||||
<div className="flex items-baseline gap-1">
|
||||
<span className="text-sm font-normal text-[#6C4527] whitespace-nowrap">Due:</span>
|
||||
<span className="text-sm font-normal text-[#6C4527] whitespace-nowrap">Jan 31, 2025</span>
|
||||
</div>
|
||||
<div className="flex items-baseline gap-1">
|
||||
<span className="text-sm font-normal text-[#6C4527] whitespace-nowrap">Estimated time:</span>
|
||||
<span className="text-sm font-normal text-[#6C4527] whitespace-nowrap">Depends on number of products</span>
|
||||
</div>
|
||||
<div className="flex items-baseline gap-1">
|
||||
<span className="text-sm font-normal text-[#6C4527] whitespace-nowrap">What you'll do:</span>
|
||||
<span className="text-sm font-normal text-[#6C4527] whitespace-nowrap">Add products → select HS code & unit → enter monthly Qty & Cost →set next-quarter targets</span>
|
||||
</div>
|
||||
<div className="flex items-center space-x-1">
|
||||
<img src={clockIcon} alt="" className="h-4 w-4 ml-2" />
|
||||
<span className="font-medium">Estimated time:</span>
|
||||
<span>Depends on number of products</span>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center space-x-1">
|
||||
<img src={nextIcon} alt="" className="h-4 w-4 ml-2" />
|
||||
<span className="font-medium">What you'll do:</span>
|
||||
<span>Add products → select HS code & unit → enter monthly Qty & Cost →set next-quarter targets.</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-4 space-y-2">
|
||||
<div className="flex items-start gap-2">
|
||||
<div className="w-1.5 h-1.5 rounded-full bg-[#6C4527] mt-2 flex-shrink-0"></div>
|
||||
<span className="text-sm">
|
||||
<strong>Confidentiality:</strong> Used only for statistics; not shared for taxation or enforcement.
|
||||
</span>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
<div className="mt-4 space-y-2">
|
||||
<div className="flex items-start gap-2">
|
||||
<div className="w-1.5 h-1.5 rounded-full bg-[#6C4527] mt-2 flex-shrink-0"></div>
|
||||
<span className="text-sm font-normal text-[#6C4527]">
|
||||
<strong>Confidentiality:</strong> Used only for statistics; not shared for taxation or enforcement.
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-start gap-2">
|
||||
<div className="w-1.5 h-1.5 rounded-full bg-[#6C4527] mt-2 flex-shrink-0"></div>
|
||||
<span className="text-sm font-normal text-[#6C4527]">
|
||||
<strong>Completeness:</strong> Report all products with notable output in the quarter.
|
||||
</span>
|
||||
<div className="flex items-start gap-2">
|
||||
<div className="w-1.5 h-1.5 rounded-full bg-[#6C4527] mt-2 flex-shrink-0"></div>
|
||||
<span className="text-sm">
|
||||
<strong>Completeness:</strong> Report all products with notable output in the quarter.
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Data Definitions & Entry Rules Section */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
{/* Left card - Data definitions */}
|
||||
{showDataDefinitions && (
|
||||
<div className="bg-[#F2ECCF] rounded-md p-4 text-[#6C4527] text-sm relative">
|
||||
<button
|
||||
onClick={() => setShowDataDefinitions(false)}
|
||||
className="absolute top-2 right-2 text-gray-600 hover:text-gray-800"
|
||||
aria-label="Close data definitions"
|
||||
>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" className="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
</button>
|
||||
<h3 className="font-semibold mb-4 text-base">Data definitions</h3>
|
||||
<p className="mb-3"><strong>Product (HS Code – Name):</strong> Standard trade classification. Start typing to search by code or name.</p>
|
||||
<p className="mb-3"><strong>Quantity (Qty):</strong> Physical output produced in the month, measured in the selected unit.</p>
|
||||
<p className="mb-3"><strong>Cost (AED):</strong> Total production cost for that month, excluding VAT (e.g., materials, energy, direct labour, and manufacturing expenses). Do not include sales margins.</p>
|
||||
<p className="mb-0"><strong>Annual Installed Capacity:</strong> Maximum achievable annual output under normal operating conditions with existing equipment; do not include extraordinary overtime.</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Right card - Entry rules */}
|
||||
{showEntryRules && (
|
||||
<div className="bg-[#F2ECCF] rounded-md p-4 text-[#6C4527] text-sm relative">
|
||||
<button
|
||||
onClick={() => setShowEntryRules(false)}
|
||||
className="absolute top-2 right-2 text-gray-600 hover:text-gray-800"
|
||||
aria-label="Close entry rules"
|
||||
>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" className="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
</button>
|
||||
<h3 className="font-semibold mb-4 text-base">Entry rules & validation</h3>
|
||||
<p className="mb-3"><strong>Mandatory:</strong> All fields in this step are required except "Reason for variation/0 Target" and free-text Notes.</p>
|
||||
<p className="mb-3"><strong>Monthly entries:</strong> Enter Qty and Cost for each month in the Current and Forecast quarters. Use 0 where there is no output or cost.</p>
|
||||
<p className="mb-3"><strong>Number only:</strong> Use a decimal point where needed. No negative values.</p>
|
||||
<p className="mb-3"><strong>Units:</strong> Use one measurement unit per product across all months.</p>
|
||||
<p className="mb-0"><strong>Delete:</strong> Removing a product here affects this survey only; it does not change your profile.</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Info line section (Already exists) */}
|
||||
|
||||
{/* Data Definitions & Entry Rules Section */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
{/* Left card - Data definitions */}
|
||||
<div className="bg-[#F2ECCF] rounded-md p-4 text-[#6C4527]">
|
||||
<h3 className="font-semibold mb-2">Data definitions</h3>
|
||||
<p className="font-normal"><strong>Product (HS Code – Name):</strong> Standard trade classification. Start typing to search by code or name.</p>
|
||||
<p className="font-normal"><strong>Quantity (Qty):</strong> Physical output produced in the month, measured in the selected unit.</p>
|
||||
<p className="font-normal"><strong>Cost (AED):</strong> Total <em>production cost</em> for that month, excluding VAT (e.g., materials, energy, direct labour, and manufacturing expenses). Do not include sales margins.</p>
|
||||
<p className="font-normal"><strong>Annual Installed Capacity:</strong> Maximum achievable annual output under normal operating conditions with existing equipment; do not include extraordinary overtime.</p>
|
||||
</div>
|
||||
|
||||
{/* Right card - Entry rules */}
|
||||
<div className="bg-[#F2ECCF] rounded-md p-4 text-[#6C4527]">
|
||||
<h3 className="font-semibold mb-2">Entry rules & validation</h3>
|
||||
<p><strong>Mandatory:</strong> All fields in this step are required except "Reason for variation/0 Target" and free-text Notes.</p>
|
||||
<p><strong>Monthly entries:</strong> Enter Qty and Cost for each month in the Current and Forecast quarters. Use 0 where there is no output or cost.</p>
|
||||
<p><strong>Number only:</strong> Use a decimal point where needed. No negative values.</p>
|
||||
<p><strong>Units:</strong> Use one measurement unit per product across all months.</p>
|
||||
<p><strong>Delete:</strong> Removing a product here affects this survey only; it does not change your profile.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-3">
|
||||
<h2 className="text-lg font-semibold text-gray-900">Product Data</h2>
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
<button
|
||||
onClick={addProduct}
|
||||
disabled={!canAddMoreProducts}
|
||||
className={`inline-flex items-center gap-2 text-sm font-medium px-3 py-2 rounded ${canAddMoreProducts ? 'bg-[#92722A] text-white cursor-pointer hover:bg-[#9e782f]' : 'bg-gray-200 text-gray-500 cursor-not-allowed'}`}
|
||||
>
|
||||
<button
|
||||
onClick={addProduct}
|
||||
disabled={!canAddMoreProducts || isLoading}
|
||||
className={`inline-flex items-center gap-2 text-sm font-medium px-3 py-2 rounded ${
|
||||
!canAddMoreProducts || isLoading
|
||||
? 'bg-gray-200 text-gray-500 cursor-not-allowed'
|
||||
: 'bg-[#92722A] text-white cursor-pointer hover:bg-[#9e782f]'
|
||||
}`}
|
||||
>
|
||||
<span>+ Add Product</span>
|
||||
</button>
|
||||
{!canAddMoreProducts && (
|
||||
@ -367,7 +462,7 @@ const ProductData = ({
|
||||
</div>
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">Product (HS Code - Name)</label>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">Product (HS Code - Name) <span className="text-red-500">*</span></label>
|
||||
<Select
|
||||
placeholder={isLoadingProducts ? 'Loading products...' : 'Select product HS code'}
|
||||
options={productOptions}
|
||||
@ -379,7 +474,7 @@ const ProductData = ({
|
||||
)}
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">Unit</label>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">Unit <span className="text-red-500">*</span></label>
|
||||
<Select
|
||||
placeholder={isLoadingUnits ? 'Loading units...' : 'Select unit'}
|
||||
options={unitOptions}
|
||||
@ -391,18 +486,19 @@ const ProductData = ({
|
||||
)}
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">Annual installed capacity (in unit)</label>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">Annual installed capacity (in unit) <span className="text-red-500">*</span></label>
|
||||
<Input
|
||||
placeholder="Enter capacity"
|
||||
type="number"
|
||||
value={p.capacity || ''}
|
||||
onChange={(e) => updateProductField(p.id, 'capacity', e.target.value)}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
<SectionBox title="Q4 2024 Previous" badgeBg="#E7F5FF" badgeText="#003CFF">
|
||||
<SectionBox title="PREVIOUS QUARTER (Q4-2024)" badgeBg="#E7F5FF" badgeText="#003CFF">
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-end space-x-4">
|
||||
<div>
|
||||
@ -410,6 +506,7 @@ const ProductData = ({
|
||||
<Input
|
||||
className="w-24"
|
||||
type="number"
|
||||
placeholder="Enter"
|
||||
value={p.previousQuantity || ''}
|
||||
onChange={(e) => updateProductField(p.id, 'previousQuantity', e.target.value)}
|
||||
/>
|
||||
@ -419,6 +516,7 @@ const ProductData = ({
|
||||
<Input
|
||||
className="w-24"
|
||||
type="number"
|
||||
placeholder="Enter"
|
||||
value={p.novQuantity || ''}
|
||||
onChange={(e) => updateProductField(p.id, 'novQuantity', e.target.value)}
|
||||
/>
|
||||
@ -428,6 +526,7 @@ const ProductData = ({
|
||||
<Input
|
||||
className="w-24"
|
||||
type="number"
|
||||
placeholder="Enter"
|
||||
value={p.decQuantity || ''}
|
||||
onChange={(e) => updateProductField(p.id, 'decQuantity', e.target.value)}
|
||||
/>
|
||||
@ -440,6 +539,7 @@ const ProductData = ({
|
||||
<Input
|
||||
className="w-24"
|
||||
type="number"
|
||||
placeholder="Enter"
|
||||
value={p.octCost || ''}
|
||||
onChange={(e) => updateProductField(p.id, 'octCost', e.target.value)}
|
||||
/>
|
||||
@ -449,6 +549,7 @@ const ProductData = ({
|
||||
<Input
|
||||
className="w-24"
|
||||
type="number"
|
||||
placeholder="Enter"
|
||||
value={p.novCost || ''}
|
||||
onChange={(e) => updateProductField(p.id, 'novCost', e.target.value)}
|
||||
/>
|
||||
@ -457,6 +558,7 @@ const ProductData = ({
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">Dec Cost (AED)</label>
|
||||
<Input
|
||||
className="w-24"
|
||||
placeholder="Enter"
|
||||
type="number"
|
||||
value={p.decCost || ''}
|
||||
onChange={(e) => updateProductField(p.id, 'decCost', e.target.value)}
|
||||
@ -466,7 +568,7 @@ const ProductData = ({
|
||||
</div>
|
||||
</SectionBox>
|
||||
|
||||
<SectionBox title="Q1 2025 Current" badgeBg="#F3FAF4" badgeText="#2F663C">
|
||||
<SectionBox title="CURRENT QUARTER (Q1-2025)" badgeBg="#F3FAF4" badgeText="#2F663C">
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-end space-x-4">
|
||||
<div>
|
||||
@ -492,7 +594,7 @@ const ProductData = ({
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">Mar Qty<span className="text-red-600"></span></label>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">Mar Qty<span className="text-red-600">*</span></label>
|
||||
<Input
|
||||
className="w-24"
|
||||
type="number"
|
||||
@ -505,7 +607,7 @@ const ProductData = ({
|
||||
</div>
|
||||
<div className="flex items-end space-x-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">Jan Cost (AED)<span className="text-red-600"></span></label>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">Jan Cost (AED)<span className="text-red-600">*</span></label>
|
||||
<Input
|
||||
className="w-24"
|
||||
type="number"
|
||||
@ -516,7 +618,7 @@ const ProductData = ({
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">Feb Cost (AED)<span className="text-red-600"></span></label>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">Feb Cost (AED)<span className="text-red-600">*</span></label>
|
||||
<Input
|
||||
className="w-24"
|
||||
type="number"
|
||||
@ -527,7 +629,7 @@ const ProductData = ({
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">Mar Cost (AED)<span className="text-red-600"></span></label>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">Mar Cost (AED)<span className="text-red-600">*</span></label>
|
||||
<Input
|
||||
className="w-24"
|
||||
type="number"
|
||||
@ -553,11 +655,11 @@ const ProductData = ({
|
||||
</div>
|
||||
</SectionBox>
|
||||
|
||||
<SectionBox title="Q2 2025 Forecast" badgeBg="#F9F7ED" badgeText="#7C5E24">
|
||||
<SectionBox title="NEXT QUARTER FORECAST (Q2-2025)" badgeBg="#FFF7E9" badgeText="#F29F0E">
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-end space-x-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">Apr Qty</label>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">Apr Qty<span className="text-red-600">*</span></label>
|
||||
<Input
|
||||
className="w-24"
|
||||
type="number"
|
||||
@ -568,31 +670,29 @@ const ProductData = ({
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">May Qty</label>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">May Qty<span className="text-red-600">*</span></label>
|
||||
<Input
|
||||
className="w-24"
|
||||
type="number"
|
||||
required
|
||||
placeholder="Enter"
|
||||
value={p.mayQuantity || ''}
|
||||
onChange={(e) => updateProductField(p.id, 'mayQuantity', e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">Jun Qty</label>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">Jun Qty<span className="text-red-600">*</span></label>
|
||||
<Input
|
||||
className="w-24"
|
||||
type="number"
|
||||
required
|
||||
placeholder="Enter"
|
||||
value={p.junQuantity || ''}
|
||||
onChange={(e) => updateProductField(p.id, 'junQuantity', e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-end space-x-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">Apr Cost (AED)</label>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">Apr Cost (AED)<span className="text-red-600">*</span></label>
|
||||
<Input
|
||||
className="w-24"
|
||||
type="number"
|
||||
@ -603,7 +703,7 @@ const ProductData = ({
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">May Cost (AED)</label>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">May Cost (AED)<span className="text-red-600">*</span></label>
|
||||
<Input
|
||||
className="w-24"
|
||||
type="number"
|
||||
@ -614,7 +714,7 @@ const ProductData = ({
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">Jun Cost (AED)</label>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">Jun Cost (AED)<span className="text-red-600">*</span></label>
|
||||
<Input
|
||||
className="w-24"
|
||||
type="number"
|
||||
@ -626,7 +726,7 @@ const ProductData = ({
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">Reason for 0 Target<span className="text-red-600">*</span></label>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">Reason for 0 Target</label>
|
||||
<Select
|
||||
placeholder={isLoadingZeroReasons ? 'Loading reasons...' : 'Select reason'}
|
||||
options={zeroTargetReasons}
|
||||
@ -664,6 +764,23 @@ const ProductData = ({
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{(hasError || isInitialLoad) && (
|
||||
<div className="mt-4">
|
||||
{isInitialLoad && !hasError && (
|
||||
<p className="text-sm text-[#5F646D]">Loading product data...</p>
|
||||
)}
|
||||
{hasError && (
|
||||
<div className="text-sm text-red-600 space-y-1">
|
||||
{error && <p>{error}</p>}
|
||||
{reasonsError && <p>{reasonsError}</p>}
|
||||
{zeroReasonsError && <p>{zeroReasonsError}</p>}
|
||||
{productsError && <p>{productsError}</p>}
|
||||
{unitsError && <p>{unitsError}</p>}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@ -85,7 +85,17 @@ const History = () => {
|
||||
};
|
||||
}, []);
|
||||
|
||||
const headers = ['Year', 'Quarter', 'Submission Window', 'Total Products', 'Products Submitted', 'Total Cost (AED)', 'Status', 'Date Submitted', 'Actions'];
|
||||
const headers = [
|
||||
{ name: 'Year', className: 'text-left' },
|
||||
{ name: 'Quarter', className: 'text-left' },
|
||||
{ name: 'Submission Window', className: 'text-left' },
|
||||
{ name: 'Total Products', className: 'text-center' },
|
||||
{ name: 'Products Submitted', className: 'text-center' },
|
||||
{ name: 'Total Cost (AED)', className: 'text-right' },
|
||||
{ name: 'Status', className: 'text-center' },
|
||||
{ name: 'Date Submitted', className: 'text-left' },
|
||||
{ name: 'Actions', className: 'text-center' }
|
||||
];
|
||||
|
||||
const statusOptions = React.useMemo(
|
||||
() => [
|
||||
|
||||
240
ipi-survey-platform/src/pages/Overview/Overview.jsx
Normal file
240
ipi-survey-platform/src/pages/Overview/Overview.jsx
Normal file
@ -0,0 +1,240 @@
|
||||
import React, { useState } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import HeaderBar from '@/components/layout/HeaderBar';
|
||||
import Table from '@/components/common/Table';
|
||||
import DetailedOverview from '@/components/overview/DetailedOverview';
|
||||
|
||||
const searchIconSrc = '/assets/images/material-symbols_search-rounded.svg';
|
||||
const caretDownSrc = '/assets/images/CaretDown.svg';
|
||||
const downloadIconSrc = '/assets/images/DownloadSimple.svg';
|
||||
|
||||
const statusStyles = {
|
||||
approved: 'text-[#2F663C] bg-[#F3FAF4] ',
|
||||
submitted: 'text-[#003CFF] bg-[#E7F5FF]',
|
||||
pending: 'text-[#B45309] bg-[#FEF2F2]',
|
||||
rejected: 'text-[#B52520] bg-[#FEF2F2]',
|
||||
};
|
||||
|
||||
const mockOverview = [
|
||||
{
|
||||
year: '2025',
|
||||
quarter: 'Q4',
|
||||
window: 'Oct-Dec 2025',
|
||||
totalProducts: 10,
|
||||
submittedProducts: 4,
|
||||
totalCost: 195000,
|
||||
status: 'Approved',
|
||||
submittedOn: '18/10/2025',
|
||||
},
|
||||
{
|
||||
year: '2025',
|
||||
quarter: 'Q3',
|
||||
window: 'Jul-Sep 2025',
|
||||
totalProducts: 10,
|
||||
submittedProducts: 6,
|
||||
totalCost: 295000,
|
||||
status: 'Submitted',
|
||||
submittedOn: '28/08/2025',
|
||||
},
|
||||
{
|
||||
year: '2025',
|
||||
quarter: 'Q2',
|
||||
window: 'Apr-Jun 2025',
|
||||
totalProducts: 10,
|
||||
submittedProducts: 6,
|
||||
totalCost: 395000,
|
||||
status: 'Rejected',
|
||||
submittedOn: '24/05/2025',
|
||||
},
|
||||
{
|
||||
year: '2025',
|
||||
quarter: 'Q1',
|
||||
window: 'Jan-Mar 2025',
|
||||
totalProducts: 10,
|
||||
submittedProducts: 6,
|
||||
totalCost: 495000,
|
||||
status: 'Rejected',
|
||||
submittedOn: '21/03/2025',
|
||||
},
|
||||
];
|
||||
|
||||
const formatCurrency = (amount) => {
|
||||
if (amount === null || amount === undefined) return '—';
|
||||
return new Intl.NumberFormat('en-AE').format(amount);
|
||||
};
|
||||
|
||||
const Overview = () => {
|
||||
const [searchTerm, setSearchTerm] = useState('');
|
||||
const [selectedYear, setSelectedYear] = useState('');
|
||||
const [selectedQuarter, setSelectedQuarter] = useState('');
|
||||
const [selectedStatus, setSelectedStatus] = useState('');
|
||||
const [selectedSubmission, setSelectedSubmission] = useState(null);
|
||||
const navigate = useNavigate();
|
||||
|
||||
const filteredRows = React.useMemo(() => {
|
||||
const normalizedTerm = searchTerm.trim().toLowerCase();
|
||||
return mockOverview.filter((record) => {
|
||||
const matchesSearch =
|
||||
!normalizedTerm ||
|
||||
record.year.toLowerCase().includes(normalizedTerm) ||
|
||||
record.quarter.toLowerCase().includes(normalizedTerm) ||
|
||||
record.window.toLowerCase().includes(normalizedTerm) ||
|
||||
record.status.toLowerCase().includes(normalizedTerm);
|
||||
const matchesStatus =
|
||||
selectedStatus === '' || record.status.toLowerCase() === selectedStatus;
|
||||
return matchesSearch && matchesStatus;
|
||||
});
|
||||
}, [searchTerm, selectedStatus]);
|
||||
|
||||
const rows = filteredRows.map((record) => [
|
||||
record.year,
|
||||
record.quarter,
|
||||
record.window,
|
||||
record.totalProducts,
|
||||
record.submittedProducts,
|
||||
`AED ${formatCurrency(record.totalCost)}`,
|
||||
record.status,
|
||||
record.submittedOn,
|
||||
'View Details',
|
||||
]);
|
||||
|
||||
const downloadCsv = React.useCallback(() => {
|
||||
if (!filteredRows.length) return;
|
||||
const headers = ['Year', 'Quarter', 'Submission Window', 'Total Products', 'Products Submitted', 'Total Cost (AED)', 'Status', 'Date Submitted'];
|
||||
const csvRows = [headers.join(',')];
|
||||
filteredRows.forEach((record) => {
|
||||
csvRows.push([
|
||||
record.year,
|
||||
record.quarter,
|
||||
record.window,
|
||||
record.totalProducts,
|
||||
record.submittedProducts,
|
||||
record.totalCost,
|
||||
record.status,
|
||||
record.submittedOn,
|
||||
].map((value) => `"${String(value).replace(/"/g, '""')}"`).join(','));
|
||||
});
|
||||
const blob = new Blob([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', 'quarter-overview.csv');
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
URL.revokeObjectURL(url);
|
||||
}, [filteredRows]);
|
||||
|
||||
const toolbar = (
|
||||
<div className="px-6 pt-6 pb-4 flex flex-col gap-4 md:flex-row md:items-center md:justify-between">
|
||||
<div>
|
||||
<h2 className="text-[18px] font-medium text-[#232528]">Quarter Overview</h2>
|
||||
</div>
|
||||
<div className="flex flex-col md:flex-row md:items-center gap-3">
|
||||
<div className="relative w-full md:w-72">
|
||||
<input
|
||||
type="text"
|
||||
value={searchTerm}
|
||||
onChange={(event) => setSearchTerm(event.target.value)}
|
||||
placeholder="Search establishment"
|
||||
className="w-full h-10 rounded-md border border-[#E2E8F0] pl-10 pr-3 text-sm text-[#232528] focus:outline-none"
|
||||
/>
|
||||
<img
|
||||
src={searchIconSrc}
|
||||
alt="Search"
|
||||
className="pointer-events-none absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4"
|
||||
/>
|
||||
</div>
|
||||
<div className="relative w-full md:w-40">
|
||||
<select
|
||||
value={selectedStatus}
|
||||
onChange={(event) => setSelectedStatus(event.target.value)}
|
||||
className="h-10 w-full appearance-none rounded-md border border-[#E2E8F0] bg-white pl-3 pr-9 text-sm text-[#232528] focus:outline-none cursor-pointer"
|
||||
>
|
||||
<option value="">All Status</option>
|
||||
<option value="approved">Approved</option>
|
||||
<option value="submitted">Submitted</option>
|
||||
<option value="rejected">Rejected</option>
|
||||
</select>
|
||||
<img
|
||||
src={caretDownSrc}
|
||||
alt="Open"
|
||||
className="pointer-events-none absolute right-3 top-1/2 -translate-y-1/2 h-4 w-4"
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={downloadCsv}
|
||||
disabled={!filteredRows.length}
|
||||
className={`inline-flex items-center gap-2 h-10 rounded-md border px-4 text-sm font-medium ${filteredRows.length ? 'border-[#92722A] text-[#92722A] hover:bg-[#F2ECCF]' : 'border-gray-200 text-gray-400 cursor-not-allowed'}`}
|
||||
>
|
||||
<img src={downloadIconSrc} alt="Export" className="h-4 w-4" />
|
||||
<span>Export CSV</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
const handleViewDetails = (rowIndex) => {
|
||||
setSelectedSubmission(filteredRows[rowIndex]);
|
||||
};
|
||||
|
||||
if (selectedSubmission) {
|
||||
return (
|
||||
<div className="min-h-screen bg-[#F8FAFC]">
|
||||
<HeaderBar />
|
||||
<main className="max-w-[1280px] mx-auto px-4 py-6">
|
||||
<DetailedOverview
|
||||
submission={selectedSubmission}
|
||||
onBack={() => setSelectedSubmission(null)}
|
||||
/>
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-[#F8FAFC]">
|
||||
<HeaderBar />
|
||||
<main className="max-w-[1280px] mx-auto px-4 py-6 overflow-x-auto">
|
||||
<div className="flex items-center justify-between text-sm text-[#6B7280] mb-4">
|
||||
<h1 className="text-xl font-semibold text-[#232528]">Overview</h1>
|
||||
</div>
|
||||
<section className="bg-white rounded-lg border border-[#E2E8F0] shadow-sm">
|
||||
<Table
|
||||
headers={['Year', 'Quarter', 'Submission Window', 'Total Products', 'Products Submitted', 'Total Cost (AED)', 'Status', 'Date Submitted', 'Action']}
|
||||
rows={rows}
|
||||
beforeHeader={toolbar}
|
||||
separated
|
||||
rowGapClass="border-spacing-y-2"
|
||||
className="w-full min-w-[1200px] [&_td]:whitespace-nowrap [&_th]:whitespace-nowrap"
|
||||
renderCell={(value, rowIndex, columnIndex) => {
|
||||
if (columnIndex === 6) {
|
||||
const normalized = String(value).toLowerCase();
|
||||
const style = statusStyles[normalized] || 'text-[#232528] bg-[#F8FAFC] ';
|
||||
return (
|
||||
<span className={`inline-flex items-center justify-center rounded-md px-3 py-1 text-xs font-medium ${style}`}>
|
||||
{value}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
if (columnIndex === 8) {
|
||||
return (
|
||||
<button
|
||||
className="text-[#92722A] hover:underline text-sm font-medium"
|
||||
onClick={() => handleViewDetails(rowIndex)}
|
||||
>
|
||||
{value}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
return <span className="inline-block">{value}</span>;
|
||||
}}
|
||||
/>
|
||||
</section>
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Overview;
|
||||
240
ipi-survey-platform/src/pages/Overview/Overviewreview.jsx
Normal file
240
ipi-survey-platform/src/pages/Overview/Overviewreview.jsx
Normal file
@ -0,0 +1,240 @@
|
||||
import React, { useState } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import HeaderBar from '@/components/layout/HeaderBar';
|
||||
import Table from '@/components/common/Table';
|
||||
import DetailedOverview from '@/components/overview/DetailedOverview';
|
||||
|
||||
const searchIconSrc = '/assets/images/material-symbols_search-rounded.svg';
|
||||
const caretDownSrc = '/assets/images/CaretDown.svg';
|
||||
const downloadIconSrc = '/assets/images/DownloadSimple.svg';
|
||||
|
||||
const statusStyles = {
|
||||
approved: 'text-[#2F663C] bg-[#F3FAF4] ',
|
||||
submitted: 'text-[#003CFF] bg-[#E7F5FF]',
|
||||
pending: 'text-[#B45309] bg-[#FEF2F2]',
|
||||
rejected: 'text-[#B52520] bg-[#FEF2F2]',
|
||||
};
|
||||
|
||||
const mockOverview = [
|
||||
{
|
||||
year: '2025',
|
||||
quarter: 'Q4',
|
||||
window: 'Oct-Dec 2025',
|
||||
totalProducts: 10,
|
||||
submittedProducts: 4,
|
||||
totalCost: 195000,
|
||||
status: 'Approved',
|
||||
submittedOn: '18/10/2025',
|
||||
},
|
||||
{
|
||||
year: '2025',
|
||||
quarter: 'Q3',
|
||||
window: 'Jul-Sep 2025',
|
||||
totalProducts: 10,
|
||||
submittedProducts: 6,
|
||||
totalCost: 295000,
|
||||
status: 'Submitted',
|
||||
submittedOn: '28/08/2025',
|
||||
},
|
||||
{
|
||||
year: '2025',
|
||||
quarter: 'Q2',
|
||||
window: 'Apr-Jun 2025',
|
||||
totalProducts: 10,
|
||||
submittedProducts: 6,
|
||||
totalCost: 395000,
|
||||
status: 'Rejected',
|
||||
submittedOn: '24/05/2025',
|
||||
},
|
||||
{
|
||||
year: '2025',
|
||||
quarter: 'Q1',
|
||||
window: 'Jan-Mar 2025',
|
||||
totalProducts: 10,
|
||||
submittedProducts: 6,
|
||||
totalCost: 495000,
|
||||
status: 'Rejected',
|
||||
submittedOn: '21/03/2025',
|
||||
},
|
||||
];
|
||||
|
||||
const formatCurrency = (amount) => {
|
||||
if (amount === null || amount === undefined) return '—';
|
||||
return new Intl.NumberFormat('en-AE').format(amount);
|
||||
};
|
||||
|
||||
const Overview = () => {
|
||||
const [searchTerm, setSearchTerm] = useState('');
|
||||
const [selectedYear, setSelectedYear] = useState('');
|
||||
const [selectedQuarter, setSelectedQuarter] = useState('');
|
||||
const [selectedStatus, setSelectedStatus] = useState('');
|
||||
const [selectedSubmission, setSelectedSubmission] = useState(null);
|
||||
const navigate = useNavigate();
|
||||
|
||||
const filteredRows = React.useMemo(() => {
|
||||
const normalizedTerm = searchTerm.trim().toLowerCase();
|
||||
return mockOverview.filter((record) => {
|
||||
const matchesSearch =
|
||||
!normalizedTerm ||
|
||||
record.year.toLowerCase().includes(normalizedTerm) ||
|
||||
record.quarter.toLowerCase().includes(normalizedTerm) ||
|
||||
record.window.toLowerCase().includes(normalizedTerm) ||
|
||||
record.status.toLowerCase().includes(normalizedTerm);
|
||||
const matchesStatus =
|
||||
selectedStatus === '' || record.status.toLowerCase() === selectedStatus;
|
||||
return matchesSearch && matchesStatus;
|
||||
});
|
||||
}, [searchTerm, selectedStatus]);
|
||||
|
||||
const rows = filteredRows.map((record) => [
|
||||
record.year,
|
||||
record.quarter,
|
||||
record.window,
|
||||
record.totalProducts,
|
||||
record.submittedProducts,
|
||||
`AED ${formatCurrency(record.totalCost)}`,
|
||||
record.status,
|
||||
record.submittedOn,
|
||||
'View Details',
|
||||
]);
|
||||
|
||||
const downloadCsv = React.useCallback(() => {
|
||||
if (!filteredRows.length) return;
|
||||
const headers = ['Year', 'Quarter', 'Submission Window', 'Total Products', 'Products Submitted', 'Total Cost (AED)', 'Status', 'Date Submitted'];
|
||||
const csvRows = [headers.join(',')];
|
||||
filteredRows.forEach((record) => {
|
||||
csvRows.push([
|
||||
record.year,
|
||||
record.quarter,
|
||||
record.window,
|
||||
record.totalProducts,
|
||||
record.submittedProducts,
|
||||
record.totalCost,
|
||||
record.status,
|
||||
record.submittedOn,
|
||||
].map((value) => `"${String(value).replace(/"/g, '""')}"`).join(','));
|
||||
});
|
||||
const blob = new Blob([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', 'quarter-overview.csv');
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
URL.revokeObjectURL(url);
|
||||
}, [filteredRows]);
|
||||
|
||||
const toolbar = (
|
||||
<div className="px-6 pt-6 pb-4 flex flex-col gap-4 md:flex-row md:items-center md:justify-between">
|
||||
<div>
|
||||
<h2 className="text-[18px] font-medium text-[#232528]">Quarter Overview</h2>
|
||||
</div>
|
||||
<div className="flex flex-col md:flex-row md:items-center gap-3">
|
||||
<div className="relative w-full md:w-72">
|
||||
<input
|
||||
type="text"
|
||||
value={searchTerm}
|
||||
onChange={(event) => setSearchTerm(event.target.value)}
|
||||
placeholder="Search establishment"
|
||||
className="w-full h-10 rounded-md border border-[#E2E8F0] pl-10 pr-3 text-sm text-[#232528] focus:outline-none"
|
||||
/>
|
||||
<img
|
||||
src={searchIconSrc}
|
||||
alt="Search"
|
||||
className="pointer-events-none absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4"
|
||||
/>
|
||||
</div>
|
||||
<div className="relative w-full md:w-40">
|
||||
<select
|
||||
value={selectedStatus}
|
||||
onChange={(event) => setSelectedStatus(event.target.value)}
|
||||
className="h-10 w-full appearance-none rounded-md border border-[#E2E8F0] bg-white pl-3 pr-9 text-sm text-[#232528] focus:outline-none cursor-pointer"
|
||||
>
|
||||
<option value="">All Status</option>
|
||||
<option value="approved">Approved</option>
|
||||
<option value="submitted">Submitted</option>
|
||||
<option value="rejected">Rejected</option>
|
||||
</select>
|
||||
<img
|
||||
src={caretDownSrc}
|
||||
alt="Open"
|
||||
className="pointer-events-none absolute right-3 top-1/2 -translate-y-1/2 h-4 w-4"
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={downloadCsv}
|
||||
disabled={!filteredRows.length}
|
||||
className={`inline-flex items-center gap-2 h-10 rounded-md border px-4 text-sm font-medium ${filteredRows.length ? 'border-[#92722A] text-[#92722A] hover:bg-[#F2ECCF]' : 'border-gray-200 text-gray-400 cursor-not-allowed'}`}
|
||||
>
|
||||
<img src={downloadIconSrc} alt="Export" className="h-4 w-4" />
|
||||
<span>Export CSV</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
const handleViewDetails = (rowIndex) => {
|
||||
setSelectedSubmission(filteredRows[rowIndex]);
|
||||
};
|
||||
|
||||
if (selectedSubmission) {
|
||||
return (
|
||||
<div className="min-h-screen bg-[#F8FAFC]">
|
||||
<HeaderBar />
|
||||
<main className="max-w-[1280px] mx-auto px-4 py-6">
|
||||
<DetailedOverview
|
||||
submission={selectedSubmission}
|
||||
onBack={() => setSelectedSubmission(null)}
|
||||
/>
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-[#F8FAFC]">
|
||||
<HeaderBar />
|
||||
<main className="max-w-[1280px] mx-auto px-4 py-6 overflow-x-auto">
|
||||
<div className="flex items-center justify-between text-sm text-[#6B7280] mb-4">
|
||||
<h1 className="text-xl font-semibold text-[#232528]">Overview</h1>
|
||||
</div>
|
||||
<section className="bg-white rounded-lg border border-[#E2E8F0] shadow-sm">
|
||||
<Table
|
||||
headers={['Year', 'Quarter', 'Submission Window', 'Total Products', 'Products Submitted', 'Total Cost (AED)', 'Status', 'Date Submitted', 'Action']}
|
||||
rows={rows}
|
||||
beforeHeader={toolbar}
|
||||
separated
|
||||
rowGapClass="border-spacing-y-2"
|
||||
className="w-full min-w-[1200px] [&_td]:whitespace-nowrap [&_th]:whitespace-nowrap"
|
||||
renderCell={(value, rowIndex, columnIndex) => {
|
||||
if (columnIndex === 6) {
|
||||
const normalized = String(value).toLowerCase();
|
||||
const style = statusStyles[normalized] || 'text-[#232528] bg-[#F8FAFC] ';
|
||||
return (
|
||||
<span className={`inline-flex items-center justify-center rounded-md px-3 py-1 text-xs font-medium ${style}`}>
|
||||
{value}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
if (columnIndex === 8) {
|
||||
return (
|
||||
<button
|
||||
className="text-[#92722A] hover:underline text-sm font-medium"
|
||||
onClick={() => handleViewDetails(rowIndex)}
|
||||
>
|
||||
{value}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
return <span className="inline-block">{value}</span>;
|
||||
}}
|
||||
/>
|
||||
</section>
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Overview;
|
||||
@ -7,12 +7,13 @@ import HeaderBar from '@/components/layout/HeaderBar';
|
||||
import ProductData from '@/components/survey/ProductData/ProductData';
|
||||
import ReviewSubmit from '@/components/survey/ReviewSubmit/ReviewSubmit';
|
||||
|
||||
const checkCircleActiveSrc = '/assets/images/circleframe.svg';
|
||||
const checkCircleActiveSrc = '/assets/images/CircleFrame.svg';
|
||||
|
||||
const createEmptyProduct = (id) => ({
|
||||
id,
|
||||
product: '',
|
||||
unit: '',
|
||||
mand: '',
|
||||
capacity: '',
|
||||
previousQuantity: '',
|
||||
previousCost: '',
|
||||
@ -318,26 +319,80 @@ const Survey = () => {
|
||||
products: productsData.map((product) => {
|
||||
const variationId = parseNumber(product.variationReason, null);
|
||||
const zeroTargetId = parseNumber(product.zeroTargetReason, null);
|
||||
|
||||
// Map monthly data to period-specific fields
|
||||
return {
|
||||
product_id: parseNumber(product.product),
|
||||
product_id: parseNumber(product.productId) || parseNumber(product.product),
|
||||
unit_id: parseNumber(product.unit),
|
||||
annual_installed_capacity: stringify(product.capacity),
|
||||
previous_quantity: stringify(product.previousQuantity),
|
||||
previous_cost: stringify(product.previousCost),
|
||||
current_quantity: stringify(product.currentQuantity),
|
||||
current_cost: stringify(product.currentCost),
|
||||
forecast_quantity: stringify(product.forecastQuantity),
|
||||
forecast_cost: stringify(product.forecastCost),
|
||||
|
||||
// Previous Quarter (Q4) - October, November, December
|
||||
previous_quantity_period_one: stringify(product.octQuantity || '0'),
|
||||
previous_quantity_period_two: stringify(product.novQuantity || '0'),
|
||||
previous_quantity_period_three: stringify(product.decQuantity || '0'),
|
||||
previous_cost_period_one: stringify(product.octCost || '0'),
|
||||
previous_cost_period_two: stringify(product.novCost || '0'),
|
||||
previous_cost_period_three: stringify(product.decCost || '0'),
|
||||
|
||||
// Current Quarter (Q1) - January, February, March
|
||||
current_quantity_period_one: stringify(product.janQuantity || '0'),
|
||||
current_quantity_period_two: stringify(product.febQuantity || '0'),
|
||||
current_quantity_period_three: stringify(product.marQuantity || '0'),
|
||||
current_cost_period_one: stringify(product.janCost || '0'),
|
||||
current_cost_period_two: stringify(product.febCost || '0'),
|
||||
current_cost_period_three: stringify(product.marCost || '0'),
|
||||
|
||||
// Forecast Quarter (Q2) - April, May, June
|
||||
forecast_quantity_period_one: stringify(product.aprQuantity || '0'),
|
||||
forecast_quantity_period_two: stringify(product.mayQuantity || '0'),
|
||||
forecast_quantity_period_three: stringify(product.junQuantity || '0'),
|
||||
forecast_cost_period_one: stringify(product.aprCost || '0'),
|
||||
forecast_cost_period_two: stringify(product.mayCost || '0'),
|
||||
forecast_cost_period_three: stringify(product.junCost || '0'),
|
||||
|
||||
// Totals (kept for backward compatibility)
|
||||
previous_quantity: stringify(
|
||||
(parseNumber(product.octQuantity) || 0) +
|
||||
(parseNumber(product.novQuantity) || 0) +
|
||||
(parseNumber(product.decQuantity) || 0)
|
||||
),
|
||||
previous_cost: stringify(
|
||||
(parseNumber(product.octCost) || 0) +
|
||||
(parseNumber(product.novCost) || 0) +
|
||||
(parseNumber(product.decCost) || 0)
|
||||
),
|
||||
current_quantity: stringify(
|
||||
(parseNumber(product.janQuantity) || 0) +
|
||||
(parseNumber(product.febQuantity) || 0) +
|
||||
(parseNumber(product.marQuantity) || 0)
|
||||
),
|
||||
current_cost: stringify(
|
||||
(parseNumber(product.janCost) || 0) +
|
||||
(parseNumber(product.febCost) || 0) +
|
||||
(parseNumber(product.marCost) || 0)
|
||||
),
|
||||
forecast_quantity: stringify(
|
||||
(parseNumber(product.aprQuantity) || 0) +
|
||||
(parseNumber(product.mayQuantity) || 0) +
|
||||
(parseNumber(product.junQuantity) || 0)
|
||||
),
|
||||
forecast_cost: stringify(
|
||||
(parseNumber(product.aprCost) || 0) +
|
||||
(parseNumber(product.mayCost) || 0) +
|
||||
(parseNumber(product.junCost) || 0)
|
||||
),
|
||||
|
||||
// Other fields
|
||||
variation_reason_master_id: Number.isFinite(variationId) ? variationId : null,
|
||||
other_variation_reason: stringify(product.otherVariationReason),
|
||||
other_variation_reason: stringify(product.otherVariationReason || ''),
|
||||
zero_target_reason_master_id: Number.isFinite(zeroTargetId) ? zeroTargetId : null,
|
||||
other_zero_target_reason: stringify(product.otherZeroTargetReason),
|
||||
remarks: stringify(product.remarks),
|
||||
other_zero_target_reason: stringify(product.otherZeroTargetReason || ''),
|
||||
remarks: stringify(product.remarks || '')
|
||||
};
|
||||
}),
|
||||
};
|
||||
}, [establishmentData, parseNumber, productsData, stringify]);
|
||||
|
||||
console.log("productsData",productsData)
|
||||
const handleSubmit = React.useCallback(async () => {
|
||||
setIsSubmitting(true);
|
||||
try {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user