Changes in design for quarterly,product code
This commit is contained in:
parent
492931d488
commit
9d473ecaa8
@ -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 |
4
ipi-survey-platform/public/assets/images/Toggle.svg
Normal file
4
ipi-survey-platform/public/assets/images/Toggle.svg
Normal 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 |
BIN
ipi-survey-platform/public/assets/images/fcsauae_logo1.jpg
Normal file
BIN
ipi-survey-platform/public/assets/images/fcsauae_logo1.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 4.9 KiB |
@ -118,6 +118,14 @@ function App() {
|
||||
</RequireRole>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="admin-users/edit/:id"
|
||||
element={
|
||||
<RequireRole allowedRoles={['Admin']}>
|
||||
<Configuration />
|
||||
</RequireRole>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="profile"
|
||||
element={
|
||||
|
||||
@ -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]"
|
||||
|
||||
42
ipi-survey-platform/src/components/common/CustomToast.jsx
Normal file
42
ipi-survey-platform/src/components/common/CustomToast.jsx
Normal 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"
|
||||
>
|
||||
×
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default CustomToast;
|
||||
@ -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 UAE’s 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">FAQ’s</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;
|
||||
|
||||
@ -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';
|
||||
|
||||
@ -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'
|
||||
|
||||
@ -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 || '—'}
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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}
|
||||
|
||||
@ -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 && (
|
||||
|
||||
@ -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';
|
||||
|
||||
|
||||
@ -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';
|
||||
|
||||
@ -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;
|
||||
@ -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;
|
||||
@ -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;
|
||||
@ -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;
|
||||
@ -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;
|
||||
@ -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],
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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`}
|
||||
|
||||
@ -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;
|
||||
}
|
||||
};
|
||||
Loading…
Reference in New Issue
Block a user