added products in the company profike
This commit is contained in:
parent
3d27feceab
commit
cac8d1cb2e
@ -23,9 +23,9 @@ const Footer = () => {
|
||||
<div>
|
||||
<h3 className="text-[#92722A] font-semibold mb-3 ml-16 text-lg">Our Policies</h3>
|
||||
<ul className="space-y-1 ml-16 text-sm">
|
||||
<li>Disclaimer</li>
|
||||
<li>Privacy policy</li>
|
||||
<li>Terms and conditions</li>
|
||||
<li><a href="#" className="hover:underline">Disclaimer</a></li>
|
||||
<li><a href="#" className="hover:underline">Privacy policy</a></li>
|
||||
<li><a href="#" className="hover:underline">Terms and conditions</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
@ -33,9 +33,9 @@ const Footer = () => {
|
||||
<div>
|
||||
<h3 className="text-[#92722A] font-semibold mb-3 ml-1 text-lg">Information and Support</h3>
|
||||
<ul className="space-y-1 ml-1 text-sm">
|
||||
<li>Contact us</li>
|
||||
<li>FAQ's</li>
|
||||
<li>Feedback and complaints</li>
|
||||
<li><a href="#" className="hover:underline">Contact us</a></li>
|
||||
<li><a href="#" className="hover:underline">FAQ’s</a></li>
|
||||
<li><a href="#" className="hover:underline">Feedback and complaints</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
@ -67,7 +67,12 @@ const Footer = () => {
|
||||
alt="Mail Icon"
|
||||
className="h-4 w-4 opacity-80"
|
||||
/>
|
||||
<span className="text-[#232528]">info@fcsc.gov.ae</span>
|
||||
<a
|
||||
href="mailto:info@fcsc.gov.ae"
|
||||
className="text-[#232528] hover:underline"
|
||||
>
|
||||
info@fcsc.gov.ae
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div> {/* ✅ properly closed main content div */}
|
||||
|
||||
@ -394,7 +394,7 @@ const ProductData = ({
|
||||
setIsLoadingProducts(true);
|
||||
setProductsError('');
|
||||
try {
|
||||
const productsList = await fetchProducts({ signal: productsController.signal });
|
||||
|
||||
setProductOptions(productsList);
|
||||
setProductsError('');
|
||||
} catch (error) {
|
||||
|
||||
@ -15,7 +15,7 @@ import { TextField, SelectField } from '@/components/common/FormControls';
|
||||
import EditUserModal from './EditUserModal';
|
||||
import DeleteUserModal from './DeleteUserModal';
|
||||
import ResetPasswordModal from './ResetPasswordModal';
|
||||
|
||||
|
||||
const searchIconSrc = '/assets/images/material-symbols_search-rounded.svg';
|
||||
const exportIconSrc = '/assets/images/DownloadSimple.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 resetPasswordInactiveIconSrc =
|
||||
'/assets/images/hugeicons_reset-password-inactive.svg';
|
||||
|
||||
|
||||
const SummaryCard = ({ label, count, icon, iconBg = '#FDF7EB' }) => (
|
||||
<div className="flex min-h-[96px] min-w-[220px] flex-1 items-center justify-between rounded-[16px] border border-[#E6EAF5] bg-white px-6 py-5 shadow-[0_12px_24px_rgba(15,23,42,0.06)]">
|
||||
<div className="flex items-center gap-4">
|
||||
@ -44,7 +44,7 @@ const SummaryCard = ({ label, count, icon, iconBg = '#FDF7EB' }) => (
|
||||
<span className="text-2xl font-semibold text-[#232528]">{count}</span>
|
||||
</div>
|
||||
);
|
||||
|
||||
|
||||
const StatusBadge = ({ status }) => {
|
||||
const map = {
|
||||
Active: 'bg-green-50 text-green-700 ring-1 ring-green-200',
|
||||
@ -60,7 +60,7 @@ const StatusBadge = ({ status }) => {
|
||||
</span>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
const AdminUsers = () => {
|
||||
const [isAddUserModalOpen, setIsAddUserModalOpen] = useState(false);
|
||||
const [isEditModalOpen, setIsEditModalOpen] = useState(false);
|
||||
@ -71,7 +71,7 @@ const AdminUsers = () => {
|
||||
const [saving, setSaving] = useState(false);
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
const pageSize = 10;
|
||||
|
||||
|
||||
const [showDeleteModal, setShowDeleteModal] = useState(false);
|
||||
const [selectedUser, setSelectedUser] = useState(null);
|
||||
const [deletingRow, setDeletingRow] = useState(null);
|
||||
@ -83,15 +83,16 @@ const AdminUsers = () => {
|
||||
const [errors, setErrors] = useState({});
|
||||
const [toastData, setToastData] = useState(null);
|
||||
const navigate = useNavigate();
|
||||
|
||||
|
||||
// Reset password modal state
|
||||
const [isResetModalOpen, setIsResetModalOpen] = useState(false);
|
||||
const [resetUser, setResetUser] = useState(null);
|
||||
const [oldPassword, setOldPassword] = useState('');
|
||||
const [newPassword, setNewPassword] = useState('');
|
||||
const [confirmPassword, setConfirmPassword] = useState('');
|
||||
const [resettingPassword, setResettingPassword] = useState(false);
|
||||
const [resetErrors, setResetErrors] = useState({});
|
||||
|
||||
|
||||
// Helper: show toast
|
||||
const showToast = (message, type = 'success') => {
|
||||
setToastData({ message, type });
|
||||
@ -102,14 +103,14 @@ const AdminUsers = () => {
|
||||
setToastData(null);
|
||||
}, 4000);
|
||||
};
|
||||
|
||||
|
||||
// Fetch users
|
||||
useEffect(() => {
|
||||
const fetchUsers = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const res = await getAdminUser();
|
||||
|
||||
|
||||
const usersData = Array.isArray(res)
|
||||
? res
|
||||
: Array.isArray(res?.data)
|
||||
@ -117,13 +118,13 @@ const AdminUsers = () => {
|
||||
: 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';
|
||||
@ -150,7 +151,7 @@ const AdminUsers = () => {
|
||||
raw: user,
|
||||
};
|
||||
});
|
||||
|
||||
|
||||
setUsers(mappedUsers);
|
||||
} catch (error) {
|
||||
console.error('Error fetching admin users:', error);
|
||||
@ -160,14 +161,14 @@ const AdminUsers = () => {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
fetchUsers();
|
||||
}, []);
|
||||
|
||||
|
||||
const totalUsers = users.length;
|
||||
const activeUsers = users.filter((user) => user.status === 'Active').length;
|
||||
const inactiveUsers = totalUsers - activeUsers;
|
||||
|
||||
|
||||
const summaryItems = [
|
||||
{
|
||||
label: 'Total Users',
|
||||
@ -188,10 +189,10 @@ const AdminUsers = () => {
|
||||
iconBg: '#F5F5F7',
|
||||
},
|
||||
];
|
||||
|
||||
|
||||
const headers = ['Name', 'Email', 'Last Login', 'Status', 'Actions'];
|
||||
const columnWidths = [100, 130, 150, 100, 130];
|
||||
|
||||
|
||||
const rows = users.map((user) => [
|
||||
user.name,
|
||||
user.email,
|
||||
@ -199,7 +200,7 @@ const AdminUsers = () => {
|
||||
<StatusBadge status={user.status} />,
|
||||
'actions',
|
||||
]);
|
||||
|
||||
|
||||
const handleFormChange = (e) => {
|
||||
if (!e) return;
|
||||
if (e.target && e.target.name !== undefined) {
|
||||
@ -211,30 +212,30 @@ const AdminUsers = () => {
|
||||
if (errors[e.name]) setErrors((prev) => ({ ...prev, [e.name]: '' }));
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
const handleUpdateUser = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
|
||||
|
||||
const updatedUser = {
|
||||
name: formData.name,
|
||||
email: formData.email,
|
||||
is_active: formData.status === "Active",
|
||||
};
|
||||
|
||||
|
||||
const res = await updateAdminUser(currentUser.id, updatedUser);
|
||||
|
||||
|
||||
const success =
|
||||
res?.status === "success" ||
|
||||
res?.code === 200 ||
|
||||
res?.data?.status === "success" ||
|
||||
(!res.error && res);
|
||||
|
||||
|
||||
if (!success) throw new Error(res?.message || "Failed to update user");
|
||||
|
||||
|
||||
showToast("User updated successfully!", "success");
|
||||
setIsEditModalOpen(false);
|
||||
|
||||
|
||||
setUsers((prev) =>
|
||||
prev.map((u) =>
|
||||
u.id === currentUser.id
|
||||
@ -248,7 +249,7 @@ const AdminUsers = () => {
|
||||
: u
|
||||
)
|
||||
);
|
||||
|
||||
|
||||
setCurrentUser(null);
|
||||
} catch (error) {
|
||||
console.error("Error updating user:", error);
|
||||
@ -257,27 +258,27 @@ const AdminUsers = () => {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
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 handleDeleteClick = (user) => {
|
||||
setSelectedUser(user);
|
||||
setShowDeleteModal(true);
|
||||
};
|
||||
|
||||
|
||||
const handleConfirmDelete = async () => {
|
||||
try {
|
||||
if (!selectedUser) return;
|
||||
setDeletingRow(selectedUser.id);
|
||||
await deleteAdminUser(selectedUser.id);
|
||||
|
||||
|
||||
showToast("User deleted successfully!", "success");
|
||||
|
||||
|
||||
setUsers((prev) => prev.filter((u) => u.id !== selectedUser.id));
|
||||
} catch (error) {
|
||||
showToast("Failed to delete user!", "error");
|
||||
@ -286,26 +287,26 @@ const AdminUsers = () => {
|
||||
setShowDeleteModal(false);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
const handleEdit = async (user) => {
|
||||
if (!user || !user.id) {
|
||||
showToast('Invalid user selected', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
setEditingRow(user);
|
||||
setLoading(true);
|
||||
|
||||
|
||||
try {
|
||||
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,
|
||||
@ -313,13 +314,13 @@ const AdminUsers = () => {
|
||||
email,
|
||||
is_active,
|
||||
});
|
||||
|
||||
|
||||
setFormData({
|
||||
name,
|
||||
email,
|
||||
status: is_active ? 'Active' : 'Inactive',
|
||||
});
|
||||
|
||||
|
||||
setErrors({});
|
||||
setIsEditModalOpen(true);
|
||||
} catch (error) {
|
||||
@ -329,123 +330,152 @@ const AdminUsers = () => {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
const handleOpenResetModal = (user) => {
|
||||
setResetUser(user);
|
||||
setOldPassword('');
|
||||
setNewPassword('');
|
||||
setConfirmPassword('');
|
||||
setResetErrors({});
|
||||
setIsResetModalOpen(true);
|
||||
};
|
||||
|
||||
|
||||
const handleCloseResetModal = () => {
|
||||
setResetUser(null);
|
||||
setOldPassword('');
|
||||
setNewPassword('');
|
||||
setConfirmPassword('');
|
||||
setResetErrors({});
|
||||
setIsResetModalOpen(false);
|
||||
setResettingPassword(false);
|
||||
};
|
||||
|
||||
// Validate reset password form
|
||||
|
||||
// Validate reset password form - Updated to allow 8 to 12 characters
|
||||
const validateResetForm = () => {
|
||||
const newErrors = {};
|
||||
|
||||
if (!newPassword.trim()) {
|
||||
newErrors.newPassword = 'Password is required';
|
||||
} else if (newPassword.length < 6) {
|
||||
newErrors.newPassword = 'Password must be at least 6 characters';
|
||||
|
||||
if (!oldPassword.trim()) {
|
||||
newErrors.oldPassword = 'Old password is required';
|
||||
}
|
||||
|
||||
|
||||
if (!newPassword.trim()) {
|
||||
newErrors.newPassword = 'New password is required';
|
||||
} else if (newPassword.length < 8) {
|
||||
newErrors.newPassword = 'Password must be at least 8 characters';
|
||||
}
|
||||
|
||||
if (!confirmPassword.trim()) {
|
||||
newErrors.confirmPassword = 'Please confirm your password';
|
||||
} else if (newPassword !== confirmPassword) {
|
||||
// Don't show error message, just prevent submission
|
||||
newPassword !== confirmPassword;
|
||||
newErrors.confirmPassword = 'Passwords do not match';
|
||||
}
|
||||
|
||||
|
||||
return newErrors;
|
||||
};
|
||||
|
||||
|
||||
// Reset Password API Integration
|
||||
const handleResetPassword = async () => {
|
||||
if (!resetUser || !resetUser.id) {
|
||||
showToast('No user selected for password reset', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
// Validate form
|
||||
const formErrors = validateResetForm();
|
||||
if (Object.keys(formErrors).length > 0) {
|
||||
setResetErrors(formErrors);
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if passwords match (without showing error message)
|
||||
if (newPassword !== confirmPassword) {
|
||||
// Just return without showing error
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
try {
|
||||
setResettingPassword(true);
|
||||
|
||||
|
||||
// Prepare data according to API specification
|
||||
const passwordData = {
|
||||
newPassword: newPassword,
|
||||
confirmPassword: confirmPassword
|
||||
old_password: oldPassword,
|
||||
new_password: newPassword,
|
||||
confirm_password: confirmPassword
|
||||
};
|
||||
|
||||
console.log('Initiating password reset for user:', resetUser.id);
|
||||
|
||||
|
||||
console.log('🔄 Initiating admin password reset for user:', resetUser.id);
|
||||
|
||||
const res = await changeAdminUserPassword(resetUser.id, passwordData);
|
||||
|
||||
console.log('Password reset response:', res);
|
||||
|
||||
// Check for different success patterns
|
||||
|
||||
console.log('✅ Admin password reset response:', res);
|
||||
|
||||
// Check for success based on common response patterns
|
||||
const success =
|
||||
res?.status === "success" ||
|
||||
res?.status === 200 ||
|
||||
res?.code === 200 ||
|
||||
res?.data?.status === "success" ||
|
||||
res?.message?.includes("success") ||
|
||||
res?.message?.includes("updated") ||
|
||||
res?.message?.toLowerCase().includes("success") ||
|
||||
res?.message?.toLowerCase().includes("updated") ||
|
||||
res?.message?.toLowerCase().includes("changed") ||
|
||||
(!res.error && res);
|
||||
|
||||
|
||||
if (!success) {
|
||||
throw new Error(res?.message || res?.data?.message || "Failed to reset password");
|
||||
}
|
||||
|
||||
// No success toast message
|
||||
|
||||
showToast('Admin password reset successfully!', 'success');
|
||||
handleCloseResetModal();
|
||||
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error resetting password:', error);
|
||||
|
||||
// More detailed error handling
|
||||
console.error('❌ Error resetting admin password:', error);
|
||||
|
||||
// Handle specific error cases
|
||||
let errorMessage = 'Failed to reset password';
|
||||
|
||||
if (error.response?.data?.message) {
|
||||
errorMessage = error.response.data.message;
|
||||
} else if (error.response?.data?.errors) {
|
||||
// Handle validation errors from backend
|
||||
const errors = error.response.data.errors;
|
||||
errorMessage = Object.values(errors).flat().join(', ');
|
||||
} else if (error.message) {
|
||||
|
||||
if (error.message) {
|
||||
errorMessage = error.message;
|
||||
}
|
||||
|
||||
showToast(errorMessage, 'error');
|
||||
|
||||
// Check for common admin password change errors
|
||||
if (errorMessage.toLowerCase().includes('old password') ||
|
||||
errorMessage.toLowerCase().includes('current password') ||
|
||||
errorMessage.toLowerCase().includes('incorrect password')) {
|
||||
setResetErrors(prev => ({
|
||||
...prev,
|
||||
oldPassword: 'The current password is incorrect'
|
||||
}));
|
||||
showToast('The current password you entered is incorrect', 'error');
|
||||
} else if (errorMessage.toLowerCase().includes('match')) {
|
||||
setResetErrors(prev => ({
|
||||
...prev,
|
||||
confirmPassword: 'New passwords do not match'
|
||||
}));
|
||||
showToast('New passwords do not match', 'error');
|
||||
} else if (errorMessage.toLowerCase().includes('same') ||
|
||||
errorMessage.toLowerCase().includes('previous')) {
|
||||
setResetErrors(prev => ({
|
||||
...prev,
|
||||
newPassword: 'New password cannot be the same as current password'
|
||||
}));
|
||||
showToast('New password cannot be the same as current password', 'error');
|
||||
} else if (errorMessage.toLowerCase().includes('weak') ||
|
||||
errorMessage.toLowerCase().includes('strength')) {
|
||||
setResetErrors(prev => ({
|
||||
...prev,
|
||||
newPassword: 'Password is too weak. Use a stronger password.'
|
||||
}));
|
||||
showToast('Password is too weak. Please use a stronger password.', 'error');
|
||||
} else if (error.status === 404) {
|
||||
showToast('Admin user not found', 'error');
|
||||
} else {
|
||||
showToast(errorMessage, 'error');
|
||||
}
|
||||
} finally {
|
||||
setResettingPassword(false);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
const handleDelete = (rowIndex) => {
|
||||
const user = users[rowIndex];
|
||||
setSelectedUser(user);
|
||||
setShowDeleteModal(true);
|
||||
};
|
||||
|
||||
|
||||
const handleEditSubmit = async (e) => {
|
||||
e.preventDefault();
|
||||
const formErrors = validateForm();
|
||||
@ -454,12 +484,12 @@ const AdminUsers = () => {
|
||||
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 = {
|
||||
@ -467,16 +497,16 @@ const AdminUsers = () => {
|
||||
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
|
||||
@ -490,7 +520,7 @@ const AdminUsers = () => {
|
||||
: u
|
||||
)
|
||||
);
|
||||
|
||||
|
||||
showToast('User updated successfully', 'success');
|
||||
setIsEditModalOpen(false);
|
||||
setEditingRow(null);
|
||||
@ -502,7 +532,7 @@ const AdminUsers = () => {
|
||||
setSaving(false);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
const tableToolbar = (
|
||||
<div className="px-6 py-4 flex flex-col lg:flex-row lg:items-center lg:justify-between gap-3 bg-white">
|
||||
<h3 className="text-sm font-medium text-[#232528]">Admin Users</h3>
|
||||
@ -524,26 +554,26 @@ const AdminUsers = () => {
|
||||
type="button"
|
||||
onClick={() => {
|
||||
if (!users.length) return;
|
||||
|
||||
|
||||
const csvHeader = ['Name', 'Email', 'Status', 'Last Login'].join(',');
|
||||
|
||||
|
||||
const csvRows = users.map((user) =>
|
||||
[user.name, user.email, user.status, user.lastLogin]
|
||||
.map((value) => `"${String(value ?? '').replace(/"/g, '""')}"`)
|
||||
.join(',')
|
||||
);
|
||||
|
||||
|
||||
const blob = new Blob([csvHeader + '\n' + csvRows.join('\n')], {
|
||||
type: 'text/csv;charset=utf-8;',
|
||||
});
|
||||
|
||||
|
||||
const url = URL.createObjectURL(blob);
|
||||
const link = document.createElement('a');
|
||||
link.href = url;
|
||||
link.setAttribute('download', 'admin-users.csv');
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
|
||||
|
||||
document.body.removeChild(link);
|
||||
URL.revokeObjectURL(url);
|
||||
}}
|
||||
@ -557,7 +587,7 @@ const AdminUsers = () => {
|
||||
<img src={exportIconSrc} alt="Export" className="h-4 w-4" />
|
||||
<span>Export CSV</span>
|
||||
</button>
|
||||
|
||||
|
||||
<button
|
||||
onClick={() => setIsAddUserModalOpen(true)}
|
||||
className="h-9 px-3 rounded-md bg-[#B68A35] text-white text-sm inline-flex items-center gap-2 cursor-pointer"
|
||||
@ -569,7 +599,7 @@ const AdminUsers = () => {
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
|
||||
return (
|
||||
<div className="space-y-5">
|
||||
{/* Summary cards */}
|
||||
@ -578,7 +608,7 @@ const AdminUsers = () => {
|
||||
<SummaryCard key={item.label} {...item} />
|
||||
))}
|
||||
</div>
|
||||
|
||||
|
||||
{/* Table */}
|
||||
<Table
|
||||
headers={headers}
|
||||
@ -611,7 +641,7 @@ const AdminUsers = () => {
|
||||
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"
|
||||
@ -624,7 +654,7 @@ const AdminUsers = () => {
|
||||
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 ${
|
||||
@ -646,7 +676,7 @@ const AdminUsers = () => {
|
||||
return value;
|
||||
}}
|
||||
/>
|
||||
|
||||
|
||||
{/* Add User Modal */}
|
||||
{isAddUserModalOpen && (
|
||||
<AddAdminUsers
|
||||
@ -666,9 +696,10 @@ const AdminUsers = () => {
|
||||
setIsAddUserModalOpen(false);
|
||||
showToast('User added locally', 'success');
|
||||
}}
|
||||
passwordMinLength={8}
|
||||
/>
|
||||
)}
|
||||
|
||||
|
||||
{/* Edit Modal */}
|
||||
{isEditModalOpen && (
|
||||
<EditUserModal
|
||||
@ -678,7 +709,7 @@ const AdminUsers = () => {
|
||||
onUpdate={handleUpdateUser}
|
||||
/>
|
||||
)}
|
||||
|
||||
|
||||
{/* Delete Modal */}
|
||||
{showDeleteModal && (
|
||||
<DeleteUserModal
|
||||
@ -687,11 +718,13 @@ const AdminUsers = () => {
|
||||
onDelete={handleConfirmDelete}
|
||||
/>
|
||||
)}
|
||||
|
||||
|
||||
{/* Reset Password Modal */}
|
||||
<ResetPasswordModal
|
||||
isOpen={isResetModalOpen}
|
||||
onClose={handleCloseResetModal}
|
||||
oldPassword={oldPassword}
|
||||
setOldPassword={setOldPassword}
|
||||
newPassword={newPassword}
|
||||
setNewPassword={setNewPassword}
|
||||
confirmPassword={confirmPassword}
|
||||
@ -700,8 +733,9 @@ const AdminUsers = () => {
|
||||
loading={resettingPassword}
|
||||
errors={resetErrors}
|
||||
setErrors={setResetErrors}
|
||||
passwordMinLength={8}
|
||||
/>
|
||||
|
||||
|
||||
{/* Toast */}
|
||||
{toastData && (
|
||||
<div className="fixed inset-0 z-[9999] pointer-events-none">
|
||||
@ -717,5 +751,5 @@ const AdminUsers = () => {
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
export default AdminUsers;
|
||||
@ -1,71 +1,78 @@
|
||||
import React, { useState } from 'react';
|
||||
|
||||
const ResetPasswordModal = ({
|
||||
isOpen,
|
||||
onClose,
|
||||
newPassword,
|
||||
setNewPassword,
|
||||
confirmPassword,
|
||||
|
||||
const ResetPasswordModal = ({
|
||||
isOpen,
|
||||
onClose,
|
||||
oldPassword,
|
||||
setOldPassword,
|
||||
newPassword,
|
||||
setNewPassword,
|
||||
confirmPassword,
|
||||
setConfirmPassword,
|
||||
onReset,
|
||||
loading = false
|
||||
loading = false,
|
||||
errors = {},
|
||||
setErrors
|
||||
}) => {
|
||||
const [errors, setErrors] = useState({});
|
||||
|
||||
const [showOldPassword, setShowOldPassword] = useState(false);
|
||||
const [showNewPassword, setShowNewPassword] = useState(false);
|
||||
const [showConfirmPassword, setShowConfirmPassword] = useState(false);
|
||||
|
||||
const validateForm = () => {
|
||||
const newErrors = {};
|
||||
|
||||
|
||||
if (!oldPassword.trim()) {
|
||||
newErrors.oldPassword = 'Old password is required';
|
||||
}
|
||||
|
||||
if (!newPassword.trim()) {
|
||||
newErrors.newPassword = 'New password is required';
|
||||
} else if (newPassword.length < 6) {
|
||||
newErrors.newPassword = 'Password must be at least 6 characters';
|
||||
}
|
||||
|
||||
|
||||
if (!confirmPassword.trim()) {
|
||||
newErrors.confirmPassword = 'Please confirm your password';
|
||||
} else if (newPassword !== confirmPassword) {
|
||||
newErrors.confirmPassword = 'Passwords do not match';
|
||||
}
|
||||
// Removed "Passwords do not match" error display
|
||||
|
||||
|
||||
return newErrors;
|
||||
};
|
||||
|
||||
|
||||
const handleSubmit = () => {
|
||||
const formErrors = validateForm();
|
||||
|
||||
// Check if passwords match (without showing error)
|
||||
if (newPassword !== confirmPassword) {
|
||||
return; // Just return without showing error
|
||||
}
|
||||
|
||||
|
||||
if (Object.keys(formErrors).length > 0) {
|
||||
setErrors(formErrors);
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
setErrors({});
|
||||
onReset();
|
||||
};
|
||||
|
||||
|
||||
const handleInputChange = (field, value) => {
|
||||
if (field === 'oldPassword') setOldPassword(value);
|
||||
if (field === 'newPassword') setNewPassword(value);
|
||||
if (field === 'confirmPassword') setConfirmPassword(value);
|
||||
|
||||
|
||||
// Clear error when user starts typing
|
||||
if (errors[field]) {
|
||||
setErrors(prev => ({ ...prev, [field]: '' }));
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
const handleKeyPress = (e) => {
|
||||
if (e.key === 'Enter' && !loading) {
|
||||
handleSubmit();
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
if (!isOpen) return null;
|
||||
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 z-[9999] flex items-center justify-center bg-black bg-opacity-50 backdrop-blur-sm">
|
||||
<div className="fixed inset-0 z-[9999] flex items-center justify-center bg-opacity-70 backdrop-blur-sm">
|
||||
<div className="bg-white rounded-2xl w-[90%] max-w-md shadow-xl p-6 relative border border-gray-200">
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between mb-6">
|
||||
@ -80,26 +87,93 @@ const ResetPasswordModal = ({
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
|
||||
{/* Form */}
|
||||
<div className="space-y-4">
|
||||
{/* Old Password Field */}
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||
New Password
|
||||
Old Password <span className="text-red-500">*</span>
|
||||
</label>
|
||||
<input
|
||||
type="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 ${
|
||||
errors.newPassword
|
||||
? 'border-red-300 focus:ring-red-500 bg-red-50'
|
||||
: 'border-gray-300 focus:border-[#B68A35] focus:ring-2 focus:ring-[#B68A35] focus:ring-opacity-20'
|
||||
}`}
|
||||
value={newPassword}
|
||||
onChange={(e) => handleInputChange('newPassword', e.target.value)}
|
||||
onKeyPress={handleKeyPress}
|
||||
disabled={loading}
|
||||
/>
|
||||
<div className="relative">
|
||||
<input
|
||||
type={showOldPassword ? "text" : "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 ${
|
||||
errors.oldPassword
|
||||
? 'border-red-300 focus:ring-red-500 bg-red-50'
|
||||
: 'border-gray-300 focus:border-[#B68A35] focus:ring-2 focus:ring-[#B68A35] focus:ring-opacity-20'
|
||||
}`}
|
||||
value={oldPassword}
|
||||
onChange={(e) => handleInputChange('oldPassword', e.target.value)}
|
||||
onKeyPress={handleKeyPress}
|
||||
disabled={loading}
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
className="absolute right-3 top-1/2 transform -translate-y-1/2 text-gray-500 hover:text-gray-700"
|
||||
onClick={() => setShowOldPassword(!showOldPassword)}
|
||||
disabled={loading}
|
||||
>
|
||||
{showOldPassword ? (
|
||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z" />
|
||||
</svg>
|
||||
) : (
|
||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13.875 18.825A10.05 10.05 0 0112 19c-4.478 0-8.268-2.943-9.543-7a9.97 9.97 0 011.563-3.029m5.858.908a3 3 0 114.243 4.243M9.878 9.878l4.242 4.242M9.878 9.878L9 9m13 11l-4-4m0 0l-4 4m4-4V9" />
|
||||
</svg>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
{errors.oldPassword && (
|
||||
<p className="text-red-600 text-xs mt-2 flex items-center gap-1">
|
||||
<svg className="w-3 h-3" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fillRule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7 4a1 1 0 11-2 0 1 1 0 012 0zm-1-9a1 1 0 00-1 1v4a1 1 0 102 0V6a1 1 0 00-1-1z" clipRule="evenodd" />
|
||||
</svg>
|
||||
{errors.oldPassword}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* New Password Field */}
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||
New Password <span className="text-red-500">*</span>
|
||||
</label>
|
||||
<div className="relative">
|
||||
<input
|
||||
type={showNewPassword ? "text" : "password"}
|
||||
placeholder="Enter new password"
|
||||
className={`w-full border rounded-lg px-4 py-3 text-sm focus:outline-none focus:ring-2 transition-all pr-10 ${
|
||||
errors.newPassword
|
||||
? 'border-red-300 focus:ring-red-500 bg-red-50'
|
||||
: 'border-gray-300 focus:border-[#B68A35] focus:ring-2 focus:ring-[#B68A35] focus:ring-opacity-20'
|
||||
}`}
|
||||
value={newPassword}
|
||||
onChange={(e) => handleInputChange('newPassword', e.target.value)}
|
||||
onKeyPress={handleKeyPress}
|
||||
disabled={loading}
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
className="absolute right-3 top-1/2 transform -translate-y-1/2 text-gray-500 hover:text-gray-700"
|
||||
onClick={() => setShowNewPassword(!showNewPassword)}
|
||||
disabled={loading}
|
||||
>
|
||||
{showNewPassword ? (
|
||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z" />
|
||||
</svg>
|
||||
) : (
|
||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13.875 18.825A10.05 10.05 0 0112 19c-4.478 0-8.268-2.943-9.543-7a9.97 9.97 0 011.563-3.029m5.858.908a3 3 0 114.243 4.243M9.878 9.878l4.242 4.242M9.878 9.878L9 9m13 11l-4-4m0 0l-4 4m4-4V9" />
|
||||
</svg>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
{errors.newPassword && (
|
||||
<p className="text-red-600 text-xs mt-2 flex items-center gap-1">
|
||||
<svg className="w-3 h-3" fill="currentColor" viewBox="0 0 20 20">
|
||||
@ -109,24 +183,44 @@ const ResetPasswordModal = ({
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
|
||||
{/* Confirm Password Field */}
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||
Confirm Password
|
||||
Confirm Password <span className="text-red-500">*</span>
|
||||
</label>
|
||||
<input
|
||||
type="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 ${
|
||||
errors.confirmPassword
|
||||
? 'border-red-300 focus:ring-red-500 bg-red-50'
|
||||
: 'border-gray-300 focus:border-[#B68A35] focus:ring-2 focus:ring-[#B68A35] focus:ring-opacity-20'
|
||||
}`}
|
||||
value={confirmPassword}
|
||||
onChange={(e) => handleInputChange('confirmPassword', e.target.value)}
|
||||
onKeyPress={handleKeyPress}
|
||||
disabled={loading}
|
||||
/>
|
||||
<div className="relative">
|
||||
<input
|
||||
type={showConfirmPassword ? "text" : "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 ${
|
||||
errors.confirmPassword
|
||||
? 'border-red-300 focus:ring-red-500 bg-red-50'
|
||||
: 'border-gray-300 focus:border-[#B68A35] focus:ring-2 focus:ring-[#B68A35] focus:ring-opacity-20'
|
||||
}`}
|
||||
value={confirmPassword}
|
||||
onChange={(e) => handleInputChange('confirmPassword', e.target.value)}
|
||||
onKeyPress={handleKeyPress}
|
||||
disabled={loading}
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
className="absolute right-3 top-1/2 transform -translate-y-1/2 text-gray-500 hover:text-gray-700"
|
||||
onClick={() => setShowConfirmPassword(!showConfirmPassword)}
|
||||
disabled={loading}
|
||||
>
|
||||
{showConfirmPassword ? (
|
||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z" />
|
||||
</svg>
|
||||
) : (
|
||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13.875 18.825A10.05 10.05 0 0112 19c-4.478 0-8.268-2.943-9.543-7a9.97 9.97 0 011.563-3.029m5.858.908a3 3 0 114.243 4.243M9.878 9.878l4.242 4.242M9.878 9.878L9 9m13 11l-4-4m0 0l-4 4m4-4V9" />
|
||||
</svg>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
{errors.confirmPassword && (
|
||||
<p className="text-red-600 text-xs mt-2 flex items-center gap-1">
|
||||
<svg className="w-3 h-3" fill="currentColor" viewBox="0 0 20 20">
|
||||
@ -137,7 +231,7 @@ const ResetPasswordModal = ({
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
{/* Note Section */}
|
||||
<div className="mt-4 p-3 bg-blue-50 rounded-lg border border-blue-200">
|
||||
<div className="flex items-start gap-2">
|
||||
@ -149,7 +243,7 @@ const ResetPasswordModal = ({
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
{/* Action Buttons */}
|
||||
<div className="flex justify-end gap-3 mt-6 pt-4 border-t border-gray-200">
|
||||
<button
|
||||
@ -178,5 +272,5 @@ const ResetPasswordModal = ({
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
export default ResetPasswordModal;
|
||||
@ -11,6 +11,7 @@ import {
|
||||
updateEstablishment,
|
||||
deleteEstablishment,
|
||||
} from '@/services/establishments/establishmentService';
|
||||
import { fetchProducts } from '@/services/masters/masterService';
|
||||
const searchIconSrc = '/assets/images/material-symbols_search-rounded.svg';
|
||||
const downloadIconSrc = '/assets/images/DownloadSimple.svg';
|
||||
const addIconSrc = '/assets/images/ic_baseline-plus.svg';
|
||||
@ -188,6 +189,12 @@ const CompanyProfile = () => {
|
||||
const [isDeletingApi, setIsDeletingApi] = React.useState(false);
|
||||
const [totalItems, setTotalItems] = React.useState(0);
|
||||
const [currentUser, setCurrentUser] = React.useState(null);
|
||||
const [products, setProducts] = React.useState([]);
|
||||
const [availableProducts, setAvailableProducts] = React.useState([]);
|
||||
const [selectedProducts, setSelectedProducts] = React.useState([]);
|
||||
const [productSearchTerm, setProductSearchTerm] = React.useState('');
|
||||
const [isLoadingProducts, setIsLoadingProducts] = React.useState(false);
|
||||
const [isRefreshing, setIsRefreshing] = React.useState(false);
|
||||
|
||||
const showToast = React.useCallback((type, message) => {
|
||||
if (!message) return;
|
||||
@ -236,7 +243,8 @@ const CompanyProfile = () => {
|
||||
{ label: 'User Profile' },
|
||||
{ label: 'Identification Particulars' },
|
||||
{ label: 'Establishment Contact Details' },
|
||||
{ label: 'Corporate / Head Office Contact' },
|
||||
{ label: 'Products' },
|
||||
// { label: 'Corporate / Head Office Contact' },
|
||||
{ label: 'Employment in Establishment' },
|
||||
],
|
||||
[]
|
||||
@ -259,26 +267,27 @@ const CompanyProfile = () => {
|
||||
{ field: 'contactAddress', label: 'Address' },
|
||||
{ field: 'contactCityTown', label: 'City/Town' },
|
||||
{ field: 'contactEmirate', label: 'Emirate' },
|
||||
{ field: 'contactMakaniNumber', label: 'Makani Number' },
|
||||
{ field: 'contactPersonName', label: 'Contact Person Name' },
|
||||
// { field: 'contactPersonName', label: 'Contact Person Name' },
|
||||
{ field: 'contactMobileNumber', label: 'Mobile Number' },
|
||||
{ field: 'contactEmail', label: 'Email' },
|
||||
],
|
||||
3: [
|
||||
{ field: 'products', label: 'Products' },
|
||||
],
|
||||
4: [
|
||||
{ field: 'corporateName', label: 'Name' },
|
||||
{ field: 'corporateAddress', label: 'Address' },
|
||||
{ field: 'corporateCityTown', label: 'City/Town' },
|
||||
{ field: 'corporateEmirate', label: 'Emirate' },
|
||||
{ field: 'corporateMakaniNumber', label: 'Makani Number' },
|
||||
{ field: 'corporateContactPersonName', label: 'Contact Person Name' },
|
||||
// { field: 'corporateContactPersonName', label: 'Contact Person Name' },
|
||||
{ field: 'corporateMobileNumber', label: 'Mobile Number' },
|
||||
{ field: 'corporateEmail', label: 'Email' },
|
||||
],
|
||||
4: [
|
||||
{ field: 'employmentEmiratiMale', label: 'Number of Emirati Male' },
|
||||
{ field: 'employmentNonEmiratiMale', label: 'Number of Non-Emirati Male' },
|
||||
{ field: 'employmentEmiratiFemale', label: 'Number of Emirati Female' },
|
||||
{ field: 'employmentNonEmiratiFemale', label: 'Number of Non-Emirati Female' },
|
||||
5: [
|
||||
// { field: 'employmentEmiratiMale', label: 'Number of Emirati Male' },
|
||||
// { field: 'employmentNonEmiratiMale', label: 'Number of Non-Emirati Male' },
|
||||
// { field: 'employmentEmiratiFemale', label: 'Number of Emirati Female' },
|
||||
// { field: 'employmentNonEmiratiFemale', label: 'Number of Non-Emirati Female' },
|
||||
],
|
||||
}),
|
||||
[]
|
||||
@ -368,7 +377,7 @@ const CompanyProfile = () => {
|
||||
placeholder="Enter password"
|
||||
width="100%"
|
||||
type="password"
|
||||
showToggle
|
||||
showToggle={true}
|
||||
className={passwordError || passwordReuseError ? 'border-red-500' : ''}
|
||||
required
|
||||
/>
|
||||
@ -383,7 +392,7 @@ const CompanyProfile = () => {
|
||||
placeholder="Confirm password"
|
||||
width="100%"
|
||||
type="password"
|
||||
showToggle
|
||||
showToggle={true}
|
||||
className={confirmError ? 'border-red-500' : ''}
|
||||
required
|
||||
/>
|
||||
@ -402,7 +411,7 @@ const CompanyProfile = () => {
|
||||
width="100%"
|
||||
type="password"
|
||||
disabled={true} // ensures non-editable
|
||||
showToggle={false} // optional: hide toggle if disabled
|
||||
showToggle={false} // hide toggle for disabled field
|
||||
className={passwordError ? 'border-red-500' : ''}
|
||||
/>
|
||||
{passwordError && (
|
||||
@ -419,7 +428,7 @@ const CompanyProfile = () => {
|
||||
placeholder="Enter password"
|
||||
width="100%"
|
||||
type="password"
|
||||
showToggle
|
||||
showToggle={false}
|
||||
className={passwordError || passwordReuseError ? 'border-red-500' : ''}
|
||||
/>
|
||||
{passwordError && <p className="text-xs text-[#B91C1C]">{passwordError}</p>}
|
||||
@ -433,7 +442,7 @@ const CompanyProfile = () => {
|
||||
placeholder="Confirm password"
|
||||
width="100%"
|
||||
type="password"
|
||||
showToggle
|
||||
showToggle={false}
|
||||
className={confirmError ? 'border-red-500' : ''}
|
||||
/>
|
||||
{confirmError && <p className="text-xs text-[#B91C1C]">{confirmError}</p>}
|
||||
@ -592,7 +601,7 @@ const CompanyProfile = () => {
|
||||
onChange={handleFormChange('contactPersonName')}
|
||||
placeholder="Enter Name"
|
||||
width="100%"
|
||||
required
|
||||
// required
|
||||
error={fieldErrors.contactPersonName}
|
||||
/>
|
||||
<TextField
|
||||
@ -634,7 +643,7 @@ const CompanyProfile = () => {
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
case 3:
|
||||
// case 4:
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<div className="flex flex-col gap-2 md:flex-row md:items-center md:justify-between">
|
||||
@ -736,7 +745,7 @@ const CompanyProfile = () => {
|
||||
onChange={handleFormChange('corporateMakaniNumber')}
|
||||
placeholder="Enter Number"
|
||||
width="100%"
|
||||
required
|
||||
// required
|
||||
error={fieldErrors.corporateMakaniNumber}
|
||||
readOnly={form.corporateSameAs}
|
||||
/>
|
||||
@ -746,7 +755,7 @@ const CompanyProfile = () => {
|
||||
onChange={handleFormChange('corporateContactPersonName')}
|
||||
placeholder="Enter Name"
|
||||
width="100%"
|
||||
required
|
||||
// required
|
||||
error={fieldErrors.corporateContactPersonName}
|
||||
readOnly={form.corporateSameAs}
|
||||
/>
|
||||
@ -795,9 +804,7 @@ const CompanyProfile = () => {
|
||||
case 4:
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<h4 className="text-[16px] font-semibold text-[#232528]">Employment in Establishment</h4>
|
||||
</div>
|
||||
<h4 className="text-[16px] font-semibold text-[#232528] mb-4">Employment in Establishment</h4>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
|
||||
<TextField
|
||||
label="Number of Emirati Male"
|
||||
@ -806,7 +813,7 @@ const CompanyProfile = () => {
|
||||
placeholder="Enter Count"
|
||||
width="100%"
|
||||
type="number"
|
||||
required
|
||||
// required
|
||||
error={fieldErrors.employmentEmiratiMale}
|
||||
/>
|
||||
<TextField
|
||||
@ -816,7 +823,7 @@ const CompanyProfile = () => {
|
||||
placeholder="Enter Count"
|
||||
width="100%"
|
||||
type="number"
|
||||
required
|
||||
// required
|
||||
error={fieldErrors.employmentNonEmiratiMale}
|
||||
/>
|
||||
<TextField
|
||||
@ -826,7 +833,7 @@ const CompanyProfile = () => {
|
||||
placeholder="Enter Count"
|
||||
width="100%"
|
||||
type="number"
|
||||
required
|
||||
// required
|
||||
error={fieldErrors.employmentEmiratiFemale}
|
||||
/>
|
||||
<TextField
|
||||
@ -836,7 +843,7 @@ const CompanyProfile = () => {
|
||||
placeholder="Enter Count"
|
||||
width="100%"
|
||||
type="number"
|
||||
required
|
||||
// required
|
||||
error={fieldErrors.employmentNonEmiratiFemale}
|
||||
/>
|
||||
<TextField
|
||||
@ -858,7 +865,110 @@ const CompanyProfile = () => {
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
case 5:
|
||||
case 3:
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<h4 className="text-[16px] font-semibold text-[#232528]">Products</h4>
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||
{/* Available Products Panel */}
|
||||
<div className="bg-white rounded-lg border border-[#E5E7EB] overflow-hidden">
|
||||
<div className="bg-[#F9FAFB] px-4 py-3 border-b border-[#E5E7EB]">
|
||||
<h5 className="text-sm font-medium text-[#111827]">Available Products</h5>
|
||||
</div>
|
||||
<div className="p-4">
|
||||
<div className="relative mb-4">
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Search products..."
|
||||
value={productSearchTerm}
|
||||
onChange={handleProductSearch}
|
||||
className="w-full px-3 py-2 border border-[#D1D5DB] rounded-md text-sm focus:outline-none focus:ring-2 focus:ring-[#92722A] focus:border-transparent"
|
||||
/>
|
||||
<img
|
||||
src={searchIconSrc}
|
||||
alt="Search"
|
||||
className="absolute right-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-gray-400"
|
||||
/>
|
||||
</div>
|
||||
<div className="border border-[#E5E7EB] rounded-md overflow-hidden">
|
||||
{isLoadingProducts ? (
|
||||
<div className="flex justify-center items-center p-4">
|
||||
<div className="animate-spin rounded-full h-6 w-6 border-b-2 border-[#92722A]"></div>
|
||||
</div>
|
||||
) : availableProducts.length > 0 ? (
|
||||
<ul className="divide-y divide-[#E5E7EB] max-h-96 overflow-y-auto">
|
||||
{availableProducts.map((product) => (
|
||||
<li key={product.id} className="p-3 hover:bg-[#F9FAFB] flex justify-between items-center">
|
||||
<div>
|
||||
<div className="text-sm font-medium text-[#111827]">
|
||||
{product.hsCode || ''}
|
||||
{product.hsCode && (product.productName || product.label || product.product_name) ? ' - ' : ''}
|
||||
{product.productName || product.label || product.product_name || ''}
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => handleAddProduct(product)}
|
||||
className="text-[#92722A] hover:text-[#7A5F1E] text-sm font-medium"
|
||||
>
|
||||
Add
|
||||
|
||||
</button>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
) : (
|
||||
<div className="p-4 text-center text-sm text-[#6B7280]">
|
||||
{productSearchTerm ? 'No matching products found' : 'No products available'}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Selected Products Panel */}
|
||||
<div className="bg-white rounded-lg border border-[#E5E7EB] overflow-hidden">
|
||||
<div className="bg-[#F9FAFB] px-4 py-3 border-b border-[#E5E7EB] flex justify-between items-center">
|
||||
<h5 className="text-sm font-medium text-[#111827]">Selected Products</h5>
|
||||
{selectedProducts.length > 0 && (
|
||||
<span className="bg-[#FEF3C7] text-[#92400E] text-xs font-medium px-2 py-0.5 rounded-full">
|
||||
{selectedProducts.length} selected
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="p-4">
|
||||
{selectedProducts.length > 0 ? (
|
||||
<ul className="divide-y divide-[#E5E7EB] max-h-96 overflow-y-auto">
|
||||
{selectedProducts.map((product) => (
|
||||
<li key={product.id} className="p-3 hover:bg-[#F9FAFB] flex justify-between items-center">
|
||||
<div>
|
||||
<div className="text-sm font-medium text-[#111827]">
|
||||
{product.hsCode || ''}
|
||||
{product.hsCode && (product.productName || product.label || product.product_name) ? ' - ' : ''}
|
||||
{product.productName || product.label || product.product_name || ''}
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => handleRemoveProduct(product)}
|
||||
className="text-[#EF4444] hover:text-[#DC2626] text-sm font-medium"
|
||||
>
|
||||
Remove
|
||||
</button>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
) : (
|
||||
<div className="p-4 text-center text-sm text-[#6B7280] border border-[#E5E7EB] rounded-md">
|
||||
-
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
default:
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
@ -936,6 +1046,113 @@ const CompanyProfile = () => {
|
||||
}
|
||||
};
|
||||
|
||||
// Fetch products when Products step is active
|
||||
React.useEffect(() => {
|
||||
if (activeStep === 3) {
|
||||
// In the loadProducts function (around line 1044), update the product processing:
|
||||
const loadProducts = async () => {
|
||||
try {
|
||||
console.log('Fetching products...');
|
||||
setIsLoadingProducts(true);
|
||||
const response = await fetchProducts();
|
||||
console.log('Products API Response:', response);
|
||||
|
||||
let productsData = [];
|
||||
|
||||
if (Array.isArray(response)) {
|
||||
productsData = response;
|
||||
} else if (response?.data) {
|
||||
productsData = Array.isArray(response.data) ? response.data : response.data.products || [];
|
||||
}
|
||||
|
||||
console.log('Processed products data:', productsData);
|
||||
|
||||
// Update the products state with the formatted data
|
||||
const formattedProducts = productsData.map(p => ({
|
||||
id: p.value,
|
||||
label: p.label,
|
||||
value: p.value
|
||||
}));
|
||||
|
||||
setProducts(formattedProducts);
|
||||
setAvailableProducts(formattedProducts.filter(p =>
|
||||
!selectedProducts.some(sp => sp.id === p.id)
|
||||
));
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error loading products:', error);
|
||||
showToast('error', 'Failed to load products');
|
||||
} finally {
|
||||
setIsLoadingProducts(false);
|
||||
}
|
||||
};
|
||||
|
||||
loadProducts();
|
||||
}
|
||||
}, [activeStep, selectedProducts]);
|
||||
|
||||
const handleProductSearch = (e) => {
|
||||
const searchTerm = e.target.value.toLowerCase();
|
||||
setProductSearchTerm(searchTerm);
|
||||
|
||||
if (!searchTerm) {
|
||||
setAvailableProducts(products.filter(p =>
|
||||
!selectedProducts.some(sp => sp.id === p.id)
|
||||
));
|
||||
return;
|
||||
}
|
||||
|
||||
const filtered = products.filter(product =>
|
||||
product.label.toLowerCase().includes(searchTerm) &&
|
||||
!selectedProducts.some(sp => sp.id === product.id)
|
||||
);
|
||||
|
||||
setAvailableProducts(filtered);
|
||||
};
|
||||
|
||||
const handleAddProduct = (product) => {
|
||||
console.log('Adding product:', product);
|
||||
setSelectedProducts(prev => {
|
||||
const updated = [...prev, product];
|
||||
console.log('Updated selected products:', updated);
|
||||
return updated;
|
||||
});
|
||||
// Remove from available products
|
||||
setAvailableProducts(prev => prev.filter(p => p.id !== product.id));
|
||||
setAvailableProducts(prev => {
|
||||
const updated = prev.filter(p => p.id !== product.id);
|
||||
console.log('Updated available products after add:', updated);
|
||||
return updated;
|
||||
});
|
||||
};
|
||||
|
||||
const handleRemoveProduct = (product) => {
|
||||
console.log('Removing product:', product);
|
||||
setSelectedProducts(prev => {
|
||||
const updated = prev.filter(p => p.id !== product.id);
|
||||
console.log('Updated selected products after removal:', updated);
|
||||
return updated;
|
||||
});
|
||||
// Add back to available products if not already there
|
||||
setAvailableProducts(prev => {
|
||||
if (!prev.some(p => p.id === product.id)) {
|
||||
return [...prev, product];
|
||||
}
|
||||
return prev;
|
||||
});
|
||||
|
||||
// Only add back to available if it matches the search term or there's no search term
|
||||
if (!productSearchTerm ||
|
||||
(product.hs_code && product.hs_code.toLowerCase().includes(productSearchTerm)) ||
|
||||
(product.product_name && product.product_name.toLowerCase().includes(productSearchTerm))) {
|
||||
setAvailableProducts(prev => {
|
||||
const updated = [...prev, product];
|
||||
console.log('Updated available products after remove:', updated);
|
||||
return updated;
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const handleNextStep = React.useCallback(() => {
|
||||
if (!validateStepFields(activeStep)) {
|
||||
return;
|
||||
@ -1015,12 +1232,7 @@ const CompanyProfile = () => {
|
||||
);
|
||||
};
|
||||
|
||||
// Calculate pagination range
|
||||
const startIndex = (currentPage - 1) * pageSize;
|
||||
const endIndex = Math.min(startIndex + pageSize, filteredProfiles.length);
|
||||
const paginatedProfiles = filteredProfiles.slice(startIndex, endIndex);
|
||||
|
||||
const displayedRows = paginatedProfiles.map((item) => {
|
||||
const displayedRows = filteredProfiles.map((item) => {
|
||||
const isEditing =
|
||||
editingRow !== null && profiles[editingRow]?.establishmentId === item.establishmentId;
|
||||
const isDeleting =
|
||||
@ -1032,7 +1244,9 @@ const CompanyProfile = () => {
|
||||
item.emirate,
|
||||
item.isicCode,
|
||||
item.establishmentId,
|
||||
item.products,
|
||||
item.products && item.products.length > 0
|
||||
? item.products.map(p => p.hsCode ? `${p.hsCode} - ${p.productName || p.label || p.product_name || 'N/A'}` : (p.productName || p.product_name || 'N/A')).filter(Boolean).join(', ')
|
||||
: '-',
|
||||
item.totalEmployees,
|
||||
item.createdBy,
|
||||
item.createdOn,
|
||||
@ -1368,15 +1582,31 @@ const corporateFieldKeys = Object.values(contactToCorporateMap);
|
||||
|
||||
const loadProfiles = async () => {
|
||||
setIsLoading(true);
|
||||
const params = {
|
||||
page: currentPage,
|
||||
limit: pageSize,
|
||||
// First, get the total number of records
|
||||
const countParams = {
|
||||
search: debouncedSearch || undefined,
|
||||
emirateId: selectedEmirateFilter || undefined,
|
||||
status: selectedStatusFilter || undefined,
|
||||
page: 1,
|
||||
limit: 1, // Just need the total count
|
||||
};
|
||||
|
||||
try {
|
||||
// First, get the total count
|
||||
const countResponse = await fetchEstablishments({ params: countParams, signal: controller.signal });
|
||||
const countPayload = countResponse?.data ?? countResponse;
|
||||
const totalRecords = countPayload?.pagination?.total_records || 0;
|
||||
|
||||
// Then fetch all records with the total count as limit
|
||||
const params = {
|
||||
search: debouncedSearch || undefined,
|
||||
emirateId: selectedEmirateFilter || undefined,
|
||||
status: selectedStatusFilter || undefined,
|
||||
page: 1,
|
||||
limit: totalRecords || 1000,
|
||||
};
|
||||
|
||||
// Fetch all records
|
||||
const response = await fetchEstablishments({ params, signal: controller.signal });
|
||||
const payload = response?.data ?? response;
|
||||
|
||||
@ -1384,25 +1614,15 @@ const loadProfiles = async () => {
|
||||
console.group('API Response Details');
|
||||
console.log('Full response:', response);
|
||||
console.log('Response data:', payload);
|
||||
console.log('Pagination object:', payload?.pagination);
|
||||
console.log('Total records from pagination:', payload?.pagination?.total_records);
|
||||
console.groupEnd();
|
||||
|
||||
const records = Array.isArray(payload?.data) ? payload.data : Array.isArray(payload) ? payload : [];
|
||||
if (cancelled) return;
|
||||
setProfiles(records.map(mapApiEstablishmentToProfile));
|
||||
|
||||
// Get total records from pagination object in the API response
|
||||
const totalRecords = payload?.pagination?.total_records;
|
||||
if (totalRecords !== undefined) {
|
||||
console.log(`Setting total items from pagination.total_records: ${totalRecords}`);
|
||||
setTotalItems(totalRecords);
|
||||
} else {
|
||||
// Fallback to other possible locations if pagination is not available
|
||||
const fallbackTotal = payload?.total || payload?.count || records.length;
|
||||
console.warn('pagination.total_records not found, using fallback value:', fallbackTotal);
|
||||
setTotalItems(fallbackTotal);
|
||||
}
|
||||
// Set all records to the profiles state
|
||||
setProfiles(records.map(mapApiEstablishmentToProfile));
|
||||
// Set total items to the number of records
|
||||
setTotalItems(records.length);
|
||||
} catch (error) {
|
||||
if (cancelled) return;
|
||||
if (error.name === 'CanceledError' || error.name === 'AbortError') return;
|
||||
@ -1638,6 +1858,7 @@ const loadProfiles = async () => {
|
||||
}
|
||||
setModalMode(null);
|
||||
setForm(createEmptyProfile());
|
||||
setSelectedProducts([]); // Clear selected products when modal is closed
|
||||
setEditingRow(null);
|
||||
setFieldErrors({});
|
||||
setIsDirty(false);
|
||||
@ -2011,6 +2232,9 @@ const requiredFields = [
|
||||
email: form.userProfileEmail || '',
|
||||
password: form.userProfilePassword || ''
|
||||
},
|
||||
establishment_products: Array.isArray(selectedProducts) && selectedProducts.length > 0
|
||||
? selectedProducts.map(p => ({ product_id: p.id || p.product_id || 0 }))
|
||||
: [{ product_id: 0 }],
|
||||
};
|
||||
try {
|
||||
const apiResponse = await createEstablishment(apiPayload);
|
||||
@ -2020,10 +2244,18 @@ const requiredFields = [
|
||||
const createdRecord = apiResponse?.data;
|
||||
createdRecordId = createdRecord?.id ?? createdRecord?.establishment?.id ?? null;
|
||||
apiResultData = createdRecord;
|
||||
setIsRefreshing(true);
|
||||
setTimeout(() => {
|
||||
window.location.reload();
|
||||
}, 1000);
|
||||
} catch (apiError) {
|
||||
const apiMessage = apiError?.response?.data?.message || apiError?.message || '';
|
||||
if (apiMessage.includes("Field 'gender' doesn't have a default value")) {
|
||||
showToast('success', 'Establishment submitted successfully.');
|
||||
setIsRefreshing(true);
|
||||
setTimeout(() => {
|
||||
window.location.reload();
|
||||
}, 1000);
|
||||
} else {
|
||||
throw apiError;
|
||||
}
|
||||
@ -2088,6 +2320,9 @@ const requiredFields = [
|
||||
if (establishmentUserPayload.name || establishmentUserPayload.email || establishmentUserPayload.password) {
|
||||
updatePayload.establishment_user = establishmentUserPayload;
|
||||
}
|
||||
updatePayload.establishment_products = Array.isArray(selectedProducts) && selectedProducts.length > 0
|
||||
? selectedProducts.map(p => ({ product_id: p.id || p.product_id || 0 }))
|
||||
: [{ product_id: 0 }];
|
||||
const apiResponse = await updateEstablishment(targetId, updatePayload);
|
||||
console.log("apiResponse",apiResponse)
|
||||
const successMessage = apiResponse?.message || 'Establishment updated successfully.';
|
||||
@ -2355,7 +2590,7 @@ const requiredFields = [
|
||||
window.scrollTo({ top: 0, behavior: 'smooth' });
|
||||
},
|
||||
pageSize,
|
||||
totalItems: totalItems, // Use the total count from API response
|
||||
totalItems: filteredProfiles.length,
|
||||
pageSizeOptions: [10, 20, 50, 100],
|
||||
onPageSizeChange: (size) => {
|
||||
setPageSize(size);
|
||||
@ -2501,6 +2736,15 @@ const requiredFields = [
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Full Page Loader */}
|
||||
{isRefreshing && (
|
||||
<div className="fixed inset-0 z-[9999] bg-white/80 flex flex-col items-center justify-center">
|
||||
<div className="animate-spin rounded-full h-16 w-16 border-t-4 border-[#92722A]"></div>
|
||||
<p className="mt-4 text-lg font-medium text-gray-700">Updating data...</p>
|
||||
<p className="text-sm text-gray-500">Please wait while we refresh the page</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{showDeleteConfirm && deletingProfile && (
|
||||
<div className="fixed inset-0 z-50">
|
||||
<div
|
||||
|
||||
@ -3,8 +3,7 @@ import Table from '@/components/common/Table';
|
||||
import { TextField, SelectField, DateField } from '@/components/common/FormControls';
|
||||
import StatusBadge from '@/components/common/StatusBadge';
|
||||
import { createQuarterlyWindow, updateQuarterlyWindow, getQuarterlyWindows } from '@/services/configuration/quarterlyWindows';
|
||||
import CustomToast from '@/components/common/CustomToast';
|
||||
|
||||
|
||||
const downloadIconSrc = '/assets/images/DownloadSimple.svg';
|
||||
const addIconSrc = '/assets/images/ic_baseline-plus.svg';
|
||||
const pencilActiveSrc = '/assets/images/PencilSimple.svg';
|
||||
@ -12,7 +11,57 @@ const pencilInactiveSrc = '/assets/images/pencilsimple-inactive.svg';
|
||||
const caretDownActiveSrc = '/assets/images/caretdown-active.svg';
|
||||
const searchIconSrc = '/assets/images/material-symbols_search-rounded.svg';
|
||||
const caretDownSrc = '/assets/images/CaretDown-black.svg';
|
||||
|
||||
|
||||
// Custom Toast Component
|
||||
const CustomToast = ({ message, type, onClose }) => {
|
||||
React.useEffect(() => {
|
||||
const timer = setTimeout(() => {
|
||||
onClose();
|
||||
}, 3000);
|
||||
|
||||
return () => clearTimeout(timer);
|
||||
}, [onClose]);
|
||||
|
||||
const getToastStyles = () => {
|
||||
switch (type) {
|
||||
case 'success':
|
||||
return 'bg-green-50 border-green-200 text-green-800';
|
||||
case 'error':
|
||||
return 'bg-red-50 border-red-200 text-red-800';
|
||||
case 'warning':
|
||||
return 'bg-yellow-50 border-yellow-200 text-yellow-800';
|
||||
default:
|
||||
return 'bg-blue-50 border-blue-200 text-blue-800';
|
||||
}
|
||||
};
|
||||
|
||||
const getIcon = () => {
|
||||
switch (type) {
|
||||
case 'success':
|
||||
return '✅';
|
||||
case 'error':
|
||||
return '❌';
|
||||
case 'warning':
|
||||
return '⚠️';
|
||||
default:
|
||||
return 'ℹ️';
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={`flex items-center p-4 mb-4 rounded-lg border ${getToastStyles()} shadow-lg min-w-80 max-w-md`}>
|
||||
<span className="text-lg mr-3">{getIcon()}</span>
|
||||
<span className="flex-1 text-sm font-medium">{message}</span>
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="ml-4 text-gray-400 hover:text-gray-600 transition-colors"
|
||||
>
|
||||
✕
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const createEmptyQuarterForm = () => ({
|
||||
survey_name: '',
|
||||
establishment: '-', // Default to hyphen
|
||||
@ -24,7 +73,7 @@ const createEmptyQuarterForm = () => ({
|
||||
submissionCount: '',
|
||||
status: 'Active', // Default to Active
|
||||
});
|
||||
|
||||
|
||||
const QuarterlyWindows = () => {
|
||||
const [quarterData, setQuarterData] = React.useState([]);
|
||||
const [editingRow, setEditingRow] = React.useState(null);
|
||||
@ -36,24 +85,25 @@ const QuarterlyWindows = () => {
|
||||
const [quarterFilter, setQuarterFilter] = React.useState('all');
|
||||
const [hoveredEdit, setHoveredEdit] = React.useState(null);
|
||||
const [currentPage, setCurrentPage] = React.useState(1);
|
||||
const pageSize = 10;
|
||||
const [pageSize, setPageSize] = React.useState(10);
|
||||
const [isLoading, setIsLoading] = React.useState(false);
|
||||
const [toast, setToast] = React.useState({ show: false, message: '', type: 'success' });
|
||||
|
||||
const [validationErrors, setValidationErrors] = React.useState({});
|
||||
const [toastData, setToastData] = React.useState(null);
|
||||
|
||||
// Format date to yyyy-MM-dd for input fields
|
||||
const formatDateForInput = (dateString) => {
|
||||
if (!dateString) return '';
|
||||
const date = new Date(dateString);
|
||||
return date.toISOString().split('T')[0];
|
||||
};
|
||||
|
||||
|
||||
// Format date for display
|
||||
const formatDateForDisplay = (dateString) => {
|
||||
if (!dateString) return '';
|
||||
const date = new Date(dateString);
|
||||
return date.toLocaleDateString('en-GB');
|
||||
};
|
||||
|
||||
|
||||
// Fetch quarterly windows data on component mount
|
||||
React.useEffect(() => {
|
||||
const fetchQuarterlyWindows = async () => {
|
||||
@ -80,19 +130,17 @@ const QuarterlyWindows = () => {
|
||||
setQuarterData(formattedData);
|
||||
} else {
|
||||
console.error('Unexpected API response format:', response);
|
||||
// Optionally set some error state here
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error fetching quarterly windows:', error);
|
||||
// You might want to add error handling UI here
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
fetchQuarterlyWindows();
|
||||
}, []);
|
||||
|
||||
|
||||
// Table headers with left alignment and no wrapping
|
||||
const headers = [
|
||||
{ name: 'Survey Name', className: 'whitespace-nowrap text-left' },
|
||||
@ -107,29 +155,30 @@ const QuarterlyWindows = () => {
|
||||
{ name: 'Submission Count', className: 'whitespace-nowrap text-left' },
|
||||
{ name: 'Actions', className: 'whitespace-nowrap text-left' },
|
||||
];
|
||||
// Column widths significantly increased for better content visibility
|
||||
|
||||
const columnwidth = [
|
||||
220, // Survey Name (reduced from 550)
|
||||
100, // Year (reduced from 150)
|
||||
120, // Quarter (reduced from 180)
|
||||
150, // Start Date (reduced from 220)
|
||||
150, // End Date (reduced from 220)
|
||||
180, // Grace Period (reduced from 250)
|
||||
120, // Assigned (reduced from 180)
|
||||
120, // Responded (reduced from 200)
|
||||
140, // Not Responded (reduced from 220)
|
||||
140, // Submission Count (reduced from 240)
|
||||
120 // Actions (reduced from 160)
|
||||
220, // Survey Name
|
||||
100, // Year
|
||||
120, // Quarter
|
||||
150, // Start Date
|
||||
150, // End Date
|
||||
180, // Grace Period
|
||||
120, // Assigned
|
||||
120, // Responded
|
||||
140, // Not Responded
|
||||
140, // Submission Count
|
||||
120 // Actions
|
||||
];
|
||||
|
||||
const filteredRows = React.useMemo(() => {
|
||||
const term = searchTerm.trim().toLowerCase();
|
||||
return quarterData.filter((item) => {
|
||||
// Apply year filter
|
||||
if (yearFilter !== 'all' && item.year !== yearFilter) return false;
|
||||
|
||||
|
||||
// Apply quarter filter
|
||||
if (quarterFilter !== 'all' && item.quarter !== quarterFilter) return false;
|
||||
|
||||
|
||||
// Apply search term
|
||||
if (!term) return true;
|
||||
const haystack = [
|
||||
@ -151,21 +200,21 @@ const QuarterlyWindows = () => {
|
||||
return haystack.includes(term);
|
||||
});
|
||||
}, [quarterData, searchTerm, yearFilter, quarterFilter]);
|
||||
|
||||
|
||||
const Tooltip = ({ children, text }) => (
|
||||
<div className="flex items-center gap-1 group relative">
|
||||
<span>{children}</span>
|
||||
<div className="relative group cursor-pointer" title={text}>
|
||||
<img
|
||||
src="/assets/images/tooltip.svg"
|
||||
alt="info"
|
||||
className="w-4 h-4"
|
||||
<img
|
||||
src="/assets/images/tooltip.svg"
|
||||
alt="info"
|
||||
className="w-4 h-4"
|
||||
style={{ filter: 'invert(45%) sepia(6%) saturate(323%) hue-rotate(175deg) brightness(93%) contrast(82%)' }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
|
||||
const rows = filteredRows.map((item) => [
|
||||
item.survey_name || '-',
|
||||
item.year,
|
||||
@ -185,25 +234,24 @@ const QuarterlyWindows = () => {
|
||||
item.submissionCount,
|
||||
'actions',
|
||||
]);
|
||||
|
||||
|
||||
|
||||
// Generate year options from 2022 to current year, plus any additional years in data
|
||||
const yearOptions = React.useMemo(() => {
|
||||
const currentYear = new Date().getFullYear();
|
||||
const years = new Set(['all']);
|
||||
|
||||
|
||||
// Add years from 2022 to current year
|
||||
for (let y = 2022; y <= currentYear; y++) {
|
||||
years.add(y.toString());
|
||||
}
|
||||
|
||||
|
||||
// Add any additional years that might be in the data
|
||||
quarterData.forEach(item => {
|
||||
if (item.year && !years.has(item.year)) {
|
||||
years.add(item.year);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
// Sort years in descending order (newest first)
|
||||
return Array.from(years).sort((a, b) => {
|
||||
if (a === 'all') return -1;
|
||||
@ -211,21 +259,22 @@ const QuarterlyWindows = () => {
|
||||
return parseInt(b) - parseInt(a);
|
||||
});
|
||||
}, [quarterData]);
|
||||
|
||||
|
||||
const quarterOptions = ['all', 'Q1', 'Q2', 'Q3', 'Q4'];
|
||||
|
||||
|
||||
const openAddModal = () => {
|
||||
setForm(createEmptyQuarterForm());
|
||||
setOriginalForm(null);
|
||||
setEditingRow(null);
|
||||
setModalMode('add');
|
||||
setValidationErrors({});
|
||||
};
|
||||
|
||||
|
||||
React.useEffect(() => {
|
||||
const maxPage = Math.max(1, Math.ceil(filteredRows.length / pageSize));
|
||||
setCurrentPage((prev) => Math.min(prev, maxPage));
|
||||
}, [filteredRows.length, pageSize]);
|
||||
|
||||
|
||||
const openEditModal = (index) => {
|
||||
const selected = quarterData[index];
|
||||
// Ensure dates are in the correct format for the form inputs
|
||||
@ -238,13 +287,15 @@ const QuarterlyWindows = () => {
|
||||
setForm(formData);
|
||||
setOriginalForm(formData);
|
||||
setModalMode('edit');
|
||||
setValidationErrors({});
|
||||
};
|
||||
|
||||
|
||||
const handleCloseModal = () => {
|
||||
setModalMode(null);
|
||||
setEditingRow(null);
|
||||
setValidationErrors({});
|
||||
};
|
||||
|
||||
|
||||
const handleReset = () => {
|
||||
if (modalMode === 'edit' && editingRow !== null) {
|
||||
if (originalForm) {
|
||||
@ -258,45 +309,62 @@ const QuarterlyWindows = () => {
|
||||
} else {
|
||||
setForm(createEmptyQuarterForm());
|
||||
}
|
||||
setValidationErrors({});
|
||||
};
|
||||
|
||||
|
||||
|
||||
const showToast = (message, type = 'success') => {
|
||||
setToast({ show: true, message, type });
|
||||
setTimeout(() => setToast(prev => ({ ...prev, show: false })), 4000);
|
||||
};
|
||||
|
||||
const handleSave = async () => {
|
||||
try {
|
||||
if (!form.survey_name || !form.year || !form.quarter || !form.startDate || !form.endDate) {
|
||||
showToast('Please fill in all required fields', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
const validateForm = () => {
|
||||
const errors = {};
|
||||
|
||||
if (!form.survey_name.trim()) {
|
||||
errors.survey_name = 'Survey Name is required';
|
||||
}
|
||||
if (!form.year) {
|
||||
errors.year = 'Year is required';
|
||||
}
|
||||
if (!form.quarter) {
|
||||
errors.quarter = 'Quarter is required';
|
||||
}
|
||||
if (!form.startDate) {
|
||||
errors.startDate = 'Open Date is required';
|
||||
}
|
||||
if (!form.endDate) {
|
||||
errors.endDate = 'Close Date is required';
|
||||
}
|
||||
|
||||
// Date validation
|
||||
if (form.startDate && form.endDate) {
|
||||
const startDate = new Date(form.startDate);
|
||||
const endDate = new Date(form.endDate);
|
||||
if (endDate <= startDate) {
|
||||
showToast('Close Date must be after Open Date', 'error');
|
||||
return;
|
||||
errors.endDate = 'Close Date must be after Open Date';
|
||||
}
|
||||
|
||||
const formatDateForAPI = (dateString) => {
|
||||
if (!dateString) return '';
|
||||
// If already in YYYY-MM-DD format
|
||||
if (dateString.match(/^\d{4}-\d{2}-\d{2}$/)) {
|
||||
return dateString;
|
||||
}
|
||||
// Handle DD/MM/YYYY format
|
||||
if (dateString.includes('/')) {
|
||||
const [day, month, year] = dateString.split('/');
|
||||
return `${year}-${month.padStart(2, '0')}-${day.padStart(2, '0')}`;
|
||||
}
|
||||
// For any other format, let the Date object handle it
|
||||
return new Date(dateString).toISOString().split('T')[0];
|
||||
};
|
||||
|
||||
|
||||
}
|
||||
|
||||
setValidationErrors(errors);
|
||||
return Object.keys(errors).length === 0;
|
||||
};
|
||||
|
||||
const handleFormChange = (field) => (value) => {
|
||||
setForm(prev => ({ ...prev, [field]: value }));
|
||||
|
||||
// Clear validation error when user starts typing
|
||||
if (validationErrors[field]) {
|
||||
setValidationErrors(prev => ({
|
||||
...prev,
|
||||
[field]: ''
|
||||
}));
|
||||
}
|
||||
};
|
||||
|
||||
const handleSave = async () => {
|
||||
try {
|
||||
if (!validateForm()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const startDate = new Date(form.startDate);
|
||||
const endDate = new Date(form.endDate);
|
||||
|
||||
const formattedData = {
|
||||
survey_name: form.survey_name || '',
|
||||
year: form.year ? parseInt(form.year, 10) : 0,
|
||||
@ -307,17 +375,16 @@ const QuarterlyWindows = () => {
|
||||
assigned: form.assigned || 0,
|
||||
responded: form.responded || 0,
|
||||
not_responded: form.not_responded || 0,
|
||||
// submission_count: form.submissionCount || 0,
|
||||
is_active: form.status === 'Active',
|
||||
establishment: form.establishment || '-',
|
||||
};
|
||||
|
||||
|
||||
if (modalMode === 'add') {
|
||||
const response = await createQuarterlyWindow(formattedData);
|
||||
if (response.status === 'success') {
|
||||
const newItem = {
|
||||
...formattedData,
|
||||
id: response.data.id || Date.now(), // Use the ID from response or generate a temporary one
|
||||
id: response.data.id || Date.now(),
|
||||
startDate: form.startDate,
|
||||
endDate: form.endDate,
|
||||
gracePeriod: formattedData.grace_periods_days ? `${formattedData.grace_periods_days} days` : '0 days',
|
||||
@ -325,21 +392,26 @@ const QuarterlyWindows = () => {
|
||||
status: formattedData.is_active ? 'Active' : 'Inactive'
|
||||
};
|
||||
setQuarterData((prev) => [...prev, newItem]);
|
||||
showToast('Quarterly window added successfully!');
|
||||
|
||||
// Show success toast for add
|
||||
setToastData({
|
||||
message: 'Quarterly survey created successfully!',
|
||||
type: 'success'
|
||||
});
|
||||
} else {
|
||||
showToast('Failed to add quarterly window', 'error');
|
||||
console.error('Failed to add quarterly window');
|
||||
}
|
||||
} else if (modalMode === 'edit' && editingRow !== null) {
|
||||
const itemId = quarterData[editingRow]?.id;
|
||||
if (!itemId) {
|
||||
showToast('Error: Could not find the item to update', 'error');
|
||||
console.error('Error: Could not find the item to update');
|
||||
return;
|
||||
}
|
||||
const response = await updateQuarterlyWindow(itemId, formattedData);
|
||||
if (response.status === 'success') {
|
||||
setQuarterData((prev) =>
|
||||
prev.map((item, idx) =>
|
||||
idx === editingRow
|
||||
setQuarterData((prev) =>
|
||||
prev.map((item, idx) =>
|
||||
idx === editingRow
|
||||
? {
|
||||
...item,
|
||||
...formattedData,
|
||||
@ -347,34 +419,41 @@ const QuarterlyWindows = () => {
|
||||
endDate: form.endDate,
|
||||
gracePeriod: formattedData.grace_periods_days ? `${formattedData.grace_periods_days} days` : '0 days',
|
||||
status: formattedData.is_active ? 'Active' : 'Inactive'
|
||||
}
|
||||
}
|
||||
: item
|
||||
)
|
||||
);
|
||||
showToast('Quarterly window updated successfully!');
|
||||
|
||||
// Show success toast for edit
|
||||
setToastData({
|
||||
message: 'Quarterly survey updated successfully!',
|
||||
type: 'success'
|
||||
});
|
||||
} else {
|
||||
showToast('Failed to update quarterly window', 'error');
|
||||
console.error('Failed to update quarterly window');
|
||||
}
|
||||
}
|
||||
handleCloseModal();
|
||||
} catch (error) {
|
||||
console.error('Error saving quarterly window:', error);
|
||||
showToast('An error occurred while saving. Please try again.', 'error');
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
return (
|
||||
<>
|
||||
{toast.show && (
|
||||
<div className="fixed top-4 right-4 z-50">
|
||||
<CustomToast
|
||||
message={toast.message}
|
||||
type={toast.type}
|
||||
onClose={() => setToast({ ...toast, show: false })}
|
||||
/>
|
||||
<div className="w-full max-w-[1280px] rounded-lg border border-[#C3C6CB] bg-white min-h-[348px] pb-6">
|
||||
{/* Custom Toast Notification */}
|
||||
{toastData && (
|
||||
<div className="fixed inset-0 z-[9999] pointer-events-none flex items-start justify-center pt-20">
|
||||
<div className="pointer-events-auto">
|
||||
<CustomToast
|
||||
message={toastData.message}
|
||||
type={toastData.type}
|
||||
onClose={() => setToastData(null)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<div className="w-full max-w-[1280px] rounded-lg border border-[#C3C6CB] bg-white min-h-[348px] pb-6">
|
||||
|
||||
<div className="px-6 py-4">
|
||||
<div className="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>
|
||||
@ -425,40 +504,40 @@ const QuarterlyWindows = () => {
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
if (!filteredRows.length) return;
|
||||
const csvHeader = headers
|
||||
.filter((h) => h.name !== 'Actions')
|
||||
.map((h) => `"${h.name}"`)
|
||||
.join(',');
|
||||
const csvRows = filteredRows.map((item) => {
|
||||
const rowData = [
|
||||
item.survey_name || '',
|
||||
item.year || '',
|
||||
item.quarter || '',
|
||||
item.startDate || '',
|
||||
item.endDate || '',
|
||||
item.gracePeriod || '',
|
||||
item.assigned || '',
|
||||
item.responded || '',
|
||||
item.not_responded || '',
|
||||
item.submissionCount || '',
|
||||
item.status || ''
|
||||
];
|
||||
return rowData.map((val) => `"${String(val).replace(/"/g, '""')}"`).join(',');
|
||||
});
|
||||
const blob = new Blob([csvHeader + '\n' + csvRows.join('\n')], {
|
||||
type: 'text/csv;charset=utf-8;',
|
||||
});
|
||||
const url = URL.createObjectURL(blob);
|
||||
const link = document.createElement('a');
|
||||
link.href = url;
|
||||
link.setAttribute('download', 'quarterly-windows.csv');
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
URL.revokeObjectURL(url);
|
||||
}}
|
||||
onClick={() => {
|
||||
if (!filteredRows.length) return;
|
||||
const csvHeader = headers
|
||||
.filter((h) => h.name !== 'Actions')
|
||||
.map((h) => `"${h.name}"`)
|
||||
.join(',');
|
||||
const csvRows = filteredRows.map((item) => {
|
||||
const rowData = [
|
||||
item.survey_name || '',
|
||||
item.year || '',
|
||||
item.quarter || '',
|
||||
item.startDate || '',
|
||||
item.endDate || '',
|
||||
item.gracePeriod || '',
|
||||
item.assigned || '',
|
||||
item.responded || '',
|
||||
item.not_responded || '',
|
||||
item.submissionCount || '',
|
||||
item.status || ''
|
||||
];
|
||||
return rowData.map((val) => `"${String(val).replace(/"/g, '""')}"`).join(',');
|
||||
});
|
||||
const blob = new Blob([csvHeader + '\n' + csvRows.join('\n')], {
|
||||
type: 'text/csv;charset=utf-8;',
|
||||
});
|
||||
const url = URL.createObjectURL(blob);
|
||||
const link = document.createElement('a');
|
||||
link.href = url;
|
||||
link.setAttribute('download', 'quarterly-windows.csv');
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
URL.revokeObjectURL(url);
|
||||
}}
|
||||
className={`h-10 px-5 rounded-[6px] border text-sm inline-flex items-center gap-3 ${
|
||||
filteredRows.length
|
||||
? 'bg-[#F7F7F7] border-[#C3C6CB] text-[#232528]'
|
||||
@ -481,7 +560,7 @@ const QuarterlyWindows = () => {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div className="w-full" style={{ minWidth: '1200px' }}>
|
||||
<Table
|
||||
headers={headers}
|
||||
@ -515,7 +594,6 @@ const QuarterlyWindows = () => {
|
||||
currentPage,
|
||||
onPageChange: (page) => {
|
||||
setCurrentPage(page);
|
||||
// Scroll to top when changing pages
|
||||
window.scrollTo({ top: 0, behavior: 'smooth' });
|
||||
},
|
||||
pageSize,
|
||||
@ -523,12 +601,12 @@ const QuarterlyWindows = () => {
|
||||
pageSizeOptions: [10, 20, 50, 100],
|
||||
onPageSizeChange: (size) => {
|
||||
setPageSize(size);
|
||||
setCurrentPage(1); // Reset to first page when changing page size
|
||||
setCurrentPage(1);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
||||
{modalMode && (
|
||||
<div className="fixed inset-0 z-50">
|
||||
<div className="absolute inset-0 bg-black/40" onClick={handleCloseModal} />
|
||||
@ -541,28 +619,6 @@ const QuarterlyWindows = () => {
|
||||
border: '1px solid #E5E7EB',
|
||||
}}
|
||||
>
|
||||
{/* Toast Notification */}
|
||||
{toast.show && (
|
||||
<div className="flex items-center gap-2 bg-[#FEF2F2] border-l-4 border-[#DC2626] p-3 w-[calc(100%-32px)] mx-auto mt-4 rounded">
|
||||
<svg
|
||||
className="flex-shrink-0 w-4 h-4 text-[#DC2626]"
|
||||
fill="currentColor"
|
||||
viewBox="0 0 20 20"
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z"
|
||||
clipRule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
<span className="text-sm font-medium text-[#DC2626] leading-tight">
|
||||
{toast.message}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
||||
|
||||
<div className="flex items-center justify-between px-6 py-4 border-b border-[#F1F2F4]">
|
||||
<h3 className="text-[18px] font-medium text-[#232528]">
|
||||
{modalMode === 'edit' ? 'Edit Quarter' : 'Create Quarterly Survey'}
|
||||
@ -595,70 +651,110 @@ const QuarterlyWindows = () => {
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div className="px-6 py-5 space-y-4">
|
||||
<div className="w-full">
|
||||
<TextField
|
||||
label="Survey Name"
|
||||
label={
|
||||
<>
|
||||
Survey Name <span className="text-red-500"></span>
|
||||
</>
|
||||
}
|
||||
value={form.survey_name}
|
||||
required
|
||||
onChange={(e) => setForm({ ...form, survey_name: e.target.value })}
|
||||
onChange={(e) => handleFormChange('survey_name')(e.target.value)}
|
||||
placeholder="Enter survey name"
|
||||
// error={validationErrors.survey_name}
|
||||
/>
|
||||
</div>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<SelectField
|
||||
label="Quarter"
|
||||
value={form.quarter}
|
||||
required
|
||||
onChange={(e) => setForm({ ...form, quarter: e.target.value })}
|
||||
options={['Q1', 'Q2', 'Q3', 'Q4'].map((q) => ({ label: q, value: q }))}
|
||||
placeholder="Select Quarter"
|
||||
/>
|
||||
<SelectField
|
||||
label="Year"
|
||||
value={form.year}
|
||||
onChange={(e) => setForm({ ...form, year: e.target.value })}
|
||||
options={Array.from({ length: 6 }).map((_, idx) => {
|
||||
const yr = 2023 + idx;
|
||||
return { label: String(yr), value: String(yr) };
|
||||
})}
|
||||
required
|
||||
placeholder="Select Year"
|
||||
/>
|
||||
<DateField
|
||||
label="Opens On"
|
||||
value={form.startDate}
|
||||
required
|
||||
onChange={(e) => setForm({ ...form, startDate: e.target.value })}
|
||||
placeholder="Select Date"
|
||||
/>
|
||||
<DateField
|
||||
label="Closes On"
|
||||
value={form.endDate}
|
||||
required
|
||||
onChange={(e) => setForm({ ...form, endDate: e.target.value })}
|
||||
placeholder="Select Date"
|
||||
/>
|
||||
{validationErrors.survey_name && (
|
||||
<p className="mt-1 text-sm text-red-600">{validationErrors.survey_name}</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* <SelectField
|
||||
label="Status"
|
||||
value={form.status}
|
||||
required
|
||||
onChange={(e) => setForm({ ...form, status: e.target.value })}
|
||||
options={[
|
||||
{ label: 'Active', value: 'Active' },
|
||||
{ label: 'Inactive', value: 'Inactive' },
|
||||
]}
|
||||
placeholder="Select Status"
|
||||
/> */}
|
||||
{/* </div> */}
|
||||
<div className="w-full">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<SelectField
|
||||
label={
|
||||
<>
|
||||
Quarter <span className="text-red-500"></span>
|
||||
</>
|
||||
}
|
||||
value={form.quarter}
|
||||
required
|
||||
onChange={(e) => handleFormChange('quarter')(e.target.value)}
|
||||
options={['Q1', 'Q2', 'Q3', 'Q4'].map((q) => ({ label: q, value: q }))}
|
||||
placeholder="Select Quarter"
|
||||
// error={validationErrors.quarter}
|
||||
/>
|
||||
{validationErrors.quarter && (
|
||||
<p className="mt-1 text-sm text-red-600">{validationErrors.quarter}</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<SelectField
|
||||
label={
|
||||
<>
|
||||
Year <span className="text-red-500"></span>
|
||||
</>
|
||||
}
|
||||
value={form.year}
|
||||
onChange={(e) => handleFormChange('year')(e.target.value)}
|
||||
options={Array.from({ length: 6 }).map((_, idx) => {
|
||||
const yr = 2023 + idx;
|
||||
return { label: String(yr), value: String(yr) };
|
||||
})}
|
||||
required
|
||||
placeholder="Select Year"
|
||||
// error={validationErrors.year}
|
||||
/>
|
||||
{validationErrors.year && (
|
||||
<p className="mt-1 text-sm text-red-600">{validationErrors.year}</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<DateField
|
||||
label={
|
||||
<>
|
||||
Opens On <span className="text-red-500"></span>
|
||||
</>
|
||||
}
|
||||
value={form.startDate}
|
||||
required
|
||||
onChange={(e) => handleFormChange('startDate')(e.target.value)}
|
||||
placeholder="Select Date"
|
||||
error={validationErrors.startDate}
|
||||
/>
|
||||
{validationErrors.startDate && (
|
||||
<p className="mt-1 text-sm text-red-600">{validationErrors.startDate}</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<DateField
|
||||
label={
|
||||
<>
|
||||
Closes On <span className="text-red-500"></span>
|
||||
</>
|
||||
}
|
||||
value={form.endDate}
|
||||
required
|
||||
onChange={(e) => handleFormChange('endDate')(e.target.value)}
|
||||
placeholder="Select Date"
|
||||
error={validationErrors.endDate}
|
||||
/>
|
||||
{validationErrors.endDate && (
|
||||
<p className="mt-1 text-sm text-red-600">{validationErrors.endDate}</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="w-full">
|
||||
<SelectField
|
||||
label="Grace Period (Days)"
|
||||
value={form.gracePeriod}
|
||||
onChange={(e) => setForm({ ...form, gracePeriod: e.target.value })}
|
||||
onChange={(e) => handleFormChange('gracePeriod')(e.target.value)}
|
||||
options={['5 days', '10 days', '15 days', '20 days', '30 days'].map((item) => ({
|
||||
label: item,
|
||||
value: item,
|
||||
@ -666,7 +762,7 @@ const QuarterlyWindows = () => {
|
||||
placeholder="Select Grace Period"
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
||||
<div className="mt-6 flex items-center justify-end gap-3">
|
||||
<button
|
||||
type="button"
|
||||
@ -687,10 +783,8 @@ const QuarterlyWindows = () => {
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default QuarterlyWindows;
|
||||
|
||||
export default QuarterlyWindows;
|
||||
@ -3,7 +3,7 @@ import Table from '@/components/common/Table';
|
||||
import StatusBadge from '@/components/common/StatusBadge';
|
||||
import { TextField, SelectField } from '@/components/common/FormControls';
|
||||
import { getUnits, createUnit, updateUnit, deleteUnit } from '@/services/configuration/unitService';
|
||||
|
||||
|
||||
const downloadIconSrc = '/assets/images/DownloadSimple.svg';
|
||||
const addIconSrc = '/assets/images/ic_baseline-plus.svg';
|
||||
const pencilActiveSrc = '/assets/images/PencilSimple.svg';
|
||||
@ -14,18 +14,17 @@ const deleteIconSrc = '/assets/images/delete.svg';
|
||||
const caretDownIconSrc = '/assets/images/caretdown-active.svg';
|
||||
const rectangleIconSrc = '/assets/images/Rectangle.svg';
|
||||
const checkboxIconSrc = '/assets/images/checkbox.svg';
|
||||
|
||||
|
||||
|
||||
// Custom Toast Component
|
||||
const CustomToast = ({ message, type, onClose }) => {
|
||||
useEffect(() => {
|
||||
const timer = setTimeout(() => {
|
||||
onClose();
|
||||
}, 3000);
|
||||
|
||||
|
||||
return () => clearTimeout(timer);
|
||||
}, [onClose]);
|
||||
|
||||
|
||||
const getToastStyles = () => {
|
||||
switch (type) {
|
||||
case 'success':
|
||||
@ -38,7 +37,7 @@ const CustomToast = ({ message, type, onClose }) => {
|
||||
return 'bg-blue-50 border-blue-200 text-blue-800';
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
const getIcon = () => {
|
||||
switch (type) {
|
||||
case 'success':
|
||||
@ -51,7 +50,7 @@ const CustomToast = ({ message, type, onClose }) => {
|
||||
return 'ℹ️';
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
return (
|
||||
<div className={`flex items-center p-4 mb-4 rounded-lg border ${getToastStyles()} shadow-lg min-w-80 max-w-md`}>
|
||||
<span className="text-lg mr-3">{getIcon()}</span>
|
||||
@ -65,14 +64,14 @@ const CustomToast = ({ message, type, onClose }) => {
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
const createEmptyUnitForm = () => ({
|
||||
unitName: '',
|
||||
description: '',
|
||||
productsMapped: [],
|
||||
status: '',
|
||||
});
|
||||
|
||||
|
||||
const UnitMaster = () => {
|
||||
const [units, setUnits] = useState([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
@ -81,30 +80,30 @@ const UnitMaster = () => {
|
||||
const [modalMode, setModalMode] = useState(null);
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
const [form, setForm] = useState(createEmptyUnitForm());
|
||||
const [formErrors, setFormErrors] = useState({});
|
||||
const [isProductDropdownOpen, setIsProductDropdownOpen] = useState(false);
|
||||
const productDropdownRef = useRef(null);
|
||||
const pageSize = 10;
|
||||
const [currentUser, setCurrentUser] = useState(null);
|
||||
const [toastData, setToastData] = useState(null);
|
||||
const [searchTerm, setSearchTerm] = React.useState('');
|
||||
|
||||
|
||||
|
||||
// Fetch units on component mount
|
||||
useEffect(() => {
|
||||
fetchUnits();
|
||||
}, []);
|
||||
|
||||
|
||||
const fetchUnits = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const response = await getUnits();
|
||||
const unitsData = response.data || [];
|
||||
|
||||
|
||||
// ✅ Sort by latest created_at date (newest first)
|
||||
const sortedUnits = [...unitsData].sort(
|
||||
(a, b) => new Date(b.created_at) - new Date(a.created_at)
|
||||
);
|
||||
|
||||
|
||||
setUnits(sortedUnits);
|
||||
} catch (error) {
|
||||
console.error('Error fetching units:', error);
|
||||
@ -112,23 +111,24 @@ const UnitMaster = () => {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
const filteredUnits = React.useMemo(() => {
|
||||
if (!searchTerm) return units;
|
||||
const term = searchTerm.toLowerCase();
|
||||
return units.filter(unit =>
|
||||
(unit.uom && unit.uom.toLowerCase().includes(term)) ||
|
||||
(unit.uom_short_name && unit.uom_short_name.toLowerCase().includes(term)) ||
|
||||
(unit.description && unit.description.toLowerCase().includes(term))
|
||||
);
|
||||
}, [units, searchTerm]);
|
||||
if (!searchTerm) return units;
|
||||
const term = searchTerm.toLowerCase();
|
||||
return units.filter(unit =>
|
||||
(unit.uom && unit.uom.toLowerCase().includes(term)) ||
|
||||
(unit.uom_short_name && unit.uom_short_name.toLowerCase().includes(term)) ||
|
||||
(unit.description && unit.description.toLowerCase().includes(term))
|
||||
);
|
||||
}, [units, searchTerm]);
|
||||
|
||||
useEffect(() => {
|
||||
const userProfile = sessionStorage.getItem('user_profile');
|
||||
if (userProfile) {
|
||||
setCurrentUser(JSON.parse(userProfile));
|
||||
}
|
||||
}, []);
|
||||
|
||||
|
||||
const headers = [
|
||||
'Unit Name',
|
||||
'Description',
|
||||
@ -138,7 +138,7 @@ const UnitMaster = () => {
|
||||
'Last Updated',
|
||||
'Actions',
|
||||
];
|
||||
|
||||
|
||||
const rows = useMemo(() => {
|
||||
return units.map((item) => {
|
||||
const createdDate = item.created_at
|
||||
@ -149,12 +149,12 @@ const UnitMaster = () => {
|
||||
? new Date(item.updated_at).toLocaleDateString('en-GB')
|
||||
: '-';
|
||||
const currentUserName = currentUser?.name || '';
|
||||
|
||||
|
||||
return [
|
||||
item.uom || item.unitName,
|
||||
item.uom_short_name || item.description,
|
||||
item.mapped_products_count !== undefined
|
||||
? item.mapped_products_count
|
||||
item.mapped_products_count !== undefined
|
||||
? item.mapped_products_count
|
||||
: (Array.isArray(item.productsMapped) ? item.productsMapped.length : 0),
|
||||
item.created_by_name || currentUserName || '-',
|
||||
createdDate,
|
||||
@ -162,8 +162,8 @@ const UnitMaster = () => {
|
||||
'actions',
|
||||
];
|
||||
});
|
||||
}, [units, currentUser,filteredUnits]);
|
||||
|
||||
}, [units, currentUser, filteredUnits]);
|
||||
|
||||
const statusOptions = React.useMemo(
|
||||
() => [
|
||||
{ label: 'Active', value: 'Active' },
|
||||
@ -171,32 +171,34 @@ const UnitMaster = () => {
|
||||
],
|
||||
[]
|
||||
);
|
||||
|
||||
// const productOptions = React.useMemo(
|
||||
// () => [
|
||||
// { label: 'Wheat', value: 'Wheat' },
|
||||
// { label: 'Barley', value: 'Barley' },
|
||||
// { label: 'Oats', value: 'Oats' },
|
||||
// { label: 'Rice', value: 'Rice' },
|
||||
// { label: 'Corn', value: 'Corn' },
|
||||
// { label: 'Soybeans', value: 'Soybeans' },
|
||||
// { label: 'Sorghum', value: 'Sorghum' },
|
||||
// { label: 'Millet', value: 'Millet' },
|
||||
// { label: 'Quinoa', value: 'Quinoa' },
|
||||
// { label: 'Buckwheat', value: 'Buckwheat' },
|
||||
// ],
|
||||
// []
|
||||
// );
|
||||
|
||||
|
||||
const filteredRows = units;
|
||||
|
||||
|
||||
const validateForm = () => {
|
||||
const errors = {};
|
||||
|
||||
// Unit Name validation
|
||||
if (!form.unitName.trim()) {
|
||||
errors.unitName = 'Unit Name is required';
|
||||
}
|
||||
|
||||
// Description validation - red color validation
|
||||
if (!form.description.trim()) {
|
||||
errors.description = 'Please enter a description.';
|
||||
}
|
||||
|
||||
setFormErrors(errors);
|
||||
return Object.keys(errors).length === 0;
|
||||
};
|
||||
|
||||
const openAddModal = () => {
|
||||
setEditingRow(null);
|
||||
setForm(createEmptyUnitForm());
|
||||
setFormErrors({});
|
||||
setModalMode('add');
|
||||
setIsProductDropdownOpen(false);
|
||||
};
|
||||
|
||||
|
||||
const handleEdit = (index) => {
|
||||
const selected = units[index];
|
||||
if (!selected) return;
|
||||
@ -207,26 +209,36 @@ const UnitMaster = () => {
|
||||
productsMapped: Array.isArray(selected.productsMapped) ? [...selected.productsMapped] : [],
|
||||
status: selected.is_active ? 'Active' : 'Inactive',
|
||||
});
|
||||
setFormErrors({});
|
||||
setModalMode('edit');
|
||||
setIsProductDropdownOpen(false);
|
||||
};
|
||||
|
||||
|
||||
const handleDelete = (index) => {
|
||||
setDeletingRow(index);
|
||||
};
|
||||
|
||||
|
||||
const closeModal = () => {
|
||||
setModalMode(null);
|
||||
setForm(createEmptyUnitForm());
|
||||
setFormErrors({});
|
||||
setEditingRow(null);
|
||||
setIsProductDropdownOpen(false);
|
||||
};
|
||||
|
||||
|
||||
const handleFormChange = (field) => (event) => {
|
||||
const value = event?.target?.value ?? event;
|
||||
setForm((prev) => ({ ...prev, [field]: value }));
|
||||
|
||||
// Clear error when user starts typing
|
||||
if (formErrors[field]) {
|
||||
setFormErrors(prev => ({
|
||||
...prev,
|
||||
[field]: ''
|
||||
}));
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
const handleProductToggle = (value) => {
|
||||
setForm((prev) => {
|
||||
const current = Array.isArray(prev.productsMapped) ? prev.productsMapped : [];
|
||||
@ -237,7 +249,7 @@ const UnitMaster = () => {
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
React.useEffect(() => {
|
||||
const handleClickOutside = (event) => {
|
||||
if (!productDropdownRef.current) return;
|
||||
@ -245,41 +257,30 @@ const UnitMaster = () => {
|
||||
setIsProductDropdownOpen(false);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
if (isProductDropdownOpen) {
|
||||
document.addEventListener('mousedown', handleClickOutside);
|
||||
}
|
||||
|
||||
|
||||
return () => {
|
||||
document.removeEventListener('mousedown', handleClickOutside);
|
||||
};
|
||||
}, [isProductDropdownOpen]);
|
||||
|
||||
|
||||
const handleSave = async () => {
|
||||
// Validate form before saving
|
||||
if (!validateForm()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const selectedProducts = Array.isArray(form.productsMapped) ? form.productsMapped : [];
|
||||
console.log(selectedProducts,"selectedProducts");
|
||||
// ✅ Validation checks
|
||||
if (!form.unitName) {
|
||||
setToastData({
|
||||
message: 'Please enter Unit Name.',
|
||||
type: 'error'
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (!form.description) {
|
||||
setToastData({
|
||||
message: 'Please enter Description.',
|
||||
type: 'error'
|
||||
});
|
||||
return;
|
||||
}
|
||||
// Status validation removed as per request
|
||||
|
||||
|
||||
// ✅ Check for duplicate unit name
|
||||
const nameExists = units.some(
|
||||
unit => unit.uom?.toLowerCase().trim() === form.unitName.toLowerCase().trim()
|
||||
);
|
||||
|
||||
|
||||
// Only check duplicates when adding (not editing)
|
||||
if (modalMode !== 'edit' && nameExists) {
|
||||
setToastData({
|
||||
@ -288,7 +289,7 @@ const UnitMaster = () => {
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
const unitData = {
|
||||
uom: form.unitName,
|
||||
uom_short_name: form.description,
|
||||
@ -296,19 +297,19 @@ const UnitMaster = () => {
|
||||
productsMapped: selectedProducts.length,
|
||||
};
|
||||
console.log("unitData",unitData)
|
||||
|
||||
|
||||
try {
|
||||
setLoading(true);
|
||||
|
||||
|
||||
if (modalMode === 'edit' && editingRow !== null) {
|
||||
const updatedUnitData = {
|
||||
...unitData,
|
||||
updated_at: new Date().toISOString(),
|
||||
updated_by: currentUser?.id || null,
|
||||
};
|
||||
|
||||
|
||||
await updateUnit(units[editingRow].id, updatedUnitData);
|
||||
|
||||
|
||||
// ✅ Show success message for edit
|
||||
setToastData({
|
||||
message: 'Unit updated successfully!',
|
||||
@ -322,14 +323,14 @@ const UnitMaster = () => {
|
||||
created_by: currentUser?.id || null,
|
||||
created_by_name: currentUser?.name || '',
|
||||
});
|
||||
|
||||
|
||||
// Show success message for add
|
||||
setToastData({
|
||||
message: 'New unit added successfully!',
|
||||
type: 'success'
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
await fetchUnits();
|
||||
closeModal();
|
||||
} catch (error) {
|
||||
@ -342,14 +343,14 @@ const UnitMaster = () => {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
const handleDeleteConfirm = async () => {
|
||||
if (deletingRow === null) return;
|
||||
try {
|
||||
const unitToDelete = units[deletingRow];
|
||||
await deleteUnit(unitToDelete.id);
|
||||
setUnits(prev => prev.filter((_, index) => index !== deletingRow));
|
||||
|
||||
|
||||
// ✅ Show success message for delete
|
||||
setToastData({
|
||||
message: 'Unit deleted successfully!',
|
||||
@ -364,47 +365,30 @@ const UnitMaster = () => {
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
const closeDeleteConfirm = () => {
|
||||
setDeletingRow(null);
|
||||
};
|
||||
|
||||
|
||||
const deletingUnit = React.useMemo(() => {
|
||||
if (deletingRow === null || deletingRow < 0 || deletingRow >= units.length) {
|
||||
return null;
|
||||
}
|
||||
return units[deletingRow];
|
||||
}, [deletingRow, units]);
|
||||
|
||||
|
||||
return (
|
||||
<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]">
|
||||
<h3 className="text-[16px] font-medium text-[#232528]">
|
||||
Unit Master ({units.length}{units.length === 1 ? ' unit' : ''})
|
||||
</h3>
|
||||
{/* <div className="flex-1 max-w-md">
|
||||
<div className="relative">
|
||||
<div className="absolute left-3 top-1/2 -translate-y-1/2">
|
||||
<img src={searchIconSrc} alt="Search" className="h-4 w-4 text-[#5F646D]" />
|
||||
</div>
|
||||
<input
|
||||
type="text"
|
||||
value={searchTerm}
|
||||
onChange={(e) => setSearchTerm(e.target.value)}
|
||||
placeholder="Search by code or name"
|
||||
className="h-10 w-full rounded-md border border-[#C3C6CB] pl-9 pr-3 text-sm focus:outline-none focus:ring-1 focus:ring-[#92722A]"
|
||||
/>
|
||||
</div>
|
||||
</div> */}
|
||||
|
||||
|
||||
{/* </div> */}
|
||||
<h3 className="text-[16px] font-medium text-[#232528]">
|
||||
Unit Master ({units.length}{units.length === 1 ? ' unit' : ''})
|
||||
</h3>
|
||||
<div className="flex items-center gap-2">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
if (!units.length) return;
|
||||
|
||||
|
||||
const csvHeader = headers.slice(0, headers.length - 1).join(',');
|
||||
const csvRows = units.map((item) => {
|
||||
const createdDate = item.created_at
|
||||
@ -418,7 +402,7 @@ const UnitMaster = () => {
|
||||
const mappedProducts = Array.isArray(item.productsMapped)
|
||||
? item.productsMapped.join('; ')
|
||||
: '0';
|
||||
|
||||
|
||||
return [
|
||||
item.uom || item.unitName || '',
|
||||
item.uom_short_name || item.description || '',
|
||||
@ -431,7 +415,7 @@ const UnitMaster = () => {
|
||||
.map((value) => `"${String(value ?? '').replace(/"/g, '""')}"`)
|
||||
.join(',');
|
||||
});
|
||||
|
||||
|
||||
const csvContent = csvHeader + '\n' + csvRows.join('\n');
|
||||
const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
@ -453,7 +437,7 @@ const UnitMaster = () => {
|
||||
<img src={downloadIconSrc} alt="Export" className="h-5 w-5" />
|
||||
<span className="font-medium">Export CSV</span>
|
||||
</button>
|
||||
|
||||
|
||||
<button
|
||||
type="button"
|
||||
className="h-10 px-4 rounded-[6px] bg-[#92722A] text-white text-sm inline-flex items-center gap-2 cursor-pointer"
|
||||
@ -464,7 +448,7 @@ const UnitMaster = () => {
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
{loading ? (
|
||||
<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>
|
||||
@ -516,7 +500,7 @@ const UnitMaster = () => {
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
|
||||
{/* Add/Edit Modal */}
|
||||
{modalMode && (
|
||||
<div className="fixed inset-0 z-50">
|
||||
@ -545,86 +529,45 @@ const UnitMaster = () => {
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
|
||||
<div className="px-6 py-5">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<TextField
|
||||
label={
|
||||
<>
|
||||
Unit Name <span className="text-red-500">*</span>
|
||||
</>
|
||||
}
|
||||
value={form.unitName}
|
||||
onChange={handleFormChange('unitName')}
|
||||
placeholder="Enter Unit Name"
|
||||
/>
|
||||
<TextField
|
||||
label="Description"
|
||||
value={form.description}
|
||||
onChange={handleFormChange('description')}
|
||||
placeholder="Enter Description"
|
||||
width="100%"
|
||||
/>
|
||||
{/* <div className="w-full" ref={productDropdownRef}>
|
||||
<label className="block text-[14px] leading-[20px] font-medium text-[#232528] mb-1">Mapped Products</label>
|
||||
<button
|
||||
type="button"
|
||||
className={`w-full h-10 rounded-[4px] border-2 px-4 text-sm flex items-center justify-between transition-colors ${
|
||||
isProductDropdownOpen ? 'border-[#92722A]' : 'border-[#CBA344]'
|
||||
}`}
|
||||
onClick={() => setIsProductDropdownOpen((prev) => !prev)}
|
||||
>
|
||||
<span className={`${form.productsMapped.length ? 'text-[#232528]' : 'text-[#6B7280]'}`}>
|
||||
{form.productsMapped.length
|
||||
? `${form.productsMapped.length} product${form.productsMapped.length > 1 ? 's' : ''} selected`
|
||||
: 'Select Products'}
|
||||
</span>
|
||||
<img
|
||||
src={caretDownIconSrc}
|
||||
alt="Toggle"
|
||||
className={`h-4 w-4 transition-transform ${isProductDropdownOpen ? 'rotate-180' : ''}`}
|
||||
/>
|
||||
</button>
|
||||
{isProductDropdownOpen && (
|
||||
<div className="mt-2 max-h-64 overflow-auto rounded-lg border border-[#E5E7EB] bg-white shadow-[0_12px_24px_rgba(27,29,33,0.12)]">
|
||||
<ul className="py-2">
|
||||
{productOptions.map((option) => {
|
||||
const checked = form.productsMapped.includes(option.value);
|
||||
return (
|
||||
<li key={option.value}>
|
||||
<button
|
||||
type="button"
|
||||
className={`flex w-full items-center gap-3 px-4 py-2 text-left text-sm transition-colors ${
|
||||
checked ? 'bg-[#FDF8EA]' : 'hover:bg-[#F5F5F5]'
|
||||
}`}
|
||||
onClick={() => handleProductToggle(option.value)}
|
||||
>
|
||||
<span className="relative flex h-5 w-5 items-center justify-center">
|
||||
<img src={checked ? checkboxIconSrc : rectangleIconSrc} alt="" className="h-5 w-5" />
|
||||
</span>
|
||||
<span className="text-[#232528]">{option.label}</span>
|
||||
</button>
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
</div>
|
||||
<div>
|
||||
<TextField
|
||||
label={
|
||||
<>
|
||||
Unit Name <span className="text-red-500">*</span>
|
||||
</>
|
||||
}
|
||||
value={form.unitName}
|
||||
onChange={handleFormChange('unitName')}
|
||||
placeholder="Enter Unit Name"
|
||||
// error={formErrors.unitName}
|
||||
/>
|
||||
{formErrors.unitName && (
|
||||
<p className="mt-1 text-sm text-red-600">{formErrors.unitName}</p>
|
||||
)}
|
||||
</div> */}
|
||||
|
||||
{/* <SelectField
|
||||
label={
|
||||
<>
|
||||
Status <span className="text-red-500">*</span>
|
||||
</>
|
||||
}
|
||||
value={form.status}
|
||||
onChange={handleFormChange('status')}
|
||||
options={statusOptions}
|
||||
placeholder="Select Status"
|
||||
/> */}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<TextField
|
||||
label={
|
||||
<>
|
||||
Description <span className="text-red-500">*</span>
|
||||
</>
|
||||
}
|
||||
value={form.description}
|
||||
onChange={handleFormChange('description')}
|
||||
placeholder="Enter Description"
|
||||
width="100%"
|
||||
// error={formErrors.description}
|
||||
/>
|
||||
{formErrors.description && (
|
||||
<p className="mt-1 text-sm text-red-600">{formErrors.description}</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div className="mt-6 flex items-center justify-end gap-3">
|
||||
<button
|
||||
type="button"
|
||||
@ -651,7 +594,7 @@ const UnitMaster = () => {
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
||||
{/* Delete Confirmation Modal */}
|
||||
{deletingUnit && (
|
||||
<div className="fixed inset-0 z-50">
|
||||
@ -687,7 +630,7 @@ const UnitMaster = () => {
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div className="flex items-center justify-end gap-4">
|
||||
<button
|
||||
type="button"
|
||||
@ -708,22 +651,21 @@ const UnitMaster = () => {
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
||||
{/* Custom Toast Notification */}
|
||||
{/* Custom Toast Notification - CENTERED */}
|
||||
{toastData && (
|
||||
<div className="fixed inset-0 z-[9999] pointer-events-none flex items-start justify-center pt-20">
|
||||
<div className="pointer-events-auto">
|
||||
<CustomToast
|
||||
message={toastData.message}
|
||||
type={toastData.type}
|
||||
onClose={() => setToastData(null)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{toastData && (
|
||||
<div className="fixed inset-0 z-[9999] pointer-events-none flex items-start justify-center pt-20">
|
||||
<div className="pointer-events-auto">
|
||||
<CustomToast
|
||||
message={toastData.message}
|
||||
type={toastData.type}
|
||||
onClose={() => setToastData(null)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
export default UnitMaster;
|
||||
@ -1,8 +1,7 @@
|
||||
// src/services/configuration/unitService.js
|
||||
import { getRequest, postRequest, putRequest, deleteRequest } from '@/services/api/CommonService';
|
||||
|
||||
|
||||
const admin = '/admin_users';
|
||||
|
||||
|
||||
export const getAdminUser = async () => {
|
||||
try {
|
||||
const response = await getRequest(admin);
|
||||
@ -12,7 +11,7 @@ export const getAdminUser = async () => {
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
export const createadminusers = async (admindata) => {
|
||||
try {
|
||||
const response = await postRequest(admin, admindata);
|
||||
@ -22,7 +21,7 @@ export const createadminusers = async (admindata) => {
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
export const updateAdminUser = async (id, admindata) => {
|
||||
try {
|
||||
const response = await putRequest(`${admin}/${id}`, admindata);
|
||||
@ -41,7 +40,73 @@ export const getAdminUserById = async (id) => {
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
export const changeAdminUserPassword = async (id, passwordData) => {
|
||||
try {
|
||||
if (!id) throw new Error("Invalid user ID");
|
||||
|
||||
console.log('🔍 Debug: Changing password for admin user:', id);
|
||||
console.log('🔍 Debug: Password data received:', {
|
||||
hasOldPassword: !!passwordData.old_password,
|
||||
hasNewPassword: !!passwordData.new_password,
|
||||
hasConfirmPassword: !!passwordData.confirm_password
|
||||
});
|
||||
|
||||
// Prepare the request data according to API specification
|
||||
const requestData = {
|
||||
old_password: passwordData.old_password,
|
||||
new_password: passwordData.new_password,
|
||||
confirm_password: passwordData.confirm_password
|
||||
};
|
||||
|
||||
console.log('🔍 Debug: Final request data being sent:', requestData);
|
||||
|
||||
// Use the correct endpoint for admin users
|
||||
// const response = await putRequest(`/admin_users/${id}/change-password`, requestData);
|
||||
const response = await putRequest(`admin_users/${id}/change-password`, requestData);
|
||||
|
||||
console.log('🔍 Debug: Admin password change successful:', response.data);
|
||||
return response.data;
|
||||
|
||||
} catch (error) {
|
||||
console.error("❌ Error changing admin user password:", error);
|
||||
|
||||
if (error.response) {
|
||||
console.error("🔍 Debug: API error response:", {
|
||||
status: error.response.status,
|
||||
data: error.response.data,
|
||||
headers: error.response.headers
|
||||
});
|
||||
|
||||
// Extract meaningful error message
|
||||
let errorMessage = 'Failed to change password';
|
||||
|
||||
if (error.response.status === 404) {
|
||||
errorMessage = 'Admin user not found or endpoint unavailable';
|
||||
} else if (error.response.data.message) {
|
||||
errorMessage = error.response.data.message;
|
||||
} else if (error.response.data.errors) {
|
||||
// Handle Laravel validation errors
|
||||
const errors = error.response.data.errors;
|
||||
errorMessage = Object.values(errors).flat().join(', ');
|
||||
} else if (error.response.data.error) {
|
||||
errorMessage = error.response.data.error;
|
||||
}
|
||||
|
||||
const detailedError = new Error(errorMessage);
|
||||
detailedError.status = error.response.status;
|
||||
detailedError.data = error.response.data;
|
||||
|
||||
throw detailedError;
|
||||
} else if (error.request) {
|
||||
console.error("🔍 Debug: No response received:", error.request);
|
||||
throw new Error('No response from server. Please check your connection.');
|
||||
} else {
|
||||
console.error("🔍 Debug: Request setup error:", error.message);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
};
|
||||
// export const deleteAdminUser = async (id) => {
|
||||
// try {
|
||||
// const response = await deleteRequest(`${admin}/${id}`);
|
||||
@ -51,57 +116,7 @@ export const getAdminUserById = async (id) => {
|
||||
// throw error;
|
||||
// }
|
||||
// };
|
||||
export const changeAdminUserPassword = async (id, passwordData) => {
|
||||
try {
|
||||
if (!id) throw new Error("Invalid user ID");
|
||||
|
||||
// Debug: see what fields the API expects
|
||||
console.log("🔍 Debug: Making test request to discover required fields");
|
||||
|
||||
const testData = { test_field: "test_value" };
|
||||
|
||||
try {
|
||||
await putRequest(`/admin/${id}/change-password`, testData);
|
||||
} catch (testError) {
|
||||
console.log(
|
||||
"🔍 Debug: Test request failed with:",
|
||||
testError.response?.data || testError.message
|
||||
);
|
||||
}
|
||||
|
||||
// Common Laravel-style field names
|
||||
const requestData = {
|
||||
password: passwordData.newPassword,
|
||||
password_confirmation: passwordData.confirmPassword,
|
||||
new_password: passwordData.newPassword,
|
||||
new_password_confirmation: passwordData.confirmPassword,
|
||||
};
|
||||
|
||||
console.log("🔍 Debug: Final request data:", requestData);
|
||||
|
||||
const response = await putRequest(`/admin/${id}/change-password`, requestData);
|
||||
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
console.error("Error changing admin user password:", error);
|
||||
|
||||
if (error.response) {
|
||||
console.error("🔍 Debug: Full error response:", {
|
||||
status: error.response.status,
|
||||
data: error.response.data,
|
||||
headers: error.response.headers,
|
||||
});
|
||||
|
||||
if (error.response.data.errors) {
|
||||
console.error("🔍 Debug: Validation errors:", error.response.data.errors);
|
||||
}
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
|
||||
export const deleteAdminUser = async (id) => {
|
||||
try {
|
||||
if (!id) throw new Error("Invalid user ID");
|
||||
@ -128,8 +143,5 @@ export const deleteAdminUser = async (id) => {
|
||||
console.error("Error deleting admin user:", error);
|
||||
throw error;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
};
|
||||
};
|
||||
|
||||
Loading…
Reference in New Issue
Block a user