bug fixed
This commit is contained in:
parent
6c20f2d769
commit
e5041738a8
@ -655,7 +655,7 @@ const ProductData = ({
|
||||
<h2 className="text-xl font-semibold text-[#232528]">Step 2: Product Data — Monthly Output & Cost</h2>
|
||||
{(quarter || year) && (
|
||||
<div className="text-sm font-medium text-gray-600">
|
||||
Current Quarter: <span className="text-[#92722A]">Q{quarter}-{year}</span>
|
||||
Current Quarter: <span className="text-[#92722A]">{quarter}-{year}</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@ -1,66 +1,87 @@
|
||||
import React, { useEffect } from 'react';
|
||||
import { Link, useNavigate, useLocation } from 'react-router-dom';
|
||||
import AdminHeader from '@/components/admin/AdminHeader';
|
||||
import QuarterlyWindows from '@/pages/Admin/configuration/QuarterlyWindows';
|
||||
import IsicHsCodes from '@/pages/Admin/configuration/IsicHsCodes';
|
||||
import UnitMaster from '@/pages/Admin/configuration/UnitMaster';
|
||||
import AdminUsers from '@/pages/Admin/configuration/AdminUsers/ListAdminUsers';
|
||||
import ListAdminUsers from '@/pages/Admin/configuration/AdminUsers/ListAdminUsers';
|
||||
import EditAdminUser from '@/pages/Admin/configuration/AdminUsers/EditAdminUser';
|
||||
import CompanyProfile from '@/pages/Admin/configuration/CompanyProfile';
|
||||
|
||||
const caretUpSrc = '/assets/images/caret-up.svg';
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { Link, useNavigate, useLocation } from "react-router-dom";
|
||||
import AdminHeader from "@/components/admin/AdminHeader";
|
||||
import QuarterlyWindows from "@/pages/Admin/configuration/QuarterlyWindows";
|
||||
import IsicHsCodes from "@/pages/Admin/configuration/IsicHsCodes";
|
||||
import UnitMaster from "@/pages/Admin/configuration/UnitMaster";
|
||||
import AdminUsers from "@/pages/Admin/configuration/AdminUsers/ListAdminUsers";
|
||||
import CompanyProfile from "@/pages/Admin/configuration/CompanyProfile";
|
||||
|
||||
const Configuration = () => {
|
||||
const navigate = useNavigate();
|
||||
const location = useLocation();
|
||||
const tabs = ['Quarterly Windows', 'HS Codes', 'Unit Master', 'Admin Users', 'Company Profile'];
|
||||
const [active, setActive] = React.useState(tabs[0]);
|
||||
|
||||
// Update URL when active tab changes
|
||||
const tabs = [
|
||||
"Quarterly Windows",
|
||||
"HS Codes",
|
||||
"Unit Master",
|
||||
"Admin Users",
|
||||
"Company Profile",
|
||||
];
|
||||
|
||||
const [active, setActive] = useState(tabs[0]);
|
||||
|
||||
// ✅ Map tab names to routes
|
||||
const tabToPath = {
|
||||
"Quarterly Windows": "quarterly",
|
||||
"HS Codes": "hscodes",
|
||||
"Unit Master": "unitmaster",
|
||||
"Admin Users": "admin-users",
|
||||
"Company Profile": "profile",
|
||||
};
|
||||
|
||||
// ✅ Map paths to tab names
|
||||
const pathToTab = {
|
||||
quarterly: "Quarterly Windows",
|
||||
hscodes: "HS Codes",
|
||||
unitmaster: "Unit Master",
|
||||
"admin-users": "Admin Users",
|
||||
profile: "Company Profile",
|
||||
};
|
||||
|
||||
// ✅ Navigate to URL when tab changes
|
||||
useEffect(() => {
|
||||
const tabToPath = {
|
||||
'Quarterly Windows': 'quarterly',
|
||||
'HS Codes': 'hscodes',
|
||||
'Unit Master': 'unitmaster',
|
||||
'Admin Users': 'admin-users',
|
||||
'Company Profile': 'profile'
|
||||
};
|
||||
|
||||
if (tabToPath[active]) {
|
||||
navigate(`/admin/configuration/${tabToPath[active]}`, { replace: true });
|
||||
}
|
||||
}, [active, navigate]);
|
||||
|
||||
// Update active tab based on URL
|
||||
useEffect(() => {
|
||||
const pathToTab = {
|
||||
'quarterly': 'Quarterly Windows',
|
||||
'hscodes': 'HS Codes',
|
||||
'unitmaster': 'Unit Master',
|
||||
'admin-users': 'Admin Users',
|
||||
'profile': 'Company Profile'
|
||||
};
|
||||
|
||||
const path = location.pathname.split('/').pop();
|
||||
if (pathToTab[path] && pathToTab[path] !== active) {
|
||||
setActive(pathToTab[path]);
|
||||
}
|
||||
}, [location.pathname]);
|
||||
// ✅ Update active tab when URL changes
|
||||
useEffect(() => {
|
||||
const path = location.pathname.split("/").pop();
|
||||
if (pathToTab[path] && pathToTab[path] !== active) {
|
||||
setActive(pathToTab[path]);
|
||||
}
|
||||
}, [location.pathname]);
|
||||
|
||||
// ✅ Breadcrumb with ">" separators
|
||||
const Breadcrumbs = (
|
||||
<nav className="mb-6 text-sm text-[#8F9299] flex items-center gap-3">
|
||||
<Link to="/admin/dashboard" className="flex items-center gap-1 text-[#232528] hover:underline">
|
||||
<nav className="mb-6 text-sm text-[#8F9299] flex items-center gap-2">
|
||||
<Link
|
||||
to="/admin/dashboard"
|
||||
className="text-[#232528] hover:underline font-medium"
|
||||
>
|
||||
Dashboard
|
||||
<img src={caretUpSrc} alt="Dashboard" className="h-3 w-3" />
|
||||
</Link>
|
||||
<span className="flex items-center gap-1 text-[#92722A] font-medium">
|
||||
<span className="text-[#8F9299]">{">"}</span>
|
||||
|
||||
<Link
|
||||
to="/admin/configuration"
|
||||
className="text-[#232528] hover:underline font-medium"
|
||||
>
|
||||
Configuration
|
||||
<img src={caretUpSrc} alt="Configuration" className="h-3 w-3" />
|
||||
</span>
|
||||
</Link>
|
||||
|
||||
{active && (
|
||||
<>
|
||||
<span className="text-[#8F9299]">{">"}</span>
|
||||
<span className="text-[#92722A] font-medium">{active}</span>
|
||||
</>
|
||||
)}
|
||||
</nav>
|
||||
);
|
||||
|
||||
// ✅ Tab header
|
||||
const PageHeader = (
|
||||
<div className="w-full mb-6">
|
||||
<div className="h-14 w-full border-b border-[#E5E7EB] flex items-end">
|
||||
@ -72,8 +93,8 @@ useEffect(() => {
|
||||
onClick={() => setActive(t)}
|
||||
className={`pb-3 text-sm whitespace-nowrap border-b-2 cursor-pointer transition-colors ${
|
||||
active === t
|
||||
? 'text-[#92722A] border-[#92722A] font-medium'
|
||||
: 'text-[#232528] border-transparent hover:text-[#92722A]'
|
||||
? "text-[#92722A] border-[#92722A] font-medium"
|
||||
: "text-[#232528] border-transparent hover:text-[#92722A]"
|
||||
}`}
|
||||
>
|
||||
{t}
|
||||
@ -84,11 +105,29 @@ useEffect(() => {
|
||||
</div>
|
||||
);
|
||||
|
||||
const isQuarterTab = active === 'Quarterly Windows';
|
||||
const isIsicTab = active === 'HS Codes';
|
||||
const isUnitMasterTab = active === 'Unit Master';
|
||||
const isAdminTab = active === 'Admin Users';
|
||||
const isCompanyProfileTab = active === 'Company Profile';
|
||||
// ✅ Render selected tab content
|
||||
const renderActiveTab = () => {
|
||||
switch (active) {
|
||||
case "Quarterly Windows":
|
||||
return <QuarterlyWindows />;
|
||||
case "HS Codes":
|
||||
return <IsicHsCodes />;
|
||||
case "Unit Master":
|
||||
return <UnitMaster />;
|
||||
case "Admin Users":
|
||||
return <AdminUsers />;
|
||||
case "Company Profile":
|
||||
return <CompanyProfile />;
|
||||
default:
|
||||
return (
|
||||
<div className="bg-white w-full rounded-lg shadow-sm ring-1 ring-gray-200">
|
||||
<div className="px-6 py-10 text-sm text-[#5F646D]">
|
||||
{active} content coming soon…
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-[#F7F7F7]">
|
||||
@ -96,22 +135,7 @@ useEffect(() => {
|
||||
<div className="max-w-[1280px] mx-auto px-4 py-6">
|
||||
{Breadcrumbs}
|
||||
{PageHeader}
|
||||
|
||||
{isQuarterTab && <QuarterlyWindows />}
|
||||
|
||||
{isIsicTab && <IsicHsCodes />}
|
||||
|
||||
{isUnitMasterTab && <UnitMaster />}
|
||||
|
||||
{isAdminTab && <AdminUsers />}
|
||||
|
||||
{isCompanyProfileTab && <CompanyProfile />}
|
||||
|
||||
{!isQuarterTab && !isIsicTab && !isUnitMasterTab && !isAdminTab && !isCompanyProfileTab && (
|
||||
<div className="bg-white w-full rounded-lg shadow-sm ring-1 ring-gray-200">
|
||||
<div className="px-6 py-10 text-sm text-[#5F646D]">{active} content coming soon…</div>
|
||||
</div>
|
||||
)}
|
||||
{renderActiveTab()}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@ -2,10 +2,15 @@ import React, { useState, useEffect } from 'react';
|
||||
import Table from '@/components/common/Table';
|
||||
import AddAdminUsers from './AddAdminUsers';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { getAdminUser, getAdminUserById, updateAdminUser } from '@/services/configuration/adminService';
|
||||
import {
|
||||
getAdminUser,
|
||||
getAdminUserById,
|
||||
updateAdminUser,
|
||||
deleteAdminUser, // ✅ Added import
|
||||
} from '@/services/configuration/adminService';
|
||||
import CustomToast from '@/components/common/CustomToast';
|
||||
import { X } from 'lucide-react';
|
||||
import { TextField, SelectField } from '@/components/common/FormControls';
|
||||
import { TextField, SelectField } from '@/components/common/FormControls';
|
||||
|
||||
const searchIconSrc = '/assets/images/material-symbols_search-rounded.svg';
|
||||
const exportIconSrc = '/assets/images/DownloadSimple.svg';
|
||||
@ -18,7 +23,8 @@ const adminUserIconSrc = '/assets/images/UserGear.svg';
|
||||
const activeUserIconSrc = '/assets/images/active-user.svg';
|
||||
const inactiveUserIconSrc = '/assets/images/inactive-user.svg';
|
||||
const resetPasswordIconSrc = '/assets/images/hugeicons_reset-password.svg';
|
||||
const resetPasswordInactiveIconSrc = '/assets/images/hugeicons_reset-password-inactive.svg';
|
||||
const resetPasswordInactiveIconSrc =
|
||||
'/assets/images/hugeicons_reset-password-inactive.svg';
|
||||
|
||||
const SummaryCard = ({ label, count, icon, iconBg = '#FDF7EB' }) => (
|
||||
<div className="flex min-h-[96px] min-w-[220px] flex-1 items-center justify-between rounded-[16px] border border-[#E6EAF5] bg-white px-6 py-5 shadow-[0_12px_24px_rgba(15,23,42,0.06)]">
|
||||
@ -38,12 +44,12 @@ const SummaryCard = ({ label, count, icon, iconBg = '#FDF7EB' }) => (
|
||||
const StatusBadge = ({ status }) => {
|
||||
const map = {
|
||||
Active: 'bg-green-50 text-green-700 ring-1 ring-green-200',
|
||||
Closed: 'bg-gray-100 text-gray-700 ring-1 ring-gray-200',
|
||||
Inactive: 'bg-gray-100 text-gray-700 ring-1 ring-gray-200',
|
||||
};
|
||||
return (
|
||||
<span
|
||||
className={`inline-flex items-center rounded-md px-2 py-0.5 text-xs font-medium ${
|
||||
map[status] || map.Closed
|
||||
map[status] || map.Inactive
|
||||
}`}
|
||||
>
|
||||
{status}
|
||||
@ -71,47 +77,70 @@ const AdminUsers = () => {
|
||||
const [toastData, setToastData] = useState(null);
|
||||
const navigate = useNavigate();
|
||||
|
||||
// ✅ Fetch users from API
|
||||
// Helper: show toast
|
||||
const showToast = (message, type = 'success') => {
|
||||
setToastData({ message, type });
|
||||
if (window.toastTimeout) {
|
||||
clearTimeout(window.toastTimeout);
|
||||
}
|
||||
window.toastTimeout = setTimeout(() => {
|
||||
setToastData(null);
|
||||
}, 4000);
|
||||
};
|
||||
|
||||
// Fetch users
|
||||
useEffect(() => {
|
||||
const fetchUsers = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const response = await getAdminUser();
|
||||
|
||||
// Check if response is an array directly or if it's in a data property
|
||||
const usersData = Array.isArray(response) ? response :
|
||||
(response?.data && Array.isArray(response.data) ? response.data : null);
|
||||
|
||||
if (usersData) {
|
||||
// Map API data into UI format
|
||||
const mappedUsers = usersData.map((user) => ({
|
||||
id: user.id,
|
||||
name: user.name || 'N/A',
|
||||
email: user.email || 'N/A',
|
||||
lastLogin: user.updatedAt || 'N/A',
|
||||
// lastLogin: user.updatedAt
|
||||
// ? new Date(user.last_login).toLocaleString('en-GB', {
|
||||
// day: '2-digit',
|
||||
// month: 'short',
|
||||
// year: 'numeric',
|
||||
// hour: '2-digit',
|
||||
// minute: '2-digit',
|
||||
// })
|
||||
// : 'Never logged in',
|
||||
status: user.is_active ? 'Active' : 'Active',
|
||||
is_active: user.is_active, // Keep the original is_active for reference
|
||||
}));
|
||||
|
||||
setUsers(mappedUsers);
|
||||
} else {
|
||||
const errorMsg = 'Invalid response format: Expected an array of users';
|
||||
console.error(errorMsg, response);
|
||||
showToast('Failed to load users. Please try again.', 'error');
|
||||
setUsers([]); // Reset to empty array
|
||||
const res = await getAdminUser();
|
||||
|
||||
const usersData = Array.isArray(res)
|
||||
? res
|
||||
: Array.isArray(res?.data)
|
||||
? res.data
|
||||
: Array.isArray(res?.data?.data)
|
||||
? res.data.data
|
||||
: null;
|
||||
|
||||
if (!usersData) {
|
||||
showToast('Failed to load users', 'error');
|
||||
setUsers([]);
|
||||
return;
|
||||
}
|
||||
|
||||
const mappedUsers = usersData.map((user) => {
|
||||
const id = user.id ?? user._id ?? user.user_id ?? null;
|
||||
const name = user.name ?? user.fullName ?? 'N/A';
|
||||
const email = user.email ?? 'N/A';
|
||||
const lastLoginRaw = user.updatedAt ?? user.last_login ?? user.lastLogin ?? null;
|
||||
const lastLogin = lastLoginRaw
|
||||
? new Date(lastLoginRaw).toLocaleString('en-GB', {
|
||||
day: '2-digit',
|
||||
month: 'short',
|
||||
year: 'numeric',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
})
|
||||
: 'Never logged in';
|
||||
const is_active =
|
||||
user.is_active !== undefined ? !!user.is_active : user.active ?? true;
|
||||
return {
|
||||
id,
|
||||
name,
|
||||
email,
|
||||
lastLogin,
|
||||
status: is_active ? 'Active' : 'Inactive',
|
||||
is_active,
|
||||
raw: user,
|
||||
};
|
||||
});
|
||||
|
||||
setUsers(mappedUsers);
|
||||
} catch (error) {
|
||||
console.error('Error fetching admin users:', error);
|
||||
CustomToast('Error fetching admin users', 'error');
|
||||
showToast('Error fetching admin users', 'error');
|
||||
setUsers([]);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
@ -147,6 +176,11 @@ const AdminUsers = () => {
|
||||
|
||||
const headers = ['Name', 'Email', 'Last Login', 'Status', 'Actions'];
|
||||
const columnWidths = [100, 130, 150, 100, 130];
|
||||
const [isResetModalOpen, setIsResetModalOpen] = useState(false);
|
||||
const [resetUser, setResetUser] = useState(null);
|
||||
const [newPassword, setNewPassword] = useState('');
|
||||
const [confirmPassword, setConfirmPassword] = useState('');
|
||||
|
||||
|
||||
const rows = users.map((user) => [
|
||||
user.name,
|
||||
@ -156,59 +190,173 @@ const AdminUsers = () => {
|
||||
'actions',
|
||||
]);
|
||||
|
||||
// Show toast helper
|
||||
const showToast = (message, type = 'success') => {
|
||||
setToastData({ message, type });
|
||||
if (window.toastTimeout) {
|
||||
clearTimeout(window.toastTimeout);
|
||||
const handleFormChange = (e) => {
|
||||
if (!e) return;
|
||||
if (e.target && e.target.name !== undefined) {
|
||||
const { name, value } = e.target;
|
||||
setFormData((prev) => ({ ...prev, [name]: value }));
|
||||
if (errors[name]) setErrors((prev) => ({ ...prev, [name]: '' }));
|
||||
} else if (e.name && e.value !== undefined) {
|
||||
setFormData((prev) => ({ ...prev, [e.name]: e.value }));
|
||||
if (errors[e.name]) setErrors((prev) => ({ ...prev, [e.name]: '' }));
|
||||
}
|
||||
window.toastTimeout = setTimeout(() => {
|
||||
setToastData(null);
|
||||
}, 4000);
|
||||
};
|
||||
|
||||
const validateForm = () => {
|
||||
const newErrors = {};
|
||||
if (!formData.name || !formData.name.trim()) newErrors.name = 'Name is required';
|
||||
if (!formData.status) newErrors.status = 'Status is required';
|
||||
return newErrors;
|
||||
};
|
||||
|
||||
const handleEdit = async (index) => {
|
||||
const user = users[index];
|
||||
if (!user || !user.id) {
|
||||
showToast('Invalid user selected', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
setEditingRow(index);
|
||||
|
||||
setLoading(true);
|
||||
|
||||
try {
|
||||
setLoading(true);
|
||||
const userData = await getAdminUserById(user.id);
|
||||
|
||||
if (!userData) {
|
||||
throw new Error('No user data received');
|
||||
}
|
||||
|
||||
// Ensure we have all required fields with fallbacks
|
||||
const processedUser = {
|
||||
...userData,
|
||||
const res = await getAdminUserById(user.id);
|
||||
const payload = res?.data ?? res;
|
||||
if (!payload) throw new Error('No user data returned from API');
|
||||
|
||||
const name = payload.name ?? payload.fullName ?? user.name ?? '';
|
||||
const email = payload.email ?? user.email ?? '';
|
||||
const is_active =
|
||||
payload.is_active !== undefined ? !!payload.is_active : !!user.is_active;
|
||||
|
||||
setCurrentUser({
|
||||
...payload,
|
||||
id: user.id,
|
||||
name: userData.name || user.name || '',
|
||||
email: userData.email || user.email || '',
|
||||
is_active: userData.is_active !== undefined ? userData.is_active : user.is_active
|
||||
};
|
||||
|
||||
setCurrentUser(processedUser);
|
||||
|
||||
setFormData({
|
||||
name: processedUser.name,
|
||||
email: processedUser.email,
|
||||
status: processedUser.is_active ? 'Active' : 'Inactive',
|
||||
name,
|
||||
email,
|
||||
is_active,
|
||||
});
|
||||
|
||||
|
||||
setFormData({
|
||||
name,
|
||||
email,
|
||||
status: is_active ? 'Active' : 'Inactive',
|
||||
});
|
||||
|
||||
setErrors({});
|
||||
setIsEditModalOpen(true);
|
||||
} catch (error) {
|
||||
console.error('Error fetching user data:', error);
|
||||
const errorMsg = error.response?.data?.message || error.message || 'Failed to load user data';
|
||||
showToast(errorMsg, 'error');
|
||||
showToast(error?.response?.data?.message || 'Failed to load user data', 'error');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDelete = (index) => {
|
||||
const handleOpenResetModal = (user) => {
|
||||
setResetUser(user);
|
||||
setNewPassword('');
|
||||
setConfirmPassword('');
|
||||
setIsResetModalOpen(true);
|
||||
};
|
||||
|
||||
const handleCloseResetModal = () => {
|
||||
setResetUser(null);
|
||||
setIsResetModalOpen(false);
|
||||
};
|
||||
|
||||
// ✅ DELETE FUNCTIONALITY IMPLEMENTED HERE
|
||||
const handleDelete = async (index) => {
|
||||
const user = users[index];
|
||||
if (!user || !user.id) {
|
||||
showToast('Invalid user selected for delete', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
const confirmDelete = window.confirm(`Are you sure you want to delete "${user.name}"?`);
|
||||
if (!confirmDelete) return;
|
||||
|
||||
setDeletingRow(index);
|
||||
// Future: open delete confirmation modal
|
||||
setLoading(true);
|
||||
|
||||
try {
|
||||
const res = await deleteAdminUser(user.id);
|
||||
|
||||
const success =
|
||||
res?.status === 'success' ||
|
||||
res?.code === 200 ||
|
||||
res?.data?.status === 'success' ||
|
||||
res?.message?.toLowerCase().includes('deleted');
|
||||
|
||||
if (!success) throw new Error(res?.message || 'Failed to delete user');
|
||||
|
||||
setUsers((prev) => prev.filter((u) => u.id !== user.id));
|
||||
showToast('User deleted successfully', 'success');
|
||||
} catch (error) {
|
||||
console.error('Error deleting user:', error);
|
||||
showToast(error?.response?.data?.message || error.message || 'Delete failed', 'error');
|
||||
} finally {
|
||||
setDeletingRow(null);
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
// ✅ END DELETE FUNCTIONALITY
|
||||
|
||||
const handleEditSubmit = async (e) => {
|
||||
e.preventDefault();
|
||||
const formErrors = validateForm();
|
||||
if (Object.keys(formErrors).length > 0) {
|
||||
setErrors(formErrors);
|
||||
showToast('Please fix the form errors', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!currentUser || !currentUser.id) {
|
||||
showToast('No user selected for update', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
setSaving(true);
|
||||
try {
|
||||
const payload = {
|
||||
name: formData.name,
|
||||
email: formData.email,
|
||||
is_active: formData.status === 'Active',
|
||||
};
|
||||
|
||||
const res = await updateAdminUser(currentUser.id, payload);
|
||||
const success =
|
||||
res?.status === 'success' ||
|
||||
res?.status === 200 ||
|
||||
res?.data?.status === 'success' ||
|
||||
(!res.error && res);
|
||||
|
||||
if (!success) throw new Error(res?.message || 'Failed to update user');
|
||||
|
||||
setUsers((prev) =>
|
||||
prev.map((u) =>
|
||||
u.id === currentUser.id
|
||||
? {
|
||||
...u,
|
||||
name: formData.name,
|
||||
email: formData.email,
|
||||
status: formData.status,
|
||||
is_active: formData.status === 'Active',
|
||||
}
|
||||
: u
|
||||
)
|
||||
);
|
||||
|
||||
showToast('User updated successfully', 'success');
|
||||
setIsEditModalOpen(false);
|
||||
setEditingRow(null);
|
||||
setCurrentUser(null);
|
||||
} catch (error) {
|
||||
console.error('Error updating user:', error);
|
||||
showToast(error?.response?.data?.message || 'Failed to update user', 'error');
|
||||
} finally {
|
||||
setSaving(false);
|
||||
}
|
||||
};
|
||||
|
||||
const tableToolbar = (
|
||||
@ -246,14 +394,14 @@ const AdminUsers = () => {
|
||||
|
||||
return (
|
||||
<div className="space-y-5">
|
||||
{/* ✅ Summary cards */}
|
||||
{/* Summary cards */}
|
||||
<div className="grid gap-4 sm:grid-cols-2 xl:grid-cols-3">
|
||||
{summaryItems.map((item) => (
|
||||
<SummaryCard key={item.label} {...item} />
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* ✅ Table */}
|
||||
{/* Table */}
|
||||
<Table
|
||||
headers={headers}
|
||||
columnWidths={columnWidths}
|
||||
@ -280,11 +428,7 @@ const AdminUsers = () => {
|
||||
onClick={() => handleEdit(rowIndex)}
|
||||
>
|
||||
<img
|
||||
src={
|
||||
editingRow === rowIndex
|
||||
? pencilActiveSrc
|
||||
: pencilInactiveSrc
|
||||
}
|
||||
src={editingRow === rowIndex ? pencilActiveSrc : pencilInactiveSrc}
|
||||
alt="Edit"
|
||||
className="h-4 w-4"
|
||||
/>
|
||||
@ -296,37 +440,90 @@ const AdminUsers = () => {
|
||||
onClick={() => handleDelete(rowIndex)}
|
||||
>
|
||||
<img
|
||||
src={
|
||||
deletingRow === rowIndex
|
||||
? trashActiveSrc
|
||||
: trashInactiveSrc
|
||||
}
|
||||
src={deletingRow === rowIndex ? trashActiveSrc : trashInactiveSrc}
|
||||
alt="Delete"
|
||||
className="h-4 w-4"
|
||||
/>
|
||||
</button>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
className={`h-8 w-8 grid place-items-center rounded hover:bg-gray-50 cursor-pointer ${
|
||||
isActive ? '' : 'opacity-60'
|
||||
}`}
|
||||
title={
|
||||
isActive
|
||||
? 'Reset password'
|
||||
: 'Reset disabled for inactive user'
|
||||
}
|
||||
disabled={!isActive}
|
||||
>
|
||||
<img
|
||||
src={
|
||||
isActive
|
||||
? resetPasswordInactiveIconSrc
|
||||
: resetPasswordInactiveIconSrc
|
||||
}
|
||||
alt="Reset password"
|
||||
className="h-4 w-4"
|
||||
/>
|
||||
</button>
|
||||
type="button"
|
||||
className={`h-8 w-8 grid place-items-center rounded hover:bg-gray-50 cursor-pointer ${
|
||||
isActive ? '' : 'opacity-60'
|
||||
}`}
|
||||
title={isActive ? 'Reset password' : 'Reset disabled for inactive user'}
|
||||
disabled={!isActive}
|
||||
onClick={() => handleOpenResetModal(user)}
|
||||
>
|
||||
<img
|
||||
src={isActive ? resetPasswordIconSrc : resetPasswordInactiveIconSrc}
|
||||
alt="Reset password"
|
||||
className="h-4 w-4"
|
||||
/>
|
||||
</button>
|
||||
|
||||
{isResetModalOpen && (
|
||||
<div className="fixed inset-0 z-[9999] flex items-center justify-center bg-black/10">
|
||||
<div className="bg-white rounded-lg w-[450px] shadow-lg p-6 relative">
|
||||
{/* Close Button */}
|
||||
<button
|
||||
className="absolute top-3 right-3 text-gray-500 hover:text-gray-800 text-xl"
|
||||
onClick={handleCloseResetModal}
|
||||
>
|
||||
✕
|
||||
</button>
|
||||
|
||||
{/* Header */}
|
||||
<h3 className="text-xl font-semibold text-gray-800 mb-6">Reset Password</h3>
|
||||
|
||||
{/* Input Fields */}
|
||||
<div className="flex flex-col gap-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">New Password</label>
|
||||
<input
|
||||
type="password"
|
||||
placeholder="Enter your new password"
|
||||
className="w-full border border-[#D9C9A3] rounded-md px-3 py-2 focus:outline-none focus:ring-1 focus:ring-[#B68A35]"
|
||||
value={newPassword}
|
||||
onChange={(e) => setNewPassword(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">Confirm Password</label>
|
||||
<input
|
||||
type="password"
|
||||
placeholder="Re-enter your new password"
|
||||
className="w-full border border-[#D9C9A3] rounded-md px-3 py-2 focus:outline-none focus:ring-1 focus:ring-[#B68A35]"
|
||||
value={confirmPassword}
|
||||
onChange={(e) => setConfirmPassword(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Buttons */}
|
||||
<div className="flex justify-end gap-3 mt-6">
|
||||
<button
|
||||
className="px-5 py-2 rounded-md border border-gray-300 text-gray-700 hover:bg-gray-100"
|
||||
onClick={handleCloseResetModal}
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
className="px-5 py-2 rounded-md bg-[#B68A35] text-white font-medium hover:bg-[#9c6f28]"
|
||||
onClick={() => {
|
||||
console.log({ newPassword, confirmPassword, user: resetUser });
|
||||
handleCloseResetModal();
|
||||
}}
|
||||
>
|
||||
Reset Password
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
)}
|
||||
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -334,206 +531,42 @@ const AdminUsers = () => {
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* ✅ Add User Modal */}
|
||||
{/* Add User Modal */}
|
||||
{isAddUserModalOpen && (
|
||||
<AddAdminUsers
|
||||
isOpen={isAddUserModalOpen}
|
||||
onClose={() => setIsAddUserModalOpen(false)}
|
||||
onSave={(newUser) => {
|
||||
setUsers(prev => [
|
||||
setUsers((prev) => [
|
||||
...prev,
|
||||
{
|
||||
...newUser,
|
||||
id: newUser.id ?? `local-${Date.now()}`,
|
||||
lastLogin: new Date().toLocaleString('en-GB'),
|
||||
status: 'Active',
|
||||
is_active: true,
|
||||
},
|
||||
]);
|
||||
setIsAddUserModalOpen(false);
|
||||
showToast('User added locally', 'success');
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* ✅ Edit User Modal */}
|
||||
{isEditModalOpen && (
|
||||
<div className="fixed inset-0 bg-black/30 backdrop-blur-sm flex items-center justify-center p-4 z-[9998]">
|
||||
<div className="bg-white rounded-2xl w-full max-w-2xl max-h-[90vh] overflow-y-auto">
|
||||
{/* Header */}
|
||||
<div className="flex justify-between items-center p-6 border-b sticky top-0 bg-white z-10">
|
||||
<h2 className="text-xl font-semibold text-[#232528]">Edit User</h2>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setIsEditModalOpen(false)}
|
||||
className="text-gray-400 hover:text-gray-500"
|
||||
aria-label="Close"
|
||||
>
|
||||
<X className="h-5 w-5" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Body */}
|
||||
<div className="p-8">
|
||||
<form onSubmit={handleEditSubmit}>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<TextField
|
||||
label="Name"
|
||||
name="name"
|
||||
value={formData.name}
|
||||
onChange={handleFormChange}
|
||||
error={errors.name}
|
||||
required
|
||||
placeholder="Enter name"
|
||||
/>
|
||||
|
||||
<TextField
|
||||
label="Email"
|
||||
name="email"
|
||||
type="email"
|
||||
value={formData.email}
|
||||
onChange={handleFormChange}
|
||||
error={errors.email}
|
||||
required
|
||||
placeholder="Enter email"
|
||||
disabled={true}
|
||||
/>
|
||||
|
||||
<div className="md:col-span-2 space-y-2">
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Status <span className="text-red-500">*</span>
|
||||
</label>
|
||||
<SelectField
|
||||
name="status"
|
||||
value={formData.status}
|
||||
onChange={handleFormChange}
|
||||
options={[
|
||||
{ label: 'Active', value: 'Active' },
|
||||
{ label: 'Inactive', value: 'Inactive' },
|
||||
]}
|
||||
error={errors.status}
|
||||
/>
|
||||
{errors.status && (
|
||||
<p className="mt-1 text-sm text-red-600">{errors.status}</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Footer */}
|
||||
<div className="flex justify-end space-x-3 pt-6">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setIsEditModalOpen(false)}
|
||||
className="px-5 py-2.5 text-sm font-medium text-gray-700 bg-white border border-gray-300 rounded-lg hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-[#B68A35] transition-colors"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
disabled={saving}
|
||||
className={`px-5 py-2.5 text-sm font-medium text-white rounded-lg focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-[#B68A35] transition-colors ${
|
||||
saving
|
||||
? 'bg-gray-400 cursor-not-allowed'
|
||||
: 'bg-[#92722A] hover:bg-[#9A762D]'
|
||||
}`}
|
||||
>
|
||||
{saving ? 'Saving...' : 'Update'}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* ✅ Toast Notification */}
|
||||
{/* Toast */}
|
||||
{toastData && (
|
||||
<div className="fixed inset-0 z-[9999]">
|
||||
<CustomToast
|
||||
message={toastData.message}
|
||||
type={toastData.type}
|
||||
onClose={() => setToastData(null)}
|
||||
/>
|
||||
<div className="fixed inset-0 z-[9999] pointer-events-none">
|
||||
<div className="absolute right-4 top-4 pointer-events-auto">
|
||||
<CustomToast
|
||||
message={toastData.message}
|
||||
type={toastData.type}
|
||||
onClose={() => setToastData(null)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// Form change handler
|
||||
const handleFormChange = (e) => {
|
||||
const { name, value } = e.target;
|
||||
setFormData(prev => ({
|
||||
...prev,
|
||||
[name]: value
|
||||
}));
|
||||
|
||||
// Clear error for the field being edited
|
||||
if (errors[name]) {
|
||||
setErrors(prev => ({
|
||||
...prev,
|
||||
[name]: ''
|
||||
}));
|
||||
}
|
||||
};
|
||||
|
||||
// Form validation
|
||||
const validateForm = () => {
|
||||
const newErrors = {};
|
||||
if (!formData.name.trim()) newErrors.name = 'Name is required';
|
||||
if (!formData.status) newErrors.status = 'Status is required';
|
||||
return newErrors;
|
||||
};
|
||||
|
||||
// Handle edit form submission
|
||||
const handleEditSubmit = async (e) => {
|
||||
e.preventDefault();
|
||||
|
||||
const formErrors = validateForm();
|
||||
if (Object.keys(formErrors).length > 0) {
|
||||
setErrors(formErrors);
|
||||
showToast('Please fix the form errors', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!currentUser || !currentUser.id) {
|
||||
showToast('No user selected for update', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
setSaving(true);
|
||||
try {
|
||||
// Update the user in the backend
|
||||
const response = await updateAdminUser(currentUser.id, {
|
||||
name: formData.name,
|
||||
email: formData.email,
|
||||
is_active: formData.status === 'Active',
|
||||
});
|
||||
|
||||
if (response && response.status === 'success') {
|
||||
// Update the user in the local state
|
||||
setUsers(prevUsers =>
|
||||
prevUsers.map(user =>
|
||||
user.id === currentUser.id
|
||||
? {
|
||||
...user,
|
||||
name: formData.name,
|
||||
status: formData.status,
|
||||
email: formData.email
|
||||
}
|
||||
: user
|
||||
)
|
||||
);
|
||||
|
||||
showToast('User updated successfully', 'success');
|
||||
setIsEditModalOpen(false);
|
||||
} else {
|
||||
throw new Error(response?.message || 'Failed to update user');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error updating user:', error);
|
||||
const errorMessage = error.response?.data?.message || error.message || 'Failed to update user';
|
||||
showToast(errorMessage, 'error');
|
||||
} finally {
|
||||
setSaving(false);
|
||||
}
|
||||
};
|
||||
|
||||
export default AdminUsers;
|
||||
|
||||
@ -50,4 +50,5 @@ export const deleteUnit = async (id) => {
|
||||
console.error('Error deleting unit:', error);
|
||||
throw error;
|
||||
}
|
||||
|
||||
};
|
||||
@ -41,4 +41,4 @@ export const deleteUnit = async (id) => {
|
||||
console.error('Error deleting unit:', error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
Loading…
Reference in New Issue
Block a user