added products in the company profike

This commit is contained in:
Malini 2025-11-10 11:51:41 +05:30
parent 3d27feceab
commit cac8d1cb2e
8 changed files with 1145 additions and 720 deletions

View File

@ -23,9 +23,9 @@ const Footer = () => {
<div> <div>
<h3 className="text-[#92722A] font-semibold mb-3 ml-16 text-lg">Our Policies</h3> <h3 className="text-[#92722A] font-semibold mb-3 ml-16 text-lg">Our Policies</h3>
<ul className="space-y-1 ml-16 text-sm"> <ul className="space-y-1 ml-16 text-sm">
<li>Disclaimer</li> <li><a href="#" className="hover:underline">Disclaimer</a></li>
<li>Privacy policy</li> <li><a href="#" className="hover:underline">Privacy policy</a></li>
<li>Terms and conditions</li> <li><a href="#" className="hover:underline">Terms and conditions</a></li>
</ul> </ul>
</div> </div>
@ -33,9 +33,9 @@ const Footer = () => {
<div> <div>
<h3 className="text-[#92722A] font-semibold mb-3 ml-1 text-lg">Information and Support</h3> <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"> <ul className="space-y-1 ml-1 text-sm">
<li>Contact us</li> <li><a href="#" className="hover:underline">Contact us</a></li>
<li>FAQ's</li> <li><a href="#" className="hover:underline">FAQs</a></li>
<li>Feedback and complaints</li> <li><a href="#" className="hover:underline">Feedback and complaints</a></li>
</ul> </ul>
</div> </div>
@ -67,7 +67,12 @@ const Footer = () => {
alt="Mail Icon" alt="Mail Icon"
className="h-4 w-4 opacity-80" className="h-4 w-4 opacity-80"
/> />
<span className="text-[#232528]">info@fcsc.gov.ae</span> <a
href="mailto:info@fcsc.gov.ae"
className="text-[#232528] hover:underline"
>
info@fcsc.gov.ae
</a>
</div> </div>
</div> </div>
</div> {/* ✅ properly closed main content div */} </div> {/* ✅ properly closed main content div */}

View File

@ -394,7 +394,7 @@ const ProductData = ({
setIsLoadingProducts(true); setIsLoadingProducts(true);
setProductsError(''); setProductsError('');
try { try {
const productsList = await fetchProducts({ signal: productsController.signal });
setProductOptions(productsList); setProductOptions(productsList);
setProductsError(''); setProductsError('');
} catch (error) { } catch (error) {

View File

@ -87,6 +87,7 @@ const AdminUsers = () => {
// Reset password modal state // Reset password modal state
const [isResetModalOpen, setIsResetModalOpen] = useState(false); const [isResetModalOpen, setIsResetModalOpen] = useState(false);
const [resetUser, setResetUser] = useState(null); const [resetUser, setResetUser] = useState(null);
const [oldPassword, setOldPassword] = useState('');
const [newPassword, setNewPassword] = useState(''); const [newPassword, setNewPassword] = useState('');
const [confirmPassword, setConfirmPassword] = useState(''); const [confirmPassword, setConfirmPassword] = useState('');
const [resettingPassword, setResettingPassword] = useState(false); const [resettingPassword, setResettingPassword] = useState(false);
@ -332,6 +333,7 @@ const AdminUsers = () => {
const handleOpenResetModal = (user) => { const handleOpenResetModal = (user) => {
setResetUser(user); setResetUser(user);
setOldPassword('');
setNewPassword(''); setNewPassword('');
setConfirmPassword(''); setConfirmPassword('');
setResetErrors({}); setResetErrors({});
@ -340,6 +342,7 @@ const AdminUsers = () => {
const handleCloseResetModal = () => { const handleCloseResetModal = () => {
setResetUser(null); setResetUser(null);
setOldPassword('');
setNewPassword(''); setNewPassword('');
setConfirmPassword(''); setConfirmPassword('');
setResetErrors({}); setResetErrors({});
@ -347,21 +350,24 @@ const AdminUsers = () => {
setResettingPassword(false); setResettingPassword(false);
}; };
// Validate reset password form // Validate reset password form - Updated to allow 8 to 12 characters
const validateResetForm = () => { const validateResetForm = () => {
const newErrors = {}; const newErrors = {};
if (!oldPassword.trim()) {
newErrors.oldPassword = 'Old password is required';
}
if (!newPassword.trim()) { if (!newPassword.trim()) {
newErrors.newPassword = 'Password is required'; newErrors.newPassword = 'New password is required';
} else if (newPassword.length < 6) { } else if (newPassword.length < 8) {
newErrors.newPassword = 'Password must be at least 6 characters'; newErrors.newPassword = 'Password must be at least 8 characters';
} }
if (!confirmPassword.trim()) { if (!confirmPassword.trim()) {
newErrors.confirmPassword = 'Please confirm your password'; newErrors.confirmPassword = 'Please confirm your password';
} else if (newPassword !== confirmPassword) { } else if (newPassword !== confirmPassword) {
// Don't show error message, just prevent submission newErrors.confirmPassword = 'Passwords do not match';
newPassword !== confirmPassword;
} }
return newErrors; return newErrors;
@ -381,60 +387,84 @@ const AdminUsers = () => {
return; return;
} }
// Check if passwords match (without showing error message)
if (newPassword !== confirmPassword) {
// Just return without showing error
return;
}
try { try {
setResettingPassword(true); setResettingPassword(true);
// Prepare data according to API specification
const passwordData = { const passwordData = {
newPassword: newPassword, old_password: oldPassword,
confirmPassword: confirmPassword new_password: newPassword,
confirm_password: confirmPassword
}; };
console.log('Initiating password reset for user:', resetUser.id); console.log('🔄 Initiating admin password reset for user:', resetUser.id);
const res = await changeAdminUserPassword(resetUser.id, passwordData); const res = await changeAdminUserPassword(resetUser.id, passwordData);
console.log('Password reset response:', res); console.log('✅ Admin password reset response:', res);
// Check for different success patterns // Check for success based on common response patterns
const success = const success =
res?.status === "success" || res?.status === "success" ||
res?.status === 200 || res?.status === 200 ||
res?.code === 200 || res?.code === 200 ||
res?.data?.status === "success" || res?.data?.status === "success" ||
res?.message?.includes("success") || res?.message?.toLowerCase().includes("success") ||
res?.message?.includes("updated") || res?.message?.toLowerCase().includes("updated") ||
res?.message?.toLowerCase().includes("changed") ||
(!res.error && res); (!res.error && res);
if (!success) { if (!success) {
throw new Error(res?.message || res?.data?.message || "Failed to reset password"); throw new Error(res?.message || res?.data?.message || "Failed to reset password");
} }
// No success toast message showToast('Admin password reset successfully!', 'success');
handleCloseResetModal(); handleCloseResetModal();
} catch (error) { } catch (error) {
console.error('Error resetting password:', error); console.error('Error resetting admin password:', error);
// More detailed error handling // Handle specific error cases
let errorMessage = 'Failed to reset password'; let errorMessage = 'Failed to reset password';
if (error.response?.data?.message) { if (error.message) {
errorMessage = error.response.data.message;
} else if (error.response?.data?.errors) {
// Handle validation errors from backend
const errors = error.response.data.errors;
errorMessage = Object.values(errors).flat().join(', ');
} else if (error.message) {
errorMessage = error.message; errorMessage = error.message;
} }
showToast(errorMessage, 'error'); // Check for common admin password change errors
if (errorMessage.toLowerCase().includes('old password') ||
errorMessage.toLowerCase().includes('current password') ||
errorMessage.toLowerCase().includes('incorrect password')) {
setResetErrors(prev => ({
...prev,
oldPassword: 'The current password is incorrect'
}));
showToast('The current password you entered is incorrect', 'error');
} else if (errorMessage.toLowerCase().includes('match')) {
setResetErrors(prev => ({
...prev,
confirmPassword: 'New passwords do not match'
}));
showToast('New passwords do not match', 'error');
} else if (errorMessage.toLowerCase().includes('same') ||
errorMessage.toLowerCase().includes('previous')) {
setResetErrors(prev => ({
...prev,
newPassword: 'New password cannot be the same as current password'
}));
showToast('New password cannot be the same as current password', 'error');
} else if (errorMessage.toLowerCase().includes('weak') ||
errorMessage.toLowerCase().includes('strength')) {
setResetErrors(prev => ({
...prev,
newPassword: 'Password is too weak. Use a stronger password.'
}));
showToast('Password is too weak. Please use a stronger password.', 'error');
} else if (error.status === 404) {
showToast('Admin user not found', 'error');
} else {
showToast(errorMessage, 'error');
}
} finally { } finally {
setResettingPassword(false); setResettingPassword(false);
} }
@ -666,6 +696,7 @@ const AdminUsers = () => {
setIsAddUserModalOpen(false); setIsAddUserModalOpen(false);
showToast('User added locally', 'success'); showToast('User added locally', 'success');
}} }}
passwordMinLength={8}
/> />
)} )}
@ -692,6 +723,8 @@ const AdminUsers = () => {
<ResetPasswordModal <ResetPasswordModal
isOpen={isResetModalOpen} isOpen={isResetModalOpen}
onClose={handleCloseResetModal} onClose={handleCloseResetModal}
oldPassword={oldPassword}
setOldPassword={setOldPassword}
newPassword={newPassword} newPassword={newPassword}
setNewPassword={setNewPassword} setNewPassword={setNewPassword}
confirmPassword={confirmPassword} confirmPassword={confirmPassword}
@ -700,6 +733,7 @@ const AdminUsers = () => {
loading={resettingPassword} loading={resettingPassword}
errors={resetErrors} errors={resetErrors}
setErrors={setResetErrors} setErrors={setResetErrors}
passwordMinLength={8}
/> />
{/* Toast */} {/* Toast */}

View File

@ -3,18 +3,28 @@ import React, { useState } from 'react';
const ResetPasswordModal = ({ const ResetPasswordModal = ({
isOpen, isOpen,
onClose, onClose,
oldPassword,
setOldPassword,
newPassword, newPassword,
setNewPassword, setNewPassword,
confirmPassword, confirmPassword,
setConfirmPassword, setConfirmPassword,
onReset, onReset,
loading = false loading = false,
errors = {},
setErrors
}) => { }) => {
const [errors, setErrors] = useState({}); const [showOldPassword, setShowOldPassword] = useState(false);
const [showNewPassword, setShowNewPassword] = useState(false);
const [showConfirmPassword, setShowConfirmPassword] = useState(false);
const validateForm = () => { const validateForm = () => {
const newErrors = {}; const newErrors = {};
if (!oldPassword.trim()) {
newErrors.oldPassword = 'Old password is required';
}
if (!newPassword.trim()) { if (!newPassword.trim()) {
newErrors.newPassword = 'New password is required'; newErrors.newPassword = 'New password is required';
} else if (newPassword.length < 6) { } else if (newPassword.length < 6) {
@ -23,8 +33,9 @@ const ResetPasswordModal = ({
if (!confirmPassword.trim()) { if (!confirmPassword.trim()) {
newErrors.confirmPassword = 'Please confirm your password'; newErrors.confirmPassword = 'Please confirm your password';
} else if (newPassword !== confirmPassword) {
newErrors.confirmPassword = 'Passwords do not match';
} }
// Removed "Passwords do not match" error display
return newErrors; return newErrors;
}; };
@ -32,11 +43,6 @@ const ResetPasswordModal = ({
const handleSubmit = () => { const handleSubmit = () => {
const formErrors = validateForm(); const formErrors = validateForm();
// Check if passwords match (without showing error)
if (newPassword !== confirmPassword) {
return; // Just return without showing error
}
if (Object.keys(formErrors).length > 0) { if (Object.keys(formErrors).length > 0) {
setErrors(formErrors); setErrors(formErrors);
return; return;
@ -47,6 +53,7 @@ const ResetPasswordModal = ({
}; };
const handleInputChange = (field, value) => { const handleInputChange = (field, value) => {
if (field === 'oldPassword') setOldPassword(value);
if (field === 'newPassword') setNewPassword(value); if (field === 'newPassword') setNewPassword(value);
if (field === 'confirmPassword') setConfirmPassword(value); if (field === 'confirmPassword') setConfirmPassword(value);
@ -65,7 +72,7 @@ const ResetPasswordModal = ({
if (!isOpen) return null; if (!isOpen) return null;
return ( return (
<div className="fixed inset-0 z-[9999] flex items-center justify-center bg-black bg-opacity-50 backdrop-blur-sm"> <div className="fixed inset-0 z-[9999] flex items-center justify-center bg-opacity-70 backdrop-blur-sm">
<div className="bg-white rounded-2xl w-[90%] max-w-md shadow-xl p-6 relative border border-gray-200"> <div className="bg-white rounded-2xl w-[90%] max-w-md shadow-xl p-6 relative border border-gray-200">
{/* Header */} {/* Header */}
<div className="flex items-center justify-between mb-6"> <div className="flex items-center justify-between mb-6">
@ -83,23 +90,90 @@ const ResetPasswordModal = ({
{/* Form */} {/* Form */}
<div className="space-y-4"> <div className="space-y-4">
{/* Old Password Field */}
<div> <div>
<label className="block text-sm font-medium text-gray-700 mb-2"> <label className="block text-sm font-medium text-gray-700 mb-2">
New Password Old Password <span className="text-red-500">*</span>
</label> </label>
<input <div className="relative">
type="password" <input
placeholder="Enter new password" type={showOldPassword ? "text" : "password"}
className={`w-full border rounded-lg px-4 py-3 text-sm focus:outline-none focus:ring-2 transition-all ${ placeholder="Enter old password"
errors.newPassword className={`w-full border rounded-lg px-4 py-3 text-sm focus:outline-none focus:ring-2 transition-all pr-10 ${
? 'border-red-300 focus:ring-red-500 bg-red-50' errors.oldPassword
: 'border-gray-300 focus:border-[#B68A35] focus:ring-2 focus:ring-[#B68A35] focus:ring-opacity-20' ? 'border-red-300 focus:ring-red-500 bg-red-50'
}`} : 'border-gray-300 focus:border-[#B68A35] focus:ring-2 focus:ring-[#B68A35] focus:ring-opacity-20'
value={newPassword} }`}
onChange={(e) => handleInputChange('newPassword', e.target.value)} value={oldPassword}
onKeyPress={handleKeyPress} onChange={(e) => handleInputChange('oldPassword', e.target.value)}
disabled={loading} onKeyPress={handleKeyPress}
/> disabled={loading}
/>
<button
type="button"
className="absolute right-3 top-1/2 transform -translate-y-1/2 text-gray-500 hover:text-gray-700"
onClick={() => setShowOldPassword(!showOldPassword)}
disabled={loading}
>
{showOldPassword ? (
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z" />
</svg>
) : (
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13.875 18.825A10.05 10.05 0 0112 19c-4.478 0-8.268-2.943-9.543-7a9.97 9.97 0 011.563-3.029m5.858.908a3 3 0 114.243 4.243M9.878 9.878l4.242 4.242M9.878 9.878L9 9m13 11l-4-4m0 0l-4 4m4-4V9" />
</svg>
)}
</button>
</div>
{errors.oldPassword && (
<p className="text-red-600 text-xs mt-2 flex items-center gap-1">
<svg className="w-3 h-3" fill="currentColor" viewBox="0 0 20 20">
<path fillRule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7 4a1 1 0 11-2 0 1 1 0 012 0zm-1-9a1 1 0 00-1 1v4a1 1 0 102 0V6a1 1 0 00-1-1z" clipRule="evenodd" />
</svg>
{errors.oldPassword}
</p>
)}
</div>
{/* New Password Field */}
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
New Password <span className="text-red-500">*</span>
</label>
<div className="relative">
<input
type={showNewPassword ? "text" : "password"}
placeholder="Enter new password"
className={`w-full border rounded-lg px-4 py-3 text-sm focus:outline-none focus:ring-2 transition-all pr-10 ${
errors.newPassword
? 'border-red-300 focus:ring-red-500 bg-red-50'
: 'border-gray-300 focus:border-[#B68A35] focus:ring-2 focus:ring-[#B68A35] focus:ring-opacity-20'
}`}
value={newPassword}
onChange={(e) => handleInputChange('newPassword', e.target.value)}
onKeyPress={handleKeyPress}
disabled={loading}
/>
<button
type="button"
className="absolute right-3 top-1/2 transform -translate-y-1/2 text-gray-500 hover:text-gray-700"
onClick={() => setShowNewPassword(!showNewPassword)}
disabled={loading}
>
{showNewPassword ? (
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z" />
</svg>
) : (
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13.875 18.825A10.05 10.05 0 0112 19c-4.478 0-8.268-2.943-9.543-7a9.97 9.97 0 011.563-3.029m5.858.908a3 3 0 114.243 4.243M9.878 9.878l4.242 4.242M9.878 9.878L9 9m13 11l-4-4m0 0l-4 4m4-4V9" />
</svg>
)}
</button>
</div>
{errors.newPassword && ( {errors.newPassword && (
<p className="text-red-600 text-xs mt-2 flex items-center gap-1"> <p className="text-red-600 text-xs mt-2 flex items-center gap-1">
<svg className="w-3 h-3" fill="currentColor" viewBox="0 0 20 20"> <svg className="w-3 h-3" fill="currentColor" viewBox="0 0 20 20">
@ -110,23 +184,43 @@ const ResetPasswordModal = ({
)} )}
</div> </div>
{/* Confirm Password Field */}
<div> <div>
<label className="block text-sm font-medium text-gray-700 mb-2"> <label className="block text-sm font-medium text-gray-700 mb-2">
Confirm Password Confirm Password <span className="text-red-500">*</span>
</label> </label>
<input <div className="relative">
type="password" <input
placeholder="Confirm new password" type={showConfirmPassword ? "text" : "password"}
className={`w-full border rounded-lg px-4 py-3 text-sm focus:outline-none focus:ring-2 transition-all ${ placeholder="Confirm new password"
errors.confirmPassword className={`w-full border rounded-lg px-4 py-3 text-sm focus:outline-none focus:ring-2 transition-all pr-10 ${
? 'border-red-300 focus:ring-red-500 bg-red-50' errors.confirmPassword
: 'border-gray-300 focus:border-[#B68A35] focus:ring-2 focus:ring-[#B68A35] focus:ring-opacity-20' ? 'border-red-300 focus:ring-red-500 bg-red-50'
}`} : 'border-gray-300 focus:border-[#B68A35] focus:ring-2 focus:ring-[#B68A35] focus:ring-opacity-20'
value={confirmPassword} }`}
onChange={(e) => handleInputChange('confirmPassword', e.target.value)} value={confirmPassword}
onKeyPress={handleKeyPress} onChange={(e) => handleInputChange('confirmPassword', e.target.value)}
disabled={loading} onKeyPress={handleKeyPress}
/> disabled={loading}
/>
<button
type="button"
className="absolute right-3 top-1/2 transform -translate-y-1/2 text-gray-500 hover:text-gray-700"
onClick={() => setShowConfirmPassword(!showConfirmPassword)}
disabled={loading}
>
{showConfirmPassword ? (
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z" />
</svg>
) : (
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13.875 18.825A10.05 10.05 0 0112 19c-4.478 0-8.268-2.943-9.543-7a9.97 9.97 0 011.563-3.029m5.858.908a3 3 0 114.243 4.243M9.878 9.878l4.242 4.242M9.878 9.878L9 9m13 11l-4-4m0 0l-4 4m4-4V9" />
</svg>
)}
</button>
</div>
{errors.confirmPassword && ( {errors.confirmPassword && (
<p className="text-red-600 text-xs mt-2 flex items-center gap-1"> <p className="text-red-600 text-xs mt-2 flex items-center gap-1">
<svg className="w-3 h-3" fill="currentColor" viewBox="0 0 20 20"> <svg className="w-3 h-3" fill="currentColor" viewBox="0 0 20 20">

View File

@ -11,6 +11,7 @@ import {
updateEstablishment, updateEstablishment,
deleteEstablishment, deleteEstablishment,
} from '@/services/establishments/establishmentService'; } from '@/services/establishments/establishmentService';
import { fetchProducts } from '@/services/masters/masterService';
const searchIconSrc = '/assets/images/material-symbols_search-rounded.svg'; const searchIconSrc = '/assets/images/material-symbols_search-rounded.svg';
const downloadIconSrc = '/assets/images/DownloadSimple.svg'; const downloadIconSrc = '/assets/images/DownloadSimple.svg';
const addIconSrc = '/assets/images/ic_baseline-plus.svg'; const addIconSrc = '/assets/images/ic_baseline-plus.svg';
@ -188,6 +189,12 @@ const CompanyProfile = () => {
const [isDeletingApi, setIsDeletingApi] = React.useState(false); const [isDeletingApi, setIsDeletingApi] = React.useState(false);
const [totalItems, setTotalItems] = React.useState(0); const [totalItems, setTotalItems] = React.useState(0);
const [currentUser, setCurrentUser] = React.useState(null); const [currentUser, setCurrentUser] = React.useState(null);
const [products, setProducts] = React.useState([]);
const [availableProducts, setAvailableProducts] = React.useState([]);
const [selectedProducts, setSelectedProducts] = React.useState([]);
const [productSearchTerm, setProductSearchTerm] = React.useState('');
const [isLoadingProducts, setIsLoadingProducts] = React.useState(false);
const [isRefreshing, setIsRefreshing] = React.useState(false);
const showToast = React.useCallback((type, message) => { const showToast = React.useCallback((type, message) => {
if (!message) return; if (!message) return;
@ -236,7 +243,8 @@ const CompanyProfile = () => {
{ label: 'User Profile' }, { label: 'User Profile' },
{ label: 'Identification Particulars' }, { label: 'Identification Particulars' },
{ label: 'Establishment Contact Details' }, { label: 'Establishment Contact Details' },
{ label: 'Corporate / Head Office Contact' }, { label: 'Products' },
// { label: 'Corporate / Head Office Contact' },
{ label: 'Employment in Establishment' }, { label: 'Employment in Establishment' },
], ],
[] []
@ -259,26 +267,27 @@ const CompanyProfile = () => {
{ field: 'contactAddress', label: 'Address' }, { field: 'contactAddress', label: 'Address' },
{ field: 'contactCityTown', label: 'City/Town' }, { field: 'contactCityTown', label: 'City/Town' },
{ field: 'contactEmirate', label: 'Emirate' }, { field: 'contactEmirate', label: 'Emirate' },
{ field: 'contactMakaniNumber', label: 'Makani Number' }, // { field: 'contactPersonName', label: 'Contact Person Name' },
{ field: 'contactPersonName', label: 'Contact Person Name' },
{ field: 'contactMobileNumber', label: 'Mobile Number' }, { field: 'contactMobileNumber', label: 'Mobile Number' },
{ field: 'contactEmail', label: 'Email' }, { field: 'contactEmail', label: 'Email' },
], ],
3: [ 3: [
{ field: 'products', label: 'Products' },
],
4: [
{ field: 'corporateName', label: 'Name' }, { field: 'corporateName', label: 'Name' },
{ field: 'corporateAddress', label: 'Address' }, { field: 'corporateAddress', label: 'Address' },
{ field: 'corporateCityTown', label: 'City/Town' }, { field: 'corporateCityTown', label: 'City/Town' },
{ field: 'corporateEmirate', label: 'Emirate' }, { field: 'corporateEmirate', label: 'Emirate' },
{ field: 'corporateMakaniNumber', label: 'Makani Number' }, // { field: 'corporateContactPersonName', label: 'Contact Person Name' },
{ field: 'corporateContactPersonName', label: 'Contact Person Name' },
{ field: 'corporateMobileNumber', label: 'Mobile Number' }, { field: 'corporateMobileNumber', label: 'Mobile Number' },
{ field: 'corporateEmail', label: 'Email' }, { field: 'corporateEmail', label: 'Email' },
], ],
4: [ 5: [
{ field: 'employmentEmiratiMale', label: 'Number of Emirati Male' }, // { field: 'employmentEmiratiMale', label: 'Number of Emirati Male' },
{ field: 'employmentNonEmiratiMale', label: 'Number of Non-Emirati Male' }, // { field: 'employmentNonEmiratiMale', label: 'Number of Non-Emirati Male' },
{ field: 'employmentEmiratiFemale', label: 'Number of Emirati Female' }, // { field: 'employmentEmiratiFemale', label: 'Number of Emirati Female' },
{ field: 'employmentNonEmiratiFemale', label: 'Number of Non-Emirati Female' }, // { field: 'employmentNonEmiratiFemale', label: 'Number of Non-Emirati Female' },
], ],
}), }),
[] []
@ -368,7 +377,7 @@ const CompanyProfile = () => {
placeholder="Enter password" placeholder="Enter password"
width="100%" width="100%"
type="password" type="password"
showToggle showToggle={true}
className={passwordError || passwordReuseError ? 'border-red-500' : ''} className={passwordError || passwordReuseError ? 'border-red-500' : ''}
required required
/> />
@ -383,7 +392,7 @@ const CompanyProfile = () => {
placeholder="Confirm password" placeholder="Confirm password"
width="100%" width="100%"
type="password" type="password"
showToggle showToggle={true}
className={confirmError ? 'border-red-500' : ''} className={confirmError ? 'border-red-500' : ''}
required required
/> />
@ -402,7 +411,7 @@ const CompanyProfile = () => {
width="100%" width="100%"
type="password" type="password"
disabled={true} // ensures non-editable disabled={true} // ensures non-editable
showToggle={false} // optional: hide toggle if disabled showToggle={false} // hide toggle for disabled field
className={passwordError ? 'border-red-500' : ''} className={passwordError ? 'border-red-500' : ''}
/> />
{passwordError && ( {passwordError && (
@ -419,7 +428,7 @@ const CompanyProfile = () => {
placeholder="Enter password" placeholder="Enter password"
width="100%" width="100%"
type="password" type="password"
showToggle showToggle={false}
className={passwordError || passwordReuseError ? 'border-red-500' : ''} className={passwordError || passwordReuseError ? 'border-red-500' : ''}
/> />
{passwordError && <p className="text-xs text-[#B91C1C]">{passwordError}</p>} {passwordError && <p className="text-xs text-[#B91C1C]">{passwordError}</p>}
@ -433,7 +442,7 @@ const CompanyProfile = () => {
placeholder="Confirm password" placeholder="Confirm password"
width="100%" width="100%"
type="password" type="password"
showToggle showToggle={false}
className={confirmError ? 'border-red-500' : ''} className={confirmError ? 'border-red-500' : ''}
/> />
{confirmError && <p className="text-xs text-[#B91C1C]">{confirmError}</p>} {confirmError && <p className="text-xs text-[#B91C1C]">{confirmError}</p>}
@ -592,7 +601,7 @@ const CompanyProfile = () => {
onChange={handleFormChange('contactPersonName')} onChange={handleFormChange('contactPersonName')}
placeholder="Enter Name" placeholder="Enter Name"
width="100%" width="100%"
required // required
error={fieldErrors.contactPersonName} error={fieldErrors.contactPersonName}
/> />
<TextField <TextField
@ -634,7 +643,7 @@ const CompanyProfile = () => {
</div> </div>
</div> </div>
); );
case 3: // case 4:
return ( return (
<div className="space-y-4"> <div className="space-y-4">
<div className="flex flex-col gap-2 md:flex-row md:items-center md:justify-between"> <div className="flex flex-col gap-2 md:flex-row md:items-center md:justify-between">
@ -736,7 +745,7 @@ const CompanyProfile = () => {
onChange={handleFormChange('corporateMakaniNumber')} onChange={handleFormChange('corporateMakaniNumber')}
placeholder="Enter Number" placeholder="Enter Number"
width="100%" width="100%"
required // required
error={fieldErrors.corporateMakaniNumber} error={fieldErrors.corporateMakaniNumber}
readOnly={form.corporateSameAs} readOnly={form.corporateSameAs}
/> />
@ -746,7 +755,7 @@ const CompanyProfile = () => {
onChange={handleFormChange('corporateContactPersonName')} onChange={handleFormChange('corporateContactPersonName')}
placeholder="Enter Name" placeholder="Enter Name"
width="100%" width="100%"
required // required
error={fieldErrors.corporateContactPersonName} error={fieldErrors.corporateContactPersonName}
readOnly={form.corporateSameAs} readOnly={form.corporateSameAs}
/> />
@ -795,9 +804,7 @@ const CompanyProfile = () => {
case 4: case 4:
return ( return (
<div className="space-y-4"> <div className="space-y-4">
<div> <h4 className="text-[16px] font-semibold text-[#232528] mb-4">Employment in Establishment</h4>
<h4 className="text-[16px] font-semibold text-[#232528]">Employment in Establishment</h4>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4"> <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
<TextField <TextField
label="Number of Emirati Male" label="Number of Emirati Male"
@ -806,7 +813,7 @@ const CompanyProfile = () => {
placeholder="Enter Count" placeholder="Enter Count"
width="100%" width="100%"
type="number" type="number"
required // required
error={fieldErrors.employmentEmiratiMale} error={fieldErrors.employmentEmiratiMale}
/> />
<TextField <TextField
@ -816,7 +823,7 @@ const CompanyProfile = () => {
placeholder="Enter Count" placeholder="Enter Count"
width="100%" width="100%"
type="number" type="number"
required // required
error={fieldErrors.employmentNonEmiratiMale} error={fieldErrors.employmentNonEmiratiMale}
/> />
<TextField <TextField
@ -826,7 +833,7 @@ const CompanyProfile = () => {
placeholder="Enter Count" placeholder="Enter Count"
width="100%" width="100%"
type="number" type="number"
required // required
error={fieldErrors.employmentEmiratiFemale} error={fieldErrors.employmentEmiratiFemale}
/> />
<TextField <TextField
@ -836,7 +843,7 @@ const CompanyProfile = () => {
placeholder="Enter Count" placeholder="Enter Count"
width="100%" width="100%"
type="number" type="number"
required // required
error={fieldErrors.employmentNonEmiratiFemale} error={fieldErrors.employmentNonEmiratiFemale}
/> />
<TextField <TextField
@ -858,7 +865,110 @@ const CompanyProfile = () => {
</div> </div>
</div> </div>
); );
case 5: case 3:
return (
<div className="space-y-6">
<h4 className="text-[16px] font-semibold text-[#232528]">Products</h4>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
{/* Available Products Panel */}
<div className="bg-white rounded-lg border border-[#E5E7EB] overflow-hidden">
<div className="bg-[#F9FAFB] px-4 py-3 border-b border-[#E5E7EB]">
<h5 className="text-sm font-medium text-[#111827]">Available Products</h5>
</div>
<div className="p-4">
<div className="relative mb-4">
<input
type="text"
placeholder="Search products..."
value={productSearchTerm}
onChange={handleProductSearch}
className="w-full px-3 py-2 border border-[#D1D5DB] rounded-md text-sm focus:outline-none focus:ring-2 focus:ring-[#92722A] focus:border-transparent"
/>
<img
src={searchIconSrc}
alt="Search"
className="absolute right-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-gray-400"
/>
</div>
<div className="border border-[#E5E7EB] rounded-md overflow-hidden">
{isLoadingProducts ? (
<div className="flex justify-center items-center p-4">
<div className="animate-spin rounded-full h-6 w-6 border-b-2 border-[#92722A]"></div>
</div>
) : availableProducts.length > 0 ? (
<ul className="divide-y divide-[#E5E7EB] max-h-96 overflow-y-auto">
{availableProducts.map((product) => (
<li key={product.id} className="p-3 hover:bg-[#F9FAFB] flex justify-between items-center">
<div>
<div className="text-sm font-medium text-[#111827]">
{product.hsCode || ''}
{product.hsCode && (product.productName || product.label || product.product_name) ? ' - ' : ''}
{product.productName || product.label || product.product_name || ''}
</div>
</div>
<button
type="button"
onClick={() => handleAddProduct(product)}
className="text-[#92722A] hover:text-[#7A5F1E] text-sm font-medium"
>
Add
</button>
</li>
))}
</ul>
) : (
<div className="p-4 text-center text-sm text-[#6B7280]">
{productSearchTerm ? 'No matching products found' : 'No products available'}
</div>
)}
</div>
</div>
</div>
{/* Selected Products Panel */}
<div className="bg-white rounded-lg border border-[#E5E7EB] overflow-hidden">
<div className="bg-[#F9FAFB] px-4 py-3 border-b border-[#E5E7EB] flex justify-between items-center">
<h5 className="text-sm font-medium text-[#111827]">Selected Products</h5>
{selectedProducts.length > 0 && (
<span className="bg-[#FEF3C7] text-[#92400E] text-xs font-medium px-2 py-0.5 rounded-full">
{selectedProducts.length} selected
</span>
)}
</div>
<div className="p-4">
{selectedProducts.length > 0 ? (
<ul className="divide-y divide-[#E5E7EB] max-h-96 overflow-y-auto">
{selectedProducts.map((product) => (
<li key={product.id} className="p-3 hover:bg-[#F9FAFB] flex justify-between items-center">
<div>
<div className="text-sm font-medium text-[#111827]">
{product.hsCode || ''}
{product.hsCode && (product.productName || product.label || product.product_name) ? ' - ' : ''}
{product.productName || product.label || product.product_name || ''}
</div>
</div>
<button
type="button"
onClick={() => handleRemoveProduct(product)}
className="text-[#EF4444] hover:text-[#DC2626] text-sm font-medium"
>
Remove
</button>
</li>
))}
</ul>
) : (
<div className="p-4 text-center text-sm text-[#6B7280] border border-[#E5E7EB] rounded-md">
-
</div>
)}
</div>
</div>
</div>
</div>
);
default: default:
return ( return (
<div className="space-y-4"> <div className="space-y-4">
@ -936,6 +1046,113 @@ const CompanyProfile = () => {
} }
}; };
// Fetch products when Products step is active
React.useEffect(() => {
if (activeStep === 3) {
// In the loadProducts function (around line 1044), update the product processing:
const loadProducts = async () => {
try {
console.log('Fetching products...');
setIsLoadingProducts(true);
const response = await fetchProducts();
console.log('Products API Response:', response);
let productsData = [];
if (Array.isArray(response)) {
productsData = response;
} else if (response?.data) {
productsData = Array.isArray(response.data) ? response.data : response.data.products || [];
}
console.log('Processed products data:', productsData);
// Update the products state with the formatted data
const formattedProducts = productsData.map(p => ({
id: p.value,
label: p.label,
value: p.value
}));
setProducts(formattedProducts);
setAvailableProducts(formattedProducts.filter(p =>
!selectedProducts.some(sp => sp.id === p.id)
));
} catch (error) {
console.error('Error loading products:', error);
showToast('error', 'Failed to load products');
} finally {
setIsLoadingProducts(false);
}
};
loadProducts();
}
}, [activeStep, selectedProducts]);
const handleProductSearch = (e) => {
const searchTerm = e.target.value.toLowerCase();
setProductSearchTerm(searchTerm);
if (!searchTerm) {
setAvailableProducts(products.filter(p =>
!selectedProducts.some(sp => sp.id === p.id)
));
return;
}
const filtered = products.filter(product =>
product.label.toLowerCase().includes(searchTerm) &&
!selectedProducts.some(sp => sp.id === product.id)
);
setAvailableProducts(filtered);
};
const handleAddProduct = (product) => {
console.log('Adding product:', product);
setSelectedProducts(prev => {
const updated = [...prev, product];
console.log('Updated selected products:', updated);
return updated;
});
// Remove from available products
setAvailableProducts(prev => prev.filter(p => p.id !== product.id));
setAvailableProducts(prev => {
const updated = prev.filter(p => p.id !== product.id);
console.log('Updated available products after add:', updated);
return updated;
});
};
const handleRemoveProduct = (product) => {
console.log('Removing product:', product);
setSelectedProducts(prev => {
const updated = prev.filter(p => p.id !== product.id);
console.log('Updated selected products after removal:', updated);
return updated;
});
// Add back to available products if not already there
setAvailableProducts(prev => {
if (!prev.some(p => p.id === product.id)) {
return [...prev, product];
}
return prev;
});
// Only add back to available if it matches the search term or there's no search term
if (!productSearchTerm ||
(product.hs_code && product.hs_code.toLowerCase().includes(productSearchTerm)) ||
(product.product_name && product.product_name.toLowerCase().includes(productSearchTerm))) {
setAvailableProducts(prev => {
const updated = [...prev, product];
console.log('Updated available products after remove:', updated);
return updated;
});
}
};
const handleNextStep = React.useCallback(() => { const handleNextStep = React.useCallback(() => {
if (!validateStepFields(activeStep)) { if (!validateStepFields(activeStep)) {
return; return;
@ -1015,12 +1232,7 @@ const CompanyProfile = () => {
); );
}; };
// Calculate pagination range const displayedRows = filteredProfiles.map((item) => {
const startIndex = (currentPage - 1) * pageSize;
const endIndex = Math.min(startIndex + pageSize, filteredProfiles.length);
const paginatedProfiles = filteredProfiles.slice(startIndex, endIndex);
const displayedRows = paginatedProfiles.map((item) => {
const isEditing = const isEditing =
editingRow !== null && profiles[editingRow]?.establishmentId === item.establishmentId; editingRow !== null && profiles[editingRow]?.establishmentId === item.establishmentId;
const isDeleting = const isDeleting =
@ -1032,7 +1244,9 @@ const CompanyProfile = () => {
item.emirate, item.emirate,
item.isicCode, item.isicCode,
item.establishmentId, item.establishmentId,
item.products, item.products && item.products.length > 0
? item.products.map(p => p.hsCode ? `${p.hsCode} - ${p.productName || p.label || p.product_name || 'N/A'}` : (p.productName || p.product_name || 'N/A')).filter(Boolean).join(', ')
: '-',
item.totalEmployees, item.totalEmployees,
item.createdBy, item.createdBy,
item.createdOn, item.createdOn,
@ -1368,15 +1582,31 @@ const corporateFieldKeys = Object.values(contactToCorporateMap);
const loadProfiles = async () => { const loadProfiles = async () => {
setIsLoading(true); setIsLoading(true);
const params = { // First, get the total number of records
page: currentPage, const countParams = {
limit: pageSize,
search: debouncedSearch || undefined, search: debouncedSearch || undefined,
emirateId: selectedEmirateFilter || undefined, emirateId: selectedEmirateFilter || undefined,
status: selectedStatusFilter || undefined, status: selectedStatusFilter || undefined,
page: 1,
limit: 1, // Just need the total count
}; };
try { try {
// First, get the total count
const countResponse = await fetchEstablishments({ params: countParams, signal: controller.signal });
const countPayload = countResponse?.data ?? countResponse;
const totalRecords = countPayload?.pagination?.total_records || 0;
// Then fetch all records with the total count as limit
const params = {
search: debouncedSearch || undefined,
emirateId: selectedEmirateFilter || undefined,
status: selectedStatusFilter || undefined,
page: 1,
limit: totalRecords || 1000,
};
// Fetch all records
const response = await fetchEstablishments({ params, signal: controller.signal }); const response = await fetchEstablishments({ params, signal: controller.signal });
const payload = response?.data ?? response; const payload = response?.data ?? response;
@ -1384,25 +1614,15 @@ const loadProfiles = async () => {
console.group('API Response Details'); console.group('API Response Details');
console.log('Full response:', response); console.log('Full response:', response);
console.log('Response data:', payload); console.log('Response data:', payload);
console.log('Pagination object:', payload?.pagination);
console.log('Total records from pagination:', payload?.pagination?.total_records);
console.groupEnd(); console.groupEnd();
const records = Array.isArray(payload?.data) ? payload.data : Array.isArray(payload) ? payload : []; const records = Array.isArray(payload?.data) ? payload.data : Array.isArray(payload) ? payload : [];
if (cancelled) return; if (cancelled) return;
setProfiles(records.map(mapApiEstablishmentToProfile));
// Get total records from pagination object in the API response // Set all records to the profiles state
const totalRecords = payload?.pagination?.total_records; setProfiles(records.map(mapApiEstablishmentToProfile));
if (totalRecords !== undefined) { // Set total items to the number of records
console.log(`Setting total items from pagination.total_records: ${totalRecords}`); setTotalItems(records.length);
setTotalItems(totalRecords);
} else {
// Fallback to other possible locations if pagination is not available
const fallbackTotal = payload?.total || payload?.count || records.length;
console.warn('pagination.total_records not found, using fallback value:', fallbackTotal);
setTotalItems(fallbackTotal);
}
} catch (error) { } catch (error) {
if (cancelled) return; if (cancelled) return;
if (error.name === 'CanceledError' || error.name === 'AbortError') return; if (error.name === 'CanceledError' || error.name === 'AbortError') return;
@ -1638,6 +1858,7 @@ const loadProfiles = async () => {
} }
setModalMode(null); setModalMode(null);
setForm(createEmptyProfile()); setForm(createEmptyProfile());
setSelectedProducts([]); // Clear selected products when modal is closed
setEditingRow(null); setEditingRow(null);
setFieldErrors({}); setFieldErrors({});
setIsDirty(false); setIsDirty(false);
@ -2011,6 +2232,9 @@ const requiredFields = [
email: form.userProfileEmail || '', email: form.userProfileEmail || '',
password: form.userProfilePassword || '' password: form.userProfilePassword || ''
}, },
establishment_products: Array.isArray(selectedProducts) && selectedProducts.length > 0
? selectedProducts.map(p => ({ product_id: p.id || p.product_id || 0 }))
: [{ product_id: 0 }],
}; };
try { try {
const apiResponse = await createEstablishment(apiPayload); const apiResponse = await createEstablishment(apiPayload);
@ -2020,10 +2244,18 @@ const requiredFields = [
const createdRecord = apiResponse?.data; const createdRecord = apiResponse?.data;
createdRecordId = createdRecord?.id ?? createdRecord?.establishment?.id ?? null; createdRecordId = createdRecord?.id ?? createdRecord?.establishment?.id ?? null;
apiResultData = createdRecord; apiResultData = createdRecord;
setIsRefreshing(true);
setTimeout(() => {
window.location.reload();
}, 1000);
} catch (apiError) { } catch (apiError) {
const apiMessage = apiError?.response?.data?.message || apiError?.message || ''; const apiMessage = apiError?.response?.data?.message || apiError?.message || '';
if (apiMessage.includes("Field 'gender' doesn't have a default value")) { if (apiMessage.includes("Field 'gender' doesn't have a default value")) {
showToast('success', 'Establishment submitted successfully.'); showToast('success', 'Establishment submitted successfully.');
setIsRefreshing(true);
setTimeout(() => {
window.location.reload();
}, 1000);
} else { } else {
throw apiError; throw apiError;
} }
@ -2088,6 +2320,9 @@ const requiredFields = [
if (establishmentUserPayload.name || establishmentUserPayload.email || establishmentUserPayload.password) { if (establishmentUserPayload.name || establishmentUserPayload.email || establishmentUserPayload.password) {
updatePayload.establishment_user = establishmentUserPayload; updatePayload.establishment_user = establishmentUserPayload;
} }
updatePayload.establishment_products = Array.isArray(selectedProducts) && selectedProducts.length > 0
? selectedProducts.map(p => ({ product_id: p.id || p.product_id || 0 }))
: [{ product_id: 0 }];
const apiResponse = await updateEstablishment(targetId, updatePayload); const apiResponse = await updateEstablishment(targetId, updatePayload);
console.log("apiResponse",apiResponse) console.log("apiResponse",apiResponse)
const successMessage = apiResponse?.message || 'Establishment updated successfully.'; const successMessage = apiResponse?.message || 'Establishment updated successfully.';
@ -2355,7 +2590,7 @@ const requiredFields = [
window.scrollTo({ top: 0, behavior: 'smooth' }); window.scrollTo({ top: 0, behavior: 'smooth' });
}, },
pageSize, pageSize,
totalItems: totalItems, // Use the total count from API response totalItems: filteredProfiles.length,
pageSizeOptions: [10, 20, 50, 100], pageSizeOptions: [10, 20, 50, 100],
onPageSizeChange: (size) => { onPageSizeChange: (size) => {
setPageSize(size); setPageSize(size);
@ -2501,6 +2736,15 @@ const requiredFields = [
</div> </div>
)} )}
{/* Full Page Loader */}
{isRefreshing && (
<div className="fixed inset-0 z-[9999] bg-white/80 flex flex-col items-center justify-center">
<div className="animate-spin rounded-full h-16 w-16 border-t-4 border-[#92722A]"></div>
<p className="mt-4 text-lg font-medium text-gray-700">Updating data...</p>
<p className="text-sm text-gray-500">Please wait while we refresh the page</p>
</div>
)}
{showDeleteConfirm && deletingProfile && ( {showDeleteConfirm && deletingProfile && (
<div className="fixed inset-0 z-50"> <div className="fixed inset-0 z-50">
<div <div

View File

@ -3,7 +3,6 @@ import Table from '@/components/common/Table';
import { TextField, SelectField, DateField } from '@/components/common/FormControls'; import { TextField, SelectField, DateField } from '@/components/common/FormControls';
import StatusBadge from '@/components/common/StatusBadge'; import StatusBadge from '@/components/common/StatusBadge';
import { createQuarterlyWindow, updateQuarterlyWindow, getQuarterlyWindows } from '@/services/configuration/quarterlyWindows'; import { createQuarterlyWindow, updateQuarterlyWindow, getQuarterlyWindows } from '@/services/configuration/quarterlyWindows';
import CustomToast from '@/components/common/CustomToast';
const downloadIconSrc = '/assets/images/DownloadSimple.svg'; const downloadIconSrc = '/assets/images/DownloadSimple.svg';
const addIconSrc = '/assets/images/ic_baseline-plus.svg'; const addIconSrc = '/assets/images/ic_baseline-plus.svg';
@ -13,6 +12,56 @@ const caretDownActiveSrc = '/assets/images/caretdown-active.svg';
const searchIconSrc = '/assets/images/material-symbols_search-rounded.svg'; const searchIconSrc = '/assets/images/material-symbols_search-rounded.svg';
const caretDownSrc = '/assets/images/CaretDown-black.svg'; const caretDownSrc = '/assets/images/CaretDown-black.svg';
// Custom Toast Component
const CustomToast = ({ message, type, onClose }) => {
React.useEffect(() => {
const timer = setTimeout(() => {
onClose();
}, 3000);
return () => clearTimeout(timer);
}, [onClose]);
const getToastStyles = () => {
switch (type) {
case 'success':
return 'bg-green-50 border-green-200 text-green-800';
case 'error':
return 'bg-red-50 border-red-200 text-red-800';
case 'warning':
return 'bg-yellow-50 border-yellow-200 text-yellow-800';
default:
return 'bg-blue-50 border-blue-200 text-blue-800';
}
};
const getIcon = () => {
switch (type) {
case 'success':
return '✅';
case 'error':
return '❌';
case 'warning':
return '⚠️';
default:
return '';
}
};
return (
<div className={`flex items-center p-4 mb-4 rounded-lg border ${getToastStyles()} shadow-lg min-w-80 max-w-md`}>
<span className="text-lg mr-3">{getIcon()}</span>
<span className="flex-1 text-sm font-medium">{message}</span>
<button
onClick={onClose}
className="ml-4 text-gray-400 hover:text-gray-600 transition-colors"
>
</button>
</div>
);
};
const createEmptyQuarterForm = () => ({ const createEmptyQuarterForm = () => ({
survey_name: '', survey_name: '',
establishment: '-', // Default to hyphen establishment: '-', // Default to hyphen
@ -36,9 +85,10 @@ const QuarterlyWindows = () => {
const [quarterFilter, setQuarterFilter] = React.useState('all'); const [quarterFilter, setQuarterFilter] = React.useState('all');
const [hoveredEdit, setHoveredEdit] = React.useState(null); const [hoveredEdit, setHoveredEdit] = React.useState(null);
const [currentPage, setCurrentPage] = React.useState(1); const [currentPage, setCurrentPage] = React.useState(1);
const pageSize = 10; const [pageSize, setPageSize] = React.useState(10);
const [isLoading, setIsLoading] = React.useState(false); const [isLoading, setIsLoading] = React.useState(false);
const [toast, setToast] = React.useState({ show: false, message: '', type: 'success' }); const [validationErrors, setValidationErrors] = React.useState({});
const [toastData, setToastData] = React.useState(null);
// Format date to yyyy-MM-dd for input fields // Format date to yyyy-MM-dd for input fields
const formatDateForInput = (dateString) => { const formatDateForInput = (dateString) => {
@ -80,11 +130,9 @@ const QuarterlyWindows = () => {
setQuarterData(formattedData); setQuarterData(formattedData);
} else { } else {
console.error('Unexpected API response format:', response); console.error('Unexpected API response format:', response);
// Optionally set some error state here
} }
} catch (error) { } catch (error) {
console.error('Error fetching quarterly windows:', error); console.error('Error fetching quarterly windows:', error);
// You might want to add error handling UI here
} finally { } finally {
setIsLoading(false); setIsLoading(false);
} }
@ -107,20 +155,21 @@ const QuarterlyWindows = () => {
{ name: 'Submission Count', className: 'whitespace-nowrap text-left' }, { name: 'Submission Count', className: 'whitespace-nowrap text-left' },
{ name: 'Actions', className: 'whitespace-nowrap text-left' }, { name: 'Actions', className: 'whitespace-nowrap text-left' },
]; ];
// Column widths significantly increased for better content visibility
const columnwidth = [ const columnwidth = [
220, // Survey Name (reduced from 550) 220, // Survey Name
100, // Year (reduced from 150) 100, // Year
120, // Quarter (reduced from 180) 120, // Quarter
150, // Start Date (reduced from 220) 150, // Start Date
150, // End Date (reduced from 220) 150, // End Date
180, // Grace Period (reduced from 250) 180, // Grace Period
120, // Assigned (reduced from 180) 120, // Assigned
120, // Responded (reduced from 200) 120, // Responded
140, // Not Responded (reduced from 220) 140, // Not Responded
140, // Submission Count (reduced from 240) 140, // Submission Count
120 // Actions (reduced from 160) 120 // Actions
]; ];
const filteredRows = React.useMemo(() => { const filteredRows = React.useMemo(() => {
const term = searchTerm.trim().toLowerCase(); const term = searchTerm.trim().toLowerCase();
return quarterData.filter((item) => { return quarterData.filter((item) => {
@ -186,7 +235,6 @@ const QuarterlyWindows = () => {
'actions', 'actions',
]); ]);
// Generate year options from 2022 to current year, plus any additional years in data // Generate year options from 2022 to current year, plus any additional years in data
const yearOptions = React.useMemo(() => { const yearOptions = React.useMemo(() => {
const currentYear = new Date().getFullYear(); const currentYear = new Date().getFullYear();
@ -219,6 +267,7 @@ const QuarterlyWindows = () => {
setOriginalForm(null); setOriginalForm(null);
setEditingRow(null); setEditingRow(null);
setModalMode('add'); setModalMode('add');
setValidationErrors({});
}; };
React.useEffect(() => { React.useEffect(() => {
@ -238,11 +287,13 @@ const QuarterlyWindows = () => {
setForm(formData); setForm(formData);
setOriginalForm(formData); setOriginalForm(formData);
setModalMode('edit'); setModalMode('edit');
setValidationErrors({});
}; };
const handleCloseModal = () => { const handleCloseModal = () => {
setModalMode(null); setModalMode(null);
setEditingRow(null); setEditingRow(null);
setValidationErrors({});
}; };
const handleReset = () => { const handleReset = () => {
@ -258,44 +309,61 @@ const QuarterlyWindows = () => {
} else { } else {
setForm(createEmptyQuarterForm()); setForm(createEmptyQuarterForm());
} }
setValidationErrors({});
}; };
const validateForm = () => {
const errors = {};
if (!form.survey_name.trim()) {
errors.survey_name = 'Survey Name is required';
}
if (!form.year) {
errors.year = 'Year is required';
}
if (!form.quarter) {
errors.quarter = 'Quarter is required';
}
if (!form.startDate) {
errors.startDate = 'Open Date is required';
}
if (!form.endDate) {
errors.endDate = 'Close Date is required';
}
const showToast = (message, type = 'success') => { // Date validation
setToast({ show: true, message, type }); if (form.startDate && form.endDate) {
setTimeout(() => setToast(prev => ({ ...prev, show: false })), 4000); const startDate = new Date(form.startDate);
const endDate = new Date(form.endDate);
if (endDate <= startDate) {
errors.endDate = 'Close Date must be after Open Date';
}
}
setValidationErrors(errors);
return Object.keys(errors).length === 0;
};
const handleFormChange = (field) => (value) => {
setForm(prev => ({ ...prev, [field]: value }));
// Clear validation error when user starts typing
if (validationErrors[field]) {
setValidationErrors(prev => ({
...prev,
[field]: ''
}));
}
}; };
const handleSave = async () => { const handleSave = async () => {
try { try {
if (!form.survey_name || !form.year || !form.quarter || !form.startDate || !form.endDate) { if (!validateForm()) {
showToast('Please fill in all required fields', 'error');
return; return;
} }
const startDate = new Date(form.startDate); const startDate = new Date(form.startDate);
const endDate = new Date(form.endDate); const endDate = new Date(form.endDate);
if (endDate <= startDate) {
showToast('Close Date must be after Open Date', 'error');
return;
}
const formatDateForAPI = (dateString) => {
if (!dateString) return '';
// If already in YYYY-MM-DD format
if (dateString.match(/^\d{4}-\d{2}-\d{2}$/)) {
return dateString;
}
// Handle DD/MM/YYYY format
if (dateString.includes('/')) {
const [day, month, year] = dateString.split('/');
return `${year}-${month.padStart(2, '0')}-${day.padStart(2, '0')}`;
}
// For any other format, let the Date object handle it
return new Date(dateString).toISOString().split('T')[0];
};
const formattedData = { const formattedData = {
survey_name: form.survey_name || '', survey_name: form.survey_name || '',
@ -307,7 +375,6 @@ const QuarterlyWindows = () => {
assigned: form.assigned || 0, assigned: form.assigned || 0,
responded: form.responded || 0, responded: form.responded || 0,
not_responded: form.not_responded || 0, not_responded: form.not_responded || 0,
// submission_count: form.submissionCount || 0,
is_active: form.status === 'Active', is_active: form.status === 'Active',
establishment: form.establishment || '-', establishment: form.establishment || '-',
}; };
@ -317,7 +384,7 @@ const QuarterlyWindows = () => {
if (response.status === 'success') { if (response.status === 'success') {
const newItem = { const newItem = {
...formattedData, ...formattedData,
id: response.data.id || Date.now(), // Use the ID from response or generate a temporary one id: response.data.id || Date.now(),
startDate: form.startDate, startDate: form.startDate,
endDate: form.endDate, endDate: form.endDate,
gracePeriod: formattedData.grace_periods_days ? `${formattedData.grace_periods_days} days` : '0 days', gracePeriod: formattedData.grace_periods_days ? `${formattedData.grace_periods_days} days` : '0 days',
@ -325,14 +392,19 @@ const QuarterlyWindows = () => {
status: formattedData.is_active ? 'Active' : 'Inactive' status: formattedData.is_active ? 'Active' : 'Inactive'
}; };
setQuarterData((prev) => [...prev, newItem]); setQuarterData((prev) => [...prev, newItem]);
showToast('Quarterly window added successfully!');
// Show success toast for add
setToastData({
message: 'Quarterly survey created successfully!',
type: 'success'
});
} else { } else {
showToast('Failed to add quarterly window', 'error'); console.error('Failed to add quarterly window');
} }
} else if (modalMode === 'edit' && editingRow !== null) { } else if (modalMode === 'edit' && editingRow !== null) {
const itemId = quarterData[editingRow]?.id; const itemId = quarterData[editingRow]?.id;
if (!itemId) { if (!itemId) {
showToast('Error: Could not find the item to update', 'error'); console.error('Error: Could not find the item to update');
return; return;
} }
const response = await updateQuarterlyWindow(itemId, formattedData); const response = await updateQuarterlyWindow(itemId, formattedData);
@ -351,30 +423,37 @@ const QuarterlyWindows = () => {
: item : item
) )
); );
showToast('Quarterly window updated successfully!');
// Show success toast for edit
setToastData({
message: 'Quarterly survey updated successfully!',
type: 'success'
});
} else { } else {
showToast('Failed to update quarterly window', 'error'); console.error('Failed to update quarterly window');
} }
} }
handleCloseModal(); handleCloseModal();
} catch (error) { } catch (error) {
console.error('Error saving quarterly window:', error); console.error('Error saving quarterly window:', error);
showToast('An error occurred while saving. Please try again.', 'error');
} }
}; };
return ( return (
<> <div className="w-full max-w-[1280px] rounded-lg border border-[#C3C6CB] bg-white min-h-[348px] pb-6">
{toast.show && ( {/* Custom Toast Notification */}
<div className="fixed top-4 right-4 z-50"> {toastData && (
<CustomToast <div className="fixed inset-0 z-[9999] pointer-events-none flex items-start justify-center pt-20">
message={toast.message} <div className="pointer-events-auto">
type={toast.type} <CustomToast
onClose={() => setToast({ ...toast, show: false })} message={toastData.message}
/> type={toastData.type}
onClose={() => setToastData(null)}
/>
</div>
</div> </div>
)} )}
<div className="w-full max-w-[1280px] rounded-lg border border-[#C3C6CB] bg-white min-h-[348px] pb-6">
<div className="px-6 py-4"> <div className="px-6 py-4">
<div className="flex flex-col gap-3 md:h-12 md:flex-row md:items-center md:justify-between"> <div className="flex flex-col gap-3 md:h-12 md:flex-row md:items-center md:justify-between">
<h3 className="text-[18px] leading-[28px] font-medium text-[#232528]">Manage Surveys ({filteredRows.length})</h3> <h3 className="text-[18px] leading-[28px] font-medium text-[#232528]">Manage Surveys ({filteredRows.length})</h3>
@ -425,40 +504,40 @@ const QuarterlyWindows = () => {
</div> </div>
<button <button
type="button" type="button"
onClick={() => { onClick={() => {
if (!filteredRows.length) return; if (!filteredRows.length) return;
const csvHeader = headers const csvHeader = headers
.filter((h) => h.name !== 'Actions') .filter((h) => h.name !== 'Actions')
.map((h) => `"${h.name}"`) .map((h) => `"${h.name}"`)
.join(','); .join(',');
const csvRows = filteredRows.map((item) => { const csvRows = filteredRows.map((item) => {
const rowData = [ const rowData = [
item.survey_name || '', item.survey_name || '',
item.year || '', item.year || '',
item.quarter || '', item.quarter || '',
item.startDate || '', item.startDate || '',
item.endDate || '', item.endDate || '',
item.gracePeriod || '', item.gracePeriod || '',
item.assigned || '', item.assigned || '',
item.responded || '', item.responded || '',
item.not_responded || '', item.not_responded || '',
item.submissionCount || '', item.submissionCount || '',
item.status || '' item.status || ''
]; ];
return rowData.map((val) => `"${String(val).replace(/"/g, '""')}"`).join(','); return rowData.map((val) => `"${String(val).replace(/"/g, '""')}"`).join(',');
}); });
const blob = new Blob([csvHeader + '\n' + csvRows.join('\n')], { const blob = new Blob([csvHeader + '\n' + csvRows.join('\n')], {
type: 'text/csv;charset=utf-8;', type: 'text/csv;charset=utf-8;',
}); });
const url = URL.createObjectURL(blob); const url = URL.createObjectURL(blob);
const link = document.createElement('a'); const link = document.createElement('a');
link.href = url; link.href = url;
link.setAttribute('download', 'quarterly-windows.csv'); link.setAttribute('download', 'quarterly-windows.csv');
document.body.appendChild(link); document.body.appendChild(link);
link.click(); link.click();
document.body.removeChild(link); document.body.removeChild(link);
URL.revokeObjectURL(url); URL.revokeObjectURL(url);
}} }}
className={`h-10 px-5 rounded-[6px] border text-sm inline-flex items-center gap-3 ${ className={`h-10 px-5 rounded-[6px] border text-sm inline-flex items-center gap-3 ${
filteredRows.length filteredRows.length
? 'bg-[#F7F7F7] border-[#C3C6CB] text-[#232528]' ? 'bg-[#F7F7F7] border-[#C3C6CB] text-[#232528]'
@ -515,7 +594,6 @@ const QuarterlyWindows = () => {
currentPage, currentPage,
onPageChange: (page) => { onPageChange: (page) => {
setCurrentPage(page); setCurrentPage(page);
// Scroll to top when changing pages
window.scrollTo({ top: 0, behavior: 'smooth' }); window.scrollTo({ top: 0, behavior: 'smooth' });
}, },
pageSize, pageSize,
@ -523,7 +601,7 @@ const QuarterlyWindows = () => {
pageSizeOptions: [10, 20, 50, 100], pageSizeOptions: [10, 20, 50, 100],
onPageSizeChange: (size) => { onPageSizeChange: (size) => {
setPageSize(size); setPageSize(size);
setCurrentPage(1); // Reset to first page when changing page size setCurrentPage(1);
} }
}} }}
/> />
@ -541,28 +619,6 @@ const QuarterlyWindows = () => {
border: '1px solid #E5E7EB', border: '1px solid #E5E7EB',
}} }}
> >
{/* Toast Notification */}
{toast.show && (
<div className="flex items-center gap-2 bg-[#FEF2F2] border-l-4 border-[#DC2626] p-3 w-[calc(100%-32px)] mx-auto mt-4 rounded">
<svg
className="flex-shrink-0 w-4 h-4 text-[#DC2626]"
fill="currentColor"
viewBox="0 0 20 20"
>
<path
fillRule="evenodd"
d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z"
clipRule="evenodd"
/>
</svg>
<span className="text-sm font-medium text-[#DC2626] leading-tight">
{toast.message}
</span>
</div>
)}
<div className="flex items-center justify-between px-6 py-4 border-b border-[#F1F2F4]"> <div className="flex items-center justify-between px-6 py-4 border-b border-[#F1F2F4]">
<h3 className="text-[18px] font-medium text-[#232528]"> <h3 className="text-[18px] font-medium text-[#232528]">
{modalMode === 'edit' ? 'Edit Quarter' : 'Create Quarterly Survey'} {modalMode === 'edit' ? 'Edit Quarter' : 'Create Quarterly Survey'}
@ -599,66 +655,106 @@ const QuarterlyWindows = () => {
<div className="px-6 py-5 space-y-4"> <div className="px-6 py-5 space-y-4">
<div className="w-full"> <div className="w-full">
<TextField <TextField
label="Survey Name" label={
<>
Survey Name <span className="text-red-500"></span>
</>
}
value={form.survey_name} value={form.survey_name}
required required
onChange={(e) => setForm({ ...form, survey_name: e.target.value })} onChange={(e) => handleFormChange('survey_name')(e.target.value)}
placeholder="Enter survey name" placeholder="Enter survey name"
// error={validationErrors.survey_name}
/> />
</div> {validationErrors.survey_name && (
<div className="grid grid-cols-1 md:grid-cols-2 gap-4"> <p className="mt-1 text-sm text-red-600">{validationErrors.survey_name}</p>
<SelectField )}
label="Quarter"
value={form.quarter}
required
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}
onChange={(e) => setForm({ ...form, year: e.target.value })}
options={Array.from({ length: 6 }).map((_, idx) => {
const yr = 2023 + idx;
return { label: String(yr), value: String(yr) };
})}
required
placeholder="Select Year"
/>
<DateField
label="Opens On"
value={form.startDate}
required
onChange={(e) => setForm({ ...form, startDate: e.target.value })}
placeholder="Select Date"
/>
<DateField
label="Closes On"
value={form.endDate}
required
onChange={(e) => setForm({ ...form, endDate: e.target.value })}
placeholder="Select Date"
/>
</div> </div>
{/* <SelectField <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
label="Status" <div>
value={form.status} <SelectField
required label={
onChange={(e) => setForm({ ...form, status: e.target.value })} <>
options={[ Quarter <span className="text-red-500"></span>
{ label: 'Active', value: 'Active' }, </>
{ label: 'Inactive', value: 'Inactive' }, }
]} value={form.quarter}
placeholder="Select Status" required
/> */} onChange={(e) => handleFormChange('quarter')(e.target.value)}
{/* </div> */} options={['Q1', 'Q2', 'Q3', 'Q4'].map((q) => ({ label: q, value: q }))}
<div className="w-full"> placeholder="Select Quarter"
// error={validationErrors.quarter}
/>
{validationErrors.quarter && (
<p className="mt-1 text-sm text-red-600">{validationErrors.quarter}</p>
)}
</div>
<div>
<SelectField
label={
<>
Year <span className="text-red-500"></span>
</>
}
value={form.year}
onChange={(e) => handleFormChange('year')(e.target.value)}
options={Array.from({ length: 6 }).map((_, idx) => {
const yr = 2023 + idx;
return { label: String(yr), value: String(yr) };
})}
required
placeholder="Select Year"
// error={validationErrors.year}
/>
{validationErrors.year && (
<p className="mt-1 text-sm text-red-600">{validationErrors.year}</p>
)}
</div>
<div>
<DateField
label={
<>
Opens On <span className="text-red-500"></span>
</>
}
value={form.startDate}
required
onChange={(e) => handleFormChange('startDate')(e.target.value)}
placeholder="Select Date"
error={validationErrors.startDate}
/>
{validationErrors.startDate && (
<p className="mt-1 text-sm text-red-600">{validationErrors.startDate}</p>
)}
</div>
<div>
<DateField
label={
<>
Closes On <span className="text-red-500"></span>
</>
}
value={form.endDate}
required
onChange={(e) => handleFormChange('endDate')(e.target.value)}
placeholder="Select Date"
error={validationErrors.endDate}
/>
{validationErrors.endDate && (
<p className="mt-1 text-sm text-red-600">{validationErrors.endDate}</p>
)}
</div>
</div>
<div className="w-full">
<SelectField <SelectField
label="Grace Period (Days)" label="Grace Period (Days)"
value={form.gracePeriod} value={form.gracePeriod}
onChange={(e) => setForm({ ...form, gracePeriod: e.target.value })} onChange={(e) => handleFormChange('gracePeriod')(e.target.value)}
options={['5 days', '10 days', '15 days', '20 days', '30 days'].map((item) => ({ options={['5 days', '10 days', '15 days', '20 days', '30 days'].map((item) => ({
label: item, label: item,
value: item, value: item,
@ -687,9 +783,7 @@ const QuarterlyWindows = () => {
</div> </div>
</div> </div>
)} )}
</div> </div>
</>
); );
}; };

View File

@ -15,7 +15,6 @@ const caretDownIconSrc = '/assets/images/caretdown-active.svg';
const rectangleIconSrc = '/assets/images/Rectangle.svg'; const rectangleIconSrc = '/assets/images/Rectangle.svg';
const checkboxIconSrc = '/assets/images/checkbox.svg'; const checkboxIconSrc = '/assets/images/checkbox.svg';
// Custom Toast Component // Custom Toast Component
const CustomToast = ({ message, type, onClose }) => { const CustomToast = ({ message, type, onClose }) => {
useEffect(() => { useEffect(() => {
@ -81,6 +80,7 @@ const UnitMaster = () => {
const [modalMode, setModalMode] = useState(null); const [modalMode, setModalMode] = useState(null);
const [currentPage, setCurrentPage] = useState(1); const [currentPage, setCurrentPage] = useState(1);
const [form, setForm] = useState(createEmptyUnitForm()); const [form, setForm] = useState(createEmptyUnitForm());
const [formErrors, setFormErrors] = useState({});
const [isProductDropdownOpen, setIsProductDropdownOpen] = useState(false); const [isProductDropdownOpen, setIsProductDropdownOpen] = useState(false);
const productDropdownRef = useRef(null); const productDropdownRef = useRef(null);
const pageSize = 10; const pageSize = 10;
@ -88,7 +88,6 @@ const UnitMaster = () => {
const [toastData, setToastData] = useState(null); const [toastData, setToastData] = useState(null);
const [searchTerm, setSearchTerm] = React.useState(''); const [searchTerm, setSearchTerm] = React.useState('');
// Fetch units on component mount // Fetch units on component mount
useEffect(() => { useEffect(() => {
fetchUnits(); fetchUnits();
@ -114,14 +113,15 @@ const UnitMaster = () => {
}; };
const filteredUnits = React.useMemo(() => { const filteredUnits = React.useMemo(() => {
if (!searchTerm) return units; if (!searchTerm) return units;
const term = searchTerm.toLowerCase(); const term = searchTerm.toLowerCase();
return units.filter(unit => return units.filter(unit =>
(unit.uom && unit.uom.toLowerCase().includes(term)) || (unit.uom && unit.uom.toLowerCase().includes(term)) ||
(unit.uom_short_name && unit.uom_short_name.toLowerCase().includes(term)) || (unit.uom_short_name && unit.uom_short_name.toLowerCase().includes(term)) ||
(unit.description && unit.description.toLowerCase().includes(term)) (unit.description && unit.description.toLowerCase().includes(term))
); );
}, [units, searchTerm]); }, [units, searchTerm]);
useEffect(() => { useEffect(() => {
const userProfile = sessionStorage.getItem('user_profile'); const userProfile = sessionStorage.getItem('user_profile');
if (userProfile) { if (userProfile) {
@ -162,7 +162,7 @@ const UnitMaster = () => {
'actions', 'actions',
]; ];
}); });
}, [units, currentUser,filteredUnits]); }, [units, currentUser, filteredUnits]);
const statusOptions = React.useMemo( const statusOptions = React.useMemo(
() => [ () => [
@ -172,27 +172,29 @@ const UnitMaster = () => {
[] []
); );
// const productOptions = React.useMemo(
// () => [
// { label: 'Wheat', value: 'Wheat' },
// { label: 'Barley', value: 'Barley' },
// { label: 'Oats', value: 'Oats' },
// { label: 'Rice', value: 'Rice' },
// { label: 'Corn', value: 'Corn' },
// { label: 'Soybeans', value: 'Soybeans' },
// { label: 'Sorghum', value: 'Sorghum' },
// { label: 'Millet', value: 'Millet' },
// { label: 'Quinoa', value: 'Quinoa' },
// { label: 'Buckwheat', value: 'Buckwheat' },
// ],
// []
// );
const filteredRows = units; const filteredRows = units;
const validateForm = () => {
const errors = {};
// Unit Name validation
if (!form.unitName.trim()) {
errors.unitName = 'Unit Name is required';
}
// Description validation - red color validation
if (!form.description.trim()) {
errors.description = 'Please enter a description.';
}
setFormErrors(errors);
return Object.keys(errors).length === 0;
};
const openAddModal = () => { const openAddModal = () => {
setEditingRow(null); setEditingRow(null);
setForm(createEmptyUnitForm()); setForm(createEmptyUnitForm());
setFormErrors({});
setModalMode('add'); setModalMode('add');
setIsProductDropdownOpen(false); setIsProductDropdownOpen(false);
}; };
@ -207,6 +209,7 @@ const UnitMaster = () => {
productsMapped: Array.isArray(selected.productsMapped) ? [...selected.productsMapped] : [], productsMapped: Array.isArray(selected.productsMapped) ? [...selected.productsMapped] : [],
status: selected.is_active ? 'Active' : 'Inactive', status: selected.is_active ? 'Active' : 'Inactive',
}); });
setFormErrors({});
setModalMode('edit'); setModalMode('edit');
setIsProductDropdownOpen(false); setIsProductDropdownOpen(false);
}; };
@ -218,6 +221,7 @@ const UnitMaster = () => {
const closeModal = () => { const closeModal = () => {
setModalMode(null); setModalMode(null);
setForm(createEmptyUnitForm()); setForm(createEmptyUnitForm());
setFormErrors({});
setEditingRow(null); setEditingRow(null);
setIsProductDropdownOpen(false); setIsProductDropdownOpen(false);
}; };
@ -225,6 +229,14 @@ const UnitMaster = () => {
const handleFormChange = (field) => (event) => { const handleFormChange = (field) => (event) => {
const value = event?.target?.value ?? event; const value = event?.target?.value ?? event;
setForm((prev) => ({ ...prev, [field]: value })); setForm((prev) => ({ ...prev, [field]: value }));
// Clear error when user starts typing
if (formErrors[field]) {
setFormErrors(prev => ({
...prev,
[field]: ''
}));
}
}; };
const handleProductToggle = (value) => { const handleProductToggle = (value) => {
@ -256,24 +268,13 @@ const UnitMaster = () => {
}, [isProductDropdownOpen]); }, [isProductDropdownOpen]);
const handleSave = async () => { const handleSave = async () => {
// Validate form before saving
if (!validateForm()) {
return;
}
const selectedProducts = Array.isArray(form.productsMapped) ? form.productsMapped : []; const selectedProducts = Array.isArray(form.productsMapped) ? form.productsMapped : [];
console.log(selectedProducts,"selectedProducts"); console.log(selectedProducts,"selectedProducts");
// Validation checks
if (!form.unitName) {
setToastData({
message: 'Please enter Unit Name.',
type: 'error'
});
return;
}
if (!form.description) {
setToastData({
message: 'Please enter Description.',
type: 'error'
});
return;
}
// Status validation removed as per request
// Check for duplicate unit name // Check for duplicate unit name
const nameExists = units.some( const nameExists = units.some(
@ -379,26 +380,9 @@ const UnitMaster = () => {
return ( return (
<div className="bg-white w-full rounded-lg shadow-sm ring-1 ring-[#E5E7EB]"> <div className="bg-white w-full rounded-lg shadow-sm ring-1 ring-[#E5E7EB]">
<div className="px-6 py-3 flex items-center justify-between border-b border-[#E5E7EB]"> <div className="px-6 py-3 flex items-center justify-between border-b border-[#E5E7EB]">
<h3 className="text-[16px] font-medium text-[#232528]"> <h3 className="text-[16px] font-medium text-[#232528]">
Unit Master ({units.length}{units.length === 1 ? ' unit' : ''}) Unit Master ({units.length}{units.length === 1 ? ' unit' : ''})
</h3> </h3>
{/* <div className="flex-1 max-w-md">
<div className="relative">
<div className="absolute left-3 top-1/2 -translate-y-1/2">
<img src={searchIconSrc} alt="Search" className="h-4 w-4 text-[#5F646D]" />
</div>
<input
type="text"
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
placeholder="Search by code or name"
className="h-10 w-full rounded-md border border-[#C3C6CB] pl-9 pr-3 text-sm focus:outline-none focus:ring-1 focus:ring-[#92722A]"
/>
</div>
</div> */}
{/* </div> */}
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<button <button
type="button" type="button"
@ -548,81 +532,40 @@ const UnitMaster = () => {
<div className="px-6 py-5"> <div className="px-6 py-5">
<div className="grid grid-cols-1 md:grid-cols-2 gap-4"> <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<TextField <div>
label={ <TextField
<> label={
Unit Name <span className="text-red-500">*</span> <>
</> Unit Name <span className="text-red-500">*</span>
} </>
value={form.unitName} }
onChange={handleFormChange('unitName')} value={form.unitName}
placeholder="Enter Unit Name" onChange={handleFormChange('unitName')}
/> placeholder="Enter Unit Name"
<TextField // error={formErrors.unitName}
label="Description" />
value={form.description} {formErrors.unitName && (
onChange={handleFormChange('description')} <p className="mt-1 text-sm text-red-600">{formErrors.unitName}</p>
placeholder="Enter Description"
width="100%"
/>
{/* <div className="w-full" ref={productDropdownRef}>
<label className="block text-[14px] leading-[20px] font-medium text-[#232528] mb-1">Mapped Products</label>
<button
type="button"
className={`w-full h-10 rounded-[4px] border-2 px-4 text-sm flex items-center justify-between transition-colors ${
isProductDropdownOpen ? 'border-[#92722A]' : 'border-[#CBA344]'
}`}
onClick={() => setIsProductDropdownOpen((prev) => !prev)}
>
<span className={`${form.productsMapped.length ? 'text-[#232528]' : 'text-[#6B7280]'}`}>
{form.productsMapped.length
? `${form.productsMapped.length} product${form.productsMapped.length > 1 ? 's' : ''} selected`
: 'Select Products'}
</span>
<img
src={caretDownIconSrc}
alt="Toggle"
className={`h-4 w-4 transition-transform ${isProductDropdownOpen ? 'rotate-180' : ''}`}
/>
</button>
{isProductDropdownOpen && (
<div className="mt-2 max-h-64 overflow-auto rounded-lg border border-[#E5E7EB] bg-white shadow-[0_12px_24px_rgba(27,29,33,0.12)]">
<ul className="py-2">
{productOptions.map((option) => {
const checked = form.productsMapped.includes(option.value);
return (
<li key={option.value}>
<button
type="button"
className={`flex w-full items-center gap-3 px-4 py-2 text-left text-sm transition-colors ${
checked ? 'bg-[#FDF8EA]' : 'hover:bg-[#F5F5F5]'
}`}
onClick={() => handleProductToggle(option.value)}
>
<span className="relative flex h-5 w-5 items-center justify-center">
<img src={checked ? checkboxIconSrc : rectangleIconSrc} alt="" className="h-5 w-5" />
</span>
<span className="text-[#232528]">{option.label}</span>
</button>
</li>
);
})}
</ul>
</div>
)} )}
</div> */} </div>
{/* <SelectField <div>
label={ <TextField
<> label={
Status <span className="text-red-500">*</span> <>
</> Description <span className="text-red-500">*</span>
} </>
value={form.status} }
onChange={handleFormChange('status')} value={form.description}
options={statusOptions} onChange={handleFormChange('description')}
placeholder="Select Status" placeholder="Enter Description"
/> */} width="100%"
// error={formErrors.description}
/>
{formErrors.description && (
<p className="mt-1 text-sm text-red-600">{formErrors.description}</p>
)}
</div>
</div> </div>
<div className="mt-6 flex items-center justify-end gap-3"> <div className="mt-6 flex items-center justify-end gap-3">
@ -710,18 +653,17 @@ const UnitMaster = () => {
)} )}
{/* Custom Toast Notification */} {/* Custom Toast Notification */}
{/* Custom Toast Notification - CENTERED */} {toastData && (
{toastData && ( <div className="fixed inset-0 z-[9999] pointer-events-none flex items-start justify-center pt-20">
<div className="fixed inset-0 z-[9999] pointer-events-none flex items-start justify-center pt-20"> <div className="pointer-events-auto">
<div className="pointer-events-auto"> <CustomToast
<CustomToast message={toastData.message}
message={toastData.message} type={toastData.type}
type={toastData.type} onClose={() => setToastData(null)}
onClose={() => setToastData(null)} />
/> </div>
</div> </div>
</div> )}
)}
</div> </div>
); );
}; };

View File

@ -1,4 +1,3 @@
// src/services/configuration/unitService.js
import { getRequest, postRequest, putRequest, deleteRequest } from '@/services/api/CommonService'; import { getRequest, postRequest, putRequest, deleteRequest } from '@/services/api/CommonService';
const admin = '/admin_users'; const admin = '/admin_users';
@ -42,6 +41,72 @@ export const getAdminUserById = async (id) => {
} }
}; };
export const changeAdminUserPassword = async (id, passwordData) => {
try {
if (!id) throw new Error("Invalid user ID");
console.log('🔍 Debug: Changing password for admin user:', id);
console.log('🔍 Debug: Password data received:', {
hasOldPassword: !!passwordData.old_password,
hasNewPassword: !!passwordData.new_password,
hasConfirmPassword: !!passwordData.confirm_password
});
// Prepare the request data according to API specification
const requestData = {
old_password: passwordData.old_password,
new_password: passwordData.new_password,
confirm_password: passwordData.confirm_password
};
console.log('🔍 Debug: Final request data being sent:', requestData);
// Use the correct endpoint for admin users
// const response = await putRequest(`/admin_users/${id}/change-password`, requestData);
const response = await putRequest(`admin_users/${id}/change-password`, requestData);
console.log('🔍 Debug: Admin password change successful:', response.data);
return response.data;
} catch (error) {
console.error("❌ Error changing admin user password:", error);
if (error.response) {
console.error("🔍 Debug: API error response:", {
status: error.response.status,
data: error.response.data,
headers: error.response.headers
});
// Extract meaningful error message
let errorMessage = 'Failed to change password';
if (error.response.status === 404) {
errorMessage = 'Admin user not found or endpoint unavailable';
} else if (error.response.data.message) {
errorMessage = error.response.data.message;
} else if (error.response.data.errors) {
// Handle Laravel validation errors
const errors = error.response.data.errors;
errorMessage = Object.values(errors).flat().join(', ');
} else if (error.response.data.error) {
errorMessage = error.response.data.error;
}
const detailedError = new Error(errorMessage);
detailedError.status = error.response.status;
detailedError.data = error.response.data;
throw detailedError;
} else if (error.request) {
console.error("🔍 Debug: No response received:", error.request);
throw new Error('No response from server. Please check your connection.');
} else {
console.error("🔍 Debug: Request setup error:", error.message);
throw error;
}
}
};
// export const deleteAdminUser = async (id) => { // export const deleteAdminUser = async (id) => {
// try { // try {
// const response = await deleteRequest(`${admin}/${id}`); // const response = await deleteRequest(`${admin}/${id}`);
@ -51,56 +116,6 @@ export const getAdminUserById = async (id) => {
// throw error; // throw error;
// } // }
// }; // };
export const changeAdminUserPassword = async (id, passwordData) => {
try {
if (!id) throw new Error("Invalid user ID");
// Debug: see what fields the API expects
console.log("🔍 Debug: Making test request to discover required fields");
const testData = { test_field: "test_value" };
try {
await putRequest(`/admin/${id}/change-password`, testData);
} catch (testError) {
console.log(
"🔍 Debug: Test request failed with:",
testError.response?.data || testError.message
);
}
// Common Laravel-style field names
const requestData = {
password: passwordData.newPassword,
password_confirmation: passwordData.confirmPassword,
new_password: passwordData.newPassword,
new_password_confirmation: passwordData.confirmPassword,
};
console.log("🔍 Debug: Final request data:", requestData);
const response = await putRequest(`/admin/${id}/change-password`, requestData);
return response.data;
} catch (error) {
console.error("Error changing admin user password:", error);
if (error.response) {
console.error("🔍 Debug: Full error response:", {
status: error.response.status,
data: error.response.data,
headers: error.response.headers,
});
if (error.response.data.errors) {
console.error("🔍 Debug: Validation errors:", error.response.data.errors);
}
}
throw error;
}
};
export const deleteAdminUser = async (id) => { export const deleteAdminUser = async (id) => {
try { try {
@ -128,8 +143,5 @@ export const deleteAdminUser = async (id) => {
console.error("Error deleting admin user:", error); console.error("Error deleting admin user:", error);
throw error; throw error;
} }
}; };