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>
|
<h2 className="text-xl font-semibold text-[#232528]">Step 2: Product Data — Monthly Output & Cost</h2>
|
||||||
{(quarter || year) && (
|
{(quarter || year) && (
|
||||||
<div className="text-sm font-medium text-gray-600">
|
<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>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -1,66 +1,87 @@
|
|||||||
import React, { useEffect } from 'react';
|
import React, { useEffect, useState } from "react";
|
||||||
import { Link, useNavigate, useLocation } from 'react-router-dom';
|
import { Link, useNavigate, useLocation } from "react-router-dom";
|
||||||
import AdminHeader from '@/components/admin/AdminHeader';
|
import AdminHeader from "@/components/admin/AdminHeader";
|
||||||
import QuarterlyWindows from '@/pages/Admin/configuration/QuarterlyWindows';
|
import QuarterlyWindows from "@/pages/Admin/configuration/QuarterlyWindows";
|
||||||
import IsicHsCodes from '@/pages/Admin/configuration/IsicHsCodes';
|
import IsicHsCodes from "@/pages/Admin/configuration/IsicHsCodes";
|
||||||
import UnitMaster from '@/pages/Admin/configuration/UnitMaster';
|
import UnitMaster from "@/pages/Admin/configuration/UnitMaster";
|
||||||
import AdminUsers from '@/pages/Admin/configuration/AdminUsers/ListAdminUsers';
|
import AdminUsers from "@/pages/Admin/configuration/AdminUsers/ListAdminUsers";
|
||||||
import ListAdminUsers from '@/pages/Admin/configuration/AdminUsers/ListAdminUsers';
|
import CompanyProfile from "@/pages/Admin/configuration/CompanyProfile";
|
||||||
import EditAdminUser from '@/pages/Admin/configuration/AdminUsers/EditAdminUser';
|
|
||||||
import CompanyProfile from '@/pages/Admin/configuration/CompanyProfile';
|
|
||||||
|
|
||||||
const caretUpSrc = '/assets/images/caret-up.svg';
|
|
||||||
|
|
||||||
const Configuration = () => {
|
const Configuration = () => {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const location = useLocation();
|
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(() => {
|
useEffect(() => {
|
||||||
const tabToPath = {
|
|
||||||
'Quarterly Windows': 'quarterly',
|
|
||||||
'HS Codes': 'hscodes',
|
|
||||||
'Unit Master': 'unitmaster',
|
|
||||||
'Admin Users': 'admin-users',
|
|
||||||
'Company Profile': 'profile'
|
|
||||||
};
|
|
||||||
|
|
||||||
if (tabToPath[active]) {
|
if (tabToPath[active]) {
|
||||||
navigate(`/admin/configuration/${tabToPath[active]}`, { replace: true });
|
navigate(`/admin/configuration/${tabToPath[active]}`, { replace: true });
|
||||||
}
|
}
|
||||||
}, [active, navigate]);
|
}, [active, navigate]);
|
||||||
|
|
||||||
// Update active tab based on URL
|
// ✅ Update active tab when URL changes
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const pathToTab = {
|
const path = location.pathname.split("/").pop();
|
||||||
'quarterly': 'Quarterly Windows',
|
if (pathToTab[path] && pathToTab[path] !== active) {
|
||||||
'hscodes': 'HS Codes',
|
setActive(pathToTab[path]);
|
||||||
'unitmaster': 'Unit Master',
|
}
|
||||||
'admin-users': 'Admin Users',
|
}, [location.pathname]);
|
||||||
'profile': 'Company Profile'
|
|
||||||
};
|
|
||||||
|
|
||||||
const path = location.pathname.split('/').pop();
|
|
||||||
if (pathToTab[path] && pathToTab[path] !== active) {
|
|
||||||
setActive(pathToTab[path]);
|
|
||||||
}
|
|
||||||
}, [location.pathname]);
|
|
||||||
|
|
||||||
|
// ✅ Breadcrumb with ">" separators
|
||||||
const Breadcrumbs = (
|
const Breadcrumbs = (
|
||||||
<nav className="mb-6 text-sm text-[#8F9299] flex items-center gap-3">
|
<nav className="mb-6 text-sm text-[#8F9299] flex items-center gap-2">
|
||||||
<Link to="/admin/dashboard" className="flex items-center gap-1 text-[#232528] hover:underline">
|
<Link
|
||||||
|
to="/admin/dashboard"
|
||||||
|
className="text-[#232528] hover:underline font-medium"
|
||||||
|
>
|
||||||
Dashboard
|
Dashboard
|
||||||
<img src={caretUpSrc} alt="Dashboard" className="h-3 w-3" />
|
|
||||||
</Link>
|
</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
|
Configuration
|
||||||
<img src={caretUpSrc} alt="Configuration" className="h-3 w-3" />
|
</Link>
|
||||||
</span>
|
|
||||||
|
{active && (
|
||||||
|
<>
|
||||||
|
<span className="text-[#8F9299]">{">"}</span>
|
||||||
|
<span className="text-[#92722A] font-medium">{active}</span>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</nav>
|
</nav>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// ✅ Tab header
|
||||||
const PageHeader = (
|
const PageHeader = (
|
||||||
<div className="w-full mb-6">
|
<div className="w-full mb-6">
|
||||||
<div className="h-14 w-full border-b border-[#E5E7EB] flex items-end">
|
<div className="h-14 w-full border-b border-[#E5E7EB] flex items-end">
|
||||||
@ -72,8 +93,8 @@ useEffect(() => {
|
|||||||
onClick={() => setActive(t)}
|
onClick={() => setActive(t)}
|
||||||
className={`pb-3 text-sm whitespace-nowrap border-b-2 cursor-pointer transition-colors ${
|
className={`pb-3 text-sm whitespace-nowrap border-b-2 cursor-pointer transition-colors ${
|
||||||
active === t
|
active === t
|
||||||
? 'text-[#92722A] border-[#92722A] font-medium'
|
? "text-[#92722A] border-[#92722A] font-medium"
|
||||||
: 'text-[#232528] border-transparent hover:text-[#92722A]'
|
: "text-[#232528] border-transparent hover:text-[#92722A]"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{t}
|
{t}
|
||||||
@ -84,11 +105,29 @@ useEffect(() => {
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
const isQuarterTab = active === 'Quarterly Windows';
|
// ✅ Render selected tab content
|
||||||
const isIsicTab = active === 'HS Codes';
|
const renderActiveTab = () => {
|
||||||
const isUnitMasterTab = active === 'Unit Master';
|
switch (active) {
|
||||||
const isAdminTab = active === 'Admin Users';
|
case "Quarterly Windows":
|
||||||
const isCompanyProfileTab = active === 'Company Profile';
|
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 (
|
return (
|
||||||
<div className="min-h-screen bg-[#F7F7F7]">
|
<div className="min-h-screen bg-[#F7F7F7]">
|
||||||
@ -96,22 +135,7 @@ useEffect(() => {
|
|||||||
<div className="max-w-[1280px] mx-auto px-4 py-6">
|
<div className="max-w-[1280px] mx-auto px-4 py-6">
|
||||||
{Breadcrumbs}
|
{Breadcrumbs}
|
||||||
{PageHeader}
|
{PageHeader}
|
||||||
|
{renderActiveTab()}
|
||||||
{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>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -2,10 +2,15 @@ import React, { useState, useEffect } from 'react';
|
|||||||
import Table from '@/components/common/Table';
|
import Table from '@/components/common/Table';
|
||||||
import AddAdminUsers from './AddAdminUsers';
|
import AddAdminUsers from './AddAdminUsers';
|
||||||
import { useNavigate } from 'react-router-dom';
|
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 CustomToast from '@/components/common/CustomToast';
|
||||||
import { X } from 'lucide-react';
|
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 searchIconSrc = '/assets/images/material-symbols_search-rounded.svg';
|
||||||
const exportIconSrc = '/assets/images/DownloadSimple.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 activeUserIconSrc = '/assets/images/active-user.svg';
|
||||||
const inactiveUserIconSrc = '/assets/images/inactive-user.svg';
|
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 = '/assets/images/hugeicons_reset-password-inactive.svg';
|
const resetPasswordInactiveIconSrc =
|
||||||
|
'/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)]">
|
||||||
@ -38,12 +44,12 @@ const SummaryCard = ({ label, count, icon, iconBg = '#FDF7EB' }) => (
|
|||||||
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',
|
||||||
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 (
|
return (
|
||||||
<span
|
<span
|
||||||
className={`inline-flex items-center rounded-md px-2 py-0.5 text-xs font-medium ${
|
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}
|
{status}
|
||||||
@ -71,47 +77,70 @@ const AdminUsers = () => {
|
|||||||
const [toastData, setToastData] = useState(null);
|
const [toastData, setToastData] = useState(null);
|
||||||
const navigate = useNavigate();
|
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(() => {
|
useEffect(() => {
|
||||||
const fetchUsers = async () => {
|
const fetchUsers = async () => {
|
||||||
try {
|
try {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
const response = await getAdminUser();
|
const res = await getAdminUser();
|
||||||
|
|
||||||
// Check if response is an array directly or if it's in a data property
|
const usersData = Array.isArray(res)
|
||||||
const usersData = Array.isArray(response) ? response :
|
? res
|
||||||
(response?.data && Array.isArray(response.data) ? response.data : null);
|
: Array.isArray(res?.data)
|
||||||
|
? res.data
|
||||||
if (usersData) {
|
: Array.isArray(res?.data?.data)
|
||||||
// Map API data into UI format
|
? res.data.data
|
||||||
const mappedUsers = usersData.map((user) => ({
|
: null;
|
||||||
id: user.id,
|
|
||||||
name: user.name || 'N/A',
|
if (!usersData) {
|
||||||
email: user.email || 'N/A',
|
showToast('Failed to load users', 'error');
|
||||||
lastLogin: user.updatedAt || 'N/A',
|
setUsers([]);
|
||||||
// lastLogin: user.updatedAt
|
return;
|
||||||
// ? 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 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) {
|
} catch (error) {
|
||||||
console.error('Error fetching admin users:', error);
|
console.error('Error fetching admin users:', error);
|
||||||
CustomToast('Error fetching admin users', 'error');
|
showToast('Error fetching admin users', 'error');
|
||||||
|
setUsers([]);
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
@ -147,6 +176,11 @@ const AdminUsers = () => {
|
|||||||
|
|
||||||
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 [isResetModalOpen, setIsResetModalOpen] = useState(false);
|
||||||
|
const [resetUser, setResetUser] = useState(null);
|
||||||
|
const [newPassword, setNewPassword] = useState('');
|
||||||
|
const [confirmPassword, setConfirmPassword] = useState('');
|
||||||
|
|
||||||
|
|
||||||
const rows = users.map((user) => [
|
const rows = users.map((user) => [
|
||||||
user.name,
|
user.name,
|
||||||
@ -156,59 +190,173 @@ const AdminUsers = () => {
|
|||||||
'actions',
|
'actions',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// Show toast helper
|
const handleFormChange = (e) => {
|
||||||
const showToast = (message, type = 'success') => {
|
if (!e) return;
|
||||||
setToastData({ message, type });
|
if (e.target && e.target.name !== undefined) {
|
||||||
if (window.toastTimeout) {
|
const { name, value } = e.target;
|
||||||
clearTimeout(window.toastTimeout);
|
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 handleEdit = async (index) => {
|
||||||
const user = users[index];
|
const user = users[index];
|
||||||
|
if (!user || !user.id) {
|
||||||
|
showToast('Invalid user selected', 'error');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
setEditingRow(index);
|
setEditingRow(index);
|
||||||
|
setLoading(true);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
setLoading(true);
|
const res = await getAdminUserById(user.id);
|
||||||
const userData = await getAdminUserById(user.id);
|
const payload = res?.data ?? res;
|
||||||
|
if (!payload) throw new Error('No user data returned from API');
|
||||||
if (!userData) {
|
|
||||||
throw new Error('No user data received');
|
const name = payload.name ?? payload.fullName ?? user.name ?? '';
|
||||||
}
|
const email = payload.email ?? user.email ?? '';
|
||||||
|
const is_active =
|
||||||
// Ensure we have all required fields with fallbacks
|
payload.is_active !== undefined ? !!payload.is_active : !!user.is_active;
|
||||||
const processedUser = {
|
|
||||||
...userData,
|
setCurrentUser({
|
||||||
|
...payload,
|
||||||
id: user.id,
|
id: user.id,
|
||||||
name: userData.name || user.name || '',
|
name,
|
||||||
email: userData.email || user.email || '',
|
email,
|
||||||
is_active: userData.is_active !== undefined ? userData.is_active : user.is_active
|
is_active,
|
||||||
};
|
|
||||||
|
|
||||||
setCurrentUser(processedUser);
|
|
||||||
|
|
||||||
setFormData({
|
|
||||||
name: processedUser.name,
|
|
||||||
email: processedUser.email,
|
|
||||||
status: processedUser.is_active ? 'Active' : 'Inactive',
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
setFormData({
|
||||||
|
name,
|
||||||
|
email,
|
||||||
|
status: is_active ? 'Active' : 'Inactive',
|
||||||
|
});
|
||||||
|
|
||||||
|
setErrors({});
|
||||||
setIsEditModalOpen(true);
|
setIsEditModalOpen(true);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error fetching user data:', error);
|
console.error('Error fetching user data:', error);
|
||||||
const errorMsg = error.response?.data?.message || error.message || 'Failed to load user data';
|
showToast(error?.response?.data?.message || 'Failed to load user data', 'error');
|
||||||
showToast(errorMsg, 'error');
|
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
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);
|
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 = (
|
const tableToolbar = (
|
||||||
@ -246,14 +394,14 @@ const AdminUsers = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-5">
|
<div className="space-y-5">
|
||||||
{/* ✅ Summary cards */}
|
{/* Summary cards */}
|
||||||
<div className="grid gap-4 sm:grid-cols-2 xl:grid-cols-3">
|
<div className="grid gap-4 sm:grid-cols-2 xl:grid-cols-3">
|
||||||
{summaryItems.map((item) => (
|
{summaryItems.map((item) => (
|
||||||
<SummaryCard key={item.label} {...item} />
|
<SummaryCard key={item.label} {...item} />
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* ✅ Table */}
|
{/* Table */}
|
||||||
<Table
|
<Table
|
||||||
headers={headers}
|
headers={headers}
|
||||||
columnWidths={columnWidths}
|
columnWidths={columnWidths}
|
||||||
@ -280,11 +428,7 @@ const AdminUsers = () => {
|
|||||||
onClick={() => handleEdit(rowIndex)}
|
onClick={() => handleEdit(rowIndex)}
|
||||||
>
|
>
|
||||||
<img
|
<img
|
||||||
src={
|
src={editingRow === rowIndex ? pencilActiveSrc : pencilInactiveSrc}
|
||||||
editingRow === rowIndex
|
|
||||||
? pencilActiveSrc
|
|
||||||
: pencilInactiveSrc
|
|
||||||
}
|
|
||||||
alt="Edit"
|
alt="Edit"
|
||||||
className="h-4 w-4"
|
className="h-4 w-4"
|
||||||
/>
|
/>
|
||||||
@ -296,37 +440,90 @@ const AdminUsers = () => {
|
|||||||
onClick={() => handleDelete(rowIndex)}
|
onClick={() => handleDelete(rowIndex)}
|
||||||
>
|
>
|
||||||
<img
|
<img
|
||||||
src={
|
src={deletingRow === rowIndex ? trashActiveSrc : trashInactiveSrc}
|
||||||
deletingRow === rowIndex
|
|
||||||
? trashActiveSrc
|
|
||||||
: trashInactiveSrc
|
|
||||||
}
|
|
||||||
alt="Delete"
|
alt="Delete"
|
||||||
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 ${
|
||||||
isActive ? '' : 'opacity-60'
|
isActive ? '' : 'opacity-60'
|
||||||
}`}
|
}`}
|
||||||
title={
|
title={isActive ? 'Reset password' : 'Reset disabled for inactive user'}
|
||||||
isActive
|
disabled={!isActive}
|
||||||
? 'Reset password'
|
onClick={() => handleOpenResetModal(user)}
|
||||||
: 'Reset disabled for inactive user'
|
>
|
||||||
}
|
<img
|
||||||
disabled={!isActive}
|
src={isActive ? resetPasswordIconSrc : resetPasswordInactiveIconSrc}
|
||||||
>
|
alt="Reset password"
|
||||||
<img
|
className="h-4 w-4"
|
||||||
src={
|
/>
|
||||||
isActive
|
</button>
|
||||||
? resetPasswordInactiveIconSrc
|
|
||||||
: resetPasswordInactiveIconSrc
|
{isResetModalOpen && (
|
||||||
}
|
<div className="fixed inset-0 z-[9999] flex items-center justify-center bg-black/10">
|
||||||
alt="Reset password"
|
<div className="bg-white rounded-lg w-[450px] shadow-lg p-6 relative">
|
||||||
className="h-4 w-4"
|
{/* Close Button */}
|
||||||
/>
|
<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>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -334,206 +531,42 @@ const AdminUsers = () => {
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* ✅ Add User Modal */}
|
{/* Add User Modal */}
|
||||||
{isAddUserModalOpen && (
|
{isAddUserModalOpen && (
|
||||||
<AddAdminUsers
|
<AddAdminUsers
|
||||||
isOpen={isAddUserModalOpen}
|
isOpen={isAddUserModalOpen}
|
||||||
onClose={() => setIsAddUserModalOpen(false)}
|
onClose={() => setIsAddUserModalOpen(false)}
|
||||||
onSave={(newUser) => {
|
onSave={(newUser) => {
|
||||||
setUsers(prev => [
|
setUsers((prev) => [
|
||||||
...prev,
|
...prev,
|
||||||
{
|
{
|
||||||
...newUser,
|
...newUser,
|
||||||
|
id: newUser.id ?? `local-${Date.now()}`,
|
||||||
lastLogin: new Date().toLocaleString('en-GB'),
|
lastLogin: new Date().toLocaleString('en-GB'),
|
||||||
status: 'Active',
|
status: 'Active',
|
||||||
|
is_active: true,
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
setIsAddUserModalOpen(false);
|
setIsAddUserModalOpen(false);
|
||||||
|
showToast('User added locally', 'success');
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* ✅ Edit User Modal */}
|
{/* Toast */}
|
||||||
{isEditModalOpen && (
|
|
||||||
<div className="fixed inset-0 bg-black/30 backdrop-blur-sm flex items-center justify-center p-4 z-[9998]">
|
|
||||||
<div className="bg-white rounded-2xl w-full max-w-2xl max-h-[90vh] overflow-y-auto">
|
|
||||||
{/* Header */}
|
|
||||||
<div className="flex justify-between items-center p-6 border-b sticky top-0 bg-white z-10">
|
|
||||||
<h2 className="text-xl font-semibold text-[#232528]">Edit User</h2>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={() => setIsEditModalOpen(false)}
|
|
||||||
className="text-gray-400 hover:text-gray-500"
|
|
||||||
aria-label="Close"
|
|
||||||
>
|
|
||||||
<X className="h-5 w-5" />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Body */}
|
|
||||||
<div className="p-8">
|
|
||||||
<form onSubmit={handleEditSubmit}>
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
|
||||||
<TextField
|
|
||||||
label="Name"
|
|
||||||
name="name"
|
|
||||||
value={formData.name}
|
|
||||||
onChange={handleFormChange}
|
|
||||||
error={errors.name}
|
|
||||||
required
|
|
||||||
placeholder="Enter name"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<TextField
|
|
||||||
label="Email"
|
|
||||||
name="email"
|
|
||||||
type="email"
|
|
||||||
value={formData.email}
|
|
||||||
onChange={handleFormChange}
|
|
||||||
error={errors.email}
|
|
||||||
required
|
|
||||||
placeholder="Enter email"
|
|
||||||
disabled={true}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<div className="md:col-span-2 space-y-2">
|
|
||||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
|
||||||
Status <span className="text-red-500">*</span>
|
|
||||||
</label>
|
|
||||||
<SelectField
|
|
||||||
name="status"
|
|
||||||
value={formData.status}
|
|
||||||
onChange={handleFormChange}
|
|
||||||
options={[
|
|
||||||
{ label: 'Active', value: 'Active' },
|
|
||||||
{ label: 'Inactive', value: 'Inactive' },
|
|
||||||
]}
|
|
||||||
error={errors.status}
|
|
||||||
/>
|
|
||||||
{errors.status && (
|
|
||||||
<p className="mt-1 text-sm text-red-600">{errors.status}</p>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Footer */}
|
|
||||||
<div className="flex justify-end space-x-3 pt-6">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={() => setIsEditModalOpen(false)}
|
|
||||||
className="px-5 py-2.5 text-sm font-medium text-gray-700 bg-white border border-gray-300 rounded-lg hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-[#B68A35] transition-colors"
|
|
||||||
>
|
|
||||||
Cancel
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
type="submit"
|
|
||||||
disabled={saving}
|
|
||||||
className={`px-5 py-2.5 text-sm font-medium text-white rounded-lg focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-[#B68A35] transition-colors ${
|
|
||||||
saving
|
|
||||||
? 'bg-gray-400 cursor-not-allowed'
|
|
||||||
: 'bg-[#92722A] hover:bg-[#9A762D]'
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
{saving ? 'Saving...' : 'Update'}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* ✅ Toast Notification */}
|
|
||||||
{toastData && (
|
{toastData && (
|
||||||
<div className="fixed inset-0 z-[9999]">
|
<div className="fixed inset-0 z-[9999] pointer-events-none">
|
||||||
<CustomToast
|
<div className="absolute right-4 top-4 pointer-events-auto">
|
||||||
message={toastData.message}
|
<CustomToast
|
||||||
type={toastData.type}
|
message={toastData.message}
|
||||||
onClose={() => setToastData(null)}
|
type={toastData.type}
|
||||||
/>
|
onClose={() => setToastData(null)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</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;
|
export default AdminUsers;
|
||||||
|
|||||||
@ -50,4 +50,5 @@ export const deleteUnit = async (id) => {
|
|||||||
console.error('Error deleting unit:', error);
|
console.error('Error deleting unit:', error);
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
|
|
||||||
};
|
};
|
||||||
@ -41,4 +41,4 @@ export const deleteUnit = async (id) => {
|
|||||||
console.error('Error deleting unit:', error);
|
console.error('Error deleting unit:', error);
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user