bug fixed

This commit is contained in:
Malini 2025-11-10 12:12:45 +05:30
parent cac8d1cb2e
commit ebd0e4c4f5
5 changed files with 340 additions and 349 deletions

View File

@ -30,20 +30,31 @@ const AddAdminUsers = ({ isOpen, onClose, onSave }) => {
setToastData(null); setToastData(null);
}, 4000); }, 4000);
}; };
const validateForm = () => { const validateForm = () => {
const newErrors = {}; const newErrors = {};
if (!formData.name.trim()) newErrors.name = 'Name is required';
if (!formData.email.trim()) newErrors.email = 'Email is required'; if (!formData.name.trim()) newErrors.name = 'Name is required';
else if (!/^\S+@\S+\.\S+$/.test(formData.email))
newErrors.email = 'Please enter a valid email'; if (!formData.email.trim()) newErrors.email = 'Email is required';
if (!formData.status) newErrors.status = 'Status is required'; else if (!/^\S+@\S+\.\S+$/.test(formData.email))
if (!formData.password) newErrors.password = 'Password is required'; newErrors.email = 'Please enter a valid email';
else if (formData.password.length < 12)
newErrors.password = 'Password must be at least 12 characters'; if (!formData.status) newErrors.status = 'Status is required';
if (formData.password !== formData.confirmPassword)
newErrors.confirmPassword = 'Passwords do not match'; if (!formData.password) {
return newErrors; newErrors.password = 'Password is required';
}; } else if (formData.password.length < 8) {
newErrors.password = 'Password must be at least 8 characters';
} else if (formData.password.length > 12) {
newErrors.password = 'Password cannot exceed 12 characters';
}
if (formData.password !== formData.confirmPassword)
newErrors.confirmPassword = 'Passwords do not match';
return newErrors;
};
useEffect(() => { useEffect(() => {
if (isOpen) { if (isOpen) {

View File

@ -15,7 +15,7 @@ import { TextField, SelectField } from '@/components/common/FormControls';
import EditUserModal from './EditUserModal'; import EditUserModal from './EditUserModal';
import DeleteUserModal from './DeleteUserModal'; import DeleteUserModal from './DeleteUserModal';
import ResetPasswordModal from './ResetPasswordModal'; import ResetPasswordModal from './ResetPasswordModal';
const searchIconSrc = '/assets/images/material-symbols_search-rounded.svg'; const searchIconSrc = '/assets/images/material-symbols_search-rounded.svg';
const exportIconSrc = '/assets/images/DownloadSimple.svg'; const exportIconSrc = '/assets/images/DownloadSimple.svg';
const addIconSrc = '/assets/images/ic_baseline-plus.svg'; const addIconSrc = '/assets/images/ic_baseline-plus.svg';
@ -29,7 +29,7 @@ const inactiveUserIconSrc = '/assets/images/inactive-user.svg';
const resetPasswordIconSrc = '/assets/images/hugeicons_reset-password.svg'; const resetPasswordIconSrc = '/assets/images/hugeicons_reset-password.svg';
const resetPasswordInactiveIconSrc = const resetPasswordInactiveIconSrc =
'/assets/images/hugeicons_reset-password-inactive.svg'; '/assets/images/hugeicons_reset-password-inactive.svg';
const SummaryCard = ({ label, count, icon, iconBg = '#FDF7EB' }) => ( 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 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"> <div className="flex items-center gap-4">
@ -44,7 +44,7 @@ const SummaryCard = ({ label, count, icon, iconBg = '#FDF7EB' }) => (
<span className="text-2xl font-semibold text-[#232528]">{count}</span> <span className="text-2xl font-semibold text-[#232528]">{count}</span>
</div> </div>
); );
const StatusBadge = ({ status }) => { const StatusBadge = ({ status }) => {
const map = { const map = {
Active: 'bg-green-50 text-green-700 ring-1 ring-green-200', Active: 'bg-green-50 text-green-700 ring-1 ring-green-200',
@ -60,7 +60,7 @@ const StatusBadge = ({ status }) => {
</span> </span>
); );
}; };
const AdminUsers = () => { const AdminUsers = () => {
const [isAddUserModalOpen, setIsAddUserModalOpen] = useState(false); const [isAddUserModalOpen, setIsAddUserModalOpen] = useState(false);
const [isEditModalOpen, setIsEditModalOpen] = useState(false); const [isEditModalOpen, setIsEditModalOpen] = useState(false);
@ -71,7 +71,7 @@ const AdminUsers = () => {
const [saving, setSaving] = useState(false); const [saving, setSaving] = useState(false);
const [currentPage, setCurrentPage] = useState(1); const [currentPage, setCurrentPage] = useState(1);
const pageSize = 10; const pageSize = 10;
const [showDeleteModal, setShowDeleteModal] = useState(false); const [showDeleteModal, setShowDeleteModal] = useState(false);
const [selectedUser, setSelectedUser] = useState(null); const [selectedUser, setSelectedUser] = useState(null);
const [deletingRow, setDeletingRow] = useState(null); const [deletingRow, setDeletingRow] = useState(null);
@ -83,7 +83,7 @@ const AdminUsers = () => {
const [errors, setErrors] = useState({}); const [errors, setErrors] = useState({});
const [toastData, setToastData] = useState(null); const [toastData, setToastData] = useState(null);
const navigate = useNavigate(); const navigate = useNavigate();
// 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);
@ -92,7 +92,7 @@ const AdminUsers = () => {
const [confirmPassword, setConfirmPassword] = useState(''); const [confirmPassword, setConfirmPassword] = useState('');
const [resettingPassword, setResettingPassword] = useState(false); const [resettingPassword, setResettingPassword] = useState(false);
const [resetErrors, setResetErrors] = useState({}); const [resetErrors, setResetErrors] = useState({});
// Helper: show toast // Helper: show toast
const showToast = (message, type = 'success') => { const showToast = (message, type = 'success') => {
setToastData({ message, type }); setToastData({ message, type });
@ -103,14 +103,14 @@ const AdminUsers = () => {
setToastData(null); setToastData(null);
}, 4000); }, 4000);
}; };
// Fetch users // Fetch users
useEffect(() => { useEffect(() => {
const fetchUsers = async () => { const fetchUsers = async () => {
try { try {
setLoading(true); setLoading(true);
const res = await getAdminUser(); const res = await getAdminUser();
const usersData = Array.isArray(res) const usersData = Array.isArray(res)
? res ? res
: Array.isArray(res?.data) : Array.isArray(res?.data)
@ -118,13 +118,13 @@ const AdminUsers = () => {
: Array.isArray(res?.data?.data) : Array.isArray(res?.data?.data)
? res.data.data ? res.data.data
: null; : null;
if (!usersData) { if (!usersData) {
showToast('Failed to load users', 'error'); showToast('Failed to load users', 'error');
setUsers([]); setUsers([]);
return; return;
} }
const mappedUsers = usersData.map((user) => { const mappedUsers = usersData.map((user) => {
const id = user.id ?? user._id ?? user.user_id ?? null; const id = user.id ?? user._id ?? user.user_id ?? null;
const name = user.name ?? user.fullName ?? 'N/A'; const name = user.name ?? user.fullName ?? 'N/A';
@ -151,7 +151,7 @@ const AdminUsers = () => {
raw: user, raw: user,
}; };
}); });
setUsers(mappedUsers); setUsers(mappedUsers);
} catch (error) { } catch (error) {
console.error('Error fetching admin users:', error); console.error('Error fetching admin users:', error);
@ -161,14 +161,14 @@ const AdminUsers = () => {
setLoading(false); setLoading(false);
} }
}; };
fetchUsers(); fetchUsers();
}, []); }, []);
const totalUsers = users.length; const totalUsers = users.length;
const activeUsers = users.filter((user) => user.status === 'Active').length; const activeUsers = users.filter((user) => user.status === 'Active').length;
const inactiveUsers = totalUsers - activeUsers; const inactiveUsers = totalUsers - activeUsers;
const summaryItems = [ const summaryItems = [
{ {
label: 'Total Users', label: 'Total Users',
@ -189,10 +189,10 @@ const AdminUsers = () => {
iconBg: '#F5F5F7', iconBg: '#F5F5F7',
}, },
]; ];
const headers = ['Name', 'Email', 'Last Login', 'Status', 'Actions']; const headers = ['Name', 'Email', 'Last Login', 'Status', 'Actions'];
const columnWidths = [100, 130, 150, 100, 130]; const columnWidths = [100, 130, 150, 100, 130];
const rows = users.map((user) => [ const rows = users.map((user) => [
user.name, user.name,
user.email, user.email,
@ -200,7 +200,7 @@ const AdminUsers = () => {
<StatusBadge status={user.status} />, <StatusBadge status={user.status} />,
'actions', 'actions',
]); ]);
const handleFormChange = (e) => { const handleFormChange = (e) => {
if (!e) return; if (!e) return;
if (e.target && e.target.name !== undefined) { if (e.target && e.target.name !== undefined) {
@ -212,30 +212,30 @@ const AdminUsers = () => {
if (errors[e.name]) setErrors((prev) => ({ ...prev, [e.name]: '' })); if (errors[e.name]) setErrors((prev) => ({ ...prev, [e.name]: '' }));
} }
}; };
const handleUpdateUser = async () => { const handleUpdateUser = async () => {
try { try {
setLoading(true); setLoading(true);
const updatedUser = { const updatedUser = {
name: formData.name, name: formData.name,
email: formData.email, email: formData.email,
is_active: formData.status === "Active", is_active: formData.status === "Active",
}; };
const res = await updateAdminUser(currentUser.id, updatedUser); const res = await updateAdminUser(currentUser.id, updatedUser);
const success = const success =
res?.status === "success" || res?.status === "success" ||
res?.code === 200 || res?.code === 200 ||
res?.data?.status === "success" || res?.data?.status === "success" ||
(!res.error && res); (!res.error && res);
if (!success) throw new Error(res?.message || "Failed to update user"); if (!success) throw new Error(res?.message || "Failed to update user");
showToast("User updated successfully!", "success"); showToast("User updated successfully!", "success");
setIsEditModalOpen(false); setIsEditModalOpen(false);
setUsers((prev) => setUsers((prev) =>
prev.map((u) => prev.map((u) =>
u.id === currentUser.id u.id === currentUser.id
@ -249,7 +249,7 @@ const AdminUsers = () => {
: u : u
) )
); );
setCurrentUser(null); setCurrentUser(null);
} catch (error) { } catch (error) {
console.error("Error updating user:", error); console.error("Error updating user:", error);
@ -258,27 +258,27 @@ const AdminUsers = () => {
setLoading(false); setLoading(false);
} }
}; };
const validateForm = () => { const validateForm = () => {
const newErrors = {}; const newErrors = {};
if (!formData.name || !formData.name.trim()) newErrors.name = 'Name is required'; if (!formData.name || !formData.name.trim()) newErrors.name = 'Name is required';
if (!formData.status) newErrors.status = 'Status is required'; if (!formData.status) newErrors.status = 'Status is required';
return newErrors; return newErrors;
}; };
const handleDeleteClick = (user) => { const handleDeleteClick = (user) => {
setSelectedUser(user); setSelectedUser(user);
setShowDeleteModal(true); setShowDeleteModal(true);
}; };
const handleConfirmDelete = async () => { const handleConfirmDelete = async () => {
try { try {
if (!selectedUser) return; if (!selectedUser) return;
setDeletingRow(selectedUser.id); setDeletingRow(selectedUser.id);
await deleteAdminUser(selectedUser.id); await deleteAdminUser(selectedUser.id);
showToast("User deleted successfully!", "success"); showToast("User deleted successfully!", "success");
setUsers((prev) => prev.filter((u) => u.id !== selectedUser.id)); setUsers((prev) => prev.filter((u) => u.id !== selectedUser.id));
} catch (error) { } catch (error) {
showToast("Failed to delete user!", "error"); showToast("Failed to delete user!", "error");
@ -287,26 +287,26 @@ const AdminUsers = () => {
setShowDeleteModal(false); setShowDeleteModal(false);
} }
}; };
const handleEdit = async (user) => { const handleEdit = async (user) => {
if (!user || !user.id) { if (!user || !user.id) {
showToast('Invalid user selected', 'error'); showToast('Invalid user selected', 'error');
return; return;
} }
setEditingRow(user); setEditingRow(user);
setLoading(true); setLoading(true);
try { try {
const res = await getAdminUserById(user.id); const res = await getAdminUserById(user.id);
const payload = res?.data ?? res; const payload = res?.data ?? res;
if (!payload) throw new Error('No user data returned from API'); if (!payload) throw new Error('No user data returned from API');
const name = payload.name ?? payload.fullName ?? user.name ?? ''; const name = payload.name ?? payload.fullName ?? user.name ?? '';
const email = payload.email ?? user.email ?? ''; const email = payload.email ?? user.email ?? '';
const is_active = const is_active =
payload.is_active !== undefined ? !!payload.is_active : !!user.is_active; payload.is_active !== undefined ? !!payload.is_active : !!user.is_active;
setCurrentUser({ setCurrentUser({
...payload, ...payload,
id: user.id, id: user.id,
@ -314,13 +314,13 @@ const AdminUsers = () => {
email, email,
is_active, is_active,
}); });
setFormData({ setFormData({
name, name,
email, email,
status: is_active ? 'Active' : 'Inactive', status: is_active ? 'Active' : 'Inactive',
}); });
setErrors({}); setErrors({});
setIsEditModalOpen(true); setIsEditModalOpen(true);
} catch (error) { } catch (error) {
@ -330,7 +330,7 @@ const AdminUsers = () => {
setLoading(false); setLoading(false);
} }
}; };
const handleOpenResetModal = (user) => { const handleOpenResetModal = (user) => {
setResetUser(user); setResetUser(user);
setOldPassword(''); setOldPassword('');
@ -339,7 +339,7 @@ const AdminUsers = () => {
setResetErrors({}); setResetErrors({});
setIsResetModalOpen(true); setIsResetModalOpen(true);
}; };
const handleCloseResetModal = () => { const handleCloseResetModal = () => {
setResetUser(null); setResetUser(null);
setOldPassword(''); setOldPassword('');
@ -349,60 +349,67 @@ const AdminUsers = () => {
setIsResetModalOpen(false); setIsResetModalOpen(false);
setResettingPassword(false); setResettingPassword(false);
}; };
// Validate reset password form - Updated to allow 8 to 12 characters // Validate reset password form - Updated to allow 8 to 12 characters
const validateResetForm = () => { const validateResetForm = () => {
const newErrors = {}; const newErrors = {};
if (!oldPassword.trim()) { // Old password validation
newErrors.oldPassword = 'Old password is required'; if (!oldPassword?.trim()) {
} newErrors.oldPassword = 'Old password is required';
}
if (!newPassword.trim()) {
newErrors.newPassword = 'New password is required'; // New password validation
} else if (newPassword.length < 8) { if (!newPassword?.trim()) {
newErrors.newPassword = 'Password must be at least 8 characters'; newErrors.newPassword = 'New password is required';
} } else if (newPassword.length < 8) {
newErrors.newPassword = 'Password must be at least 8 characters long';
if (!confirmPassword.trim()) { }
newErrors.confirmPassword = 'Please confirm your password';
} else if (newPassword !== confirmPassword) { // Confirm password validation
newErrors.confirmPassword = 'Passwords do not match'; if (!confirmPassword?.trim()) {
} newErrors.confirmPassword = 'Please confirm your password';
} else if (newPassword !== confirmPassword) {
return newErrors; newErrors.confirmPassword = 'Passwords do not match';
}; }
return newErrors;
};
// Reset Password API Integration // Reset Password API Integration
const handleResetPassword = async () => { const handleResetPassword = async () => {
if (!resetUser || !resetUser.id) { if (!resetUser || !resetUser.id) {
showToast('No user selected for password reset', 'error'); showToast('No user selected for password reset', 'error');
return; return;
} }
// Validate form // Validate form
const formErrors = validateResetForm(); const formErrors = validateResetForm();
if (Object.keys(formErrors).length > 0) { if (Object.keys(formErrors).length > 0) {
setResetErrors(formErrors); setResetErrors(formErrors);
return; return;
} }
try { try {
setResettingPassword(true); setResettingPassword(true);
// Prepare data according to API specification // Prepare data according to API specification
const passwordData = { const passwordData = {
old_password: oldPassword, old_password: oldPassword,
new_password: newPassword, new_password: newPassword,
confirm_password: confirmPassword confirm_password: confirmPassword
}; };
console.log('🔄 Initiating admin 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('✅ Admin password reset response:', res); console.log('✅ Admin password reset response:', res);
// Check for success based on common response patterns // Check for success based on common response patterns
const success = const success =
res?.status === "success" || res?.status === "success" ||
@ -413,26 +420,26 @@ const AdminUsers = () => {
res?.message?.toLowerCase().includes("updated") || res?.message?.toLowerCase().includes("updated") ||
res?.message?.toLowerCase().includes("changed") || 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");
} }
showToast('Admin password reset successfully!', 'success'); showToast('Admin password reset successfully!', 'success');
handleCloseResetModal(); handleCloseResetModal();
} catch (error) { } catch (error) {
console.error('❌ Error resetting admin password:', error); console.error('❌ Error resetting admin password:', error);
// Handle specific error cases // Handle specific error cases
let errorMessage = 'Failed to reset password'; let errorMessage = 'Failed to reset password';
if (error.message) { if (error.message) {
errorMessage = error.message; errorMessage = error.message;
} }
// Check for common admin password change errors // Check for common admin password change errors
if (errorMessage.toLowerCase().includes('old password') || if (errorMessage.toLowerCase().includes('old password') ||
errorMessage.toLowerCase().includes('current password') || errorMessage.toLowerCase().includes('current password') ||
errorMessage.toLowerCase().includes('incorrect password')) { errorMessage.toLowerCase().includes('incorrect password')) {
setResetErrors(prev => ({ setResetErrors(prev => ({
@ -446,14 +453,14 @@ const AdminUsers = () => {
confirmPassword: 'New passwords do not match' confirmPassword: 'New passwords do not match'
})); }));
showToast('New passwords do not match', 'error'); showToast('New passwords do not match', 'error');
} else if (errorMessage.toLowerCase().includes('same') || } else if (errorMessage.toLowerCase().includes('same') ||
errorMessage.toLowerCase().includes('previous')) { errorMessage.toLowerCase().includes('previous')) {
setResetErrors(prev => ({ setResetErrors(prev => ({
...prev, ...prev,
newPassword: 'New password cannot be the same as current password' newPassword: 'New password cannot be the same as current password'
})); }));
showToast('New password cannot be the same as current password', 'error'); showToast('New password cannot be the same as current password', 'error');
} else if (errorMessage.toLowerCase().includes('weak') || } else if (errorMessage.toLowerCase().includes('weak') ||
errorMessage.toLowerCase().includes('strength')) { errorMessage.toLowerCase().includes('strength')) {
setResetErrors(prev => ({ setResetErrors(prev => ({
...prev, ...prev,
@ -469,13 +476,13 @@ const AdminUsers = () => {
setResettingPassword(false); setResettingPassword(false);
} }
}; };
const handleDelete = (rowIndex) => { const handleDelete = (rowIndex) => {
const user = users[rowIndex]; const user = users[rowIndex];
setSelectedUser(user); setSelectedUser(user);
setShowDeleteModal(true); setShowDeleteModal(true);
}; };
const handleEditSubmit = async (e) => { const handleEditSubmit = async (e) => {
e.preventDefault(); e.preventDefault();
const formErrors = validateForm(); const formErrors = validateForm();
@ -484,12 +491,12 @@ const AdminUsers = () => {
showToast('Please fix the form errors', 'error'); showToast('Please fix the form errors', 'error');
return; return;
} }
if (!currentUser || !currentUser.id) { if (!currentUser || !currentUser.id) {
showToast('No user selected for update', 'error'); showToast('No user selected for update', 'error');
return; return;
} }
setSaving(true); setSaving(true);
try { try {
const payload = { const payload = {
@ -497,16 +504,16 @@ const AdminUsers = () => {
email: formData.email, email: formData.email,
is_active: formData.status === 'Active', is_active: formData.status === 'Active',
}; };
const res = await updateAdminUser(currentUser.id, payload); const res = await updateAdminUser(currentUser.id, payload);
const success = const success =
res?.status === 'success' || res?.status === 'success' ||
res?.status === 200 || res?.status === 200 ||
res?.data?.status === 'success' || res?.data?.status === 'success' ||
(!res.error && res); (!res.error && res);
if (!success) throw new Error(res?.message || 'Failed to update user'); if (!success) throw new Error(res?.message || 'Failed to update user');
setUsers((prev) => setUsers((prev) =>
prev.map((u) => prev.map((u) =>
u.id === currentUser.id u.id === currentUser.id
@ -520,7 +527,7 @@ const AdminUsers = () => {
: u : u
) )
); );
showToast('User updated successfully', 'success'); showToast('User updated successfully', 'success');
setIsEditModalOpen(false); setIsEditModalOpen(false);
setEditingRow(null); setEditingRow(null);
@ -532,7 +539,7 @@ const AdminUsers = () => {
setSaving(false); setSaving(false);
} }
}; };
const tableToolbar = ( const tableToolbar = (
<div className="px-6 py-4 flex flex-col lg:flex-row lg:items-center lg:justify-between gap-3 bg-white"> <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> <h3 className="text-sm font-medium text-[#232528]">Admin Users</h3>
@ -554,26 +561,26 @@ const AdminUsers = () => {
type="button" type="button"
onClick={() => { onClick={() => {
if (!users.length) return; if (!users.length) return;
const csvHeader = ['Name', 'Email', 'Status', 'Last Login'].join(','); const csvHeader = ['Name', 'Email', 'Status', 'Last Login'].join(',');
const csvRows = users.map((user) => const csvRows = users.map((user) =>
[user.name, user.email, user.status, user.lastLogin] [user.name, user.email, user.status, user.lastLogin]
.map((value) => `"${String(value ?? '').replace(/"/g, '""')}"`) .map((value) => `"${String(value ?? '').replace(/"/g, '""')}"`)
.join(',') .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', 'admin-users.csv'); link.setAttribute('download', 'admin-users.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);
}} }}
@ -587,7 +594,7 @@ const AdminUsers = () => {
<img src={exportIconSrc} alt="Export" className="h-4 w-4" /> <img src={exportIconSrc} alt="Export" className="h-4 w-4" />
<span>Export CSV</span> <span>Export CSV</span>
</button> </button>
<button <button
onClick={() => setIsAddUserModalOpen(true)} onClick={() => setIsAddUserModalOpen(true)}
className="h-9 px-3 rounded-md bg-[#B68A35] text-white text-sm inline-flex items-center gap-2 cursor-pointer" className="h-9 px-3 rounded-md bg-[#B68A35] text-white text-sm inline-flex items-center gap-2 cursor-pointer"
@ -599,7 +606,7 @@ const AdminUsers = () => {
</div> </div>
</div> </div>
); );
return ( return (
<div className="space-y-5"> <div className="space-y-5">
{/* Summary cards */} {/* Summary cards */}
@ -608,7 +615,7 @@ const AdminUsers = () => {
<SummaryCard key={item.label} {...item} /> <SummaryCard key={item.label} {...item} />
))} ))}
</div> </div>
{/* Table */} {/* Table */}
<Table <Table
headers={headers} headers={headers}
@ -641,7 +648,7 @@ const AdminUsers = () => {
className="h-4 w-4" className="h-4 w-4"
/> />
</button> </button>
<button <button
type="button" type="button"
className="h-8 w-8 grid place-items-center rounded hover:bg-gray-50 cursor-pointer" className="h-8 w-8 grid place-items-center rounded hover:bg-gray-50 cursor-pointer"
@ -654,7 +661,7 @@ const AdminUsers = () => {
className="h-4 w-4" className="h-4 w-4"
/> />
</button> </button>
<button <button
type="button" type="button"
className={`h-8 w-8 grid place-items-center rounded hover:bg-gray-50 cursor-pointer ${ className={`h-8 w-8 grid place-items-center rounded hover:bg-gray-50 cursor-pointer ${
@ -676,7 +683,7 @@ const AdminUsers = () => {
return value; return value;
}} }}
/> />
{/* Add User Modal */} {/* Add User Modal */}
{isAddUserModalOpen && ( {isAddUserModalOpen && (
<AddAdminUsers <AddAdminUsers
@ -697,9 +704,10 @@ const AdminUsers = () => {
showToast('User added locally', 'success'); showToast('User added locally', 'success');
}} }}
passwordMinLength={8} passwordMinLength={8}
passwordMaxLength={12}
/> />
)} )}
{/* Edit Modal */} {/* Edit Modal */}
{isEditModalOpen && ( {isEditModalOpen && (
<EditUserModal <EditUserModal
@ -709,7 +717,7 @@ const AdminUsers = () => {
onUpdate={handleUpdateUser} onUpdate={handleUpdateUser}
/> />
)} )}
{/* Delete Modal */} {/* Delete Modal */}
{showDeleteModal && ( {showDeleteModal && (
<DeleteUserModal <DeleteUserModal
@ -718,7 +726,7 @@ const AdminUsers = () => {
onDelete={handleConfirmDelete} onDelete={handleConfirmDelete}
/> />
)} )}
{/* Reset Password Modal */} {/* Reset Password Modal */}
<ResetPasswordModal <ResetPasswordModal
isOpen={isResetModalOpen} isOpen={isResetModalOpen}
@ -734,8 +742,9 @@ const AdminUsers = () => {
errors={resetErrors} errors={resetErrors}
setErrors={setResetErrors} setErrors={setResetErrors}
passwordMinLength={8} passwordMinLength={8}
passwordMaxLength={12}
/> />
{/* Toast */} {/* Toast */}
{toastData && ( {toastData && (
<div className="fixed inset-0 z-[9999] pointer-events-none"> <div className="fixed inset-0 z-[9999] pointer-events-none">
@ -751,5 +760,5 @@ const AdminUsers = () => {
</div> </div>
); );
}; };
export default AdminUsers; export default AdminUsers;

View File

@ -1,13 +1,14 @@
import { Eye, EyeOff } from 'lucide-react';
import React, { useState } from 'react'; import React, { useState } from 'react';
const ResetPasswordModal = ({ const ResetPasswordModal = ({
isOpen, isOpen,
onClose, onClose,
oldPassword, oldPassword,
setOldPassword, setOldPassword,
newPassword, newPassword,
setNewPassword, setNewPassword,
confirmPassword, confirmPassword,
setConfirmPassword, setConfirmPassword,
onReset, onReset,
loading = false, loading = false,
@ -17,63 +18,65 @@ const ResetPasswordModal = ({
const [showOldPassword, setShowOldPassword] = useState(false); const [showOldPassword, setShowOldPassword] = useState(false);
const [showNewPassword, setShowNewPassword] = useState(false); const [showNewPassword, setShowNewPassword] = useState(false);
const [showConfirmPassword, setShowConfirmPassword] = useState(false); const [showConfirmPassword, setShowConfirmPassword] = useState(false);
// Validation: minimum 8 characters (no 12 limit)
const validateForm = () => { const validateForm = () => {
const newErrors = {}; const newErrors = {};
if (!oldPassword.trim()) { if (!oldPassword.trim()) {
newErrors.oldPassword = 'Old password is required'; 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 < 8) {
newErrors.newPassword = 'Password must be at least 6 characters'; newErrors.newPassword = 'Password must be at least 8 characters long';
} }
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) {
newErrors.confirmPassword = 'Passwords do not match'; newErrors.confirmPassword = 'Passwords do not match';
} }
return newErrors; return newErrors;
}; };
const handleSubmit = () => { const handleSubmit = () => {
const formErrors = validateForm(); const formErrors = validateForm();
if (Object.keys(formErrors).length > 0) { if (Object.keys(formErrors).length > 0) {
setErrors(formErrors); setErrors(formErrors);
return; return;
} }
setErrors({}); setErrors({});
onReset(); onReset();
}; };
const handleInputChange = (field, value) => { const handleInputChange = (field, value) => {
if (field === 'oldPassword') setOldPassword(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);
// Clear error when user starts typing // Clear error when user starts typing
if (errors[field]) { if (errors[field]) {
setErrors(prev => ({ ...prev, [field]: '' })); setErrors(prev => ({ ...prev, [field]: '' }));
} }
}; };
const handleKeyPress = (e) => { const handleKeyPress = (e) => {
if (e.key === 'Enter' && !loading) { if (e.key === 'Enter' && !loading) {
handleSubmit(); handleSubmit();
} }
}; };
if (!isOpen) return null; if (!isOpen) return null;
return ( return (
<div className="fixed inset-0 z-[9999] flex items-center justify-center bg-opacity-70 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">
<h3 className="text-xl font-bold text-gray-900">Reset Password</h3> <h3 className="text-xl font-bold text-gray-900">Reset Password</h3>
@ -87,10 +90,10 @@ const ResetPasswordModal = ({
</svg> </svg>
</button> </button>
</div> </div>
{/* Form */} {/* Form Fields */}
<div className="space-y-4"> <div className="space-y-4">
{/* Old Password Field */} {/* Old Password */}
<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">
Old Password <span className="text-red-500">*</span> Old Password <span className="text-red-500">*</span>
@ -100,8 +103,8 @@ const ResetPasswordModal = ({
type={showOldPassword ? "text" : "password"} type={showOldPassword ? "text" : "password"}
placeholder="Enter old password" placeholder="Enter old password"
className={`w-full border rounded-lg px-4 py-3 text-sm focus:outline-none focus:ring-2 transition-all pr-10 ${ className={`w-full border rounded-lg px-4 py-3 text-sm focus:outline-none focus:ring-2 transition-all pr-10 ${
errors.oldPassword errors.oldPassword
? 'border-red-300 focus:ring-red-500 bg-red-50' ? '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' : 'border-gray-300 focus:border-[#B68A35] focus:ring-2 focus:ring-[#B68A35] focus:ring-opacity-20'
}`} }`}
value={oldPassword} value={oldPassword}
@ -115,29 +118,16 @@ const ResetPasswordModal = ({
onClick={() => setShowOldPassword(!showOldPassword)} onClick={() => setShowOldPassword(!showOldPassword)}
disabled={loading} disabled={loading}
> >
{showOldPassword ? ( {showOldPassword ? <EyeOff className="w-5 h-5 text-[#B68A35]" /> : <Eye className="w-5 h-5 text-[#B68A35]" />}
<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> </button>
</div> </div>
{errors.oldPassword && ( {errors.oldPassword && (
<p className="text-red-600 text-xs mt-2 flex items-center gap-1"> <p className="text-red-600 text-xs mt-2">{errors.oldPassword}</p>
<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> </div>
{/* New Password Field */} {/* New Password */}
<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 <span className="text-red-500">*</span> New Password <span className="text-red-500">*</span>
@ -147,8 +137,8 @@ const ResetPasswordModal = ({
type={showNewPassword ? "text" : "password"} type={showNewPassword ? "text" : "password"}
placeholder="Enter new 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 ${ className={`w-full border rounded-lg px-4 py-3 text-sm focus:outline-none focus:ring-2 transition-all pr-10 ${
errors.newPassword errors.newPassword
? 'border-red-300 focus:ring-red-500 bg-red-50' ? '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' : 'border-gray-300 focus:border-[#B68A35] focus:ring-2 focus:ring-[#B68A35] focus:ring-opacity-20'
}`} }`}
value={newPassword} value={newPassword}
@ -162,29 +152,16 @@ const ResetPasswordModal = ({
onClick={() => setShowNewPassword(!showNewPassword)} onClick={() => setShowNewPassword(!showNewPassword)}
disabled={loading} disabled={loading}
> >
{showNewPassword ? ( {showNewPassword ? <EyeOff className="w-5 h-5 text-[#B68A35]" /> : <Eye className="w-5 h-5 text-[#B68A35]" />}
<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> </button>
</div> </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">{errors.newPassword}</p>
<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.newPassword}
</p>
)} )}
</div> </div>
{/* Confirm Password Field */} {/* Confirm Password */}
<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 <span className="text-red-500">*</span> Confirm Password <span className="text-red-500">*</span>
@ -194,8 +171,8 @@ const ResetPasswordModal = ({
type={showConfirmPassword ? "text" : "password"} type={showConfirmPassword ? "text" : "password"}
placeholder="Confirm new password" placeholder="Confirm new password"
className={`w-full border rounded-lg px-4 py-3 text-sm focus:outline-none focus:ring-2 transition-all pr-10 ${ className={`w-full border rounded-lg px-4 py-3 text-sm focus:outline-none focus:ring-2 transition-all pr-10 ${
errors.confirmPassword errors.confirmPassword
? 'border-red-300 focus:ring-red-500 bg-red-50' ? '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' : 'border-gray-300 focus:border-[#B68A35] focus:ring-2 focus:ring-[#B68A35] focus:ring-opacity-20'
}`} }`}
value={confirmPassword} value={confirmPassword}
@ -209,52 +186,39 @@ const ResetPasswordModal = ({
onClick={() => setShowConfirmPassword(!showConfirmPassword)} onClick={() => setShowConfirmPassword(!showConfirmPassword)}
disabled={loading} disabled={loading}
> >
{showConfirmPassword ? ( {showConfirmPassword ? <EyeOff className="w-5 h-5 text-[#B68A35]" /> : <Eye className="w-5 h-5 text-[#B68A35]" />}
<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> </button>
</div> </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">{errors.confirmPassword}</p>
<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.confirmPassword}
</p>
)} )}
</div> </div>
</div> </div>
{/* Note Section */} {/* Note */}
<div className="mt-4 p-3 bg-blue-50 rounded-lg border border-blue-200"> <div className="mt-4 p-3 bg-blue-50 rounded-lg border border-blue-200">
<div className="flex items-start gap-2"> <div className="flex items-start gap-2">
<svg className="w-4 h-4 text-blue-600 mt-0.5 flex-shrink-0" fill="currentColor" viewBox="0 0 20 20"> <svg className="w-4 h-4 text-blue-600 mt-0.5" 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 0zM9 9a1 1 0 000 2v3a1 1 0 001 1h1a1 1 0 100-2v-3a1 1 0 00-1-1H9z" clipRule="evenodd" /> <path fillRule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zM9 9a1 1 0 000 2v3a1 1 0 001 1h1a1 1 0 100-2v-3a1 1 0 00-1-1H9z" clipRule="evenodd" />
</svg> </svg>
<p className="text-sm text-blue-700"> <p className="text-sm text-blue-700">
<span className="font-medium">Note:</span> Password must be at least 6 characters long. <span className="font-medium">Note:</span> Password must be at least <b>8 characters</b> long.
</p> </p>
</div> </div>
</div> </div>
{/* Action Buttons */} {/* Buttons */}
<div className="flex justify-end gap-3 mt-6 pt-4 border-t border-gray-200"> <div className="flex justify-end gap-3 mt-6 pt-4 border-t border-gray-200">
<button <button
className="px-6 py-2.5 rounded-lg border border-gray-300 text-gray-700 font-medium hover:bg-gray-50 disabled:opacity-50 disabled:cursor-not-allowed transition-colors" className="px-6 py-2.5 rounded-lg border border-gray-300 text-gray-700 font-medium hover:bg-gray-50 disabled:opacity-50"
onClick={onClose} onClick={onClose}
disabled={loading} disabled={loading}
> >
Cancel Cancel
</button> </button>
<button <button
className="px-6 py-2.5 rounded-lg bg-[#B68A35] text-white font-medium hover:bg-[#9c6f28] disabled:opacity-50 disabled:cursor-not-allowed flex items-center gap-2 transition-colors" className="px-6 py-2.5 rounded-lg bg-[#B68A35] text-white font-medium hover:bg-[#9c6f28] disabled:opacity-50 flex items-center gap-2"
onClick={handleSubmit} onClick={handleSubmit}
disabled={loading} disabled={loading}
> >
@ -272,5 +236,5 @@ const ResetPasswordModal = ({
</div> </div>
); );
}; };
export default ResetPasswordModal; export default ResetPasswordModal;

View File

@ -3,7 +3,7 @@ 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';
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';
const pencilActiveSrc = '/assets/images/PencilSimple.svg'; const pencilActiveSrc = '/assets/images/PencilSimple.svg';
@ -11,17 +11,17 @@ const pencilInactiveSrc = '/assets/images/pencilsimple-inactive.svg';
const caretDownActiveSrc = '/assets/images/caretdown-active.svg'; 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 // Custom Toast Component
const CustomToast = ({ message, type, onClose }) => { const CustomToast = ({ message, type, onClose }) => {
React.useEffect(() => { React.useEffect(() => {
const timer = setTimeout(() => { const timer = setTimeout(() => {
onClose(); onClose();
}, 3000); }, 3000);
return () => clearTimeout(timer); return () => clearTimeout(timer);
}, [onClose]); }, [onClose]);
const getToastStyles = () => { const getToastStyles = () => {
switch (type) { switch (type) {
case 'success': case 'success':
@ -34,7 +34,7 @@ const CustomToast = ({ message, type, onClose }) => {
return 'bg-blue-50 border-blue-200 text-blue-800'; return 'bg-blue-50 border-blue-200 text-blue-800';
} }
}; };
const getIcon = () => { const getIcon = () => {
switch (type) { switch (type) {
case 'success': case 'success':
@ -47,7 +47,7 @@ const CustomToast = ({ message, type, onClose }) => {
return ''; return '';
} }
}; };
return ( return (
<div className={`flex items-center p-4 mb-4 rounded-lg border ${getToastStyles()} shadow-lg min-w-80 max-w-md`}> <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="text-lg mr-3">{getIcon()}</span>
@ -61,7 +61,7 @@ const CustomToast = ({ message, type, onClose }) => {
</div> </div>
); );
}; };
const createEmptyQuarterForm = () => ({ const createEmptyQuarterForm = () => ({
survey_name: '', survey_name: '',
establishment: '-', // Default to hyphen establishment: '-', // Default to hyphen
@ -73,7 +73,7 @@ const createEmptyQuarterForm = () => ({
submissionCount: '', submissionCount: '',
status: 'Active', // Default to Active status: 'Active', // Default to Active
}); });
const QuarterlyWindows = () => { const QuarterlyWindows = () => {
const [quarterData, setQuarterData] = React.useState([]); const [quarterData, setQuarterData] = React.useState([]);
const [editingRow, setEditingRow] = React.useState(null); const [editingRow, setEditingRow] = React.useState(null);
@ -89,21 +89,21 @@ const QuarterlyWindows = () => {
const [isLoading, setIsLoading] = React.useState(false); const [isLoading, setIsLoading] = React.useState(false);
const [validationErrors, setValidationErrors] = React.useState({}); const [validationErrors, setValidationErrors] = React.useState({});
const [toastData, setToastData] = React.useState(null); 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) => {
if (!dateString) return ''; if (!dateString) return '';
const date = new Date(dateString); const date = new Date(dateString);
return date.toISOString().split('T')[0]; return date.toISOString().split('T')[0];
}; };
// Format date for display // Format date for display
const formatDateForDisplay = (dateString) => { const formatDateForDisplay = (dateString) => {
if (!dateString) return ''; if (!dateString) return '';
const date = new Date(dateString); const date = new Date(dateString);
return date.toLocaleDateString('en-GB'); return date.toLocaleDateString('en-GB');
}; };
// Fetch quarterly windows data on component mount // Fetch quarterly windows data on component mount
React.useEffect(() => { React.useEffect(() => {
const fetchQuarterlyWindows = async () => { const fetchQuarterlyWindows = async () => {
@ -137,10 +137,10 @@ const QuarterlyWindows = () => {
setIsLoading(false); setIsLoading(false);
} }
}; };
fetchQuarterlyWindows(); fetchQuarterlyWindows();
}, []); }, []);
// Table headers with left alignment and no wrapping // Table headers with left alignment and no wrapping
const headers = [ const headers = [
{ name: 'Survey Name', className: 'whitespace-nowrap text-left' }, { name: 'Survey Name', className: 'whitespace-nowrap text-left' },
@ -155,7 +155,7 @@ 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' },
]; ];
const columnwidth = [ const columnwidth = [
220, // Survey Name 220, // Survey Name
100, // Year 100, // Year
@ -169,16 +169,16 @@ const QuarterlyWindows = () => {
140, // Submission Count 140, // Submission Count
120 // Actions 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) => {
// Apply year filter // Apply year filter
if (yearFilter !== 'all' && item.year !== yearFilter) return false; if (yearFilter !== 'all' && item.year !== yearFilter) return false;
// Apply quarter filter // Apply quarter filter
if (quarterFilter !== 'all' && item.quarter !== quarterFilter) return false; if (quarterFilter !== 'all' && item.quarter !== quarterFilter) return false;
// Apply search term // Apply search term
if (!term) return true; if (!term) return true;
const haystack = [ const haystack = [
@ -200,21 +200,21 @@ const QuarterlyWindows = () => {
return haystack.includes(term); return haystack.includes(term);
}); });
}, [quarterData, searchTerm, yearFilter, quarterFilter]); }, [quarterData, searchTerm, yearFilter, quarterFilter]);
const Tooltip = ({ children, text }) => ( const Tooltip = ({ children, text }) => (
<div className="flex items-center gap-1 group relative"> <div className="flex items-center gap-1 group relative">
<span>{children}</span> <span>{children}</span>
<div className="relative group cursor-pointer" title={text}> <div className="relative group cursor-pointer" title={text}>
<img <img
src="/assets/images/tooltip.svg" src="/assets/images/tooltip.svg"
alt="info" alt="info"
className="w-4 h-4" className="w-4 h-4"
style={{ filter: 'invert(45%) sepia(6%) saturate(323%) hue-rotate(175deg) brightness(93%) contrast(82%)' }} style={{ filter: 'invert(45%) sepia(6%) saturate(323%) hue-rotate(175deg) brightness(93%) contrast(82%)' }}
/> />
</div> </div>
</div> </div>
); );
const rows = filteredRows.map((item) => [ const rows = filteredRows.map((item) => [
item.survey_name || '-', item.survey_name || '-',
item.year, item.year,
@ -234,24 +234,24 @@ const QuarterlyWindows = () => {
item.submissionCount, item.submissionCount,
'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();
const years = new Set(['all']); const years = new Set(['all']);
// Add years from 2022 to current year // Add years from 2022 to current year
for (let y = 2022; y <= currentYear; y++) { for (let y = 2022; y <= currentYear; y++) {
years.add(y.toString()); years.add(y.toString());
} }
// Add any additional years that might be in the data // Add any additional years that might be in the data
quarterData.forEach(item => { quarterData.forEach(item => {
if (item.year && !years.has(item.year)) { if (item.year && !years.has(item.year)) {
years.add(item.year); years.add(item.year);
} }
}); });
// Sort years in descending order (newest first) // Sort years in descending order (newest first)
return Array.from(years).sort((a, b) => { return Array.from(years).sort((a, b) => {
if (a === 'all') return -1; if (a === 'all') return -1;
@ -259,9 +259,9 @@ const QuarterlyWindows = () => {
return parseInt(b) - parseInt(a); return parseInt(b) - parseInt(a);
}); });
}, [quarterData]); }, [quarterData]);
const quarterOptions = ['all', 'Q1', 'Q2', 'Q3', 'Q4']; const quarterOptions = ['all', 'Q1', 'Q2', 'Q3', 'Q4'];
const openAddModal = () => { const openAddModal = () => {
setForm(createEmptyQuarterForm()); setForm(createEmptyQuarterForm());
setOriginalForm(null); setOriginalForm(null);
@ -269,12 +269,12 @@ const QuarterlyWindows = () => {
setModalMode('add'); setModalMode('add');
setValidationErrors({}); setValidationErrors({});
}; };
React.useEffect(() => { React.useEffect(() => {
const maxPage = Math.max(1, Math.ceil(filteredRows.length / pageSize)); const maxPage = Math.max(1, Math.ceil(filteredRows.length / pageSize));
setCurrentPage((prev) => Math.min(prev, maxPage)); setCurrentPage((prev) => Math.min(prev, maxPage));
}, [filteredRows.length, pageSize]); }, [filteredRows.length, pageSize]);
const openEditModal = (index) => { const openEditModal = (index) => {
const selected = quarterData[index]; const selected = quarterData[index];
// Ensure dates are in the correct format for the form inputs // Ensure dates are in the correct format for the form inputs
@ -289,13 +289,13 @@ const QuarterlyWindows = () => {
setModalMode('edit'); setModalMode('edit');
setValidationErrors({}); setValidationErrors({});
}; };
const handleCloseModal = () => { const handleCloseModal = () => {
setModalMode(null); setModalMode(null);
setEditingRow(null); setEditingRow(null);
setValidationErrors({}); setValidationErrors({});
}; };
const handleReset = () => { const handleReset = () => {
if (modalMode === 'edit' && editingRow !== null) { if (modalMode === 'edit' && editingRow !== null) {
if (originalForm) { if (originalForm) {
@ -311,10 +311,10 @@ const QuarterlyWindows = () => {
} }
setValidationErrors({}); setValidationErrors({});
}; };
const validateForm = () => { const validateForm = () => {
const errors = {}; const errors = {};
if (!form.survey_name.trim()) { if (!form.survey_name.trim()) {
errors.survey_name = 'Survey Name is required'; errors.survey_name = 'Survey Name is required';
} }
@ -330,7 +330,7 @@ const QuarterlyWindows = () => {
if (!form.endDate) { if (!form.endDate) {
errors.endDate = 'Close Date is required'; errors.endDate = 'Close Date is required';
} }
// Date validation // Date validation
if (form.startDate && form.endDate) { if (form.startDate && form.endDate) {
const startDate = new Date(form.startDate); const startDate = new Date(form.startDate);
@ -339,14 +339,14 @@ const QuarterlyWindows = () => {
errors.endDate = 'Close Date must be after Open Date'; errors.endDate = 'Close Date must be after Open Date';
} }
} }
setValidationErrors(errors); setValidationErrors(errors);
return Object.keys(errors).length === 0; return Object.keys(errors).length === 0;
}; };
const handleFormChange = (field) => (value) => { const handleFormChange = (field) => (value) => {
setForm(prev => ({ ...prev, [field]: value })); setForm(prev => ({ ...prev, [field]: value }));
// Clear validation error when user starts typing // Clear validation error when user starts typing
if (validationErrors[field]) { if (validationErrors[field]) {
setValidationErrors(prev => ({ setValidationErrors(prev => ({
@ -355,16 +355,16 @@ const QuarterlyWindows = () => {
})); }));
} }
}; };
const handleSave = async () => { const handleSave = async () => {
try { try {
if (!validateForm()) { if (!validateForm()) {
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);
const formattedData = { const formattedData = {
survey_name: form.survey_name || '', survey_name: form.survey_name || '',
year: form.year ? parseInt(form.year, 10) : 0, year: form.year ? parseInt(form.year, 10) : 0,
@ -378,7 +378,7 @@ const QuarterlyWindows = () => {
is_active: form.status === 'Active', is_active: form.status === 'Active',
establishment: form.establishment || '-', establishment: form.establishment || '-',
}; };
if (modalMode === 'add') { if (modalMode === 'add') {
const response = await createQuarterlyWindow(formattedData); const response = await createQuarterlyWindow(formattedData);
if (response.status === 'success') { if (response.status === 'success') {
@ -392,7 +392,7 @@ const QuarterlyWindows = () => {
status: formattedData.is_active ? 'Active' : 'Inactive' status: formattedData.is_active ? 'Active' : 'Inactive'
}; };
setQuarterData((prev) => [...prev, newItem]); setQuarterData((prev) => [...prev, newItem]);
// Show success toast for add // Show success toast for add
setToastData({ setToastData({
message: 'Quarterly survey created successfully!', message: 'Quarterly survey created successfully!',
@ -409,9 +409,9 @@ const QuarterlyWindows = () => {
} }
const response = await updateQuarterlyWindow(itemId, formattedData); const response = await updateQuarterlyWindow(itemId, formattedData);
if (response.status === 'success') { if (response.status === 'success') {
setQuarterData((prev) => setQuarterData((prev) =>
prev.map((item, idx) => prev.map((item, idx) =>
idx === editingRow idx === editingRow
? { ? {
...item, ...item,
...formattedData, ...formattedData,
@ -419,11 +419,11 @@ const QuarterlyWindows = () => {
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',
status: formattedData.is_active ? 'Active' : 'Inactive' status: formattedData.is_active ? 'Active' : 'Inactive'
} }
: item : item
) )
); );
// Show success toast for edit // Show success toast for edit
setToastData({ setToastData({
message: 'Quarterly survey updated successfully!', message: 'Quarterly survey updated successfully!',
@ -438,7 +438,7 @@ const QuarterlyWindows = () => {
console.error('Error saving quarterly window:', error); console.error('Error saving quarterly window:', error);
} }
}; };
return ( return (
<div className="w-full max-w-[1280px] rounded-lg border border-[#C3C6CB] bg-white min-h-[348px] pb-6"> <div className="w-full max-w-[1280px] rounded-lg border border-[#C3C6CB] bg-white min-h-[348px] pb-6">
{/* Custom Toast Notification */} {/* Custom Toast Notification */}
@ -453,7 +453,7 @@ const QuarterlyWindows = () => {
</div> </div>
</div> </div>
)} )}
<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>
@ -560,7 +560,7 @@ const QuarterlyWindows = () => {
</div> </div>
</div> </div>
</div> </div>
<div className="w-full" style={{ minWidth: '1200px' }}> <div className="w-full" style={{ minWidth: '1200px' }}>
<Table <Table
headers={headers} headers={headers}
@ -606,7 +606,7 @@ const QuarterlyWindows = () => {
}} }}
/> />
</div> </div>
{modalMode && ( {modalMode && (
<div className="fixed inset-0 z-50"> <div className="fixed inset-0 z-50">
<div className="absolute inset-0 bg-black/40" onClick={handleCloseModal} /> <div className="absolute inset-0 bg-black/40" onClick={handleCloseModal} />
@ -651,7 +651,7 @@ const QuarterlyWindows = () => {
</button> </button>
</div> </div>
</div> </div>
<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
@ -664,13 +664,14 @@ const QuarterlyWindows = () => {
required required
onChange={(e) => handleFormChange('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} error={validationErrors.survey_name}
className={validationErrors.survey_name ? 'border-red-500 focus:border-red-500 focus:ring-red-500' : ''}
/> />
{validationErrors.survey_name && ( {validationErrors.survey_name && (
<p className="mt-1 text-sm text-red-600">{validationErrors.survey_name}</p> <p className="mt-1 text-sm text-red-600"></p>
)} )}
</div> </div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4"> <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div> <div>
<SelectField <SelectField
@ -684,13 +685,14 @@ const QuarterlyWindows = () => {
onChange={(e) => handleFormChange('quarter')(e.target.value)} onChange={(e) => handleFormChange('quarter')(e.target.value)}
options={['Q1', 'Q2', 'Q3', 'Q4'].map((q) => ({ label: q, value: q }))} options={['Q1', 'Q2', 'Q3', 'Q4'].map((q) => ({ label: q, value: q }))}
placeholder="Select Quarter" placeholder="Select Quarter"
// error={validationErrors.quarter} error={validationErrors.quarter}
className={validationErrors.quarter ? 'border-red-500 focus:border-red-500 focus:ring-red-500' : ''}
/> />
{validationErrors.quarter && ( {validationErrors.quarter && (
<p className="mt-1 text-sm text-red-600">{validationErrors.quarter}</p> <p className="mt-1 text-sm text-red-600"></p>
)} )}
</div> </div>
<div> <div>
<SelectField <SelectField
label={ label={
@ -706,13 +708,14 @@ const QuarterlyWindows = () => {
})} })}
required required
placeholder="Select Year" placeholder="Select Year"
// error={validationErrors.year} error={validationErrors.year}
className={validationErrors.year ? 'border-red-500 focus:border-red-500 focus:ring-red-500' : ''}
/> />
{validationErrors.year && ( {validationErrors.year && (
<p className="mt-1 text-sm text-red-600">{validationErrors.year}</p> <p className="mt-1 text-sm text-red-600"></p>
)} )}
</div> </div>
<div> <div>
<DateField <DateField
label={ label={
@ -725,12 +728,13 @@ const QuarterlyWindows = () => {
onChange={(e) => handleFormChange('startDate')(e.target.value)} onChange={(e) => handleFormChange('startDate')(e.target.value)}
placeholder="Select Date" placeholder="Select Date"
error={validationErrors.startDate} error={validationErrors.startDate}
className={validationErrors.startDate ? 'border-red-500 focus:border-red-500 focus:ring-red-500' : ''}
/> />
{validationErrors.startDate && ( {validationErrors.startDate && (
<p className="mt-1 text-sm text-red-600">{validationErrors.startDate}</p> <p className="mt-1 text-sm text-red-500">{validationErrors.startDate}</p>
)} )}
</div> </div>
<div> <div>
<DateField <DateField
label={ label={
@ -743,6 +747,7 @@ const QuarterlyWindows = () => {
onChange={(e) => handleFormChange('endDate')(e.target.value)} onChange={(e) => handleFormChange('endDate')(e.target.value)}
placeholder="Select Date" placeholder="Select Date"
error={validationErrors.endDate} error={validationErrors.endDate}
className={validationErrors.endDate ? 'border-red-500 focus:border-red-500 focus:ring-red-500' : ''}
/> />
{validationErrors.endDate && ( {validationErrors.endDate && (
<p className="mt-1 text-sm text-red-600">{validationErrors.endDate}</p> <p className="mt-1 text-sm text-red-600">{validationErrors.endDate}</p>
@ -762,7 +767,7 @@ const QuarterlyWindows = () => {
placeholder="Select Grace Period" placeholder="Select Grace Period"
/> />
</div> </div>
<div className="mt-6 flex items-center justify-end gap-3"> <div className="mt-6 flex items-center justify-end gap-3">
<button <button
type="button" type="button"
@ -786,5 +791,5 @@ const QuarterlyWindows = () => {
</div> </div>
); );
}; };
export default QuarterlyWindows; export default QuarterlyWindows;

View File

@ -3,7 +3,7 @@ import Table from '@/components/common/Table';
import StatusBadge from '@/components/common/StatusBadge'; import StatusBadge from '@/components/common/StatusBadge';
import { TextField, SelectField } from '@/components/common/FormControls'; import { TextField, SelectField } from '@/components/common/FormControls';
import { getUnits, createUnit, updateUnit, deleteUnit } from '@/services/configuration/unitService'; import { getUnits, createUnit, updateUnit, deleteUnit } from '@/services/configuration/unitService';
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';
const pencilActiveSrc = '/assets/images/PencilSimple.svg'; const pencilActiveSrc = '/assets/images/PencilSimple.svg';
@ -14,17 +14,17 @@ const deleteIconSrc = '/assets/images/delete.svg';
const caretDownIconSrc = '/assets/images/caretdown-active.svg'; 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(() => {
const timer = setTimeout(() => { const timer = setTimeout(() => {
onClose(); onClose();
}, 3000); }, 3000);
return () => clearTimeout(timer); return () => clearTimeout(timer);
}, [onClose]); }, [onClose]);
const getToastStyles = () => { const getToastStyles = () => {
switch (type) { switch (type) {
case 'success': case 'success':
@ -37,7 +37,7 @@ const CustomToast = ({ message, type, onClose }) => {
return 'bg-blue-50 border-blue-200 text-blue-800'; return 'bg-blue-50 border-blue-200 text-blue-800';
} }
}; };
const getIcon = () => { const getIcon = () => {
switch (type) { switch (type) {
case 'success': case 'success':
@ -50,7 +50,7 @@ const CustomToast = ({ message, type, onClose }) => {
return ''; return '';
} }
}; };
return ( return (
<div className={`flex items-center p-4 mb-4 rounded-lg border ${getToastStyles()} shadow-lg min-w-80 max-w-md`}> <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="text-lg mr-3">{getIcon()}</span>
@ -64,14 +64,14 @@ const CustomToast = ({ message, type, onClose }) => {
</div> </div>
); );
}; };
const createEmptyUnitForm = () => ({ const createEmptyUnitForm = () => ({
unitName: '', unitName: '',
description: '', description: '',
productsMapped: [], productsMapped: [],
status: '', status: '',
}); });
const UnitMaster = () => { const UnitMaster = () => {
const [units, setUnits] = useState([]); const [units, setUnits] = useState([]);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
@ -87,23 +87,23 @@ const UnitMaster = () => {
const [currentUser, setCurrentUser] = useState(null); const [currentUser, setCurrentUser] = useState(null);
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();
}, []); }, []);
const fetchUnits = async () => { const fetchUnits = async () => {
try { try {
setLoading(true); setLoading(true);
const response = await getUnits(); const response = await getUnits();
const unitsData = response.data || []; const unitsData = response.data || [];
// Sort by latest created_at date (newest first) // Sort by latest created_at date (newest first)
const sortedUnits = [...unitsData].sort( const sortedUnits = [...unitsData].sort(
(a, b) => new Date(b.created_at) - new Date(a.created_at) (a, b) => new Date(b.created_at) - new Date(a.created_at)
); );
setUnits(sortedUnits); setUnits(sortedUnits);
} catch (error) { } catch (error) {
console.error('Error fetching units:', error); console.error('Error fetching units:', error);
@ -111,24 +111,24 @@ const UnitMaster = () => {
setLoading(false); setLoading(false);
} }
}; };
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) {
setCurrentUser(JSON.parse(userProfile)); setCurrentUser(JSON.parse(userProfile));
} }
}, []); }, []);
const headers = [ const headers = [
'Unit Name', 'Unit Name',
'Description', 'Description',
@ -138,7 +138,7 @@ const UnitMaster = () => {
'Last Updated', 'Last Updated',
'Actions', 'Actions',
]; ];
const rows = useMemo(() => { const rows = useMemo(() => {
return units.map((item) => { return units.map((item) => {
const createdDate = item.created_at const createdDate = item.created_at
@ -149,12 +149,12 @@ const UnitMaster = () => {
? new Date(item.updated_at).toLocaleDateString('en-GB') ? new Date(item.updated_at).toLocaleDateString('en-GB')
: '-'; : '-';
const currentUserName = currentUser?.name || ''; const currentUserName = currentUser?.name || '';
return [ return [
item.uom || item.unitName, item.uom || item.unitName,
item.uom_short_name || item.description, item.uom_short_name || item.description,
item.mapped_products_count !== undefined item.mapped_products_count !== undefined
? item.mapped_products_count ? item.mapped_products_count
: (Array.isArray(item.productsMapped) ? item.productsMapped.length : 0), : (Array.isArray(item.productsMapped) ? item.productsMapped.length : 0),
item.created_by_name || currentUserName || '-', item.created_by_name || currentUserName || '-',
createdDate, createdDate,
@ -163,7 +163,7 @@ const UnitMaster = () => {
]; ];
}); });
}, [units, currentUser, filteredUnits]); }, [units, currentUser, filteredUnits]);
const statusOptions = React.useMemo( const statusOptions = React.useMemo(
() => [ () => [
{ label: 'Active', value: 'Active' }, { label: 'Active', value: 'Active' },
@ -171,26 +171,26 @@ const UnitMaster = () => {
], ],
[] []
); );
const filteredRows = units; const filteredRows = units;
const validateForm = () => { const validateForm = () => {
const errors = {}; const errors = {};
// Unit Name validation // Unit Name validation
if (!form.unitName.trim()) { if (!form.unitName.trim()) {
errors.unitName = 'Unit Name is required'; errors.unitName = 'Unit Name is required';
} }
// Description validation - red color validation // Description validation - red color validation
if (!form.description.trim()) { if (!form.description.trim()) {
errors.description = 'Please enter a description.'; errors.description = 'Please enter a description.';
} }
setFormErrors(errors); setFormErrors(errors);
return Object.keys(errors).length === 0; return Object.keys(errors).length === 0;
}; };
const openAddModal = () => { const openAddModal = () => {
setEditingRow(null); setEditingRow(null);
setForm(createEmptyUnitForm()); setForm(createEmptyUnitForm());
@ -198,7 +198,7 @@ const UnitMaster = () => {
setModalMode('add'); setModalMode('add');
setIsProductDropdownOpen(false); setIsProductDropdownOpen(false);
}; };
const handleEdit = (index) => { const handleEdit = (index) => {
const selected = units[index]; const selected = units[index];
if (!selected) return; if (!selected) return;
@ -213,11 +213,11 @@ const UnitMaster = () => {
setModalMode('edit'); setModalMode('edit');
setIsProductDropdownOpen(false); setIsProductDropdownOpen(false);
}; };
const handleDelete = (index) => { const handleDelete = (index) => {
setDeletingRow(index); setDeletingRow(index);
}; };
const closeModal = () => { const closeModal = () => {
setModalMode(null); setModalMode(null);
setForm(createEmptyUnitForm()); setForm(createEmptyUnitForm());
@ -225,11 +225,11 @@ const UnitMaster = () => {
setEditingRow(null); setEditingRow(null);
setIsProductDropdownOpen(false); setIsProductDropdownOpen(false);
}; };
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 // Clear error when user starts typing
if (formErrors[field]) { if (formErrors[field]) {
setFormErrors(prev => ({ setFormErrors(prev => ({
@ -238,7 +238,7 @@ const UnitMaster = () => {
})); }));
} }
}; };
const handleProductToggle = (value) => { const handleProductToggle = (value) => {
setForm((prev) => { setForm((prev) => {
const current = Array.isArray(prev.productsMapped) ? prev.productsMapped : []; const current = Array.isArray(prev.productsMapped) ? prev.productsMapped : [];
@ -249,7 +249,7 @@ const UnitMaster = () => {
}; };
}); });
}; };
React.useEffect(() => { React.useEffect(() => {
const handleClickOutside = (event) => { const handleClickOutside = (event) => {
if (!productDropdownRef.current) return; if (!productDropdownRef.current) return;
@ -257,30 +257,30 @@ const UnitMaster = () => {
setIsProductDropdownOpen(false); setIsProductDropdownOpen(false);
} }
}; };
if (isProductDropdownOpen) { if (isProductDropdownOpen) {
document.addEventListener('mousedown', handleClickOutside); document.addEventListener('mousedown', handleClickOutside);
} }
return () => { return () => {
document.removeEventListener('mousedown', handleClickOutside); document.removeEventListener('mousedown', handleClickOutside);
}; };
}, [isProductDropdownOpen]); }, [isProductDropdownOpen]);
const handleSave = async () => { const handleSave = async () => {
// Validate form before saving // Validate form before saving
if (!validateForm()) { if (!validateForm()) {
return; 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");
// Check for duplicate unit name // Check for duplicate unit name
const nameExists = units.some( const nameExists = units.some(
unit => unit.uom?.toLowerCase().trim() === form.unitName.toLowerCase().trim() unit => unit.uom?.toLowerCase().trim() === form.unitName.toLowerCase().trim()
); );
// Only check duplicates when adding (not editing) // Only check duplicates when adding (not editing)
if (modalMode !== 'edit' && nameExists) { if (modalMode !== 'edit' && nameExists) {
setToastData({ setToastData({
@ -289,7 +289,7 @@ const UnitMaster = () => {
}); });
return; return;
} }
const unitData = { const unitData = {
uom: form.unitName, uom: form.unitName,
uom_short_name: form.description, uom_short_name: form.description,
@ -297,19 +297,19 @@ const UnitMaster = () => {
productsMapped: selectedProducts.length, productsMapped: selectedProducts.length,
}; };
console.log("unitData",unitData) console.log("unitData",unitData)
try { try {
setLoading(true); setLoading(true);
if (modalMode === 'edit' && editingRow !== null) { if (modalMode === 'edit' && editingRow !== null) {
const updatedUnitData = { const updatedUnitData = {
...unitData, ...unitData,
updated_at: new Date().toISOString(), updated_at: new Date().toISOString(),
updated_by: currentUser?.id || null, updated_by: currentUser?.id || null,
}; };
await updateUnit(units[editingRow].id, updatedUnitData); await updateUnit(units[editingRow].id, updatedUnitData);
// Show success message for edit // Show success message for edit
setToastData({ setToastData({
message: 'Unit updated successfully!', message: 'Unit updated successfully!',
@ -323,14 +323,14 @@ const UnitMaster = () => {
created_by: currentUser?.id || null, created_by: currentUser?.id || null,
created_by_name: currentUser?.name || '', created_by_name: currentUser?.name || '',
}); });
// Show success message for add // Show success message for add
setToastData({ setToastData({
message: 'New unit added successfully!', message: 'New unit added successfully!',
type: 'success' type: 'success'
}); });
} }
await fetchUnits(); await fetchUnits();
closeModal(); closeModal();
} catch (error) { } catch (error) {
@ -343,14 +343,14 @@ const UnitMaster = () => {
setLoading(false); setLoading(false);
} }
}; };
const handleDeleteConfirm = async () => { const handleDeleteConfirm = async () => {
if (deletingRow === null) return; if (deletingRow === null) return;
try { try {
const unitToDelete = units[deletingRow]; const unitToDelete = units[deletingRow];
await deleteUnit(unitToDelete.id); await deleteUnit(unitToDelete.id);
setUnits(prev => prev.filter((_, index) => index !== deletingRow)); setUnits(prev => prev.filter((_, index) => index !== deletingRow));
// Show success message for delete // Show success message for delete
setToastData({ setToastData({
message: 'Unit deleted successfully!', message: 'Unit deleted successfully!',
@ -365,18 +365,18 @@ const UnitMaster = () => {
}); });
} }
}; };
const closeDeleteConfirm = () => { const closeDeleteConfirm = () => {
setDeletingRow(null); setDeletingRow(null);
}; };
const deletingUnit = React.useMemo(() => { const deletingUnit = React.useMemo(() => {
if (deletingRow === null || deletingRow < 0 || deletingRow >= units.length) { if (deletingRow === null || deletingRow < 0 || deletingRow >= units.length) {
return null; return null;
} }
return units[deletingRow]; return units[deletingRow];
}, [deletingRow, units]); }, [deletingRow, units]);
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]">
@ -388,7 +388,7 @@ const UnitMaster = () => {
type="button" type="button"
onClick={() => { onClick={() => {
if (!units.length) return; if (!units.length) return;
const csvHeader = headers.slice(0, headers.length - 1).join(','); const csvHeader = headers.slice(0, headers.length - 1).join(',');
const csvRows = units.map((item) => { const csvRows = units.map((item) => {
const createdDate = item.created_at const createdDate = item.created_at
@ -402,7 +402,7 @@ const UnitMaster = () => {
const mappedProducts = Array.isArray(item.productsMapped) const mappedProducts = Array.isArray(item.productsMapped)
? item.productsMapped.join('; ') ? item.productsMapped.join('; ')
: '0'; : '0';
return [ return [
item.uom || item.unitName || '', item.uom || item.unitName || '',
item.uom_short_name || item.description || '', item.uom_short_name || item.description || '',
@ -415,7 +415,7 @@ const UnitMaster = () => {
.map((value) => `"${String(value ?? '').replace(/"/g, '""')}"`) .map((value) => `"${String(value ?? '').replace(/"/g, '""')}"`)
.join(','); .join(',');
}); });
const csvContent = csvHeader + '\n' + csvRows.join('\n'); const csvContent = csvHeader + '\n' + csvRows.join('\n');
const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' }); const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' });
const url = URL.createObjectURL(blob); const url = URL.createObjectURL(blob);
@ -437,7 +437,7 @@ const UnitMaster = () => {
<img src={downloadIconSrc} alt="Export" className="h-5 w-5" /> <img src={downloadIconSrc} alt="Export" className="h-5 w-5" />
<span className="font-medium">Export CSV</span> <span className="font-medium">Export CSV</span>
</button> </button>
<button <button
type="button" type="button"
className="h-10 px-4 rounded-[6px] bg-[#92722A] text-white text-sm inline-flex items-center gap-2 cursor-pointer" className="h-10 px-4 rounded-[6px] bg-[#92722A] text-white text-sm inline-flex items-center gap-2 cursor-pointer"
@ -448,7 +448,7 @@ const UnitMaster = () => {
</button> </button>
</div> </div>
</div> </div>
{loading ? ( {loading ? (
<div className="flex justify-center items-center h-64"> <div className="flex justify-center items-center h-64">
<div className="animate-spin rounded-full h-12 w-12 border-t-2 border-b-2 border-gray-900"></div> <div className="animate-spin rounded-full h-12 w-12 border-t-2 border-b-2 border-gray-900"></div>
@ -500,7 +500,7 @@ const UnitMaster = () => {
}} }}
/> />
)} )}
{/* Add/Edit Modal */} {/* Add/Edit Modal */}
{modalMode && ( {modalMode && (
<div className="fixed inset-0 z-50"> <div className="fixed inset-0 z-50">
@ -529,7 +529,7 @@ const UnitMaster = () => {
</svg> </svg>
</button> </button>
</div> </div>
<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">
<div> <div>
@ -542,13 +542,14 @@ const UnitMaster = () => {
value={form.unitName} value={form.unitName}
onChange={handleFormChange('unitName')} onChange={handleFormChange('unitName')}
placeholder="Enter Unit Name" placeholder="Enter Unit Name"
// error={formErrors.unitName} error={formErrors.unitName}
className={formErrors.unitName ? 'border-red-500 focus:border-red-500 focus:ring-red-500' : ''}
/> />
{formErrors.unitName && ( {formErrors.unitName && (
<p className="mt-1 text-sm text-red-600">{formErrors.unitName}</p> <p className="mt-1 text-sm text-red-600"></p>
)} )}
</div> </div>
<div> <div>
<TextField <TextField
label={ label={
@ -560,14 +561,15 @@ const UnitMaster = () => {
onChange={handleFormChange('description')} onChange={handleFormChange('description')}
placeholder="Enter Description" placeholder="Enter Description"
width="100%" width="100%"
// error={formErrors.description} error={formErrors.description}
className={formErrors.description ? 'border-red-500 focus:border-red-500 focus:ring-red-500' : ''}
/> />
{formErrors.description && ( {formErrors.description && (
<p className="mt-1 text-sm text-red-600">{formErrors.description}</p> <p className="mt-1 text-sm text-red-600"></p>
)} )}
</div> </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">
<button <button
type="button" type="button"
@ -594,7 +596,7 @@ const UnitMaster = () => {
</div> </div>
</div> </div>
)} )}
{/* Delete Confirmation Modal */} {/* Delete Confirmation Modal */}
{deletingUnit && ( {deletingUnit && (
<div className="fixed inset-0 z-50"> <div className="fixed inset-0 z-50">
@ -630,7 +632,7 @@ const UnitMaster = () => {
</p> </p>
</div> </div>
</div> </div>
<div className="flex items-center justify-end gap-4"> <div className="flex items-center justify-end gap-4">
<button <button
type="button" type="button"
@ -651,7 +653,7 @@ const UnitMaster = () => {
</div> </div>
</div> </div>
)} )}
{/* Custom Toast Notification */} {/* Custom Toast Notification */}
{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">
@ -667,5 +669,5 @@ const UnitMaster = () => {
</div> </div>
); );
}; };
export default UnitMaster; export default UnitMaster;