Changes in design for quarterly,product code

This commit is contained in:
Malini 2025-11-03 09:22:45 +05:30
parent 492931d488
commit 9d473ecaa8
25 changed files with 1898 additions and 564 deletions

View File

@ -0,0 +1,4 @@
<svg width="40" height="20" viewBox="0 0 40 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect y="2.5" width="40" height="15" rx="7.5" fill="#F7F7F7"/>
<rect x="0.5" y="0.5" width="19" height="19" rx="9.5" fill="white" stroke="#E1E3E5"/>
</svg>

After

Width:  |  Height:  |  Size: 252 B

View File

@ -0,0 +1,4 @@
<svg width="40" height="20" viewBox="0 0 40 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect y="2.5" width="40" height="15" rx="7.5" fill="#D7BC6D"/>
<rect x="20.5" y="0.5" width="19" height="19" rx="9.5" fill="white" stroke="#92722A"/>
</svg>

After

Width:  |  Height:  |  Size: 253 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.9 KiB

View File

@ -118,6 +118,14 @@ function App() {
</RequireRole>
}
/>
<Route
path="admin-users/edit/:id"
element={
<RequireRole allowedRoles={['Admin']}>
<Configuration />
</RequireRole>
}
/>
<Route
path="profile"
element={

View File

@ -106,7 +106,7 @@ 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
{/* <button
type="button"
className="w-full px-4 py-2.5 text-center text-sm text-[#232528] hover:bg-[#F2ECCF]"
onClick={() => {
@ -115,7 +115,7 @@ const AdminHeader = () => {
}}
>
Profile
</button>
</button> */}
<button
type="button"
className="w-full px-4 py-2.5 text-center text-sm text-[#232528] hover:bg-[#F2ECCF]"

View File

@ -0,0 +1,42 @@
import React, { useEffect } from 'react';
import { CheckCircle, XCircle } from 'lucide-react';
const CustomToast = ({ message, type = 'success', onClose, duration = 4000 }) => {
useEffect(() => {
const timer = setTimeout(onClose, duration);
return () => clearTimeout(timer);
}, [onClose, duration]);
const isSuccess = type === 'success';
const bgColor = isSuccess
? 'bg-green-50 border border-green-500 text-green-700'
: 'bg-red-50 border border-red-500 text-red-700';
const Icon = isSuccess ? CheckCircle : XCircle;
// In CustomToast.jsx
// return (
// In CustomToast.jsx
return (
<div
className={`fixed top-4 left-1/2 -translate-x-1/2 flex items-start gap-3 p-4 rounded-lg shadow-md ${bgColor} animate-fade-in`}
style={{
zIndex: 10000,
minWidth: '320px',
maxWidth: '90%',
animation: 'fadeIn 0.3s ease-out',
}}
>
<Icon className="w-5 h-5 mt-0.5 flex-shrink-0" />
<div className="flex-1 text-sm font-medium">{message}</div>
<button
onClick={onClose}
className="text-gray-500 hover:text-gray-700 ml-2"
aria-label="Close"
>
&times;
</button>
</div>
);
};
export default CustomToast;

View File

@ -1,71 +1,93 @@
import React from 'react';
import { Phone, Smartphone, Headphones, Instagram, Facebook, Linkedin, Youtube } from 'lucide-react';
const Footer = () => {
return (
<footer className="bg-white border-t border-gray-200 mt-10">
<div className="h-[3px] bg-[#92722A]" />
const mailIconSrc = '/assets/images/material-symbols_mail-outline.svg';
const phoneIconSrc = '/assets/images/line-md_phone.svg';
<div className="max-w-7xl mx-auto px-6 py-10 grid grid-cols-1 md:grid-cols-4 gap-8 text-[#232528]">
return (
<footer className="bg-white border-t border-gray-200 mt-12">
{/* Top Gold Divider Line */}
<div className="h-[2px] bg-[#92722A] mb-8" />
{/* Main Footer Content */}
<div className="max-w-7xl mx-auto px-6 py-8 grid grid-cols-1 md:grid-cols-4 gap-8 md:gap-12 text-[#232528]">
{/* About FCSC */}
<div>
<h3 className="text-[#92722A] font-semibold mb-2">The Ministry</h3>
<h3 className="text-[#92722A] font-semibold mb-3 text-lg">About FCSC</h3>
<ul className="space-y-1 text-sm">
<li><a href="#" className="hover:underline">About us</a></li>
<li><a href="#" className="hover:underline">The Federal Competitiveness and Statistics Centre (FCSC) provides reliable data and insights to enhance the UAEs competitiveness and support sustainable national development.</a></li>
</ul>
</div>
{/* Our Policies */}
<div>
<h3 className="text-[#92722A] font-semibold mb-2">Using the website</h3>
<ul className="space-y-1 text-sm">
<h3 className="text-[#92722A] font-semibold mb-3 ml-16 text-lg">Our Policies</h3>
<ul className="space-y-1 ml-16 text-sm">
<li><a href="#" className="hover:underline">Disclaimer</a></li>
<li><a href="#" className="hover:underline">Privacy policy</a></li>
<li><a href="#" className="hover:underline">Terms and conditions</a></li>
</ul>
</div>
{/* Information and Support */}
<div>
<h3 className="text-[#92722A] font-semibold mb-2">Information and support</h3>
<ul className="space-y-1 text-sm">
<h3 className="text-[#92722A] font-semibold mb-3 ml-1 text-lg">Information and Support</h3>
<ul className="space-y-1 ml-1 text-sm">
<li><a href="#" className="hover:underline">Contact us</a></li>
<li><a href="#" className="hover:underline">FAQs</a></li>
<li><a href="#" className="hover:underline">Feedback and complaints</a></li>
</ul>
</div>
<div className="flex flex-col items-start md:items-end gap-2">
<img src="/assets/images/TawasolLogo.png" alt="Tawasul" className="h-12 mb-2" />
{/* Contact Details */}
<div className="flex flex-col items-start md:items-end gap-4">
<img
src="/assets/images/FCSCLogo.svg"
alt="FCSC Logo"
className="h-10 mb-2"
/>
{/* Phone Numbers */}
<div className="flex items-start gap-2 text-sm">
<img
src={phoneIconSrc}
alt="Phone Icon"
className="h-4 w-4 mt-[2px] opacity-80"
/>
<div className="flex flex-col leading-[1.4]">
<span className="font-semibold text-[#232528]">+971 4 608 0000</span>
<span className="font-semibold text-[#232528]">+971 4 327 3535</span>
</div>
</div>
{/* Email */}
<div className="flex items-center gap-2 text-sm">
<Phone size={16} className="text-green-600" />
<span className="font-semibold">171</span>
<Smartphone size={16} className="text-green-600 ml-3" />
<span className="font-semibold">04-7771777</span>
</div>
<div className="flex items-center gap-2 text-sm mt-1">
<Headphones size={16} className="text-gray-500" />
<span className="text-gray-700">Toll free: <span className="font-semibold">800 12</span></span>
<img
src={mailIconSrc}
alt="Mail Icon"
className="h-4 w-4 opacity-80"
/>
<a
href="mailto:info@fcsc.gov.ae"
className="text-[#232528] hover:underline"
>
info@fcsc.gov.ae
</a>
</div>
</div>
</div>
</div> {/* ✅ properly closed main content div */}
<div className="border-t border-gray-200 my-2" />
{/* Bottom Divider */}
<div className="border-t border-gray-200" />
<div className="max-w-7xl mx-auto px-6 py-4 flex flex-col md:flex-row justify-between items-center text-xs text-gray-600">
{/* Copyright Section */}
<div className="max-w-7xl mx-auto px-6 py-3 flex flex-col md:flex-row justify-between items-center text-xs text-gray-600">
<p>
© 2023. Ministry of Human Resources & Emiratisation. All rights reserved.{' '}
<span className="text-gray-400">Last updated on 24/04/2023 at 15:45</span>
Copyright © 2025 Federal Competitiveness and Statistics Centre, All Rights Reserved
</p>
<div className="flex items-center gap-3 mt-3 md:mt-0">
<span>Follow us on:</span>
<a href="#" className="hover:text-[#92722A]"><Facebook size={16} /></a>
<a href="#" className="hover:text-[#92722A]"><Instagram size={16} /></a>
<a href="#" className="hover:text-[#92722A]"><Linkedin size={16} /></a>
<a href="#" className="hover:text-[#92722A]"><Youtube size={16} /></a>
</div>
</div>
</footer>
);
};
export default Footer;
export default Footer;

View File

@ -35,13 +35,136 @@ const idleBorderColors = {
toolbar: '#C3C6CB',
};
// export const TextField = ({
// label,
// placeholder,
// value,
// onChange,
// type = 'text',
// width = 'auto', // 'auto' | number | string
// className = '',
// style = {},
// name,
// id,
// leftIcon,
// onFocus,
// onBlur,
// variant = 'default',
// required = false,
// showToggle = false,
// toggleLabels = { show: 'Show', hide: 'Hide' },
// error = '',
// readOnly = false,
// }) => {
// const wrapperStyle = {};
// if (width !== 'auto') {
// wrapperStyle.width = typeof width === 'number' ? `${width}px` : width;
// }
// const [focused, setFocused] = React.useState(false);
// const isPasswordField = type === 'password';
// const [showPassword, setShowPassword] = React.useState(false);
// React.useEffect(() => {
// if (!isPasswordField) {
// setShowPassword(false);
// }
// }, [isPasswordField]);
// const handleFocus = (event) => {
// setFocused(true);
// onFocus?.(event);
// };
// const handleBlur = (event) => {
// setFocused(false);
// onBlur?.(event);
// };
// const baseStyle = baseInputStyles[variant] || baseInputStyles.default;
// const inputStyle = { ...baseStyle, ...style };
// if (leftIcon) {
// inputStyle.paddingLeft = '40px';
// }
// if (isPasswordField && showToggle) {
// inputStyle.paddingRight = '44px';
// }
// const hasError = Boolean(error);
// inputStyle.borderColor = hasError
// ? '#B91C1C'
// : focused && !readOnly
// ? focusBorderColors[variant]
// : idleBorderColors[variant] || idleBorderColors.default;
// if (hasError) {
// inputStyle.backgroundColor = '#FEF2F2';
// inputStyle.color = '#B91C1C';
// }
// if (!hasError && variant !== 'toolbar') {
// inputStyle.color = '#232528';
// }
// if (readOnly) {
// inputStyle.backgroundColor = '#E6D7A2';
// inputStyle.cursor = 'not-allowed';
// inputStyle.opacity = '0.7';
// }
// if (!hasError && variant === 'toolbar') {
// inputStyle.color = '#5F646D';
// }
// return (
// <div style={wrapperStyle} className="w-full">
// {label && (
// <label htmlFor={id || name} className="block text-[14px] leading-[20px] font-medium text-[#232528] mb-1">
// {label}
// {required && <span className="text-[#B91C1C] ml-1">*</span>}
// </label>
// )}
// <div className="relative">
// <input
// id={id || name}
// name={name}
// type={isPasswordField ? (showPassword ? 'text' : 'password') : type}
// value={value}
// onChange={onChange}
// placeholder={placeholder}
// className={`w-full text-sm focus:outline-none ${className}`}
// style={inputStyle}
// onFocus={handleFocus}
// onBlur={handleBlur}
// required={required}
// readOnly={readOnly}
// />
// {leftIcon && (
// <img src={leftIcon} alt="" className="pointer-events-none absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4" />
// )}
// {isPasswordField && showToggle && (
// <button
// type="button"
// onClick={() => setShowPassword((prev) => !prev)}
// className="absolute right-3 top-1/2 -translate-y-1/2 text-[#92722A] hover:text-[#7b5c1f]"
// aria-label={showPassword ? toggleLabels.hide : toggleLabels.show}
// title={showPassword ? toggleLabels.hide : toggleLabels.show}
// >
// {showPassword ? (
// <svg xmlns="http://www.w3.org/2000/svg" className="h-4 w-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
// <path strokeLinecap="round" strokeLinejoin="round" d="M3 3l18 18" />
// <path strokeLinecap="round" strokeLinejoin="round" d="M10.58 10.58A2 2 0 0012 14a2 2 0 001.42-.58M16.68 16.68C15.28 17.54 13.69 18 12 18 7 18 3.73 14.82 2 12c.58-.9 1.34-1.83 2.26-2.68M9.88 4.13C10.56 4.05 11.27 4 12 4c5 0 8.27 3.18 10 6-.46.72-1.03 1.43-1.71 2.1" />
// </svg>
// ) : (
// <svg xmlns="http://www.w3.org/2000/svg" className="h-4 w-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
// <path strokeLinecap="round" strokeLinejoin="round" d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8S1 12 1 12z" />
// <circle cx="12" cy="12" r="3" />
// </svg>
// )}
// </button>
// )}
// </div>
// {error && <p className="mt-1 text-xs text-[#B91C1C]">{error}</p>}
// </div>
// );
// };
export const TextField = ({
label,
placeholder,
value,
onChange,
type = 'text',
width = 'auto', // 'auto' | number | string
width = 'auto',
className = '',
style = {},
name,
@ -60,49 +183,57 @@ export const TextField = ({
if (width !== 'auto') {
wrapperStyle.width = typeof width === 'number' ? `${width}px` : width;
}
const [focused, setFocused] = React.useState(false);
const isPasswordField = type === 'password';
const [showPassword, setShowPassword] = React.useState(false);
React.useEffect(() => {
if (!isPasswordField) {
setShowPassword(false);
}
if (!isPasswordField) setShowPassword(false);
}, [isPasswordField]);
const handleFocus = (event) => {
setFocused(true);
if (!readOnly) setFocused(true);
onFocus?.(event);
};
const handleBlur = (event) => {
setFocused(false);
onBlur?.(event);
};
// Base styles
const baseStyle = baseInputStyles[variant] || baseInputStyles.default;
const inputStyle = { ...baseStyle, ...style };
if (leftIcon) {
inputStyle.paddingLeft = '40px';
}
if (isPasswordField && showToggle) {
inputStyle.paddingRight = '44px';
}
if (leftIcon) inputStyle.paddingLeft = '40px';
if (isPasswordField && showToggle) inputStyle.paddingRight = '44px';
// Error, focus, idle, readOnly colors
const hasError = Boolean(error);
inputStyle.borderColor = hasError
? '#B91C1C'
: focused && !readOnly
? focusBorderColors[variant]
: idleBorderColors[variant] || idleBorderColors.default;
if (hasError) {
inputStyle.borderColor = '#B91C1C'; // red border
inputStyle.backgroundColor = '#FEF2F2';
inputStyle.color = '#B91C1C';
} else if (readOnly) {
inputStyle.borderColor = '#CBA344'; // standard
inputStyle.outline = 'none';
inputStyle.backgroundColor = '#FFFFFF'; // disabled color
inputStyle.cursor = 'not-allowed';
inputStyle.opacity = '0.8';
} else if (focused) {
inputStyle.borderColor = '#92722A'; // focused color
inputStyle.outline = 'none';
inputStyle.backgroundColor = '#FFFFFF';
} else {
inputStyle.borderColor = '#CBA344'; // standard color
inputStyle.outline = 'none';
inputStyle.backgroundColor = '#FFFFFF';
}
if (!hasError && variant !== 'toolbar') {
inputStyle.color = '#232528';
}
if (readOnly) {
inputStyle.backgroundColor = '#F9FAFB';
}
if (!hasError && variant === 'toolbar') {
inputStyle.color = '#5F646D';
}
inputStyle.color = hasError ? '#B91C1C' : '#232528';
return (
<div style={wrapperStyle} className="w-full">
{label && (
@ -127,7 +258,11 @@ export const TextField = ({
readOnly={readOnly}
/>
{leftIcon && (
<img src={leftIcon} alt="" className="pointer-events-none absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4" />
<img
src={leftIcon}
alt=""
className="pointer-events-none absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4"
/>
)}
{isPasswordField && showToggle && (
<button
@ -138,12 +273,30 @@ export const TextField = ({
title={showPassword ? toggleLabels.hide : toggleLabels.show}
>
{showPassword ? (
<svg xmlns="http://www.w3.org/2000/svg" className="h-4 w-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
<svg
xmlns="http://www.w3.org/2000/svg"
className="h-4 w-4"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
>
<path strokeLinecap="round" strokeLinejoin="round" d="M3 3l18 18" />
<path strokeLinecap="round" strokeLinejoin="round" d="M10.58 10.58A2 2 0 0012 14a2 2 0 001.42-.58M16.68 16.68C15.28 17.54 13.69 18 12 18 7 18 3.73 14.82 2 12c.58-.9 1.34-1.83 2.26-2.68M9.88 4.13C10.56 4.05 11.27 4 12 4c5 0 8.27 3.18 10 6-.46.72-1.03 1.43-1.71 2.1" />
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M10.58 10.58A2 2 0 0012 14a2 2 0 001.42-.58M16.68 16.68C15.28 17.54 13.69 18 12 18 7 18 3.73 14.82 2 12c.58-.9 1.34-1.83 2.26-2.68M9.88 4.13C10.56 4.05 11.27 4 12 4c5 0 8.27 3.18 10 6-.46.72-1.03 1.43-1.71 2.1"
/>
</svg>
) : (
<svg xmlns="http://www.w3.org/2000/svg" className="h-4 w-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
<svg
xmlns="http://www.w3.org/2000/svg"
className="h-4 w-4"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
>
<path strokeLinecap="round" strokeLinejoin="round" d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8S1 12 1 12z" />
<circle cx="12" cy="12" r="3" />
</svg>
@ -170,7 +323,7 @@ export const SelectField = ({
placeholder,
onFocus,
onBlur,
variant = 'default',
variant = '#92722A',
allowClear = false,
onClear,
clearValue = '',
@ -205,7 +358,7 @@ export const SelectField = ({
? focusBorderColors[variant]
: idleBorderColors[variant] || idleBorderColors.default;
if (hasError) {
inputStyle.backgroundColor = '#FEF2F2';
inputStyle.backgroundColor = '#E6D7A2';
inputStyle.color = '#B91C1C';
} else if (variant === 'toolbar') {
inputStyle.color = hasValue && value !== 'All' && value !== 'Status' && value !== 'Year' && value !== 'Quarter' && value !== 'Emirates' ? '#232528' : '#5F646D';

View File

@ -109,7 +109,7 @@ export const SubmissionHistory = ({ history = [], loading = false, error = '' })
const toolbar = (
<div className="px-4 pt-6 pb-2 flex flex-col gap-3 md:flex-row md:items-center md:justify-between">
<div className="flex flex-col gap-1">
<h3 className="text-[18px] leading-[28px] font-medium text-[#232528]">
<h3 className="text-[18px] leading-[28px] ml-2 font-medium text-[#232528]">
Submission History
</h3>
<div className="flex gap-3 text-sm text-gray-500">
@ -138,7 +138,7 @@ export const SubmissionHistory = ({ history = [], loading = false, error = '' })
type="button"
onClick={() => downloadCsv(rowsData)}
disabled={!rowsData.length}
className={`inline-flex h-10 items-center gap-2 rounded-md border px-4 text-sm font-medium ${
className={`inline-flex h-10 items-center gap-2 rounded-md border px-4 mr-2 text-sm font-medium ${
rowsData.length
? 'border-[#92722A] text-[#92722A] hover:bg-[#F2ECCF]'
: 'border-gray-200 text-gray-400 cursor-not-allowed'

View File

@ -30,8 +30,8 @@ const SurveyCarousel = ({
setCurrentIndex((prev) => (prev === surveys.length - 1 ? 0 : prev + 1));
return (
<div className="bg-white rounded-lg shadow-sm ring-1 ring-gray-200 mb-8 overflow-hidden">
<div className="rounded-none bg-[#F2ECCF] px-6 py-4 min-h-[148px] flex items-center justify-between transition-all duration-500">
<div className="bg-black rounded-lg shadow-sm ring-1 ring-gray-200 mb-8 ml-2 ">
<div className="rounded-none bg-[#F2ECCF] px-8 py-8 min-h-[148px] flex items-center justify-between transition-all duration-500">
<div className="flex-1">
<h3 className="text-[18px] leading-[28px] font-medium text-[#232528]">
{current?.title || '—'}

View File

@ -99,18 +99,24 @@ export const DetailedOverview = ({ submission, onBack }) => {
}, [searchTerm, selectedStatus, submissionData]);
const downloadCsv = useCallback(() => {
if (!filteredProducts.length || !submissionData) return;
if (!filteredProducts.length || !submissionData || !quarterPeriods) return;
// Get the quarter and year from quarterPeriods
const prevQuarter = quarterPeriods.previous_quarter || 'Q1';
const currQuarter = quarterPeriods.current_quarter || 'Q2';
const forecastQuarter = quarterPeriods.forecast_quarter || 'Q3';
const year = quarterPeriods.year || new Date().getFullYear();
const headers = [
'HS Code',
'Product',
'Unit',
'Previous Quarter Quantity',
'Previous Quarter Cost (AED)',
'Current Quarter Quantity',
'Current Quarter Cost (AED)',
'Forecast Quarter Quantity',
'Forecast Quarter Cost (AED)',
`Quantity (${prevQuarter} ${year})`,
`Cost (${prevQuarter} ${year}) (AED)`,
`Quantity (${currQuarter} ${year})`,
`Cost (${currQuarter} ${year}) (AED)`,
`Forecast Quantity (${forecastQuarter} ${year})`,
`Forecast Cost (${forecastQuarter} ${year}) (AED)`,
'Status'
];
@ -166,10 +172,6 @@ export const DetailedOverview = ({ submission, onBack }) => {
unit.uom || 'N/A',
product.previous_quantity || '0',
`AED ${product.previous_cost || '0'}`,
product.current_quantity || '0',
`AED ${product.current_cost || '0'}`,
product.forecast_quantity || '0',
`AED ${product.forecast_cost || '0'}`,
<span
key={`status-${index}`}
className={`inline-flex items-center justify-center rounded-md px-3 py-1 text-xs font-medium ${
@ -179,14 +181,23 @@ export const DetailedOverview = ({ submission, onBack }) => {
>
{product.is_active ? 'Approved' : 'Pending'}
</span>,
new Date(product.updated_at || product.created_at).toLocaleString('en-US', {
year: 'numeric',
month: 'short',
day: 'numeric',
hour: '2-digit',
minute: '2-digit',
hour12: true
}),
<button
key={`view-${index}`}
onClick={() => handleViewDetails(product)}
disabled={loadingProduct}
className="text-[#92722A] hover:underline text-sm font-medium disabled:opacity-50"
className="text-[#92722A] hover:underline text-sm font-medium disabled:opacity-50 pl-6"
>
{loadingProduct && selectedProduct?.id === product.id ? 'Loading...' : 'View Details'}
</button>
</button>
];
});
}, [filteredProducts]);
@ -282,21 +293,18 @@ export const DetailedOverview = ({ submission, onBack }) => {
'HS Code',
'Product',
'Unit',
'Previous Qty',
'Previous Cost (AED)',
'Current Qty',
'Current Cost (AED)',
'Forecast Qty',
'Forecast Cost (AED)',
'Quantity',
'Cost(AED)',
'Status',
'Submission Date & Time',
'Action'
];
// Calculate total products and average cost
const totalProducts = filteredProducts.length;
const totalCost = submissionData.products?.reduce((sum, product) => {
return sum + parseFloat(product.current_cost || 0);
}, 0);
const totalProducts = filteredProducts?.length || 0;
const totalCost = filteredProducts?.reduce((sum, product) => {
return sum + (parseFloat(product.previous_cost) || 0);
}, 0) || 0;
const averageCost = totalProducts > 0 ? (totalCost / totalProducts).toFixed(2) : 0;
// If a product is selected, show the product details view
@ -371,11 +379,12 @@ export const DetailedOverview = ({ submission, onBack }) => {
<section className="bg-white rounded-lg border border-[#E2E8F0] shadow-sm">
<Table
headers={columns}
// columnwidth={columnwidth}
rows={rows}
beforeHeader={toolbar}
separated
rowGapClass="border-spacing-y-2"
className="w-full min-w-[1200px] [&_td]:whitespace-nowrap [&_th]:whitespace-nowrap"
className="w-full border-collapse [&_td]:whitespace-nowrap [&_th]:whitespace-nowrap [&_th]:px-3 [&_td]:px-3 [&_th:last-child]:text-right [&_td:last-child]:text-right [&_th:last-child]:pr-4 [&_td:last-child]:pr-4"
/>
</section>
</div>

View File

@ -115,12 +115,12 @@ const Card = ({ children }) => (
</div>
);
const SectionBox = ({ title, children, badgeBg = '#F3F4F6', badgeText = '#374151' }) => (
const SectionBox = ({ title, children, badgeBg = '#ffffff', badgeText = '#374151' }) => (
<div className="rounded-md ring-1 ring-gray-200 bg-white">
<div className="p-5">
<div className="mb-4">
<div className="font-medium mb-2">
<span
className="inline-flex items-center rounded text-xs font-medium px-2 py-1"
className="inline-flex items-center rounded text-sm font-medium px-2 py-1"
style={{ backgroundColor: badgeBg, color: badgeText }}
>
{title}

View File

@ -256,6 +256,7 @@ const ReviewSubmit = ({
hasSubmitted = false,
}) => {
const [confirm, setConfirm] = React.useState(false);
const [remarks, setRemarks] = React.useState('');
const establishmentDetails = React.useMemo(() => buildEstablishmentDetails(establishment), [establishment]);
const employmentDetails = React.useMemo(() => buildEmploymentDetails(establishment), [establishment]);
const productRows = React.useMemo(() => buildProductRows(products), [products]);
@ -693,16 +694,16 @@ const ReviewSubmit = ({
</div>
{/* Remarks */}
<div className="mt-4">
{/* <div className="mt-4">
<p className="text-sm font-semibold text-[#2E2E2E] mb-2">Remarks</p>
<div className="border border-[#CBA344] rounded-md p-3 text-sm w-[50%] min-h-[60px]">
<div className="border border-[#CBA344] rounded-md p-3 text-sm w-[100%] min-h-[60px]">
{product.remarks?.trim() ? (
product.remarks
) : (
<span className="text-gray-400 ">Add Short Note..</span>
)}
</div>
</div>
</div> */}
</section>
))}
@ -710,6 +711,19 @@ const ReviewSubmit = ({
{/* Remarks Section */}
<div className="rounded-[16px] border border-[#E6EAF5] bg-white p-6 shadow-[0_16px_32px_rgba(15,23,42,0.08)] mb-6">
<h3 className="text-base font-semibold text-gray-900 mb-4">Remarks</h3>
<div className="space-y-2">
<textarea
placeholder="Enter your remarks here..."
value={remarks}
onChange={(e) => setRemarks(e.target.value)}
className="w-full min-h-[100px] p-3 border border-#CBA344 rounded-md focus:ring-2 focus:ring-[#CBA344] focus:border-transparent"
/>
</div>
</div>
{/* Confirmation and Buttons */}
<div className="rounded-[16px] border border-[#E6EAF5] bg-white p-6 shadow-[0_16px_32px_rgba(15,23,42,0.08)]">
{!hasSubmitted && (

View File

@ -1,7 +1,7 @@
import React from 'react';
import { Link } from 'react-router-dom';
import AdminHeader from '@/components/admin/AdminHeader';
import AdminUsersContent from './configuration/AdminUsers';
// import AdminUsersContent from './configuration/AdminUsers';
const caretUpSrc = '/assets/images/caret-up.svg';

View File

@ -4,7 +4,9 @@ import AdminHeader from '@/components/admin/AdminHeader';
import QuarterlyWindows from '@/pages/Admin/configuration/QuarterlyWindows';
import IsicHsCodes from '@/pages/Admin/configuration/IsicHsCodes';
import UnitMaster from '@/pages/Admin/configuration/UnitMaster';
import AdminUsers from '@/pages/Admin/configuration/AdminUsers';
import AdminUsers from '@/pages/Admin/configuration/AdminUsers/ListAdminUsers';
import ListAdminUsers from '@/pages/Admin/configuration/AdminUsers/ListAdminUsers';
import EditAdminUser from '@/pages/Admin/configuration/AdminUsers/EditAdminUser';
import CompanyProfile from '@/pages/Admin/configuration/CompanyProfile';
const caretUpSrc = '/assets/images/caret-up.svg';

View File

@ -1,218 +0,0 @@
import React from 'react';
import Table from '@/components/common/Table';
const searchIconSrc = '/assets/images/material-symbols_search-rounded.svg';
const exportIconSrc = '/assets/images/downloadsimple.svg';
const addIconSrc = '/assets/images/ic_baseline-plus.svg';
const pencilActiveSrc = '/assets/images/pencilsimple.svg';
const pencilInactiveSrc = '/assets/images/pencilsimple-inactive.svg';
const trashActiveSrc = '/assets/images/trash - active.svg';
const trashInactiveSrc = '/assets/images/trash - inactive.svg';
const adminUserIconSrc = '/assets/images/usergear.svg';
const activeUserIconSrc = '/assets/images/active-user.svg';
const inactiveUserIconSrc = '/assets/images/inactive-user.svg';
const resetPasswordIconSrc = '/assets/images/hugeicons_reset-password.svg';
const resetPasswordInactiveIconSrc = '/assets/images/hugeicons_reset-password-inactive.svg';
const SummaryCard = ({ label, count, icon, iconBg = '#FDF7EB' }) => (
<div className="flex min-h-[96px] min-w-[220px] flex-1 items-center justify-between rounded-[16px] border border-[#E6EAF5] bg-white px-6 py-5 shadow-[0_12px_24px_rgba(15,23,42,0.06)]">
<div className="flex items-center gap-4">
<span
className="flex h-12 w-12 items-center justify-center rounded-full"
style={{ backgroundColor: iconBg }}
>
{icon && <img src={icon} alt={label} className="h-6 w-6" />}
</span>
<span className="text-sm font-medium text-[#5F646D]">{label}</span>
</div>
<span className="text-2xl font-semibold text-[#232528]">{count}</span>
</div>
);
const StatusBadge = ({ status }) => {
const map = {
Active: 'bg-green-50 text-green-700 ring-1 ring-green-200',
Closed: 'bg-gray-100 text-gray-700 ring-1 ring-gray-200',
};
return (
<span className={`inline-flex items-center rounded-md px-2 py-0.5 text-xs font-medium ${map[status] || map.Closed}`}>
{status}
</span>
);
};
const AdminUsers = () => {
const [users, setUsers] = React.useState([
{
name: 'Ahmed Al-Mansouri',
email: 'ahmed.mansouri@fcsc.gov.ae',
lastLogin: '11th Mar, 2025 14:30',
status: 'Active',
},
{
name: 'Sarah Johnson',
email: 'sarah.johnson@fcsc.gov.ae',
lastLogin: '13th Mar, 2025 13:00',
status: 'Active',
},
{
name: 'Mohammed Hassan',
email: 'mohammed.hassan@fcsc.gov.ae',
lastLogin: '18th Mar, 2025 22:40',
status: 'Active',
},
{
name: 'Emily Davis',
email: 'emily.davis@fcsc.gov.ae',
lastLogin: '20th Mar, 2025 18:40',
status: 'Closed',
},
]);
const [editingRow, setEditingRow] = React.useState(null);
const [deletingRow, setDeletingRow] = React.useState(null);
const totalUsers = users.length;
const activeUsers = users.filter((user) => user.status === 'Active').length;
const inactiveUsers = totalUsers - activeUsers;
const summaryItems = [
{
label: 'Total Users',
count: totalUsers,
icon: adminUserIconSrc,
iconBg: '#FDF7EB',
},
{
label: 'Active User',
count: activeUsers,
icon: activeUserIconSrc,
iconBg: '#E7F2FF',
},
{
label: 'Inactive User',
count: inactiveUsers,
icon: inactiveUserIconSrc,
iconBg: '#F5F5F7',
},
];
const headers = ['Name', 'Email', 'Last Login', 'Status', 'Actions'];
const rows = users.map((user) => [
user.name,
user.email,
user.lastLogin,
<StatusBadge status={user.status} />,
'actions',
]);
const handleEdit = (index) => {
setEditingRow(index);
// Future: open edit modal
};
const handleDelete = (index) => {
setDeletingRow(index);
// Future: open delete confirmation
};
const tableToolbar = (
<div className="px-6 py-4 flex flex-col lg:flex-row lg:items-center lg:justify-between gap-3 bg-white">
<h3 className="text-sm font-medium text-[#232528]">Admin Users</h3>
<div className="flex flex-col sm:flex-row sm:items-center sm:gap-3 w-full lg:w-auto">
<div className="relative flex-1 min-w-[220px]">
<input
type="text"
placeholder="Search by Names, Email or Role..."
className="w-full h-10 rounded-md border border-[#E5E7EB] pl-10 pr-4 text-sm focus:outline-none"
/>
<img
src={searchIconSrc}
alt="Search"
className="absolute left-3 top-1/2 -translate-y-1/2 h-5 w-5"
/>
</div>
<div className="flex items-center gap-2 sm:justify-end">
<button className="h-9 px-3 rounded-md bg-[#F7F7F7] border border-[#C3C6CB] text-sm inline-flex items-center gap-2 cursor-pointer">
<img src={exportIconSrc} alt="Export" className="h-4 w-4" />
<span>Export CSV</span>
</button>
<button className="h-9 px-3 rounded-md bg-[#B68A35] text-white text-sm inline-flex items-center gap-2 cursor-pointer">
<img src={addIconSrc} alt="Add" className="h-4 w-4" />
<span>Add User</span>
</button>
</div>
</div>
</div>
);
return (
<div className="space-y-5">
<div className="grid gap-4 sm:grid-cols-2 xl:grid-cols-3">
{summaryItems.map((item) => (
<SummaryCard key={item.label} {...item} />
))}
</div>
<Table
headers={headers}
rows={rows}
beforeHeader={tableToolbar}
renderCell={(value, rowIndex, colIndex) => {
if (colIndex === 4) {
const user = users[rowIndex];
const isActive = user.status === 'Active';
return (
<div className="flex items-center gap-2">
<button
type="button"
className="h-8 w-8 grid place-items-center rounded hover:bg-gray-50 cursor-pointer"
title="Edit"
aria-label="Edit user"
onClick={() => handleEdit(rowIndex)}
>
<img
src={editingRow === rowIndex ? pencilActiveSrc : pencilInactiveSrc}
alt="Edit"
className="h-4 w-4"
/>
</button>
<button
type="button"
className="h-8 w-8 grid place-items-center rounded hover:bg-gray-50 cursor-pointer"
title="Delete"
aria-label="Delete user"
onClick={() => handleDelete(rowIndex)}
>
<img
src={deletingRow === rowIndex ? trashActiveSrc : trashInactiveSrc}
alt="Delete"
className="h-4 w-4"
/>
</button>
<button
type="button"
className={`h-8 w-8 grid place-items-center rounded hover:bg-gray-50 cursor-pointer ${
isActive ? '' : 'opacity-60'
}`}
title={isActive ? 'Reset password' : 'Reset disabled for inactive user'}
aria-label="Reset user password"
disabled={!isActive}
>
<img
src={isActive ? resetPasswordIconSrc : resetPasswordInactiveIconSrc}
alt="Reset password"
className="h-4 w-4"
/>
</button>
</div>
);
}
return value;
}}
/>
</div>
);
}
;
export default AdminUsers;

View File

@ -0,0 +1,232 @@
import React, { useState, useEffect } from 'react';
import { X } from 'lucide-react';
import { TextField, SelectField } from '@/components/common/FormControls';
import { createadminusers } from '@/services/configuration/adminService';
import CustomToast from '@/components/common/CustomToast';
const AddAdminUsers = ({ isOpen, onClose, onSave }) => {
const [formData, setFormData] = useState({
name: '',
email: '',
password: '',
confirmPassword: '',
role: 'Super Admin',
status: 'Active',
});
const [errors, setErrors] = useState({});
const [loading, setLoading] = useState(false);
const [toastData, setToastData] = useState(null); // 👈 for custom toast
// show toast helper
const showToast = (message, type = 'success') => {
setToastData({ message, type });
// Clear any existing timeout
if (window.toastTimeout) {
clearTimeout(window.toastTimeout);
}
// Set new timeout
window.toastTimeout = setTimeout(() => {
setToastData(null);
}, 4000);
};
const validateForm = () => {
const newErrors = {};
if (!formData.name.trim()) newErrors.name = 'Name is required';
if (!formData.email.trim()) newErrors.email = 'Email is required';
else if (!/^\S+@\S+\.\S+$/.test(formData.email))
newErrors.email = 'Please enter a valid email';
if (!formData.status) newErrors.status = 'Status is required';
if (!formData.password) newErrors.password = 'Password is required';
else if (formData.password.length < 12)
newErrors.password = 'Password must be at least 12 characters';
if (formData.password !== formData.confirmPassword)
newErrors.confirmPassword = 'Passwords do not match';
return newErrors;
};
useEffect(() => {
if (isOpen) {
setFormData({
name: '',
email: '',
password: '',
confirmPassword: '',
role: 'Super Admin',
status: 'Active',
});
setErrors({});
}
}, [isOpen]);
const handleChange = (e) => {
const { name, value } = e.target;
setFormData((prev) => ({ ...prev, [name]: value }));
if (errors[name]) setErrors((prev) => ({ ...prev, [name]: '' }));
};
const handleSubmit = async (e) => {
e.preventDefault();
const formErrors = validateForm();
if (Object.keys(formErrors).length > 0) {
setErrors(formErrors);
showToast('Please fix the form errors before submitting', 'error');
return;
}
setLoading(true);
try {
const adminData = {
name: formData.name,
email: formData.email,
password: formData.password,
is_active: formData.status === 'Active',
};
const response = await createadminusers(adminData);
showToast('Admin user created successfully!', 'success');
// Add a small delay before closing to show the success message
setTimeout(() => {
onSave?.(response?.data || {});
onClose();
}, 1000);
} catch (error) {
console.error('Error creating admin user:', error);
const errorMessage = error.response?.data?.message || 'Failed to create admin user';
showToast(errorMessage, 'error');
} finally {
setLoading(false);
}
};
if (!isOpen) return null;
return (
<>
<div className="fixed inset-0 bg-black/30 backdrop-blur-sm flex items-center justify-center p-4 z-[9998]">
<div className="bg-white rounded-2xl w-[628px] max-h-[calc(100vh-2rem)] overflow-y-auto">
{/* Header */}
<div className="flex justify-between items-center p-6 border-b sticky top-0 bg-white z-10">
<h2 className="text-xl font-semibold text-[#232528]">Add User</h2>
<button
type="button"
onClick={onClose}
className="text-gray-400 hover:text-gray-500"
aria-label="Close"
>
<X className="h-5 w-5" />
</button>
</div>
{/* Body */}
<div className="p-8">
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<TextField
label="Name"
name="name"
value={formData.name}
onChange={handleChange}
error={errors.name}
required
placeholder="Enter name"
/>
<TextField
label="Email"
name="email"
type="email"
value={formData.email}
onChange={handleChange}
error={errors.email}
required
placeholder="Enter email"
/>
<TextField
label="Password"
name="password"
type="password"
value={formData.password}
onChange={handleChange}
error={errors.password}
required
placeholder="Enter password"
showToggle
/>
<TextField
label="Confirm Password"
name="confirmPassword"
type="password"
value={formData.confirmPassword}
onChange={handleChange}
error={errors.confirmPassword}
required
placeholder="Confirm password"
showToggle
/>
<div className="md:col-span-2 space-y-2">
<label className="block text-sm font-medium text-gray-700 mb-1">
Status <span className="text-red-500">*</span>
</label>
<SelectField
name="status"
value={formData.status}
onChange={handleChange}
options={[
{ label: 'Active', value: 'Active' },
{ label: 'Inactive', value: 'Inactive' },
]}
error={errors.status}
/>
{errors.status && (
<p className="mt-1 text-sm text-red-600">{errors.status}</p>
)}
</div>
</div>
{/* Footer */}
<div className="flex justify-end space-x-3 pt-6">
<button
type="button"
onClick={onClose}
className="px-5 py-2.5 text-sm font-medium text-gray-700 bg-white border border-gray-300 rounded-lg hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-[#B68A35] transition-colors"
>
Cancel
</button>
<button
type="button"
onClick={handleSubmit}
disabled={loading}
className={`px-5 py-2.5 text-sm font-medium text-white rounded-lg focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-[#B68A35] transition-colors ${
loading
? 'bg-gray-400 cursor-not-allowed'
: 'bg-[#92722A] hover:bg-[#9A762D]'
}`}
>
{loading ? 'Saving...' : 'Save'}
</button>
</div>
</div>
</div>
</div>
{/* ✅ Custom Toast Display */}
{toastData && (
<div className="fixed inset-0 z-[9999]">
<CustomToast
message={toastData.message}
type={toastData.type}
onClose={() => setToastData(null)}
/>
</div>
)}
</>
);
};
export default AddAdminUsers;

View File

@ -0,0 +1,264 @@
import React, { useState, useEffect } from 'react';
import { X } from 'lucide-react';
import { TextField, SelectField } from '@/components/common/FormControls';
import CustomToast from '@/components/common/CustomToast';
const AdminUserForm = ({
isOpen,
onClose,
onSave,
mode = 'add',
initialData = null
}) => {
const [formData, setFormData] = useState({
name: '',
email: '',
password: '',
confirmPassword: '',
role: 'Super Admin',
status: 'Active',
});
const [errors, setErrors] = useState({});
const [loading, setLoading] = useState(false);
const [toastData, setToastData] = useState(null);
// Set form data when initialData changes (for edit mode)
useEffect(() => {
if (mode === 'edit' && initialData) {
setFormData({
name: initialData.name || '',
email: initialData.email || '',
password: '',
confirmPassword: '',
role: initialData.role || 'Super Admin',
status: initialData.is_active ? 'Active' : 'Inactive',
});
} else if (mode === 'add') {
// Reset form for add mode
setFormData({
name: '',
email: '',
password: '',
confirmPassword: '',
role: 'Super Admin',
status: 'Active',
});
setErrors({});
}
}, [mode, initialData]);
const showToast = (message, type = 'success') => {
setToastData({ message, type });
if (window.toastTimeout) {
clearTimeout(window.toastTimeout);
}
window.toastTimeout = setTimeout(() => {
setToastData(null);
}, 4000);
};
const validateForm = () => {
const newErrors = {};
if (!formData.name.trim()) newErrors.name = 'Name is required';
if (!formData.email.trim()) newErrors.email = 'Email is required';
else if (!/^\S+@\S+\.\S+$/.test(formData.email))
newErrors.email = 'Please enter a valid email';
if (!formData.status) newErrors.status = 'Status is required';
// Only validate password if it's add mode or password is being changed
if (mode === 'add' || formData.password) {
if (!formData.password) newErrors.password = 'Password is required';
else if (formData.password.length < 12)
newErrors.password = 'Password must be at least 12 characters';
if (formData.password !== formData.confirmPassword)
newErrors.confirmPassword = 'Passwords do not match';
}
return newErrors;
};
const handleChange = (e) => {
const { name, value } = e.target;
setFormData((prev) => ({ ...prev, [name]: value }));
if (errors[name]) setErrors((prev) => ({ ...prev, [name]: '' }));
};
const handleSubmit = async (e) => {
e.preventDefault();
const formErrors = validateForm();
if (Object.keys(formErrors).length > 0) {
setErrors(formErrors);
showToast('Please fix the form errors before submitting', 'error');
return;
}
setLoading(true);
try {
const adminData = {
name: formData.name,
email: formData.email,
...(formData.password && { password: formData.password }), // Only include password if provided
is_active: formData.status === 'Active',
};
await onSave(adminData);
showToast(
mode === 'add'
? 'Admin user created successfully!'
: 'Admin user updated successfully!',
'success'
);
// Add a small delay before closing to show the success message
setTimeout(() => {
if (mode === 'add') {
onClose();
}
}, 1000);
} catch (error) {
console.error(`Error ${mode === 'add' ? 'creating' : 'updating'} admin user:`, error);
const errorMessage = error.response?.data?.message ||
`Failed to ${mode === 'add' ? 'create' : 'update'} admin user`;
showToast(errorMessage, 'error');
} finally {
setLoading(false);
}
};
if (!isOpen) return null;
return (
<>
<div className="fixed inset-0 bg-black/30 backdrop-blur-sm flex items-center justify-center p-4 z-[9998]">
<div className="bg-white rounded-2xl w-[628px] max-h-[calc(100vh-2rem)] overflow-y-auto">
{/* Header */}
<div className="flex justify-between items-center p-6 border-b sticky top-0 bg-white z-10">
<h2 className="text-xl font-semibold text-[#232528]">
{mode === 'add' ? 'Add User' : 'Edit User'}
</h2>
<button
type="button"
onClick={onClose}
className="text-gray-400 hover:text-gray-500"
aria-label="Close"
>
<X className="h-5 w-5" />
</button>
</div>
{/* Body */}
<div className="p-8">
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<TextField
label="Name"
name="name"
value={formData.name}
onChange={handleChange}
error={errors.name}
required
placeholder="Enter name"
/>
<TextField
label="Email"
name="email"
type="email"
value={formData.email}
onChange={handleChange}
error={errors.email}
required
placeholder="Enter email"
disabled={mode === 'edit'} // Disable email in edit mode
/>
<TextField
label={mode === 'add' ? 'Password' : 'New Password (leave blank to keep current)'}
name="password"
type="password"
value={formData.password}
onChange={handleChange}
error={errors.password}
required={mode === 'add'}
placeholder={mode === 'add' ? 'Enter password' : 'Enter new password'}
showToggle
/>
<TextField
label={mode === 'add' ? 'Confirm Password' : 'Confirm New Password'}
name="confirmPassword"
type="password"
value={formData.confirmPassword}
onChange={handleChange}
error={errors.confirmPassword}
required={mode === 'add'}
placeholder={mode === 'add' ? 'Confirm password' : 'Confirm new password'}
showToggle
/>
<div className="md:col-span-2 space-y-2">
<label className="block text-sm font-medium text-gray-700 mb-1">
Status <span className="text-red-500">*</span>
</label>
<SelectField
name="status"
value={formData.status}
onChange={handleChange}
options={[
{ label: 'Active', value: 'Active' },
{ label: 'Inactive', value: 'Inactive' },
]}
error={errors.status}
/>
{errors.status && (
<p className="mt-1 text-sm text-red-600">{errors.status}</p>
)}
</div>
</div>
{/* Footer */}
<div className="flex justify-end space-x-3 pt-6">
<button
type="button"
onClick={onClose}
className="px-5 py-2.5 text-sm font-medium text-gray-700 bg-white border border-gray-300 rounded-lg hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-[#B68A35] transition-colors"
>
Cancel
</button>
<button
type="button"
onClick={handleSubmit}
disabled={loading}
className={`px-5 py-2.5 text-sm font-medium text-white rounded-lg focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-[#B68A35] transition-colors ${
loading
? 'bg-gray-400 cursor-not-allowed'
: 'bg-[#92722A] hover:bg-[#9A762D]'
}`}
>
{loading
? (mode === 'add' ? 'Saving...' : 'Updating...')
: (mode === 'add' ? 'Save' : 'Update')
}
</button>
</div>
</div>
</div>
</div>
{/* Custom Toast Display */}
{toastData && (
<div className="fixed inset-0 z-[9999]">
<CustomToast
message={toastData.message}
type={toastData.type}
onClose={() => setToastData(null)}
/>
</div>
)}
</>
);
};
export default AdminUserForm;

View File

@ -0,0 +1,220 @@
import React, { useState, useEffect } from 'react';
import { useParams, useNavigate, useLocation } from 'react-router-dom';
import { X } from 'lucide-react';
import { TextField, SelectField } from '@/components/common/FormControls';
import { getAdminUserById, updateAdminUser } from '@/services/configuration/adminService';
import CustomToast from '@/components/common/CustomToast';
const EditAdminUser = () => {
const { id } = useParams();
const navigate = useNavigate();
const location = useLocation();
const [formData, setFormData] = useState({
name: '',
email: '',
status: 'Active',
});
const [errors, setErrors] = useState({});
const [loading, setLoading] = useState(true);
const [saving, setSaving] = useState(false);
const [toastData, setToastData] = useState(null);
// Show toast helper
const showToast = (message, type = 'success') => {
setToastData({ message, type });
if (window.toastTimeout) {
clearTimeout(window.toastTimeout);
}
window.toastTimeout = setTimeout(() => {
setToastData(null);
}, 4000);
};
// Fetch user data
useEffect(() => {
const fetchUser = async () => {
try {
setLoading(true);
const userData = location.state?.user || await getAdminUserById(id);
setFormData({
name: userData.name || '',
email: userData.email || '',
status: userData.is_active ? 'Active' : 'Inactive',
});
} catch (error) {
console.error('Error fetching admin user:', error);
showToast('Failed to load user data', 'error');
} finally {
setLoading(false);
}
};
fetchUser();
}, [id, location.state]);
const validateForm = () => {
const newErrors = {};
if (!formData.name.trim()) newErrors.name = 'Name is required';
if (!formData.email.trim()) newErrors.email = 'Email is required';
else if (!/^\S+@\S+\.\S+$/.test(formData.email))
newErrors.email = 'Please enter a valid email';
if (!formData.status) newErrors.status = 'Status is required';
return newErrors;
};
const handleChange = (e) => {
const { name, value } = e.target;
setFormData(prev => ({ ...prev, [name]: value }));
if (errors[name]) setErrors(prev => ({ ...prev, [name]: '' }));
};
const handleSubmit = async (e) => {
e.preventDefault();
const formErrors = validateForm();
if (Object.keys(formErrors).length > 0) {
setErrors(formErrors);
showToast('Please fix the form errors before submitting', 'error');
return;
}
setSaving(true);
try {
const adminData = {
name: formData.name,
email: formData.email,
is_active: formData.status === 'Active',
};
await updateAdminUser(id, adminData);
showToast('Admin user updated successfully!', 'success');
// Navigate back after a short delay
setTimeout(() => {
navigate('/admin/configuration/admin-users');
}, 1000);
} catch (error) {
console.error('Error updating admin user:', error);
const errorMessage = error.response?.data?.message || 'Failed to update admin user';
showToast(errorMessage, 'error');
} finally {
setSaving(false);
}
};
const handleClose = () => {
navigate('/admin/configuration/admin-users');
};
if (loading) {
return (
<div className="flex items-center justify-center min-h-screen">
<div className="animate-spin rounded-full h-12 w-12 border-t-2 border-b-2 border-[#92722A]"></div>
</div>
);
}
return (
<div className="min-h-screen bg-[#F7F7F7] p-6">
<div className="max-w-4xl mx-auto bg-white rounded-2xl shadow-sm overflow-hidden">
{/* Header */}
<div className="flex justify-between items-center p-6 border-b sticky top-0 bg-white z-10">
<h2 className="text-xl font-semibold text-[#232528]">Edit User</h2>
<button
type="button"
onClick={handleClose}
className="text-gray-400 hover:text-gray-500"
aria-label="Close"
>
<X className="h-5 w-5" />
</button>
</div>
{/* Body */}
<div className="p-8">
<form onSubmit={handleSubmit}>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<TextField
label="Name"
name="name"
value={formData.name}
onChange={handleChange}
error={errors.name}
required
placeholder="Enter name"
/>
<TextField
label="Email"
name="email"
type="email"
value={formData.email}
onChange={handleChange}
error={errors.email}
required
placeholder="Enter email"
disabled={true} // Email is typically not editable
/>
<div className="md:col-span-2 space-y-2">
<label className="block text-sm font-medium text-gray-700 mb-1">
Status <span className="text-red-500">*</span>
</label>
<SelectField
name="status"
value={formData.status}
onChange={handleChange}
options={[
{ label: 'Active', value: 'Active' },
{ label: 'Inactive', value: 'Inactive' },
]}
error={errors.status}
/>
{errors.status && (
<p className="mt-1 text-sm text-red-600">{errors.status}</p>
)}
</div>
</div>
{/* Footer */}
<div className="flex justify-end space-x-3 pt-6">
<button
type="button"
onClick={handleClose}
className="px-5 py-2.5 text-sm font-medium text-gray-700 bg-white border border-gray-300 rounded-lg hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-[#B68A35] transition-colors"
>
Cancel
</button>
<button
type="submit"
disabled={saving}
className={`px-5 py-2.5 text-sm font-medium text-white rounded-lg focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-[#B68A35] transition-colors ${
saving
? 'bg-gray-400 cursor-not-allowed'
: 'bg-[#92722A] hover:bg-[#9A762D]'
}`}
>
{saving ? 'Saving...' : 'Update'}
</button>
</div>
</form>
</div>
</div>
{/* Toast Notification */}
{toastData && (
<div className="fixed inset-0 z-[9999]">
<CustomToast
message={toastData.message}
type={toastData.type}
onClose={() => setToastData(null)}
/>
</div>
)}
</div>
);
};
export default EditAdminUser;

View File

@ -0,0 +1,544 @@
import React, { useState, useEffect } from 'react';
import Table from '@/components/common/Table';
import AddAdminUsers from './AddAdminUsers';
import { useNavigate } from 'react-router-dom';
import { getAdminUser, getAdminUserById, updateAdminUser } from '@/services/configuration/adminService';
import CustomToast from '@/components/common/CustomToast';
import { X } from 'lucide-react';
import { TextField, SelectField } from '@/components/common/FormControls';
const searchIconSrc = '/assets/images/material-symbols_search-rounded.svg';
const exportIconSrc = '/assets/images/DownloadSimple.svg';
const addIconSrc = '/assets/images/ic_baseline-plus.svg';
const pencilActiveSrc = '/assets/images/PencilSimple.svg';
const pencilInactiveSrc = '/assets/images/pencilsimple-inactive.svg';
const trashActiveSrc = '/assets/images/Trash - active.svg';
const trashInactiveSrc = '/assets/images/Trash - Inactive.svg';
const adminUserIconSrc = '/assets/images/UserGear.svg';
const activeUserIconSrc = '/assets/images/active-user.svg';
const inactiveUserIconSrc = '/assets/images/inactive-user.svg';
const resetPasswordIconSrc = '/assets/images/hugeicons_reset-password.svg';
const resetPasswordInactiveIconSrc = '/assets/images/hugeicons_reset-password-inactive.svg';
const SummaryCard = ({ label, count, icon, iconBg = '#FDF7EB' }) => (
<div className="flex min-h-[96px] min-w-[220px] flex-1 items-center justify-between rounded-[16px] border border-[#E6EAF5] bg-white px-6 py-5 shadow-[0_12px_24px_rgba(15,23,42,0.06)]">
<div className="flex items-center gap-4">
<span
className="flex h-12 w-12 items-center justify-center rounded-full"
style={{ backgroundColor: iconBg }}
>
{icon && <img src={icon} alt={label} className="h-6 w-6" />}
</span>
<span className="text-sm font-medium text-[#5F646D]">{label}</span>
</div>
<span className="text-2xl font-semibold text-[#232528]">{count}</span>
</div>
);
const StatusBadge = ({ status }) => {
const map = {
Active: 'bg-green-50 text-green-700 ring-1 ring-green-200',
Closed: 'bg-gray-100 text-gray-700 ring-1 ring-gray-200',
};
return (
<span
className={`inline-flex items-center rounded-md px-2 py-0.5 text-xs font-medium ${
map[status] || map.Closed
}`}
>
{status}
</span>
);
};
const AdminUsers = () => {
const [isAddUserModalOpen, setIsAddUserModalOpen] = useState(false);
const [isEditModalOpen, setIsEditModalOpen] = useState(false);
const [currentUser, setCurrentUser] = useState(null);
const [users, setUsers] = useState([]);
const [editingRow, setEditingRow] = useState(null);
const [deletingRow, setDeletingRow] = useState(null);
const [loading, setLoading] = useState(false);
const [saving, setSaving] = useState(false);
const [currentPage, setCurrentPage] = useState(1);
const pageSize = 10;
const [formData, setFormData] = useState({
name: '',
email: '',
status: 'Active',
});
const [errors, setErrors] = useState({});
const [toastData, setToastData] = useState(null);
const navigate = useNavigate();
// Fetch users from API
useEffect(() => {
const fetchUsers = async () => {
try {
setLoading(true);
console.log('Fetching admin users...'); // Debug log
const response = await getAdminUser();
console.log('API Response:', response); // Debug log
// Check if response is an array directly or if it's in a data property
const usersData = Array.isArray(response) ? response :
(response?.data && Array.isArray(response.data) ? response.data : null);
if (usersData) {
// Map API data into UI format
const mappedUsers = usersData.map((user) => ({
id: user.id,
name: user.name || 'N/A',
email: user.email || 'N/A',
lastLogin: user.updatedAt || 'N/A',
// lastLogin: user.updatedAt
// ? new Date(user.last_login).toLocaleString('en-GB', {
// day: '2-digit',
// month: 'short',
// year: 'numeric',
// hour: '2-digit',
// minute: '2-digit',
// })
// : 'Never logged in',
status: user.is_active ? 'Active' : 'Active',
is_active: user.is_active, // Keep the original is_active for reference
}));
console.log('Mapped users:', mappedUsers); // Debug log
setUsers(mappedUsers);
} else {
const errorMsg = 'Invalid response format: Expected an array of users';
console.error(errorMsg, response);
showToast('Failed to load users. Please try again.', 'error');
setUsers([]); // Reset to empty array
}
} catch (error) {
console.error('Error fetching admin users:', error);
CustomToast('Error fetching admin users', 'error');
} finally {
setLoading(false);
}
};
fetchUsers();
}, []);
const totalUsers = users.length;
const activeUsers = users.filter((user) => user.status === 'Active').length;
const inactiveUsers = totalUsers - activeUsers;
const summaryItems = [
{
label: 'Total Users',
count: totalUsers,
icon: adminUserIconSrc,
iconBg: '#FDF7EB',
},
{
label: 'Active User',
count: activeUsers,
icon: activeUserIconSrc,
iconBg: '#E7F2FF',
},
{
label: 'Inactive User',
count: inactiveUsers,
icon: inactiveUserIconSrc,
iconBg: '#F5F5F7',
},
];
const headers = ['Name', 'Email', 'Last Login', 'Status', 'Actions'];
const columnWidths = [100, 130, 150, 100, 130];
const rows = users.map((user) => [
user.name,
user.email,
user.lastLogin,
<StatusBadge status={user.status} />,
'actions',
]);
// Show toast helper
const showToast = (message, type = 'success') => {
setToastData({ message, type });
if (window.toastTimeout) {
clearTimeout(window.toastTimeout);
}
window.toastTimeout = setTimeout(() => {
setToastData(null);
}, 4000);
};
const handleEdit = async (index) => {
const user = users[index];
setEditingRow(index);
try {
setLoading(true);
console.log('Fetching user data for ID:', user.id); // Debug log
const userData = await getAdminUserById(user.id);
console.log('User data received:', userData); // Debug log
if (!userData) {
throw new Error('No user data received');
}
// Ensure we have all required fields with fallbacks
const processedUser = {
...userData,
id: user.id,
name: userData.name || user.name || '',
email: userData.email || user.email || '',
is_active: userData.is_active !== undefined ? userData.is_active : user.is_active
};
setCurrentUser(processedUser);
setFormData({
name: processedUser.name,
email: processedUser.email,
status: processedUser.is_active ? 'Active' : 'Inactive',
});
setIsEditModalOpen(true);
} catch (error) {
console.error('Error fetching user data:', error);
const errorMsg = error.response?.data?.message || error.message || 'Failed to load user data';
showToast(errorMsg, 'error');
} finally {
setLoading(false);
}
};
const handleDelete = (index) => {
setDeletingRow(index);
// Future: open delete confirmation modal
};
const tableToolbar = (
<div className="px-6 py-4 flex flex-col lg:flex-row lg:items-center lg:justify-between gap-3 bg-white">
<h3 className="text-sm font-medium text-[#232528]">Admin Users</h3>
<div className="flex flex-col sm:flex-row sm:items-center sm:gap-3 w-full lg:w-auto">
<div className="relative flex-1 min-w-[220px]">
<input
type="text"
placeholder="Search by Names, Email"
className="w-full h-10 rounded-md border border-[#E5E7EB] pl-10 pr-4 text-sm focus:outline-none"
/>
<img
src={searchIconSrc}
alt="Search"
className="absolute left-3 top-1/2 -translate-y-1/2 h-5 w-5"
/>
</div>
<div className="flex items-center gap-2 sm:justify-end">
<button className="h-9 px-3 rounded-md bg-[#F7F7F7] border border-[#C3C6CB] text-sm inline-flex items-center gap-2 cursor-pointer">
<img src={exportIconSrc} alt="Export" className="h-4 w-4" />
<span>Export CSV</span>
</button>
<button
onClick={() => setIsAddUserModalOpen(true)}
className="h-9 px-3 rounded-md bg-[#B68A35] text-white text-sm inline-flex items-center gap-2 cursor-pointer"
>
<img src={addIconSrc} alt="Add" className="h-4 w-4" />
<span>Add User</span>
</button>
</div>
</div>
</div>
);
return (
<div className="space-y-5">
{/* ✅ Summary cards */}
<div className="grid gap-4 sm:grid-cols-2 xl:grid-cols-3">
{summaryItems.map((item) => (
<SummaryCard key={item.label} {...item} />
))}
</div>
{/* ✅ Table */}
<Table
headers={headers}
columnWidths={columnWidths}
rows={rows}
loading={loading}
beforeHeader={tableToolbar}
pagination={{
currentPage,
onPageChange: setCurrentPage,
pageSize,
totalItems: users.length,
pageSizeOptions: [10, 20, 50, 100],
}}
renderCell={(value, rowIndex, colIndex) => {
if (colIndex === 4) {
const user = users[rowIndex];
const isActive = user.status === 'Active';
return (
<div className="flex items-center gap-2">
<button
type="button"
className="h-8 w-8 grid place-items-center rounded hover:bg-gray-50 cursor-pointer"
title="Edit"
onClick={() => handleEdit(rowIndex)}
>
<img
src={
editingRow === rowIndex
? pencilActiveSrc
: pencilInactiveSrc
}
alt="Edit"
className="h-4 w-4"
/>
</button>
<button
type="button"
className="h-8 w-8 grid place-items-center rounded hover:bg-gray-50 cursor-pointer"
title="Delete"
onClick={() => handleDelete(rowIndex)}
>
<img
src={
deletingRow === rowIndex
? trashActiveSrc
: trashInactiveSrc
}
alt="Delete"
className="h-4 w-4"
/>
</button>
<button
type="button"
className={`h-8 w-8 grid place-items-center rounded hover:bg-gray-50 cursor-pointer ${
isActive ? '' : 'opacity-60'
}`}
title={
isActive
? 'Reset password'
: 'Reset disabled for inactive user'
}
disabled={!isActive}
>
<img
src={
isActive
? resetPasswordInactiveIconSrc
: resetPasswordInactiveIconSrc
}
alt="Reset password"
className="h-4 w-4"
/>
</button>
</div>
);
}
return value;
}}
/>
{/* ✅ Add User Modal */}
{isAddUserModalOpen && (
<AddAdminUsers
isOpen={isAddUserModalOpen}
onClose={() => setIsAddUserModalOpen(false)}
onSave={(newUser) => {
setUsers(prev => [
...prev,
{
...newUser,
lastLogin: new Date().toLocaleString('en-GB'),
status: 'Active',
},
]);
setIsAddUserModalOpen(false);
}}
/>
)}
{/* ✅ Edit User Modal */}
{isEditModalOpen && (
<div className="fixed inset-0 bg-black/30 backdrop-blur-sm flex items-center justify-center p-4 z-[9998]">
<div className="bg-white rounded-2xl w-full max-w-2xl max-h-[90vh] overflow-y-auto">
{/* Header */}
<div className="flex justify-between items-center p-6 border-b sticky top-0 bg-white z-10">
<h2 className="text-xl font-semibold text-[#232528]">Edit User</h2>
<button
type="button"
onClick={() => setIsEditModalOpen(false)}
className="text-gray-400 hover:text-gray-500"
aria-label="Close"
>
<X className="h-5 w-5" />
</button>
</div>
{/* Body */}
<div className="p-8">
<form onSubmit={handleEditSubmit}>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<TextField
label="Name"
name="name"
value={formData.name}
onChange={handleFormChange}
error={errors.name}
required
placeholder="Enter name"
/>
<TextField
label="Email"
name="email"
type="email"
value={formData.email}
onChange={handleFormChange}
error={errors.email}
required
placeholder="Enter email"
disabled={true}
/>
<div className="md:col-span-2 space-y-2">
<label className="block text-sm font-medium text-gray-700 mb-1">
Status <span className="text-red-500">*</span>
</label>
<SelectField
name="status"
value={formData.status}
onChange={handleFormChange}
options={[
{ label: 'Active', value: 'Active' },
{ label: 'Inactive', value: 'Inactive' },
]}
error={errors.status}
/>
{errors.status && (
<p className="mt-1 text-sm text-red-600">{errors.status}</p>
)}
</div>
</div>
{/* Footer */}
<div className="flex justify-end space-x-3 pt-6">
<button
type="button"
onClick={() => setIsEditModalOpen(false)}
className="px-5 py-2.5 text-sm font-medium text-gray-700 bg-white border border-gray-300 rounded-lg hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-[#B68A35] transition-colors"
>
Cancel
</button>
<button
type="submit"
disabled={saving}
className={`px-5 py-2.5 text-sm font-medium text-white rounded-lg focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-[#B68A35] transition-colors ${
saving
? 'bg-gray-400 cursor-not-allowed'
: 'bg-[#92722A] hover:bg-[#9A762D]'
}`}
>
{saving ? 'Saving...' : 'Update'}
</button>
</div>
</form>
</div>
</div>
</div>
)}
{/* ✅ Toast Notification */}
{toastData && (
<div className="fixed inset-0 z-[9999]">
<CustomToast
message={toastData.message}
type={toastData.type}
onClose={() => setToastData(null)}
/>
</div>
)}
</div>
);
};
// Form change handler
const handleFormChange = (e) => {
const { name, value } = e.target;
setFormData(prev => ({
...prev,
[name]: value
}));
// Clear error for the field being edited
if (errors[name]) {
setErrors(prev => ({
...prev,
[name]: ''
}));
}
};
// Form validation
const validateForm = () => {
const newErrors = {};
if (!formData.name.trim()) newErrors.name = 'Name is required';
if (!formData.status) newErrors.status = 'Status is required';
return newErrors;
};
// Handle edit form submission
const handleEditSubmit = async (e) => {
e.preventDefault();
const formErrors = validateForm();
if (Object.keys(formErrors).length > 0) {
setErrors(formErrors);
showToast('Please fix the form errors', 'error');
return;
}
if (!currentUser || !currentUser.id) {
showToast('No user selected for update', 'error');
return;
}
setSaving(true);
try {
// Update the user in the backend
const response = await updateAdminUser(currentUser.id, {
name: formData.name,
email: formData.email,
is_active: formData.status === 'Active',
});
if (response && response.status === 'success') {
// Update the user in the local state
setUsers(prevUsers =>
prevUsers.map(user =>
user.id === currentUser.id
? {
...user,
name: formData.name,
status: formData.status,
email: formData.email
}
: user
)
);
showToast('User updated successfully', 'success');
setIsEditModalOpen(false);
} else {
throw new Error(response?.message || 'Failed to update user');
}
} catch (error) {
console.error('Error updating user:', error);
const errorMessage = error.response?.data?.message || error.message || 'Failed to update user';
showToast(errorMessage, 'error');
} finally {
setSaving(false);
}
};
export default AdminUsers;

View File

@ -16,11 +16,14 @@ const downloadIconSrc = '/assets/images/DownloadSimple.svg';
const addIconSrc = '/assets/images/ic_baseline-plus.svg';
const pencilActiveSrc = '/assets/images/PencilSimple.svg';
const pencilInactiveSrc = '/assets/images/pencilsimple-inactive.svg';
const trashActiveSrc = '/assets/images/Trash - active.svg';
const trashInactiveSrc = '/assets/images/Trash - Inactive.svg';
const trashActiveSrc = '/assets/images/Trash-active.svg';
const trashInactiveSrc = '/assets/images/Trash-Inactive.svg';
const resetPasswordActiveSrc = '/assets/images/hugeicons_reset-password.svg';
const resetPasswordInactiveSrc = '/assets/images/hugeicons_reset-password-inactive.svg';
const deleteIconSrc = '/assets/images/delete.svg';
const activeToggleSrc = '/assets/images/Toggle.svg';
const inactiveToggleSrc = '/assets/images/Toggle-inactive.svg';
const PASSWORD_POLICY = {
minLength: 12,
@ -290,7 +293,7 @@ const CompanyProfile = () => {
const [editingRow, setEditingRow] = React.useState(null);
const [deletingRow, setDeletingRow] = React.useState(null);
const [showDeleteConfirm, setShowDeleteConfirm] = React.useState(false);
const pageSizeOptions = [10, 25, 50, 100];
const pageSizeOptions = [10, 20, 50, 100];
const [currentPage, setCurrentPage] = React.useState(1);
const [pageSize, setPageSize] = React.useState(pageSizeOptions[0]);
const [searchTerm, setSearchTerm] = React.useState('');
@ -1065,6 +1068,19 @@ const CompanyProfile = () => {
[corporateFieldKeys]
);
const handleToggleStatus = (item) => {
const updatedStatus = item.status === 'Active' ? 'Inactive' : 'Active';
// Optional: call your API to update backend
// await updateEstablishmentStatus(item.id, updatedStatus);
setEstablishments((prev) =>
prev.map((est) =>
est.id === item.id ? { ...est, status: updatedStatus } : est
)
);
};
const filteredProfiles = React.useMemo(() => {
if (!debouncedSearch.trim()) return profiles;
const normalized = debouncedSearch.toLowerCase();
@ -1160,8 +1176,8 @@ const CompanyProfile = () => {
const isActiveStatus = normalized !== 'inactive';
const label = isActiveStatus ? 'Active' : 'Inactive';
const baseClasses =
'inline-flex items-center justify-center px-3 py-1 text-xs font-medium rounded-md border';
const activeClasses = 'text-[#1A7F37] bg-[#ECFDF3] border-[#ABEFC6]';
'inline-flex items-center justify-center px-3 py-1 text-xs font-medium rounded-md';
const activeClasses = 'text-[#2F663C] bg-[#F3FAF4]';
const inactiveClasses = 'text-[#344054] bg-[#F8F9FC] border-[#D0D5DD]';
return (
<span
@ -1173,7 +1189,14 @@ const CompanyProfile = () => {
);
}, []);
const displayedRows = sortedProfiles.map((item) => {
// Apply pagination to sorted profiles
const paginatedProfiles = React.useMemo(() => {
const startIndex = (currentPage - 1) * pageSize;
const endIndex = startIndex + pageSize;
return sortedProfiles.slice(startIndex, endIndex);
}, [sortedProfiles, currentPage, pageSize]);
const displayedRows = paginatedProfiles.map((item) => {
const isEditing =
editingRow !== null && profiles[editingRow]?.establishmentId === item.establishmentId;
const isDeleting =
@ -1195,7 +1218,7 @@ const CompanyProfile = () => {
<div className="flex items-center gap-2" key={`actions-${item.establishmentId}`}>
<button
type="button"
className="h-8 w-8 grid place-items-center rounded hover:bg-gray-50 cursor-pointer"
className="h-[24px] w-[24px] grid place-items-center rounded hover:bg-gray-50 cursor-pointer"
title="Edit"
aria-label="Edit profile"
onClick={() => handleEdit(item.apiId ?? item.establishmentId)}
@ -1203,36 +1226,39 @@ const CompanyProfile = () => {
<img
src={isEditing ? pencilActiveSrc : pencilInactiveSrc}
alt="Edit"
className="h-4 w-4"
className="h-[24px] w-[24px]"
/>
</button>
<button
type="button"
className="h-8 w-8 grid place-items-center rounded hover:bg-gray-50 cursor-pointer"
title="Delete"
aria-label="Delete establishment"
onClick={() => handleDelete(item.apiId ?? item.establishmentId)}
>
<img
src={isDeleting ? trashActiveSrc : trashInactiveSrc}
alt="Delete"
className="h-4 w-4"
/>
</button>
<button
type="button"
className={`h-8 w-8 grid place-items-center rounded hover:bg-gray-50 cursor-pointer ${item.userProfileEmail ? '' : 'opacity-50 cursor-not-allowed'}`}
title={item.userProfileEmail ? 'Reset establishment password' : 'No email on file'}
aria-label="Reset password"
onClick={() => handleResetPassword(item)}
disabled={!item.userProfileEmail}
>
<img
src={item.userProfileEmail ? resetPasswordActiveSrc : resetPasswordInactiveSrc}
alt="Reset password"
className="h-4 w-4"
/>
</button>
type="button"
className="h-[20px] w-[40px] grid place-items-center rounded hover:bg-gray-50 cursor-pointer"
title={item.status === 'Active' ? 'Deactivate' : 'Activate'}
aria-label={item.status === 'Active' ? 'Deactivate establishment' : 'Activate establishment'}
onClick={() => handleToggleStatus(item)}
>
<img
src={item.status === 'Active' ? activeToggleSrc : inactiveToggleSrc}
alt={item.status === 'Active' ? 'Deactivate' : 'Activate'}
className="h-[20px] w-[40px] object-contain"
/>
</button>
<button
type="button"
className="h-6 w-6 grid place-items-center rounded hover:bg-gray-50 cursor-pointer"
title={item.userProfileEmail ? 'Reset password' : 'Reset password'}
aria-label="Reset password"
onClick={() => handleResetPassword(item)}
disabled={!item.userProfileEmail}
>
<img
src={item.userProfileEmail ? resetPasswordActiveSrc : resetPasswordInactiveSrc}
alt="Reset password"
className="h-4 w-4"
/>
</button>
</div>
),
];
@ -2138,7 +2164,7 @@ const CompanyProfile = () => {
<div className="relative min-w-[260px]">
<input
type="text"
placeholder="Search By Names, Email or Role..."
placeholder="Search By Establishment Name"
value={searchTerm}
onChange={(event) => setSearchTerm(event.target.value)}
className="w-full h-10 rounded-md border border-[#E5E7EB] pl-10 pr-4 text-sm focus:outline-none"
@ -2205,8 +2231,7 @@ const CompanyProfile = () => {
onPageChange: setCurrentPage,
pageSize,
totalItems: filteredProfiles.length,
pageSizeOptions,
onPageSizeChange: setPageSize,
pageSizeOptions: [10, 20, 50, 100],
}}
/>
)}

View File

@ -2,6 +2,7 @@ import React from 'react';
import Table from '@/components/common/Table';
import { TextField, SelectField, DateField } from '@/components/common/FormControls';
import StatusBadge from '@/components/common/StatusBadge';
import { createQuarterlyWindow, updateQuarterlyWindow, getQuarterlyWindows } from '@/services/configuration/quarterlyWindows';
const downloadIconSrc = '/assets/images/DownloadSimple.svg';
const addIconSrc = '/assets/images/ic_baseline-plus.svg';
@ -15,7 +16,7 @@ const searchIconSrc = '/assets/images/material-symbols_search-rounded.svg';
const caretDownSrc = '/assets/images/CaretDown-black.svg';
const createEmptyQuarterForm = () => ({
establishment: '',
establishment: '-', // Default to hyphen
year: '',
quarter: '',
startDate: '',
@ -26,173 +27,7 @@ const createEmptyQuarterForm = () => ({
});
const QuarterlyWindows = () => {
const [quarterData, setQuarterData] = React.useState([
{
establishment: 'Cascade Marine Foods LLC',
year: '2025',
quarter: 'Q1',
startDate: '11/12/2025',
endDate: '31/03/2025',
gracePeriod: '15 days',
submissionCount: '0',
status: 'Active',
},
{
establishment: 'Khazan Meat Factory',
year: '2025',
quarter: 'Q1',
startDate: '11/12/2025',
endDate: '31/03/2025',
gracePeriod: '15 days',
submissionCount: '40',
status: 'Active',
},
{
establishment: 'Spinneys Fresh Food Industries L.L.C',
year: '2025',
quarter: 'Q1',
startDate: '11/12/2025',
endDate: '31/03/2025',
gracePeriod: '15 days',
submissionCount: '30',
status: 'Active',
},
{
establishment: 'Freshly Frozen Foods Factory L.L.C',
year: '2025',
quarter: 'Q1',
startDate: '11/12/2025',
endDate: '31/03/2025',
gracePeriod: '15 days',
submissionCount: '15',
status: 'Active',
},
{
establishment: 'Sahar Food Industry (L.L.C)',
year: '2025',
quarter: 'Q1',
startDate: '11/12/2025',
endDate: '31/03/2025',
gracePeriod: '15 days',
submissionCount: '25',
status: 'Active',
},
{
establishment: 'Continental Food Processing LLC',
year: '2025',
quarter: 'Q1',
startDate: '11/12/2025',
endDate: '31/03/2025',
gracePeriod: '15 days',
submissionCount: '60',
status: 'Inactive',
},
{
establishment: 'Al Khazna Poultry Farm',
year: '2025',
quarter: 'Q1',
startDate: '11/12/2025',
endDate: '31/03/2025',
gracePeriod: '15 days',
submissionCount: '15',
status: 'Active',
},
{
establishment: 'Diamond Meat Processing L.L.C',
year: '2025',
quarter: 'Q1',
startDate: '11/12/2025',
endDate: '31/03/2025',
gracePeriod: '15 days',
submissionCount: '10',
status: 'Active',
},
{
establishment: 'Alliance Foods Co L.L.C',
year: '2025',
quarter: 'Q1',
startDate: '11/12/2025',
endDate: '31/03/2025',
gracePeriod: '15 days',
submissionCount: '44',
status: 'Active',
},
{
establishment: 'Krustasia Foods L.L.C',
year: '2024',
quarter: 'Q4',
startDate: '14/10/2024',
endDate: '31/12/2024',
gracePeriod: '15 days',
submissionCount: '25',
status: 'Inactive',
},
{
establishment: 'Gulf Coast Seafood Exports',
year: '2025',
quarter: 'Q2',
startDate: '01/04/2025',
endDate: '30/06/2025',
gracePeriod: '15 days',
submissionCount: '32',
status: 'Active',
},
{
establishment: 'Emirates Food Logistics',
year: '2025',
quarter: 'Q2',
startDate: '01/04/2025',
endDate: '30/06/2025',
gracePeriod: '15 days',
submissionCount: '18',
status: 'Active',
},
{
establishment: 'Global Seafood Traders',
year: '2025',
quarter: 'Q2',
startDate: '01/04/2025',
endDate: '30/06/2025',
gracePeriod: '15 days',
submissionCount: '22',
status: 'Active',
},{
establishment: 'Global Seafood Traders',
year: '2025',
quarter: 'Q2',
startDate: '01/04/2025',
endDate: '30/06/2025',
gracePeriod: '15 days',
status: 'Active',
},
{
establishment: 'Global Seafood Traders',
year: '2025',
quarter: 'Q2',
startDate: '01/04/2025',
endDate: '30/06/2025',
gracePeriod: '15 days',
status: 'Active',
},
{
establishment: 'Global Seafood Traders',
year: '2025',
quarter: 'Q2',
startDate: '01/04/2025',
endDate: '30/06/2025',
gracePeriod: '15 days',
status: 'Active',
},
{
establishment: 'Global Seafood Traders',
year: '2025',
quarter: 'Q2',
startDate: '01/04/2025',
endDate: '30/06/2025',
gracePeriod: '15 days',
status: 'Active',
},
]);
const [quarterData, setQuarterData] = React.useState([]);
const [editingRow, setEditingRow] = React.useState(null);
const [deletingRow, setDeletingRow] = React.useState(null);
const [modalMode, setModalMode] = React.useState(null);
@ -205,6 +40,42 @@ const QuarterlyWindows = () => {
const [hoveredDelete, setHoveredDelete] = React.useState(null);
const [currentPage, setCurrentPage] = React.useState(1);
const pageSize = 10;
const [isLoading, setIsLoading] = React.useState(false);
// Fetch quarterly windows data on component mount
React.useEffect(() => {
const fetchQuarterlyWindows = async () => {
setIsLoading(true);
try {
const response = await getQuarterlyWindows();
if (response.status === 'success' && Array.isArray(response.data)) {
// Transform the API response to match the expected format
const formattedData = response.data.map(item => ({
id: item.id,
establishment: item.establishment || '-', // Default to hyphen if empty
year: item.year?.toString() || '',
quarter: item.quarter || '',
startDate: item.start_date ? new Date(item.start_date).toLocaleDateString('en-GB') : '',
endDate: item.end_date ? new Date(item.end_date).toLocaleDateString('en-GB') : '',
gracePeriod: item.grace_periods_days ? `${item.grace_periods_days} days` : '0 days',
submissionCount: '0', // Default value since it's not in the API response
status: item.is_active ? 'Active' : 'Inactive'
}));
setQuarterData(formattedData);
} else {
console.error('Unexpected API response format:', response);
// Optionally set some error state here
}
} catch (error) {
console.error('Error fetching quarterly windows:', error);
// You might want to add error handling UI here
} finally {
setIsLoading(false);
}
};
fetchQuarterlyWindows();
}, []);
const deletingQuarter = React.useMemo(() => {
if (deletingRow === null || deletingRow < 0 || deletingRow >= quarterData.length) {
@ -213,8 +84,8 @@ const QuarterlyWindows = () => {
return quarterData[deletingRow];
}, [deletingRow, quarterData]);
const headers = ['Year', 'Quarter', 'Start Date', 'End Date', 'Grace Period', 'Submission Count', 'Status', 'Actions'];
const headers = ['Survey Name', 'Year', 'Quarter', 'Start Date', 'End Date', 'Grace Period', 'Submission Count', 'Status', 'Actions'];
const columnwidth = [300, 100, 100, 100, 100, 100, 100, 100, 100];
const filteredRows = React.useMemo(() => {
const term = searchTerm.trim().toLowerCase();
const statusValue = statusFilter.toLowerCase();
@ -240,6 +111,7 @@ const QuarterlyWindows = () => {
}, [quarterData, searchTerm, statusFilter]);
const rows = filteredRows.map((item) => [
item.establishment,
item.year,
item.quarter,
item.startDate,
@ -309,13 +181,62 @@ const QuarterlyWindows = () => {
setDeletingRow(null);
};
const handleSave = () => {
if (modalMode === 'add') {
setQuarterData((prev) => [...prev, form]);
} else if (modalMode === 'edit' && editingRow !== null) {
setQuarterData((prev) => prev.map((item, idx) => (idx === editingRow ? form : item)));
const handleSave = async () => {
try {
const formatDate = (dateString) => {
if (!dateString) return '';
const [day, month, year] = dateString.split('/');
return `${year}-${month.padStart(2, '0')}-${day.padStart(2, '0')}`;
};
const formattedData = {
year: parseInt(form.year, 10) || 0,
quarter: form.quarter || 'Q1',
start_date: formatDate(form.startDate) || new Date().toISOString().split('T')[0],
end_date: formatDate(form.endDate) || new Date().toISOString().split('T')[0],
grace_periods_days: parseInt(form.gracePeriod?.split(' ')[0], 10) || 0,
is_active: form.status === 'Active',
establishment: '-', // Default to hyphen if empty
};
if (modalMode === 'add') {
const response = await createQuarterlyWindow(formattedData);
if (response.status === 'success') {
const newItem = {
...formattedData,
id: response.data.id || Date.now(), // Use the ID from response or generate a temporary one
startDate: form.startDate,
endDate: form.endDate,
gracePeriod: formattedData.grace_periods_days ? `${formattedData.grace_periods_days} days` : '0 days',
submissionCount: '0',
status: formattedData.is_active ? 'Active' : 'Inactive'
};
setQuarterData((prev) => [...prev, newItem]);
}
} else if (modalMode === 'edit' && editingRow !== null) {
const response = await updateQuarterlyWindow(quarterData[editingRow].id, formattedData);
if (response.status === 'success') {
setQuarterData((prev) =>
prev.map((item, idx) =>
idx === editingRow
? {
...item,
...formattedData,
startDate: form.startDate,
endDate: form.endDate,
gracePeriod: formattedData.grace_periods_days ? `${formattedData.grace_periods_days} days` : '0 days',
status: formattedData.is_active ? 'Active' : 'Inactive'
}
: item
)
);
}
}
handleCloseModal();
} catch (error) {
console.error('Error saving quarterly window:', error);
// You might want to add error handling UI here
}
handleCloseModal();
};
const handleDelete = () => {
@ -402,6 +323,7 @@ const QuarterlyWindows = () => {
<Table
headers={headers}
columnWidth={columnwidth}
rows={rows}
renderCell={(value, rowIndex, colIndex) => {
if (colIndex === headers.length - 1) {
@ -464,7 +386,7 @@ const QuarterlyWindows = () => {
>
<div className="flex items-center justify-between px-6 py-4 border-b border-[#F1F2F4]">
<h3 className="text-[18px] font-medium text-[#232528]">
{modalMode === 'edit' ? 'Edit Quarter' : 'Add Quarter'}
{modalMode === 'edit' ? 'Edit Quarter' : 'Create Quarterly Survey'}
</h3>
<div className="flex items-center gap-4">
{modalMode === 'edit' && (
@ -495,8 +417,23 @@ const QuarterlyWindows = () => {
</div>
</div>
<div className="px-6 py-5">
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 ">
<div className="px-6 py-5 space-y-4">
<div className="w-full">
<TextField
label="Survey Name"
value={form.establishment}
onChange={(e) => setForm({ ...form, establishment: e.target.value })}
placeholder="Enter survey name"
/>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<SelectField
label="Quarter"
value={form.quarter}
onChange={(e) => setForm({ ...form, quarter: e.target.value })}
options={['Q1', 'Q2', 'Q3', 'Q4'].map((q) => ({ label: q, value: q }))}
placeholder="Select Quarter"
/>
<SelectField
label="Year"
value={form.year}
@ -507,27 +444,20 @@ const QuarterlyWindows = () => {
})}
placeholder="Select Year"
/>
<SelectField
label="Quarter"
value={form.quarter}
onChange={(e) => setForm({ ...form, quarter: e.target.value })}
options={['Q1', 'Q2', 'Q3', 'Q4'].map((q) => ({ label: q, value: q }))}
placeholder="Select Quarter"
/>
<DateField
label="Start Date"
label="Opens On"
value={form.startDate}
onChange={(e) => setForm({ ...form, startDate: e.target.value })}
placeholder="Select Start Date"
placeholder="Select Date"
/>
<DateField
label="End Date"
label="Closes On"
value={form.endDate}
onChange={(e) => setForm({ ...form, endDate: e.target.value })}
placeholder="Select End Date"
placeholder="Select Date"
/>
<SelectField
label="Grace Period"
label="Grace Period (Days)"
value={form.gracePeriod}
onChange={(e) => setForm({ ...form, gracePeriod: e.target.value })}
options={['5 days', '10 days', '15 days', '20 days', '30 days'].map((item) => ({
@ -561,7 +491,7 @@ const QuarterlyWindows = () => {
className="h-10 px-8 rounded-md bg-[#92722A] text-sm font-medium text-white shadow-[0_8px_18px_rgba(146,114,42,0.35)]"
onClick={handleSave}
>
{modalMode === 'edit' ? 'Update Quarter' : 'Add'}
{modalMode === 'edit' ? 'Update Quarter' : 'Publish Now'}
</button>
</div>
</div>

View File

@ -1,5 +1,5 @@
// src/pages/Overview/Overview.jsx
import React, { useState, useEffect } from 'react';
import React, { useState, useEffect, useMemo } from 'react';
import { useNavigate } from 'react-router-dom';
import { getSubmissions } from '@/services/submissions/submissionService';
import HeaderBar from '@/components/layout/HeaderBar';
@ -29,6 +29,8 @@ const Overview = () => {
const [submissions, setSubmissions] = useState([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
const [currentPage, setCurrentPage] = useState(1);
const pageSize = 10; // Number of items per page
const [pagination, setPagination] = useState({
page: 1,
limit: 100,
@ -76,17 +78,27 @@ const Overview = () => {
});
}, [searchTerm, selectedStatus, submissions]);
const rows = filteredRows.map((record) => [
// Calculate paginated data
const paginatedRows = useMemo(() => {
if (!filteredRows.length) return [];
const startIndex = (currentPage - 1) * pageSize;
const endIndex = startIndex + pageSize;
return filteredRows.slice(startIndex, endIndex);
}, [filteredRows, currentPage, pageSize]);
const rows = useMemo(() => {
return paginatedRows.map((record) => [
record.year,
record.quarter,
`${record.quarter} ${record.year}`,
10, // Always show 10 for Total Products
record.product_count || 0, // Show actual submitted products count
`-`, // Total cost not in API
record.status,
formatDate(record.created_at),
'View Details',
]);
record.status,
formatDate(record.created_at),
'View Details',
]);
}, [paginatedRows]);
const downloadCsv = React.useCallback(() => {
if (!filteredRows.length) return;
@ -173,8 +185,15 @@ const Overview = () => {
);
const handleViewDetails = (rowIndex) => {
setSelectedSubmission(filteredRows[rowIndex]);
// Calculate the actual index in the filtered array
const actualIndex = (currentPage - 1) * pageSize + rowIndex;
setSelectedSubmission(filteredRows[actualIndex]);
};
// Reset to first page when search or filter changes
useEffect(() => {
setCurrentPage(1);
}, [searchTerm, selectedStatus]);
if (loading) {
return (
@ -239,6 +258,13 @@ const Overview = () => {
beforeHeader={toolbar}
separated
rowGapClass="border-spacing-y-2"
pagination={{
currentPage,
onPageChange: setCurrentPage,
pageSize,
totalItems: filteredRows.length,
pageSizeOptions: [10, 25, 50, 100]
}}
className="w-full min-w-[1200px] [&_td]:whitespace-nowrap [&_th]:whitespace-nowrap"
renderCell={(value, rowIndex, columnIndex) => {
if (columnIndex === 6) { // Status column

View File

@ -74,8 +74,8 @@ const PublicContactForm = () => {
const validate = (data = formData) => {
const newErrors = {};
if (!data.establishmentName.trim()) newErrors.establishmentName = "Establishment Name is required.";
if (!data.email.trim()) newErrors.email = "Registered Email is required.";
if (!data.establishmentName.trim()) newErrors.establishmentName = "Required.";
if (!data.email.trim()) newErrors.email = "Required.";
else if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(data.email))
newErrors.email = "Enter a valid email address.";
@ -280,15 +280,15 @@ const PublicContactForm = () => {
</button>
</div>
<p className="text-xs text-gray-500 mt-1">
If you cannot read the code, click "Refresh" or type:{" "}
<span className="font-mono">{captcha}</span>
Enter the characters in the image. Can't read it? Refresh for a new code.
<span className="font-mono"></span>
</p>
<input
type="text"
name="captcha"
value={userCaptcha}
onChange={handleCaptchaChange}
placeholder="Enter the code shown above"
placeholder="Type the characters"
className={`mt-2 block w-full h-10 rounded-md border-2 ${
captchaError ? "border-red-500" : "border-[#92722A]"
} focus:border-[#92722A] focus:ring-0 px-4 text-sm bg-white`}

View File

@ -0,0 +1,53 @@
// src/services/configuration/unitService.js
import { getRequest, postRequest, putRequest, deleteRequest } from '@/services/api/CommonService';
const admin = '/admin_users';
export const getAdminUser = async () => {
try {
const response = await getRequest(admin);
return response.data || [];
} catch (error) {
console.error('Error fetching units:', error);
throw error;
}
};
export const createadminusers = async (admindata) => {
try {
const response = await postRequest(admin, admindata);
return response.data;
} catch (error) {
console.error('Error creating unit:', error);
throw error;
}
};
export const updateAdminUser = async (id, admindata) => {
try {
const response = await putRequest(`${admin}/${id}`, admindata);
return response.data;
} catch (error) {
console.error('Error updating unit:', error);
throw error;
}
};
export const getAdminUserById = async (id) => {
try {
const response = await getRequest(`${admin}/${id}`);
return response.data;
} catch (error) {
console.error('Error fetching admin user:', error);
throw error;
}
};
export const deleteUnit = async (id) => {
try {
const response = await deleteRequest(`${UNIT_MASTER_ENDPOINT}/${id}`);
return response.data;
} catch (error) {
console.error('Error deleting unit:', error);
throw error;
}
};