fixed security issues bugs and graph added for products

This commit is contained in:
Malini 2025-12-04 23:39:35 +05:30
parent 2bda726d40
commit 30057d56c0
31 changed files with 1558 additions and 515 deletions

File diff suppressed because it is too large Load Diff

View File

@ -11,13 +11,16 @@
}, },
"dependencies": { "dependencies": {
"axios": "^1.12.2", "axios": "^1.12.2",
"chart.js": "^4.5.1",
"lucide-react": "^0.548.0", "lucide-react": "^0.548.0",
"react": "^19.1.1", "react": "^19.1.1",
"react-chartjs-2": "^5.3.1",
"react-dom": "^19.1.1", "react-dom": "^19.1.1",
"react-google-recaptcha": "^3.1.0", "react-google-recaptcha": "^3.1.0",
"react-icons": "^5.5.0", "react-icons": "^5.5.0",
"react-router-dom": "^7.9.4", "react-router-dom": "^7.9.4",
"react-toastify": "^11.0.5" "react-toastify": "^11.0.5",
"recharts": "^3.5.0"
}, },
"devDependencies": { "devDependencies": {
"@eslint/js": "^9.36.0", "@eslint/js": "^9.36.0",

View File

@ -17,30 +17,27 @@ import EditCompanyProfile from '@/pages/Admin/configuration/EditCompanyProfile';
import Navbar from './pages/LandingPage/component/Navbar'; import Navbar from './pages/LandingPage/component/Navbar';
const RequireRole = ({ allowedRoles = [], children }) => { const RequireRole = ({ allowedRoles = [], children }) => {
let role; let role;
let token;
try { try {
role = sessionStorage.getItem('user_role'); role = localStorage.getItem("user_role");
token = sessionStorage.getItem('auth_token'); } catch {
} catch (error) {
role = null; role = null;
token = null;
} }
const normalizedRole = String(role || '').toLowerCase(); const normalizedRole = String(role || "").toLowerCase();
const isAllowed =
Boolean(token) && const isAllowed = allowedRoles.some(
allowedRoles.some( (allowed) => String(allowed || "").toLowerCase() === normalizedRole
(allowed) => String(allowed || '').toLowerCase() === normalizedRole
); );
if (isAllowed) { if (isAllowed) {
return children; return children;
} }
// return <Navigate to="/login" replace />; return <Navigate to="/index" replace />;
return <Navigate to="/index" replace />
}; };
const SessionWarningModal = ({ show, remainingTime, onExtend, onLogout }) => { const SessionWarningModal = ({ show, remainingTime, onExtend, onLogout }) => {
if (!show) return null; if (!show) return null;

View File

@ -72,7 +72,7 @@ const AdminHeader = () => {
React.useEffect(() => { React.useEffect(() => {
try { try {
const stored = sessionStorage.getItem('user_profile'); const stored = localStorage.getItem('user_profile');
if (stored) { if (stored) {
const parsed = JSON.parse(stored); const parsed = JSON.parse(stored);
if (parsed?.name || parsed?.username) { if (parsed?.name || parsed?.username) {

View File

@ -33,7 +33,8 @@ const SubmissionTable = ({ selectedQuarter, selectedYear }) => {
params.append('year', year); params.append('year', year);
} }
const response = await apiClient.get(`/admin_dashboard?${params.toString()}`); // const response = await apiClient.get(`/admin_dashboard?${params.toString()}`);
const response = await apiClient.get(`/admin_dashboard?${params.toString()}`, { withCredentials: true });
const result = response?.data; const result = response?.data;
if (result?.status === 'success') { if (result?.status === 'success') {
// Map the data to include the updated_at as reviewerOn // Map the data to include the updated_at as reviewerOn
@ -139,7 +140,7 @@ const SubmissionTable = ({ selectedQuarter, selectedYear }) => {
} }
}; };
const userProfile = JSON.parse(sessionStorage.getItem("user_profile")); const userProfile = JSON.parse(localStorage.getItem("user_profile"));
const handleActionClick = (submission, type) => { const handleActionClick = (submission, type) => {
setSelectedSubmission(submission); setSelectedSubmission(submission);
@ -181,7 +182,7 @@ const handleApproveConfirm = async () => {
if (ok) { if (ok) {
toast.success('Submission approved successfully'); toast.success('Submission approved successfully');
const currentUser = JSON.parse(sessionStorage.getItem("user_profile"))?.name || 'Admin'; const currentUser = JSON.parse(localStorage.getItem("user_profile"))?.name || 'Admin';
const updatedAt = response?.data?.updated_at || new Date().toISOString(); const updatedAt = response?.data?.updated_at || new Date().toISOString();
// Update the status of the current item in the data array // Update the status of the current item in the data array
@ -235,7 +236,7 @@ const handleRejectConfirm = async () => {
if (ok) { if (ok) {
toast.error('Submission has been rejected'); toast.error('Submission has been rejected');
const currentUser = JSON.parse(sessionStorage.getItem("user_profile"))?.name || 'Admin'; const currentUser = JSON.parse(localStorage.getItem("user_profile"))?.name || 'Admin';
const updatedAt = response?.data?.updated_at || new Date().toISOString(); const updatedAt = response?.data?.updated_at || new Date().toISOString();
// Update the status of the current item in the data array // Update the status of the current item in the data array
@ -309,7 +310,7 @@ const tableRows = filtered.map((item) => [
renderStatusBadge(item?.status), renderStatusBadge(item?.status),
// Reviewer - show reviewer_name after approval/rejection, otherwise show '-' // Reviewer - show reviewer_name after approval/rejection, otherwise show '-'
item?.status?.toLowerCase() === 'approved' || item?.status?.toLowerCase() === 'rejected' item?.status?.toLowerCase() === 'approved' || item?.status?.toLowerCase() === 'rejected'
? (item?.reviewer_name || JSON.parse(sessionStorage.getItem("user_profile"))?.name || 'Admin') ? (item?.reviewer_name || JSON.parse(localStorage.getItem("user_profile"))?.name || 'Admin')
: '-', : '-',
// Reviewed On - show updated_at after approval/rejection, otherwise show '-' // Reviewed On - show updated_at after approval/rejection, otherwise show '-'
item?.status?.toLowerCase() === 'approved' || item?.status?.toLowerCase() === 'rejected' item?.status?.toLowerCase() === 'approved' || item?.status?.toLowerCase() === 'rejected'

View File

@ -36,7 +36,7 @@ export const WelcomeSection = ({ data = null, loading = false, error = '' }) =>
Welcome{' '} Welcome{' '}
{(() => { {(() => {
try { try {
const userProfile = JSON.parse(sessionStorage.getItem('user_profile')); const userProfile = JSON.parse(localStorage.getItem('user_profile'));
return userProfile?.name ? userProfile.name : ''; return userProfile?.name ? userProfile.name : '';
} catch (error) { } catch (error) {
console.error('Error reading user_profile from sessionStorage:', error); console.error('Error reading user_profile from sessionStorage:', error);

View File

@ -18,7 +18,7 @@ const HeaderBar = () => {
const [userOpen, setUserOpen] = React.useState(false); const [userOpen, setUserOpen] = React.useState(false);
const profileRef = React.useRef(null); const profileRef = React.useRef(null);
const navigate = useNavigate(); const navigate = useNavigate();
const establishmentId = sessionStorage.getItem("establishment_id") || ""; const establishmentId = localStorage.getItem("establishment_id") || "";
// Close dropdown when clicking outside // Close dropdown when clicking outside
React.useEffect(() => { React.useEffect(() => {
@ -37,7 +37,7 @@ const HeaderBar = () => {
React.useEffect(() => { React.useEffect(() => {
let parsed; let parsed;
try { try {
const stored = sessionStorage.getItem('user_profile'); const stored = localStorage.getItem('user_profile');
if (stored) { if (stored) {
parsed = JSON.parse(stored); parsed = JSON.parse(stored);
} }

View File

@ -1,4 +1,10 @@
import React, { useEffect, useState, useMemo } from 'react'; import React, { useEffect, useState, useMemo } from 'react';
// Get and encode establishment ID for URL
const getSafeEstablishmentId = () => {
const establishmentId = localStorage.getItem('establishment_id') || '';
return encodeURIComponent(establishmentId);
};
import { useNavigate, useLocation } from 'react-router-dom'; import { useNavigate, useLocation } from 'react-router-dom';
import { getEstablishmentProducts } from '../../../services/submissions/submissionService'; import { getEstablishmentProducts } from '../../../services/submissions/submissionService';
import Table from '@/components/common/Table'; import Table from '@/components/common/Table';
@ -109,7 +115,7 @@ const EstablishmentInfo = ({
// Fetch establishment products // Fetch establishment products
React.useEffect(() => { React.useEffect(() => {
const fetchProducts = async () => { const fetchProducts = async () => {
const establishmentId = sessionStorage.getItem('establishment_id'); const establishmentId = localStorage.getItem('establishment_id');
if (!establishmentId) return; if (!establishmentId) return;
setLoadingProducts(true); setLoadingProducts(true);
@ -280,14 +286,14 @@ const EstablishmentInfo = ({
const handleEditProfile = (e) => { const handleEditProfile = (e) => {
e.preventDefault(); e.preventDefault();
const establishmentId = sessionStorage.getItem('establishment_id'); const establishmentId = localStorage.getItem('establishment_id');
if (!establishmentId) { if (!establishmentId) {
alert('No establishment ID found in session'); alert('No establishment ID found in session');
return; return;
} }
localStorage.setItem('returnTo', window.location.pathname); localStorage.setItem('returnTo', window.location.pathname);
sessionStorage.setItem('edit_profile_from', 'EstablishmentUser'); localStorage.setItem('edit_profile_from', 'EstablishmentUser');
navigate(`/edit-profile/${establishmentId}`); navigate(`/edit-profile/${establishmentId}`);
}; };
@ -687,7 +693,7 @@ const EstablishmentInfo = ({
<p className="text-sm text-[#043DFF] bg-[#E7F5FF]"> <p className="text-sm text-[#043DFF] bg-[#E7F5FF]">
<span className="font-medium">Need to update details?</span> Go to Profile {' '} <span className="font-medium">Need to update details?</span> Go to Profile {' '}
<a <a
href={`/admin/configuration/edit-profile/${sessionStorage.getItem('establishment_id') || ''}`} href={`/admin/configuration/edit-profile/${getSafeEstablishmentId()}`}
className="underline font-medium text-[#043DFF] hover:text-[#063B82]" className="underline font-medium text-[#043DFF] hover:text-[#063B82]"
onClick={handleEditProfile} onClick={handleEditProfile}
> >

View File

@ -503,7 +503,7 @@ const ProductData = ({
useEffect(() => { useEffect(() => {
const fetchEstablishmentProducts = async () => { const fetchEstablishmentProducts = async () => {
try { try {
const establishmentId = sessionStorage.getItem('establishment_id'); const establishmentId = localStorage.getItem('establishment_id');
if (!establishmentId) return; if (!establishmentId) return;
const fetchProducts = async () => { const fetchProducts = async () => {
@ -902,7 +902,7 @@ const location = useLocation();
// Fetch previous forecast data when product is selected // Fetch previous forecast data when product is selected
if (selectedProduct?.value) { if (selectedProduct?.value) {
const establishmentId = sessionStorage.getItem('establishment_id') || const establishmentId = localStorage.getItem('establishment_id') ||
localStorage.getItem('establishmentId') || localStorage.getItem('establishmentId') ||
localStorage.getItem('establishment_id'); localStorage.getItem('establishment_id');
@ -915,7 +915,7 @@ const location = useLocation();
establishmentId, establishmentId,
quarter, quarter,
year, year,
storageSource: sessionStorage.getItem('establishment_id') ? 'sessionStorage' : 'localStorage' storageSource: localStorage.getItem('establishment_id') ? 'sessionStorage' : 'localStorage'
}); });
// Call handleProductSelect with the product ID // Call handleProductSelect with the product ID
@ -1354,7 +1354,7 @@ const location = useLocation();
if (selectedProduct?.value) { if (selectedProduct?.value) {
try { try {
// Get establishment ID from sessionStorage or localStorage // Get establishment ID from sessionStorage or localStorage
const establishmentId = sessionStorage.getItem('establishment_id') || const establishmentId = localStorage.getItem('establishment_id') ||
localStorage.getItem('establishmentId') || localStorage.getItem('establishmentId') ||
localStorage.getItem('establishment_id'); localStorage.getItem('establishment_id');

View File

@ -0,0 +1,8 @@
export const ASSETS = {
IMAGES: {
LOGO: '/assets/images/FCSCLogo.svg'
},
CLASSES: {
AUTH_CONTAINER: 'min-h-screen bg-[#F7F7F7] flex items-center justify-center px-4'
}
};

View File

@ -0,0 +1,16 @@
export const VALIDATION_MESSAGES = {
OLD_PASSWORD_REQUIRED: 'Old password is required',
PASSWORD_REQUIRED: 'Password is required',
PASSWORD_MIN_LENGTH: 'Password must be at least 8 characters',
PASSWORD_UPPERCASE: 'Must contain at least one uppercase letter',
PASSWORD_LOWERCASE: 'Must contain at least one lowercase letter',
PASSWORD_NUMBER: 'Must contain at least one number',
PASSWORD_SPECIAL_CHAR: 'Must contain at least one special character (!@#$%^&*)',
CONFIRM_PASSWORD_REQUIRED: 'Please confirm your password',
PASSWORDS_DONT_MATCH: 'Passwords do not match',
INCORRECT_CURRENT_PASSWORD: 'The current password is incorrect',
PASSWORD_SAME_AS_CURRENT: 'New password cannot be the same as current password',
PASSWORD_TOO_WEAK: 'Password is too weak. Use a stronger password.',
PASSWORD_COMPLEXITY_ERROR: 'Password must meet complexity requirements.'
};

View File

@ -55,7 +55,7 @@ const AdminDashboard = () => {
params.append('quarter', quarter || 'All'); params.append('quarter', quarter || 'All');
params.append('year', year || 'All'); params.append('year', year || 'All');
const response = await apiClient.get(`/admin_dashboard?${params.toString()}`); const response = await apiClient.get(`/admin_dashboard?${params.toString()}`, { withCredentials: true });
if (!isMounted.current) return; if (!isMounted.current) return;

View File

@ -2,7 +2,656 @@ import React from 'react';
import { Link, useNavigate, useParams, useLocation } from 'react-router-dom'; import { Link, useNavigate, useParams, useLocation } from 'react-router-dom';
import AdminHeader from '@/components/admin/AdminHeader'; import AdminHeader from '@/components/admin/AdminHeader';
import { getSubmissionById } from '@/services/admin/submission'; import { getSubmissionById } from '@/services/admin/submission';
import { getProductSubmissionHistory } from '@/services/submissions/submissionService';
import apiClient from '@/services/api/apiClient'; import apiClient from '@/services/api/apiClient';
import { Chart as ChartJS, CategoryScale, LinearScale, BarElement, LineElement, PointElement, Title, Tooltip, Legend } from 'chart.js';
import { Line, Bar } from 'react-chartjs-2';
// Register ChartJS components
ChartJS.register(CategoryScale, LinearScale, BarElement, LineElement, PointElement, Title, Tooltip, Legend);
// Static Quarterly Data Component
const StaticQuarterlyChart = () => {
const staticChartData = {
labels: ['Q1 2023', 'Q2 2023', 'Q3 2023', 'Q4 2023', 'Q1 2024', 'Q2 2024', 'Q3 2024', 'Q4 2024', 'Q1 2025', 'Q2 2025', 'Q3 2025', 'Q4 2025'],
datasets: [
{
label: 'Details',
data: [1200, 1300, 1250, 1400, 1350, 1450, 1500, 1550, 1600, 1650, 1700, 1750],
backgroundColor: 'rgba(75, 192, 192, 0.6)',
borderColor: 'rgba(75, 192, 192, 1)',
borderWidth: 1
},
{
label: 'Capacity',
data: [1500, 1500, 1500, 1500, 1600, 1600, 1600, 1600, 1700, 1700, 1700, 1700],
backgroundColor: 'rgba(54, 162, 235, 0.6)',
borderColor: 'rgba(54, 162, 235, 1)',
borderWidth: 1
},
{
label: 'Difference',
data: [300, 200, 250, 100, 250, 150, 100, 50, 100, 50, 0, -50],
backgroundColor: 'rgba(255, 99, 132, 0.6)',
borderColor: 'rgba(255, 99, 132, 1)',
borderWidth: 1
}
]
};
const options = {
responsive: true,
maintainAspectRatio: false,
scales: {
y: {
beginAtZero: true,
title: {
display: true,
text: 'Quantity',
font: {
weight: 'bold'
}
}
}
},
plugins: {
legend: {
position: 'top',
},
tooltip: {
callbacks: {
label: function(context) {
return `${context.dataset.label}: ${context.parsed.y.toLocaleString()}`;
}
}
}
}
};
return (
<div className="bg-white rounded-lg p-6 h-96 mb-6 shadow">
<h2 className="text-lg font-semibold text-gray-900 mb-4">Quarterly Overview (2023-2025)</h2>
<div className="h-full w-full">
<Bar
options={options}
data={staticChartData}
style={{ height: '100%' }}
/>
</div>
</div>
);
};
// Wrapper component to render multiple charts for products
const CombinedProductsChart = ({ products, establishmentId }) => {
if (!products?.length) return null;
return (
<div className="space-y-8">
{products.map((product, index) => (
<div key={product.product_id || index} className="bg-white rounded-lg p-6 shadow">
<h3 className="text-lg font-semibold text-gray-900 mb-4">
{product.product_name || `Product ${index + 1}`} Metrics
</h3>
<ProductChart
product={product}
establishmentId={establishmentId}
/>
</div>
))}
</div>
);
};
const ProductChart = ({ product, establishmentId }) => {
const [productHistory, setProductHistory] = React.useState(null);
const [loading, setLoading] = React.useState(true);
const [error, setError] = React.useState(null);
React.useEffect(() => {
const fetchProductHistory = async () => {
if (!establishmentId || !product?.product_id) return;
try {
setLoading(true);
const result = await getProductSubmissionHistory(establishmentId, product.product_id);
setProductHistory(result);
} catch (err) {
console.error(`Error fetching history for product ${product.product_id}:`, err);
setError(`Failed to load history for product ${product.product_name || product.product_id}`);
} finally {
setLoading(false);
}
};
fetchProductHistory();
}, [establishmentId, product]);
// Process the data for the chart
const processChartData = () => {
if (loading || error || !productHistory?.data?.length) {
return {
labels: [],
datasets: []
};
}
// Create an array to hold all quarter entries
const allQuarters = [];
// Extract all quarters from the data
productHistory.data.forEach(item => {
if (!item.quarter_periods) return;
// Add previous quarter
allQuarters.push({
type: 'previous',
quarter: item.quarter_periods.previous_quarter,
year: item.quarter_periods.previous_year,
quantity: item.previous_quantity,
cost: item.previous_cost,
capacity: item.annual_installed_capacity
});
// Add current quarter
allQuarters.push({
type: 'current',
quarter: item.quarter_periods.current_quarter,
year: item.quarter_periods.current_year,
quantity: item.current_quantity,
cost: item.current_cost,
capacity: item.annual_installed_capacity
});
// Add forecast quarter
allQuarters.push({
type: 'forecast',
quarter: item.quarter_periods.forecast_quarter,
year: item.quarter_periods.forecast_year,
quantity: item.forecast_quantity,
cost: item.forecast_cost,
capacity: item.annual_installed_capacity
});
});
// Sort quarters chronologically
allQuarters.sort((a, b) => {
const yearDiff = parseInt(a.year) - parseInt(b.year);
if (yearDiff !== 0) return yearDiff;
const qOrder = { 'Q1': 1, 'Q2': 2, 'Q3': 3, 'Q4': 4 };
const typeOrder = { 'previous': 1, 'current': 2, 'forecast': 3 };
const qA = qOrder[a.quarter] || 0;
const qB = qOrder[b.quarter] || 0;
if (qA !== qB) return qA - qB;
return typeOrder[a.type] - typeOrder[b.type];
});
// Prepare labels and data points
const labels = [];
const quantityData = [];
const capacityData = [];
const costData = [];
const backgroundColors = [];
allQuarters.forEach(q => {
const typeLabel = q.type.charAt(0).toUpperCase() + q.type.slice(1);
labels.push(`${typeLabel} (${q.quarter} ${q.year})`);
quantityData.push(parseInt(q.quantity) || 0);
capacityData.push(parseInt(q.capacity) || 0);
costData.push(parseFloat(q.cost) || 0);
// Set different background colors for different types
if (q.type === 'previous') {
backgroundColors.push('rgba(199, 177, 133, 0.8)');
} else if (q.type === 'current') {
backgroundColors.push('rgba(106, 84, 48, 0.8)');
} else {
backgroundColors.push('rgba(245, 238, 207, 0.8)');
}
});
// Prepare datasets for the chart
const datasets = [
// Quantity line
{
type: 'line',
label: 'Quantity',
data: quantityData,
borderColor: '#3B82F6',
backgroundColor: 'transparent',
borderWidth: 2,
yAxisID: 'y1',
tension: 0.3,
pointRadius: 3,
pointHoverRadius: 5,
},
// Capacity line (dashed)
{
type: 'line',
label: 'Annual Capacity',
data: capacityData,
borderColor: '#10B981',
backgroundColor: 'transparent',
borderWidth: 1.5,
borderDash: [5, 5],
pointRadius: 0,
borderCapStyle: 'round',
yAxisID: 'y1',
},
// Cost bars
{
type: 'bar',
label: 'Cost (AED)',
data: costData,
backgroundColor: backgroundColors,
borderColor: backgroundColors.map(c => c.replace('0.8', '1')),
borderWidth: 1,
yAxisID: 'y',
barPercentage: 0.6,
categoryPercentage: 0.8,
}
];
return {
labels: labels,
datasets: datasets
};
};
const chartData = processChartData();
const options = {
responsive: true,
maintainAspectRatio: false,
interaction: {
mode: 'index',
intersect: false,
},
scales: {
y: {
type: 'linear',
display: true,
position: 'left',
title: {
display: true,
text: 'Total Cost (AED)',
font: {
weight: 'bold'
}
},
grid: {
drawOnChartArea: false
},
ticks: {
beginAtZero: true,
callback: function(value) {
// Format large numbers with commas
return value.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ",");
}
}
},
y1: {
type: 'linear',
display: true,
position: 'right',
title: {
display: true,
text: 'Quantity',
font: {
weight: 'bold'
}
},
grid: {
drawOnChartArea: false
},
ticks: {
beginAtZero: true
}
},
x: {
grid: {
display: false
}
}
},
plugins: {
legend: {
position: 'top',
align: 'center',
labels: {
usePointStyle: true,
boxWidth: 12,
boxHeight: 12,
padding: 12,
font: {
size: 12,
lineHeight: '16px'
},
generateLabels: function(chart) {
const data = chart.data;
if (data.labels.length && data.datasets.length) {
return chart.data.datasets.map((dataset, i) => {
const meta = chart.getDatasetMeta(i);
let label = dataset.label || '';
label = ' ' + label.trim();
// For Annual Capacity (dashed line)
if (label.includes('Annual Capacity')) {
return {
text: label,
fillStyle: 'transparent',
hidden: !meta.visible,
lineDash: [3, 3],
lineWidth: 2,
strokeStyle: dataset.borderColor,
pointStyle: 'line',
rotation: 0,
datasetIndex: i
};
}
// For Quantity (solid line)
if (label.includes('Quantity')) {
return {
text: label,
fillStyle: 'transparent',
hidden: !meta.visible,
lineDash: [],
lineWidth: 2,
strokeStyle: dataset.borderColor,
pointStyle: 'line',
rotation: 0,
datasetIndex: i
};
}
// For bar items (Previous, Current, Forecast) - square boxes
return {
text: label,
fillStyle: dataset.backgroundColor,
hidden: !meta.visible,
lineWidth: 0,
strokeStyle: 'transparent',
pointStyle: 'rect',
rotation: 0,
datasetIndex: i
};
});
}
return [];
}
}
},
tooltip: {
callbacks: {
label: function(context) {
let label = context.dataset.label || '';
if (label) {
label += ': ';
}
if (context.parsed.y !== null) {
label += context.parsed.y.toLocaleString();
// Add unit to the tooltip
if (context.dataset.yAxisID === 'y') {
label += ' (Cost)';
} else {
label += ' (Qty)';
}
}
return label;
}
}
}
}
};
if (loading) {
return (
<div className="bg-white rounded-lg p-6 h-96 mb-6 shadow flex items-center justify-center">
<div className="flex items-center">
<svg className="animate-spin h-6 w-6 mr-3 text-[#92722A]" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8v8z"></path>
</svg>
<span>Loading product history...</span>
</div>
</div>
);
}
if (error) {
return (
<div className="bg-white rounded-lg p-6 h-96 mb-6 shadow flex items-center justify-center">
<div className="text-red-500">{error}</div>
</div>
);
}
if (!chartData.labels.length) {
return (
<div className="bg-white rounded-lg p-6 h-96 mb-6 shadow flex items-center justify-center">
<div>No historical data available for this product</div>
</div>
);
}
// Color legend component
const ColorLegend = () => (
<div className="flex flex-wrap gap-6 mb-4 p-3 bg-gray-50 rounded-md">
<div className="flex items-center">
<div className="w-4 h-4 bg-[#C7B185] mr-2 rounded-sm"></div>
<span className="text-sm font-medium text-gray-700">Previous Quarter</span>
</div>
<div className="flex items-center">
<div className="w-4 h-4 bg-[#6A5430] mr-2 rounded-sm"></div>
<span className="text-sm font-medium text-gray-700">Current Quarter</span>
</div>
<div className="flex items-center">
<div className="w-4 h-4 bg-[#F5EECF] border border-gray-300 mr-2 rounded-sm"></div>
<span className="text-sm font-medium text-gray-700">Forecast Quarter</span>
</div>
<div className="flex items-center">
<div className="w-4 h-0 border-t-2 border-[#3B82F6] mr-2"></div>
<span className="text-sm font-medium text-gray-700">Quantity</span>
</div>
<div className="flex items-center">
<div className="w-4 h-0 border-t-2 border-[#10B981] border-dashed mr-2"></div>
<span className="text-sm font-medium text-gray-700">Annual Capacity</span>
</div>
</div>
);
return (
<div className="bg-white rounded-lg p-6 mb-6 shadow">
<div className="flex justify-between items-center mb-4">
<h2 className="text-lg font-semibold text-gray-900">Products Overview</h2>
</div>
<ColorLegend />
<div className="overflow-x-auto">
<div style={{ minWidth: `${Math.max(600, chartData.labels.length * 100)}px` }}>
<div className="h-96 w-full">
<Line
options={options}
data={chartData}
style={{ height: '100%' }}
/>
</div>
</div>
</div>
</div>
);
};
const BarChart = ({ product, quarterPeriods }) => {
if (!product || !quarterPeriods) return null;
// Get month names for x-axis labels
const getMonthName = (monthIndex) => {
const date = new Date(2000, monthIndex - 1, 1);
return date.toLocaleString('default', { month: 'short' });
};
// Generate x-axis labels based on the quarters
const getMonthLabels = () => {
const months = [];
if (quarterPeriods?.previous_month) {
months.push(
getMonthName(quarterPeriods.previous_month.previous_period_one),
getMonthName(quarterPeriods.previous_month.previous_period_two),
getMonthName(quarterPeriods.previous_month.previous_period_three)
);
}
if (quarterPeriods?.current_month) {
months.push(
getMonthName(quarterPeriods.current_month.current_period_one),
getMonthName(quarterPeriods.current_month.current_period_two),
getMonthName(quarterPeriods.current_month.current_period_three)
);
}
if (quarterPeriods?.forecast_month) {
months.push(
getMonthName(quarterPeriods.forecast_month.forecast_period_one),
getMonthName(quarterPeriods.forecast_month.forecast_period_two),
getMonthName(quarterPeriods.forecast_month.forecast_period_three)
);
}
return months;
};
// Prepare data for the chart
const chartData = {
labels: getMonthLabels(),
datasets: [
// Previous Quarter
{
label: `Previous (${quarterPeriods.previous_quarter} ${quarterPeriods.previous_year})`,
data: [
product.previous_quantity_period_one,
product.previous_quantity_period_two,
product.previous_quantity_period_three,
null, null, null, // Empty for current quarter
null, null, null // Empty for forecast quarter
],
borderColor: '#92722A', // Gray color for previous
backgroundColor: 'transparent',
borderWidth: 2,
tension: 0.3,
pointRadius: 3,
pointHoverRadius: 5
},
// Current Quarter
{
label: `Current (${quarterPeriods.current_quarter} ${quarterPeriods.current_year})`,
data: [
null, null, null, // Empty for previous quarter
product.current_quantity_period_one,
product.current_quantity_period_two,
product.current_quantity_period_three,
null, null, null // Empty for forecast quarter
],
borderColor: '#10B981', // Green color for current
backgroundColor: 'transparent',
borderWidth: 2,
tension: 0.3,
pointRadius: 3,
pointHoverRadius: 5
},
// Forecast Quarter
{
label: `Forecast (${quarterPeriods.forecast_quarter} ${quarterPeriods.forecast_year})`,
data: [
null, null, null, // Empty for previous quarter
null, null, null, // Empty for current quarter
product.forecast_quantity_period_one,
product.forecast_quantity_period_two,
product.forecast_quantity_period_three
],
borderColor: '#F59E0B', // Yellow color for forecast
backgroundColor: 'transparent',
borderWidth: 2,
tension: 0.3,
pointRadius: 3,
pointHoverRadius: 5
},
// Annual Capacity Line
{
label: 'Annual Capacity',
data: new Array(9).fill(product.annual_installed_capacity),
borderColor: '#3B82F6', // Blue color for capacity line
backgroundColor: 'transparent',
borderWidth: 1,
borderDash: [5, 5],
pointRadius: 0,
borderCapStyle: 'round'
}
]
};
const options = {
responsive: true,
maintainAspectRatio: false,
interaction: {
mode: 'index',
intersect: false,
},
scales: {
y: {
beginAtZero: true,
title: {
display: true,
text: 'Quantity'
},
grid: {
drawBorder: false
}
},
x: {
grid: {
display: false
}
}
},
plugins: {
legend: {
position: 'top',
align: 'end',
labels: {
usePointStyle: true,
boxWidth: 10,
padding: 20
}
},
tooltip: {
callbacks: {
label: function(context) {
let label = context.dataset.label || '';
if (label) {
label += ': ';
}
if (context.parsed.y !== null) {
label += context.parsed.y;
}
return label;
}
}
}
}
};
return (
<div className="bg-white rounded-lg p-4 h-96">
<Line
options={options}
data={chartData}
style={{ height: '100%' }}
/>
</div>
);
};
const caretUpSrc = '/assets/images/caret-up.svg'; const caretUpSrc = '/assets/images/caret-up.svg';
const establishmentIconSrc = '/assets/images/Establishment.svg'; const establishmentIconSrc = '/assets/images/Establishment.svg';
const establishmentIdIconSrc = '/assets/images/Establishmentid.svg'; const establishmentIdIconSrc = '/assets/images/Establishmentid.svg';
@ -36,6 +685,8 @@ const ValidationReview = () => {
const [submissionData, setSubmissionData] = React.useState(null); const [submissionData, setSubmissionData] = React.useState(null);
const [quarterPeriods, setQuarterPeriods] = React.useState(null); const [quarterPeriods, setQuarterPeriods] = React.useState(null);
const [isApproveOpen, setIsApproveOpen] = React.useState(false); const [isApproveOpen, setIsApproveOpen] = React.useState(false);
const [establishmentId, setEstablishmentId] = React.useState(null);
const [products, setProducts] = React.useState([]);
React.useEffect(() => { React.useEffect(() => {
const fetchSubmission = async () => { const fetchSubmission = async () => {
@ -71,6 +722,12 @@ const ValidationReview = () => {
} }
setSubmissionData(data); setSubmissionData(data);
if (data.establishment?.id) {
setEstablishmentId(data.establishment.id);
}
if (data.products) {
setProducts(data.products);
}
} catch (err) { } catch (err) {
console.error('Failed to load submission details:', err); console.error('Failed to load submission details:', err);
setError(err.message || 'Unable to load submission details. Please try again later.'); setError(err.message || 'Unable to load submission details. Please try again later.');
@ -255,8 +912,8 @@ const ValidationReview = () => {
const forecastQuarter = quarterPeriods?.forecast_quarter; const forecastQuarter = quarterPeriods?.forecast_quarter;
const year = submissionData?.year || new Date().getFullYear(); const year = submissionData?.year || new Date().getFullYear();
const products = submissionData?.products || []; const submissionProducts = submissionData?.products || [];
const productDetails = Array.isArray(products) ? products.map((product) => { const productDetails = Array.isArray(submissionProducts) ? submissionProducts.map((product) => {
const quarters = [ const quarters = [
{ label: `${previousQuarter}-${year}`, color: '#0C64F6', cellColor: '#F4F8FF', type: 'previous' }, { label: `${previousQuarter}-${year}`, color: '#0C64F6', cellColor: '#F4F8FF', type: 'previous' },
{ label: `${currentQuarter}-${year}`, color: '#1E7C34', cellColor: '#F3FAF4', type: 'current' }, { label: `${currentQuarter}-${year}`, color: '#1E7C34', cellColor: '#F3FAF4', type: 'current' },
@ -366,7 +1023,11 @@ const ValidationReview = () => {
{employmentDetails.map((item) => ( {employmentDetails.map((item) => (
<div key={item.label} className="flex items-start gap-3"> <div key={item.label} className="flex items-start gap-3">
{employmentIcons[item.label] ? ( {employmentIcons[item.label] ? (
<img src={employmentIcons[item.label]} alt={item.label} className="h-5 w-5" /> <img
src={employmentIcons[item.label]}
alt={item.label}
className={item.label === 'Female Employees' ? 'h-8 w-8' : 'h-5 w-5'}
/>
) : ( ) : (
<span className="mt-1 h-2.5 w-2.5 rounded-full bg-[#92722A]" /> <span className="mt-1 h-2.5 w-2.5 rounded-full bg-[#92722A]" />
)} )}
@ -382,14 +1043,10 @@ const ValidationReview = () => {
</div> </div>
</section> </section>
{submissionData?.products?.length > 0 ? ( {/* Products will be rendered below with their respective charts */}
<div
className={`grid gap-4 ${ {submissionData?.products?.length > 0 ? (
submissionData.products.length === 1 <div className="grid gap-4 grid-cols-1">
? 'grid-cols-1'
: 'grid-cols-1 lg:grid-cols-2'
}`}
>
{submissionData.products.map((product, idx) => ( {submissionData.products.map((product, idx) => (
<section <section
key={product.id || idx} key={product.id || idx}
@ -404,18 +1061,29 @@ const ValidationReview = () => {
</span> </span>
</div> </div>
<div className="flex flex-wrap items-center gap-2"> <div className="flex flex-wrap items-center gap-2">
<span className="px-2 py-[2px] border border-[#D0D5DD] rounded-md text-xs"> <span className="px-2 py-[2px] border border-[#D0D5DB] rounded-md text-xs">
Submitted By: <strong>System</strong> Submitted By: <strong>System</strong>
</span> </span>
<span className="px-2 py-[2px] border border-[#D0D5DD] rounded-md text-xs"> <span className="px-2 py-[2px] border border-[#D0D5DB] rounded-md text-xs">
Unit: <strong>{product?.unit?.uom || 'units'}</strong> Unit: <strong>{product?.unit?.uom || 'units'}</strong>
</span> </span>
<span className="px-2 py-[2px] border border-[#D0D5DD] rounded-md text-xs"> <span className="px-2 py-[2px] border border-[#D0D5DB] rounded-md text-xs">
Capacity: <strong>{product?.annual_installed_capacity}</strong> Capacity: <strong>{product?.annual_installed_capacity}</strong>
</span> </span>
</div> </div>
</div> </div>
{/* Product Metrics with Chart */}
<div className="mb-6">
<h3 className="text-sm font-medium text-gray-700 mb-4">Product Metrics</h3>
<div className="bg-white rounded-lg p-4 shadow mb-4">
<ProductChart
product={product}
establishmentId={submissionData?.establishment?.id}
/>
</div>
</div>
{/* Table Section */} {/* Table Section */}
<div className="overflow-x-auto rounded-[12px] border border-[#E5E7EB]"> <div className="overflow-x-auto rounded-[12px] border border-[#E5E7EB]">
<table className="min-w-full text-sm text-left border-collapse"> <table className="min-w-full text-sm text-left border-collapse">
@ -587,12 +1255,11 @@ const ValidationReview = () => {
</section> </section>
))} ))}
</div> </div>
) : ( ) : (
<div className="rounded-lg bg-amber-50 border border-amber-200 p-6 text-center"> <div className="rounded-lg bg-amber-50 border border-amber-200 p-6 text-center">
<p className="text-amber-800">No products found for this submission.</p> <p className="text-amber-800">No products found for this submission.</p>
</div> </div>
)} )}
{actionType !== 'view' && ( {actionType !== 'view' && (
<section className="rounded-[16px] bg-white p-6 shadow-[0_16px_32px_rgba(15,23,42,0.08)] ring-1 ring-[#E5E7EB]"> <section className="rounded-[16px] bg-white p-6 shadow-[0_16px_32px_rgba(15,23,42,0.08)] ring-1 ring-[#E5E7EB]">

View File

@ -145,7 +145,7 @@ const ManageSubmissions = () => {
showToast('success', 'Submission approved successfully'); showToast('success', 'Submission approved successfully');
// Refresh the submissions list // Refresh the submissions list
const data = await fetchDashboardData(selectedQuarter, selectedYear); const data = await fetchDashboardData(selectedQuarter, selectedYear);
const userProfile = JSON.parse(sessionStorage.getItem("user_profile")); const userProfile = JSON.parse(localStorage.getItem("user_profile"));
const mapped = (data || []).map((item) => ({ const mapped = (data || []).map((item) => ({
id: item.id, id: item.id,
establishment: item?.establishment?.factory_name || '-', establishment: item?.establishment?.factory_name || '-',
@ -305,7 +305,8 @@ React.useEffect(() => {
setLoading(true); setLoading(true);
// First, get the current quarter and year // First, get the current quarter and year
const response = await apiClient.get('/admin_dashboard'); // const response = await apiClient.get('/admin_dashboard');
const response = await apiClient.get(`/admin_dashboard?${params.toString()}`, { withCredentials: true });
const currentQuarter = response?.data?.selected_quarter || 'Q1'; const currentQuarter = response?.data?.selected_quarter || 'Q1';
// const currentQuarter = 'All'; // const currentQuarter = 'All';
const currentYear = response?.data?.selected_year || new Date().getFullYear().toString(); const currentYear = response?.data?.selected_year || new Date().getFullYear().toString();

View File

@ -3,6 +3,7 @@ import { X } from 'lucide-react';
import { TextField, SelectField } from '@/components/common/FormControls'; import { TextField, SelectField } from '@/components/common/FormControls';
import { createadminusers } from '@/services/configuration/adminService'; import { createadminusers } from '@/services/configuration/adminService';
import CustomToast from '@/components/common/CustomToast'; import CustomToast from '@/components/common/CustomToast';
import { VALIDATION_MESSAGES } from '../../../../constants/validationMessages';
const AddAdminUsers = ({ isOpen, onClose, onSave }) => { const AddAdminUsers = ({ isOpen, onClose, onSave }) => {
const [formData, setFormData] = useState({ const [formData, setFormData] = useState({
@ -49,7 +50,7 @@ const AddAdminUsers = ({ isOpen, onClose, onSave }) => {
} }
if (formData.password !== formData.confirmPassword) { if (formData.password !== formData.confirmPassword) {
newErrors.confirmPassword = 'Passwords do not match'; newErrors.confirmPassword = VALIDATION_MESSAGES.PASSWORDS_DONT_MATCH;
} }
return newErrors; return newErrors;
@ -71,12 +72,12 @@ const AddAdminUsers = ({ isOpen, onClose, onSave }) => {
}, [isOpen]); }, [isOpen]);
const validatePassword = (password) => { const validatePassword = (password) => {
if (!password) return 'Password is required'; if (!password) return VALIDATION_MESSAGES.PASSWORD_REQUIRED;
if (password.length < 8) return 'Password must be at least 8 characters'; if (password.length < 8) return VALIDATION_MESSAGES.PASSWORD_MIN_LENGTH;
if (!/[A-Z]/.test(password)) return 'Must contain at least one uppercase letter'; if (!/[A-Z]/.test(password)) return VALIDATION_MESSAGES.PASSWORD_UPPERCASE;
if (!/[a-z]/.test(password)) return 'Must contain at least one lowercase letter'; if (!/[a-z]/.test(password)) return VALIDATION_MESSAGES.PASSWORD_LOWERCASE;
if (!/\d/.test(password)) return 'Must contain at least one number'; if (!/\d/.test(password)) return VALIDATION_MESSAGES.PASSWORD_NUMBER;
if (!/[!@#$%^&*]/.test(password)) return 'Must contain at least one special character (!@#$%^&*)'; if (!/[!@#$%^&*]/.test(password)) return VALIDATION_MESSAGES.PASSWORD_SPECIAL_CHAR;
return ''; // Return empty string if password is valid return ''; // Return empty string if password is valid
}; };
@ -269,7 +270,7 @@ const AddAdminUsers = ({ isOpen, onClose, onSave }) => {
</div> </div>
</div> </div>
{/*Custom Toast Display */} {/*Custom Toast Display */}
{toastData && ( {toastData && (
<div className="fixed inset-0 z-[9999]"> <div className="fixed inset-0 z-[9999]">
<CustomToast <CustomToast

View File

@ -5,7 +5,7 @@ const EditUserModal = ({ formData, setFormData, onClose, onUpdate, showToast })
e.preventDefault(); e.preventDefault();
// Check if the deactivated user is the currently logged-in user // Check if the deactivated user is the currently logged-in user
const currentUser = JSON.parse(sessionStorage.getItem('user_profile') || '{}'); const currentUser = JSON.parse(localStorage.getItem('user_profile') || '{}');
const currentUserEmail = currentUser?.email?.toLowerCase()?.trim(); const currentUserEmail = currentUser?.email?.toLowerCase()?.trim();
const updatedUserEmail = formData.email?.toLowerCase()?.trim(); const updatedUserEmail = formData.email?.toLowerCase()?.trim();
const shouldLogout = currentUserEmail === updatedUserEmail && formData.status === "Inactive"; const shouldLogout = currentUserEmail === updatedUserEmail && formData.status === "Inactive";

View File

@ -2,6 +2,7 @@ import React, { useState, useEffect } from 'react';
import Table from '@/components/common/Table'; import Table from '@/components/common/Table';
import AddAdminUsers from './AddAdminUsers'; import AddAdminUsers from './AddAdminUsers';
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import { VALIDATION_MESSAGES } from '../../../../constants/validationMessages';
import { import {
getAdminUser, getAdminUser,
getAdminUserById, getAdminUserById,
@ -484,21 +485,21 @@ const AdminUsers = () => {
// Old password validation // Old password validation
if (!oldPassword?.trim()) { if (!oldPassword?.trim()) {
newErrors.oldPassword = 'Old password is required'; newErrors.oldPassword = VALIDATION_MESSAGES.OLD_PASSWORD_REQUIRED;
} }
// New password validation // New password validation
if (!newPassword?.trim()) { if (!newPassword?.trim()) {
newErrors.newPassword = 'New password is required'; newErrors.newPassword = VALIDATION_MESSAGES.PASSWORD_REQUIRED;
} else if (newPassword.length < 8) { } else if (newPassword.length < 8) {
newErrors.newPassword = 'Password must be at least 8 characters long'; newErrors.newPassword = VALIDATION_MESSAGES.PASSWORD_MIN_LENGTH;
} }
// Confirm password validation // Confirm password validation
if (!confirmPassword?.trim()) { if (!confirmPassword?.trim()) {
newErrors.confirmPassword = 'Please confirm your password'; newErrors.confirmPassword = VALIDATION_MESSAGES.CONFIRM_PASSWORD_REQUIRED;
} else if (newPassword !== confirmPassword) { } else if (newPassword !== confirmPassword) {
newErrors.confirmPassword = 'Passwords do not match'; newErrors.confirmPassword = VALIDATION_MESSAGES.PASSWORDS_DONT_MATCH;
} }
return newErrors; return newErrors;
@ -555,7 +556,7 @@ const AdminUsers = () => {
handleCloseResetModal(); handleCloseResetModal();
} catch (error) { } catch (error) {
console.error('Error resetting admin password:', error); console.error('Error resetting admin password:', error);
// Handle specific error cases // Handle specific error cases
let errorMessage = 'Failed to reset password'; let errorMessage = 'Failed to reset password';
@ -570,27 +571,28 @@ const AdminUsers = () => {
errorMessage.toLowerCase().includes('incorrect password')) { errorMessage.toLowerCase().includes('incorrect password')) {
setResetErrors(prev => ({ setResetErrors(prev => ({
...prev, ...prev,
oldPassword: 'The current password is incorrect' oldPassword: VALIDATION_MESSAGES.INCORRECT_CURRENT_PASSWORD
})); }));
showToast('The current password you entered is incorrect', 'error'); showToast(VALIDATION_MESSAGES.INCORRECT_CURRENT_PASSWORD, 'error');
} else if (errorMessage.toLowerCase().includes('match')) { } else if (errorMessage.toLowerCase().includes('match')) {
setResetErrors(prev => ({ setResetErrors(prev => ({
...prev, ...prev,
confirmPassword: 'New passwords do not match' confirmPassword: VALIDATION_MESSAGES.PASSWORDS_DONT_MATCH
})); }));
showToast(VALIDATION_MESSAGES.PASSWORDS_DONT_MATCH, 'error');
showToast('New passwords do not match', 'error'); showToast('New passwords do not match', 'error');
} else if (errorMessage.toLowerCase().includes('same') || } else if (errorMessage.toLowerCase().includes('same') ||
errorMessage.toLowerCase().includes('previous')) { errorMessage.toLowerCase().includes('previous')) {
setResetErrors(prev => ({ setResetErrors(prev => ({
...prev, ...prev,
newPassword: 'New password cannot be the same as current password' newPassword: VALIDATION_MESSAGES.PASSWORD_SAME_AS_CURRENT
})); }));
showToast('New password cannot be the same as current password', 'error'); showToast(VALIDATION_MESSAGES.PASSWORD_SAME_AS_CURRENT, 'error');
} else if (errorMessage.toLowerCase().includes('weak') || } else if (errorMessage.toLowerCase().includes('weak') ||
errorMessage.toLowerCase().includes('strength')) { errorMessage.toLowerCase().includes('strength')) {
setResetErrors(prev => ({ setResetErrors(prev => ({
...prev, ...prev,
newPassword: 'Password is too weak. Use a stronger password.' newPassword: VALIDATION_MESSAGES.PASSWORD_TOO_WEAK
})); }));
showToast('Password is too weak. Please use a stronger password.', 'error'); showToast('Password is too weak. Please use a stronger password.', 'error');
} else if (error.status === 404) { } else if (error.status === 404) {

View File

@ -1,5 +1,6 @@
import { Eye, EyeOff } from 'lucide-react'; import { Eye, EyeOff } from 'lucide-react';
import React, { useState } from 'react'; import React, { useState } from 'react';
import { VALIDATION_MESSAGES } from '../../../../constants/validationMessages';
const ResetPasswordModal = ({ const ResetPasswordModal = ({
isOpen, isOpen,
@ -22,12 +23,12 @@ const ResetPasswordModal = ({
const [confirmPasswordTouched, setConfirmPasswordTouched] = useState(false); const [confirmPasswordTouched, setConfirmPasswordTouched] = useState(false);
const validatePassword = (password) => { const validatePassword = (password) => {
if (!password) return 'Password is required'; if (!password) return VALIDATION_MESSAGES.PASSWORD_REQUIRED;
if (password.length < 8) return 'Password must be at least 8 characters'; if (password.length < 8) return VALIDATION_MESSAGES.PASSWORD_MIN_LENGTH;
if (!/[A-Z]/.test(password)) return 'Must contain at least one uppercase letter'; if (!/[A-Z]/.test(password)) return VALIDATION_MESSAGES.PASSWORD_UPPERCASE;
if (!/[a-z]/.test(password)) return 'Must contain at least one lowercase letter'; if (!/[a-z]/.test(password)) return VALIDATION_MESSAGES.PASSWORD_LOWERCASE;
if (!/\d/.test(password)) return 'Must contain at least one number'; if (!/\d/.test(password)) return VALIDATION_MESSAGES.PASSWORD_NUMBER;
if (!/[!@#$%^&*]/.test(password)) return 'Must contain at least one special character (!@#$%^&*)'; if (!/[!@#$%^&*]/.test(password)) return VALIDATION_MESSAGES.PASSWORD_SPECIAL_CHAR;
return ''; return '';
}; };
@ -37,7 +38,7 @@ const ResetPasswordModal = ({
const newErrors = {}; const newErrors = {};
if (!oldPassword.trim()) { if (!oldPassword.trim()) {
newErrors.oldPassword = 'Old password is required'; newErrors.oldPassword = VALIDATION_MESSAGES.OLD_PASSWORD_REQUIRED;
} }
if (passwordTouched && passwordError) { if (passwordTouched && passwordError) {
@ -45,9 +46,9 @@ const ResetPasswordModal = ({
} }
if (!confirmPassword.trim()) { if (!confirmPassword.trim()) {
newErrors.confirmPassword = 'Please confirm your password'; newErrors.confirmPassword = VALIDATION_MESSAGES.CONFIRM_PASSWORD_REQUIRED;
} else if (newPassword !== confirmPassword) { } else if (newPassword !== confirmPassword) {
newErrors.confirmPassword = 'Passwords do not match'; newErrors.confirmPassword = VALIDATION_MESSAGES.PASSWORDS_DONT_MATCH;
} }
return newErrors; return newErrors;

View File

@ -174,7 +174,7 @@ const createEmptyProfile = () => ({
contactMakaniNumber: '', contactMakaniNumber: '',
contactPersonName: '', contactPersonName: '',
contactPersonDesignation: '', contactPersonDesignation: '',
contactCountryCode: '+971', contactCountryCode: '',
contactMobileNumber: '', contactMobileNumber: '',
contactEmail: '', contactEmail: '',
contactWebsite: '', contactWebsite: '',
@ -191,7 +191,7 @@ const createEmptyProfile = () => ({
corporateMakaniNumber: '', corporateMakaniNumber: '',
corporateContactPersonName: '', corporateContactPersonName: '',
corporateContactPersonDesignation: '', corporateContactPersonDesignation: '',
corporateCountryCode: '+971', corporateCountryCode: '',
corporateMobileNumber: '', corporateMobileNumber: '',
corporateEmail: '', corporateEmail: '',
corporateWebsite: '', corporateWebsite: '',
@ -606,7 +606,7 @@ const CompanyProfile = () => {
// Load current user from session storage // Load current user from session storage
React.useEffect(() => { React.useEffect(() => {
try { try {
const profile = sessionStorage.getItem('user_profile'); const profile = localStorage.getItem('user_profile');
if (profile) { if (profile) {
const userData = JSON.parse(profile); const userData = JSON.parse(profile);
setCurrentUser(userData); setCurrentUser(userData);
@ -807,7 +807,7 @@ const CompanyProfile = () => {
const currentUserId = React.useMemo(() => { const currentUserId = React.useMemo(() => {
try { try {
const profile = sessionStorage.getItem('user_profile'); const profile = localStorage.getItem('user_profile');
if (!profile) return null; if (!profile) return null;
const parsed = JSON.parse(profile); const parsed = JSON.parse(profile);
return parsed; return parsed;
@ -817,10 +817,10 @@ const CompanyProfile = () => {
} }
}, []); }, []);
const profile = sessionStorage.getItem('user_profile'); const profile = localStorage.getItem('user_profile');
const userProfile = React.useMemo(() => { const userProfile = React.useMemo(() => {
try { try {
const profile = sessionStorage.getItem('user_profile'); const profile = localStorage.getItem('user_profile');
return profile ? JSON.parse(profile) : null; return profile ? JSON.parse(profile) : null;
} catch (error) { } catch (error) {
console.error('Error parsing user profile:', error); console.error('Error parsing user profile:', error);
@ -1935,7 +1935,7 @@ const displayedRows = sortedProfiles.map((item) => {
const mapApiEstablishmentToProfile = React.useCallback((item) => { const mapApiEstablishmentToProfile = React.useCallback((item) => {
let currentUser = null; let currentUser = null;
try { try {
const profile = sessionStorage.getItem('user_profile'); const profile = localStorage.getItem('user_profile');
if (profile) { if (profile) {
currentUser = JSON.parse(profile); currentUser = JSON.parse(profile);
} }

View File

@ -657,7 +657,7 @@ const EditCompanyProfile = () => {
required required
error={fieldErrors.userProfileEmail} error={fieldErrors.userProfileEmail}
/> />
<div className="space-y-1"> {/* <div className="space-y-1">
<TextField <TextField
label="Password" label="Password"
value={form.userProfilePassword || ""} value={form.userProfilePassword || ""}
@ -670,7 +670,7 @@ const EditCompanyProfile = () => {
/> />
{passwordError && <p className="text-xs text-[#B91C1C]">{passwordError}</p>} {passwordError && <p className="text-xs text-[#B91C1C]">{passwordError}</p>}
{passwordReuseError && <p className="text-xs text-[#B91C1C]">{passwordReuseError}</p>} {passwordReuseError && <p className="text-xs text-[#B91C1C]">{passwordReuseError}</p>}
</div> </div> */}
</div> </div>
</div> </div>
</div> </div>

View File

@ -405,7 +405,7 @@ const IsicHsCodes = () => {
// Get user profile from session // Get user profile from session
const userProfile = React.useMemo(() => { const userProfile = React.useMemo(() => {
try { try {
const profile = sessionStorage.getItem('user_profile'); const profile = localStorage.getItem('user_profile');
return profile ? JSON.parse(profile) : null; return profile ? JSON.parse(profile) : null;
} catch (error) { } catch (error) {
console.error('Error parsing user profile:', error); console.error('Error parsing user profile:', error);
@ -464,7 +464,7 @@ const IsicHsCodes = () => {
const createdById = product.created_by; const createdById = product.created_by;
try { try {
const userProfileStr = sessionStorage.getItem('user_profile'); const userProfileStr = localStorage.getItem('user_profile');
if (userProfileStr) { if (userProfileStr) {
const userProfile = JSON.parse(userProfileStr); const userProfile = JSON.parse(userProfileStr);
if (createdById && createdById === userProfile.id) { if (createdById && createdById === userProfile.id) {
@ -643,7 +643,7 @@ const IsicHsCodes = () => {
let displayName = item.createdBy; let displayName = item.createdBy;
if (displayName === '-' && item.createdById) { if (displayName === '-' && item.createdById) {
try { try {
const userProfileStr = sessionStorage.getItem('user_profile'); const userProfileStr = localStorage.getItem('user_profile');
if (userProfileStr) { if (userProfileStr) {
const userProfile = JSON.parse(userProfileStr); const userProfile = JSON.parse(userProfileStr);
if (item.createdById === userProfile.id) { if (item.createdById === userProfile.id) {
@ -962,7 +962,7 @@ const IsicHsCodes = () => {
// Get user profile from session storage for created_by // Get user profile from session storage for created_by
let userProfile = null; let userProfile = null;
try { try {
const userProfileStr = sessionStorage.getItem('user_profile'); const userProfileStr = localStorage.getItem('user_profile');
if (userProfileStr) { if (userProfileStr) {
userProfile = JSON.parse(userProfileStr); userProfile = JSON.parse(userProfileStr);
} }
@ -1129,7 +1129,7 @@ const IsicHsCodes = () => {
// Get user profile from session storage for created_by // Get user profile from session storage for created_by
let createdByName = '-'; let createdByName = '-';
try { try {
const userProfileStr = sessionStorage.getItem('user_profile'); const userProfileStr = localStorage.getItem('user_profile');
if (userProfileStr) { if (userProfileStr) {
const userProfile = JSON.parse(userProfileStr); const userProfile = JSON.parse(userProfileStr);
createdByName = userProfile.name || createdByName; createdByName = userProfile.name || createdByName;

View File

@ -196,7 +196,7 @@ const UnitMaster = () => {
}, [sortedUnits, searchTerm]); }, [sortedUnits, searchTerm]);
useEffect(() => { useEffect(() => {
const userProfile = sessionStorage.getItem('user_profile'); const userProfile = localStorage.getItem('user_profile');
if (userProfile) { if (userProfile) {
try { try {
setCurrentUser(JSON.parse(userProfile)); setCurrentUser(JSON.parse(userProfile));

View File

@ -1,8 +1,10 @@
import React from 'react'; import React from 'react';
import { useNavigate, useLocation } from 'react-router-dom'; import { useNavigate, useLocation } from 'react-router-dom';
import apiClient from '@/services/api/apiClient'; import apiClient from '@/services/api/apiClient';
import { VALIDATION_MESSAGES } from '@/constants/validationMessages';
import { ASSETS } from '@/constants/assets';
const logoSrc = '/assets/images/FCSCLogo.svg'; // const logoSrc = ASSETS.IMAGES.LOGO;
const ToggleableInput = ({ const ToggleableInput = ({
label, label,
@ -81,15 +83,15 @@ const ChangePassword = () => {
const validatePassword = (password) => { const validatePassword = (password) => {
const rules = [ const rules = [
{ regex: /.{8,}/, msg: "At least 8 characters" }, { regex: /.{8,}/, msg: VALIDATION_MESSAGES.PASSWORD_MIN_LENGTH },
{ regex: /[A-Z]/, msg: "At least one uppercase letter" }, { regex: /[A-Z]/, msg: VALIDATION_MESSAGES.PASSWORD_UPPERCASE },
{ regex: /[a-z]/, msg: "At least one lowercase letter" }, { regex: /[a-z]/, msg: VALIDATION_MESSAGES.PASSWORD_LOWERCASE },
{ regex: /[0-9]/, msg: "At least one number" }, { regex: /[0-9]/, msg: VALIDATION_MESSAGES.PASSWORD_NUMBER },
{ regex: /[!@#$%^&*(),.?\":{}|<>]/, msg: "At least one special character" }, { regex: /[!@#$%^&*(),.?\":{}|<>]/, msg: VALIDATION_MESSAGES.PASSWORD_SPECIAL_CHAR },
]; ];
const failedRule = rules.find(r => !r.regex.test(password)); const failedRule = rules.find(r => !r.regex.test(password));
return failedRule ? `Password must meet complexity requirements.` : ""; return failedRule ? VALIDATION_MESSAGES.PASSWORD_COMPLEXITY_ERROR : "";
}; };
const handleSubmit = async (e) => { const handleSubmit = async (e) => {
@ -105,11 +107,11 @@ const handleSubmit = async (e) => {
let hasError = false; let hasError = false;
if (!otp.trim()) { if (!otp.trim()) {
setOtpError("Verification code is required."); setOtpError("Verification code is required");
hasError = true; hasError = true;
} }
if (!newPassword.trim()) { if (!newPassword.trim()) {
setNewPasswordError("New password is required."); setNewPasswordError(VALIDATION_MESSAGES.PASSWORD_REQUIRED);
hasError = true; hasError = true;
} }
if (!confirmPassword.trim()) { if (!confirmPassword.trim()) {
@ -126,7 +128,7 @@ if (passwordError) {
} }
if (newPassword !== confirmPassword) { if (newPassword !== confirmPassword) {
setConfirmPasswordError("Passwords do not match."); setConfirmPasswordError(VALIDATION_MESSAGES.PASSWORDS_DONT_MATCH);
return; return;
} }
@ -168,11 +170,11 @@ if (newPassword !== confirmPassword) {
}; };
return ( return (
<div className="min-h-screen bg-[#F7F7F7] flex items-center justify-center px-4"> <div className={ASSETS.CLASSES.AUTH_CONTAINER}>
<div className="w-full max-w-md"> <div className="w-full max-w-md">
<div className="bg-white rounded-lg shadow-sm ring-1 ring-gray-200 overflow-hidden"> <div className="bg-white rounded-lg shadow-sm ring-1 ring-gray-200 overflow-hidden">
<div className="px-7 py-8 border-b border-gray-200 bg-[#F2ECCF] text-center"> <div className="px-7 py-8 border-b border-gray-200 bg-[#F2ECCF] text-center">
<img src={logoSrc} alt="FCSC" className="w-[188px] h-[58px] mx-auto mb-2" /> <img src={ASSETS.IMAGES.LOGO} alt="FCSC" className="w-[188px] h-[58px] mx-auto mb-2" />
<h1 className="text-base font-semibold text-[#92722A]">Reset Password</h1> <h1 className="text-base font-semibold text-[#92722A]">Reset Password</h1>
<p className="mt-1 text-sm text-gray-600"> <p className="mt-1 text-sm text-gray-600">
Enter the verification code sent to your registered email and set a new password. Enter the verification code sent to your registered email and set a new password.

View File

@ -1,8 +1,8 @@
import React from 'react'; import React from 'react';
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import { requestPasswordReset } from '@/services/auth/authService.js'; import { requestPasswordReset } from '@/services/auth/authService.js';
import { ASSETS } from '@/constants/assets';
const logoSrc = '/assets/images/FCSCLogo.svg'; const CONTAINER_CLASS = "min-h-screen bg-[#F7F7F7] flex items-center justify-center px-4";
const ForgotPassword = () => { const ForgotPassword = () => {
const navigate = useNavigate(); const navigate = useNavigate();
@ -38,11 +38,11 @@ const ForgotPassword = () => {
}; };
return ( return (
<div className="min-h-screen bg-[#F7F7F7] flex items-center justify-center px-4"> <div className={CONTAINER_CLASS}>
<div className="w-full max-w-md"> <div className="w-full max-w-md">
<div className="bg-white rounded-lg shadow-sm ring-1 ring-gray-200 overflow-hidden"> <div className="bg-white rounded-lg shadow-sm ring-1 ring-gray-200 overflow-hidden">
<div className="px-7 py-8 border-b border-gray-200 bg-[#F2ECCF] text-center"> <div className="px-7 py-8 border-b border-gray-200 bg-[#F2ECCF] text-center">
<img src={logoSrc} alt="FCSC" className="w-[188px] h-[58px] mx-auto mb-3" /> <img src={ASSETS.IMAGES.LOGO} alt="FCSC" className="w-[188px] h-[58px] mx-auto mb-3" />
<h1 className="text-base font-semibold text-[#92722A]">Reset Password</h1> <h1 className="text-base font-semibold text-[#92722A]">Reset Password</h1>
<p className="mt-1 text-sm text-gray-600">Enter your email and we will send you instructions to reset your password.</p> <p className="mt-1 text-sm text-gray-600">Enter your email and we will send you instructions to reset your password.</p>
</div> </div>

View File

@ -152,7 +152,7 @@ const Login = () => {
return valid; return valid;
}; };
const submit = async (e) => { const submit = async (e) => {
e.preventDefault(); e.preventDefault();
if (isLocked) { if (isLocked) {
@ -168,95 +168,68 @@ const Login = () => {
try { try {
const response = await login({ email, password }); const response = await login({ email, password });
console.log("Login response:", response);
if (response?.status !== 'success' || !response?.data) { // Check if login was successful
throw new Error('Invalid credentials'); if (response?.status === 'success' && response?.data) {
} const userData = response.data;
console.log("User data:", userData);
const token = response.data; // Clear any existing failed attempts
sessionStorage.setItem('auth_token', token); const storedAttempts = JSON.parse(localStorage.getItem("login_attempts") || "{}");
let payload;
try {
const [, base64Payload] = token.split('.');
payload = JSON.parse(atob(base64Payload));
} catch {
throw new Error('Invalid token format.');
}
const role = payload?.role ?? (payload?.is_admin ? 'Admin' : undefined);
if (role) sessionStorage.setItem('user_role', role);
const profile = {
id: payload?.id || payload?.user_id || '',
name: payload?.name || payload?.username || '',
email: payload?.email || '',
};
sessionStorage.setItem('user_profile', JSON.stringify(profile));
const establishmentId = payload?.establishment_id ?? payload?.establishmentId;
const isAdmin =
String(role || '').toLowerCase() === 'admin' || payload?.is_admin === true;
const successMessage = response?.message || 'Login successful.';
// Clear login attempts for this email
const storedAttempts = JSON.parse(localStorage.getItem('login_attempts') || '{}');
if (storedAttempts[email]) { if (storedAttempts[email]) {
delete storedAttempts[email]; delete storedAttempts[email];
localStorage.setItem('login_attempts', JSON.stringify(storedAttempts)); localStorage.setItem("login_attempts", JSON.stringify(storedAttempts));
} }
// Store user data in localStorage
localStorage.setItem('user_role', userData.role);
// Store establishment_id if user is an EstablishmentUser
if (userData.role === 'EstablishmentUser' && userData.establishment_id) {
localStorage.setItem('establishment_id', userData.establishment_id);
}
// Store user profile
const userProfile = {
id: userData.id,
name: userData.name,
email: userData.email
};
localStorage.setItem('user_profile', JSON.stringify(userProfile));
// Reset login attempt counters
setFailedAttempts(0); setFailedAttempts(0);
setIsLocked(false); setIsLocked(false);
setRemainingTime(0); setRemainingTime(0);
if (countdownIntervalRef.current) { if (countdownIntervalRef.current) {
clearInterval(countdownIntervalRef.current); clearInterval(countdownIntervalRef.current);
countdownIntervalRef.current = null; countdownIntervalRef.current = null;
} }
if (isAdmin) { // Show success message
sessionStorage.removeItem('establishment_id'); showToast('success', response.message || 'Login successful');
showToast('success', successMessage);
scheduleNavigate('/admin/dashboard');
return;
}
if (rememberMe) { // Redirect based on role
sessionStorage.setItem('remember_me', 'true'); if (userData.role === 'Admin') {
sessionStorage.setItem('remembered_email', email); navigate('/admin/dashboard', { replace: true });
sessionStorage.setItem('remembered_password', password);
} else { } else {
sessionStorage.removeItem('remember_me'); navigate('/dashboard');
sessionStorage.removeItem('remembered_email');
sessionStorage.removeItem('remembered_password');
} }
if (establishmentId) {
sessionStorage.setItem('establishment_id', String(establishmentId));
showToast('success', successMessage);
scheduleNavigate('/dashboard');
} else { } else {
showToast('error', 'Email or password is incorrect.'); throw new Error(response?.message || 'Invalid credentials');
} }
} catch (err) { } catch (err) {
console.error('Login failed', err); console.error('Login failed', err);
// Get the error message from the API response
const errorMessage = err.response?.data?.message || const errorMessage = err.response?.data?.message ||
err.message || err.message ||
'An error occurred during login. Please try again.'; 'An error occurred during login. Please try again.';
// Show the exact error message for inactive users // Handle failed login attempts
if (errorMessage.toLowerCase().includes('inactive')) { const storedAttempts = JSON.parse(localStorage.getItem("login_attempts") || "{}");
showToast('error', errorMessage);
return;
}
// For other errors, show the generic message and update login attempts
const storedAttempts = JSON.parse(localStorage.getItem('login_attempts') || '{}');
const attempts = (storedAttempts[email]?.attempts || 0) + 1; const attempts = (storedAttempts[email]?.attempts || 0) + 1;
// Update attempts for this email
const updatedAttempts = { const updatedAttempts = {
...storedAttempts, ...storedAttempts,
[email]: { [email]: {
@ -267,7 +240,7 @@ const Login = () => {
} }
}; };
localStorage.setItem('login_attempts', JSON.stringify(updatedAttempts)); localStorage.setItem("login_attempts", JSON.stringify(updatedAttempts));
setFailedAttempts(attempts); setFailedAttempts(attempts);
if (attempts >= MAX_FAILED_ATTEMPTS) { if (attempts >= MAX_FAILED_ATTEMPTS) {
@ -283,7 +256,7 @@ const Login = () => {
} finally { } finally {
setLoading(false); setLoading(false);
} }
}; };
const formatTime = (seconds) => { const formatTime = (seconds) => {
const mins = Math.floor(seconds / 60); const mins = Math.floor(seconds / 60);

View File

@ -32,7 +32,7 @@ const Overview = () => {
const [submissions, setSubmissions] = useState([]); const [submissions, setSubmissions] = useState([]);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [error, setError] = useState(null); const [error, setError] = useState(null);
const establishmentId = sessionStorage.getItem('establishment_id'); const establishmentId = localStorage.getItem('establishment_id');
const [pagination, setPagination] = useState({ const [pagination, setPagination] = useState({
currentPage: 1, currentPage: 1,
pageSize: 10, pageSize: 10,

View File

@ -6,6 +6,7 @@ const fallbackToken = import.meta.env.VITE_API_TOKEN;
const apiClient = axios.create({ const apiClient = axios.create({
baseURL, baseURL,
withCredentials: true,
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
Accept: 'application/json', Accept: 'application/json',
@ -19,20 +20,20 @@ apiClient.interceptors.request.use((config) => {
return config; return config;
} }
if (!config.headers.Authorization) { // if (!config.headers.Authorization) {
let token; // let token;
try { // try {
token = sessionStorage.getItem('auth_token'); // token = sessionStorage.getItem('auth_token');
} catch (error) { // } catch (error) {
token = undefined; // token = undefined;
} // }
if (!token && fallbackToken) { // if (!token && fallbackToken) {
token = fallbackToken; // token = fallbackToken;
} // }
if (token) { // if (token) {
config.headers.Authorization = `Bearer ${token}`; // config.headers.Authorization = `Bearer ${token}`;
} // }
} // }
return config; return config;
}); });

View File

@ -1,7 +1,7 @@
import apiClient from '@/services/api/apiClient'; import apiClient from '@/services/api/apiClient';
export const login = async ({ email, password }) => { export const login = async ({ email, password }) => {
const response = await apiClient.post('/auth/login', { email, password }, { skipAuth: true }); const response = await apiClient.post('/auth/login', { email, password }, { skipAuth: true , withCredentials: true});
return response.data; return response.data;
}; };

View File

@ -4,7 +4,8 @@ const admin = '/admin_users';
export const getAdminUser = async () => { export const getAdminUser = async () => {
try { try {
const response = await getRequest(admin); // const response = await getRequest(admin);
const response = await getRequest(admin, { withCredentials: true });
return response.data || []; return response.data || [];
} catch (error) { } catch (error) {
console.error('Error fetching units:', error); console.error('Error fetching units:', error);
@ -44,13 +45,6 @@ export const getAdminUserById = async (id) => {
export const changeAdminUserPassword = async (id, passwordData) => { export const changeAdminUserPassword = async (id, passwordData) => {
try { try {
if (!id) throw new Error("Invalid user ID"); if (!id) throw new Error("Invalid user ID");
console.log('Debug: Password data received:', {
hasOldPassword: !!passwordData.old_password,
hasNewPassword: !!passwordData.new_password,
hasConfirmPassword: !!passwordData.confirm_password
});
// Prepare the request data according to API specification // Prepare the request data according to API specification
const requestData = { const requestData = {
old_password: passwordData.old_password, old_password: passwordData.old_password,
@ -69,7 +63,7 @@ export const changeAdminUserPassword = async (id, passwordData) => {
console.error("Error changing admin user password:", error); console.error("Error changing admin user password:", error);
if (error.response) { if (error.response) {
console.error("🔍 Debug: API error response:", { console.error("Debug: API error response:", {
status: error.response.status, status: error.response.status,
data: error.response.data, data: error.response.data,
headers: error.response.headers headers: error.response.headers

View File

@ -161,6 +161,26 @@ export const getEstablishmentProducts = async (establishmentId, config = {}) =>
} }
}; };
export const getProductSubmissionHistory = async (establishmentId, productId, config = {}) => {
try {
if (!establishmentId || !productId) {
throw new Error('Establishment ID and Product ID are required to fetch product submission history');
}
const response = await getRequest('/submissions/getProductSubmissionHistory', {
...config,
params: {
establishment_id: establishmentId,
product_id: productId,
...(config.params || {})
}
});
return response.data;
} catch (error) {
console.error('Error fetching product submission history:', error);
throw error;
}
};
export default { export default {
getSubmissions, getSubmissions,
submitSurvey, submitSurvey,
@ -171,5 +191,6 @@ export default {
getPreviousForecastData, getPreviousForecastData,
getSubmissionHistoryByEstablishment, getSubmissionHistoryByEstablishment,
getSubmissionAuditHistory, getSubmissionAuditHistory,
getEstablishmentProducts getEstablishmentProducts,
getProductSubmissionHistory
}; };

View File

@ -15,7 +15,7 @@ export const resolveEstablishmentId = (inputId) => {
} }
let storedId; let storedId;
try { try {
storedId = sessionStorage.getItem('establishment_id'); storedId = localStorage.getItem('establishment_id');
} catch (error) { } catch (error) {
storedId = undefined; storedId = undefined;
} }