This commit is contained in:
Senthamilselvi 2025-11-05 14:23:52 +05:30
commit 6157d92d1b
4 changed files with 120 additions and 257 deletions

View File

@ -226,7 +226,7 @@ function App() {
</RequireRole> </RequireRole>
} }
/> />
<Route path="/admin/configuration"> {/* <Route path="/admin/configuration">
<Route <Route
index index
element={ element={
@ -235,7 +235,14 @@ function App() {
</RequireRole> </RequireRole>
} }
/> />
</Route> </Route> */}
<Route path="/admin/configuration/*" element={
<RequireRole allowedRoles={['Admin', 'EstablishmentUser']}>
<Configuration />
</RequireRole>
} />
{/* /> */}
{/* </Route> */}
<Route <Route
path="/admin/users" path="/admin/users"
element={ element={

View File

@ -135,16 +135,16 @@ const Table = ({
'h-9 px-2 text-sm text-[#232528] disabled:text-[#C3C6CB] disabled:cursor-not-allowed transition-colors flex items-center gap-2 hover:text-[#92722A]'; 'h-9 px-2 text-sm text-[#232528] disabled:text-[#C3C6CB] disabled:cursor-not-allowed transition-colors flex items-center gap-2 hover:text-[#92722A]';
return ( return (
<div className={`rounded-lg bg-white ${className}`}> <div className={`overflow-hidden rounded-lg bg-white ${className}`}>
{beforeHeader} {beforeHeader}
<div <div
className="w-full overflow-x-auto px-6 pt-2 pb-6" className={`${disableHorizontalScroll ? 'overflow-x-hidden' : 'overflow-x-auto'} px-6 pt-2 pb-6`}
style={{ scrollbarWidth: 'thin' }} style={disableHorizontalScroll ? undefined : { scrollbarWidth: 'thin' }}
> >
<table <table
className={`min-w-full w-auto table-fixed${ className={`min-w-[720px] w-full table-fixed rounded-2xl px-4 py-4${
separated ? ` border-separate ${rowGapClass}` : ' divide-y divide-[#C3C6CB]' separated ? `border-separate ${rowGapClass}` : 'divide-y'
} border border-[#C3C6CB] rounded-2xl`} } border border-[#C3C6CB] rounded-2xl divide-[#C3C6CB]`}
> >
<thead className={headerClassName || 'bg-[#F2ECCF] rounded-2xl'}> <thead className={headerClassName || 'bg-[#F2ECCF] rounded-2xl'}>
<tr> <tr>

View File

@ -33,20 +33,20 @@ const Configuration = () => {
}, [active, navigate]); }, [active, navigate]);
// Update active tab based on URL // Update active tab based on URL
useEffect(() => { useEffect(() => {
const pathToTab = { const pathToTab = {
'quarterly': 'Quarterly Windows', 'quarterly': 'Quarterly Windows',
'hscodes': 'HS Codes', 'hscodes': 'HS Codes',
'unitmaster': 'Unit Master', 'unitmaster': 'Unit Master',
'admin-users': 'Admin Users', 'admin-users': 'Admin Users',
'profile': 'Company Profile' 'profile': 'Company Profile'
}; };
const path = location.pathname.split('/').pop(); const path = location.pathname.split('/').pop();
if (pathToTab[path] && pathToTab[path] !== active) { if (pathToTab[path] && pathToTab[path] !== active) {
setActive(pathToTab[path]); setActive(pathToTab[path]);
} }
}, [location.pathname]); }, [location.pathname]);
const Breadcrumbs = ( const Breadcrumbs = (
<nav className="mb-6 text-sm text-[#8F9299] flex items-center gap-3"> <nav className="mb-6 text-sm text-[#8F9299] flex items-center gap-3">

View File

@ -149,7 +149,6 @@ const CompanyProfile = () => {
const [selectedEmirateFilter, setSelectedEmirateFilter] = React.useState(''); const [selectedEmirateFilter, setSelectedEmirateFilter] = React.useState('');
const [selectedStatusFilter, setSelectedStatusFilter] = React.useState(''); const [selectedStatusFilter, setSelectedStatusFilter] = React.useState('');
const [debouncedSearch, setDebouncedSearch] = React.useState(''); const [debouncedSearch, setDebouncedSearch] = React.useState('');
const [sortConfig, setSortConfig] = React.useState({ field: 'establishmentName', direction: 'asc' });
const [activeStep, setActiveStep] = React.useState(0); const [activeStep, setActiveStep] = React.useState(0);
const [confirmError, setConfirmError] = React.useState(''); const [confirmError, setConfirmError] = React.useState('');
const [passwordError, setPasswordError] = React.useState(''); const [passwordError, setPasswordError] = React.useState('');
@ -167,6 +166,7 @@ const CompanyProfile = () => {
const initialSnapshotRef = React.useRef(createEmptyProfile()); const initialSnapshotRef = React.useRef(createEmptyProfile());
const toastTimeoutRef = React.useRef(null); const toastTimeoutRef = React.useRef(null);
const [isDeletingApi, setIsDeletingApi] = React.useState(false); const [isDeletingApi, setIsDeletingApi] = React.useState(false);
const [totalItems, setTotalItems] = React.useState(0);
const showToast = React.useCallback((type, message) => { const showToast = React.useCallback((type, message) => {
if (!message) return; if (!message) return;
@ -812,112 +812,7 @@ const CompanyProfile = () => {
setActiveStep((prev) => Math.min(prev + 1, formSteps.length - 1)); setActiveStep((prev) => Math.min(prev + 1, formSteps.length - 1));
}, [activeStep, form.userProfilePassword, form.userProfileConfirmPassword, formSteps.length, validateStepFields]); }, [activeStep, form.userProfilePassword, form.userProfileConfirmPassword, formSteps.length, validateStepFields]);
const handleSort = React.useCallback( const handleToggleStatus = (item) => {
(field) => {
setSortConfig((prev) => {
if (prev.field === field) {
return { field, direction: prev.direction === 'asc' ? 'desc' : 'asc' };
}
return { field, direction: 'asc' };
});
setCurrentPage(1);
},
[setCurrentPage]
);
const renderSortableHeader = React.useCallback(
(label, field) => {
const isActive = sortConfig.field === field;
return (
<button
type="button"
onClick={() => handleSort(field)}
className="flex items-center text-left w-full group focus:outline-none"
aria-label={`Sort by ${label}`}
>
<span className={`text-xs font-medium ${isActive ? 'text-[#92722A]' : 'text-[#1B1D21]'}`}>{label}</span>
</button>
);
},
[handleSort, sortConfig]
);
const headers = [
'Establishment Name',
'Contact Name',
'Emirate',
'ISIC Code',
'Establishment ID',
'Products',
'Total Employees',
'Created By',
'Created On',
'Last Updated',
'Status',
'Actions',
];
const columnWidths = React.useMemo(() => {
const base = [
250, // Establishment Name (250px)
150, // Contact Name (150px)
120, // Emirate (120px)
120, // ISIC Code (120px)
150, // Establishment ID (150px)
100, // Products (100px)
120, // Total Employees (120px)
120, // Created By (120px)
120, // Created On (120px)
120, // Last Updated (120px)
100, // Status (100px)
150, // Actions (150px)
];
return base;
}, []);
const contactToCorporateMap = React.useMemo(
() => ({
contactName: 'corporateName',
contactAddress: 'corporateAddress',
contactCityTown: 'corporateCityTown',
contactCityTownId: 'corporateCityTownId',
contactEmirate: 'corporateEmirate',
contactEmirateId: 'corporateEmirateId',
contactPostalCode: 'corporatePostalCode',
contactPoBox: 'corporatePoBox',
contactMakaniNumber: 'corporateMakaniNumber',
contactPersonName: 'corporateContactPersonName',
contactPersonDesignation: 'corporateContactPersonDesignation',
contactCountryCode: 'corporateCountryCode',
contactMobileNumber: 'corporateMobileNumber',
contactEmail: 'corporateEmail',
contactWebsite: 'corporateWebsite',
}),
[]
);
const computeFieldErrorMessage = React.useCallback((field, value) => {
if (field === 'contactWebsite' || field === 'corporateWebsite') {
return getWebsiteValidationMessage(value);
}
return '';
}, []);
const corporateFieldKeys = React.useMemo(() => Object.values(contactToCorporateMap), [contactToCorporateMap]);
const corporateBackupRef = React.useRef(null);
const captureCorporateValues = React.useCallback(
(source) => {
const snapshot = {};
corporateFieldKeys.forEach((key) => {
snapshot[key] = source?.[key] ?? '';
});
return snapshot;
},
[corporateFieldKeys]
);
const handleToggleStatus = (item) => {
const updatedStatus = item.status === 'Active' ? 'Inactive' : 'Active'; const updatedStatus = item.status === 'Active' ? 'Inactive' : 'Active';
// Optional: call your API to update backend // Optional: call your API to update backend
@ -947,103 +842,30 @@ const CompanyProfile = () => {
.toLowerCase() .toLowerCase()
.includes(normalized) .includes(normalized)
); );
}, [profiles, debouncedSearch]); }, [profiles, debouncedSearch, selectedStatusFilter]);
const sortedProfiles = React.useMemo(() => { // Render status badge with appropriate styling
const parseDateValue = (value) => { const renderStatusBadge = (status) => {
if (!value) return 0; const statusMap = {
if (value instanceof Date) return value.getTime(); active: { text: 'Active', bgColor: 'bg-[#F3FAF4]', textColor: 'text-[#2F663C]' },
if (typeof value === 'number') return value; inactive: { text: 'Inactive', bgColor: 'bg-[#FEF2F2]', textColor: 'text-[#B91C1C]' },
if (typeof value === 'string') { pending: { text: 'Pending', bgColor: 'bg-[#FFFBEB]', textColor: 'text-[#B45309]' },
const parts = value.split(/[\/-]/).map((part) => part.trim()); // Add more status mappings as needed
if (parts.length === 3) {
const day = Number(parts[0]);
const month = Number(parts[1]);
const year = Number(parts[2]);
if ([day, month, year].every((part) => Number.isFinite(part))) {
const timestamp = Date.UTC(year, month - 1, day);
if (!Number.isNaN(timestamp)) {
return timestamp;
}
}
}
const parsed = Date.parse(value);
return Number.isNaN(parsed) ? 0 : parsed;
}
return 0;
}; };
if (!sortConfig?.field) { const statusConfig = statusMap[status?.toLowerCase()] || { text: status, bgColor: 'bg-gray-100', textColor: 'text-gray-800' };
return filteredProfiles;
}
const multiplier = sortConfig.direction === 'asc' ? 1 : -1;
const items = [...filteredProfiles];
items.sort((a, b) => {
let result = 0;
switch (sortConfig.field) {
case 'totalEmployees': {
const aValue = Number(a.totalEmployees) || 0;
const bValue = Number(b.totalEmployees) || 0;
result = aValue - bValue;
break;
}
case 'createdOn': {
const aValue = parseDateValue(a.createdOn);
const bValue = parseDateValue(b.createdOn);
result = aValue - bValue;
break;
}
case 'lastUpdated': {
const aValue = (a.lastUpdated || '').toString();
const bValue = (b.lastUpdated || '').toString();
result = aValue.localeCompare(bValue, undefined, { sensitivity: 'base' });
break;
}
default: {
const aValue = (a.establishmentName || '').toString();
const bValue = (b.establishmentName || '').toString();
result = aValue.localeCompare(bValue, undefined, { sensitivity: 'base' });
break;
}
}
if (result === 0) {
const fallbackA = (a.establishmentName || '').toString();
const fallbackB = (b.establishmentName || '').toString();
result = fallbackA.localeCompare(fallbackB, undefined, { sensitivity: 'base' });
}
return result * multiplier;
});
return items;
}, [filteredProfiles, sortConfig]);
const renderStatusBadge = React.useCallback((status) => {
const normalized = typeof status === 'string' ? status.trim().toLowerCase() : '';
const isActiveStatus = normalized !== 'inactive';
const label = isActiveStatus ? 'Active' : 'Inactive';
const baseClasses =
'inline-flex items-center justify-center px-3 py-1 text-xs font-medium rounded-md';
const activeClasses = 'text-[#2F663C] bg-[#F3FAF4]';
const inactiveClasses = 'text-[#344054] bg-[#F8F9FC] border-[#D0D5DD]';
return ( return (
<span <span className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium ${statusConfig.bgColor} ${statusConfig.textColor}`}>
className={`${baseClasses} ${isActiveStatus ? activeClasses : inactiveClasses}`} {statusConfig.text}
aria-label={`Status: ${label}`}
>
{label}
</span> </span>
); );
}, []); };
// Apply pagination to sorted profiles // Calculate pagination range
const paginatedProfiles = React.useMemo(() => { const startIndex = (currentPage - 1) * pageSize;
const startIndex = (currentPage - 1) * pageSize; const endIndex = Math.min(startIndex + pageSize, filteredProfiles.length);
const endIndex = startIndex + pageSize; const paginatedProfiles = filteredProfiles.slice(startIndex, endIndex);
return sortedProfiles.slice(startIndex, endIndex);
}, [sortedProfiles, currentPage, pageSize]);
const displayedRows = paginatedProfiles.map((item) => { const displayedRows = paginatedProfiles.map((item) => {
const isEditing = const isEditing =
@ -1326,37 +1148,39 @@ const CompanyProfile = () => {
let cancelled = false; let cancelled = false;
const controller = new AbortController(); const controller = new AbortController();
const loadProfiles = async () => { const loadProfiles = async () => {
setIsLoading(true); setIsLoading(true);
const params = { const params = {
page: exportInProgressRef.current ? 1 : currentPage, page: currentPage,
limit: exportInProgressRef.current ? pageSize : pageSize, limit: pageSize,
search: debouncedSearch || undefined, search: debouncedSearch || undefined,
emirateId: selectedEmirateFilter || undefined, emirateId: selectedEmirateFilter || undefined,
isicCode: undefined, status: selectedStatusFilter || undefined,
status: selectedStatusFilter || undefined, };
sortBy: sortConfig.field === 'establishmentName' ? 'factory_name' : sortConfig.field,
sortOrder: sortConfig.direction?.toUpperCase(), try {
}; const response = await fetchEstablishments({ params, signal: controller.signal });
try { const payload = response?.data ?? response;
const response = await fetchEstablishments({ params, signal: controller.signal }); const records = Array.isArray(payload?.data) ? payload.data : Array.isArray(payload) ? payload : [];
const payload = response?.data ?? response;
const records = Array.isArray(payload?.data) ? payload.data : Array.isArray(payload) ? payload : []; if (!cancelled) {
if (!cancelled) { setProfiles(records.map(mapApiEstablishmentToProfile));
setProfiles(records.map(mapApiEstablishmentToProfile)); // Update total items from API response or use records length as fallback
} const apiTotal = payload?.total || payload?.count || records.length;
} catch (error) { setTotalItems(apiTotal);
if (cancelled) return; }
if (error.name === 'CanceledError' || error.name === 'AbortError') return; } catch (error) {
console.error('Failed to fetch establishments list.', error); if (cancelled) return;
const message = error?.response?.data?.message || error?.message || 'Unable to load establishments. Using default data.'; if (error.name === 'CanceledError' || error.name === 'AbortError') return;
showToast('error', message); console.error('Failed to fetch establishments list.', error);
} finally { const message = error?.response?.data?.message || error?.message || 'Unable to load establishments.';
if (!cancelled) { showToast('error', message);
setIsLoading(false); } finally {
} if (!cancelled) {
} setIsLoading(false);
}; }
}
};
if (!exportInProgressRef.current) { if (!exportInProgressRef.current) {
loadProfiles(); loadProfiles();
@ -1367,7 +1191,7 @@ const CompanyProfile = () => {
controller.abort(); controller.abort();
setIsLoading(false); setIsLoading(false);
}; };
}, [currentPage, pageSize, debouncedSearch, sortConfig, selectedEmirateFilter, selectedStatusFilter, mapApiEstablishmentToProfile, showToast]); }, [currentPage, pageSize, debouncedSearch, selectedEmirateFilter, selectedStatusFilter, mapApiEstablishmentToProfile, showToast]);
const handleExport = React.useCallback(async () => { const handleExport = React.useCallback(async () => {
if (isLoading) return; if (isLoading) return;
@ -1380,8 +1204,6 @@ const CompanyProfile = () => {
search: debouncedSearch || undefined, search: debouncedSearch || undefined,
emirateId: selectedEmirateFilter || undefined, emirateId: selectedEmirateFilter || undefined,
status: selectedStatusFilter || undefined, status: selectedStatusFilter || undefined,
sortBy: sortConfig.field === 'establishmentName' ? 'factory_name' : sortConfig.field,
sortOrder: sortConfig.direction?.toUpperCase(),
export: 'excel', export: 'excel',
}, },
responseType: 'blob', responseType: 'blob',
@ -1410,7 +1232,7 @@ const CompanyProfile = () => {
} finally { } finally {
exportInProgressRef.current = false; exportInProgressRef.current = false;
} }
}, [currentPage, pageSize, debouncedSearch, sortConfig, selectedEmirateFilter, selectedStatusFilter, isLoading, showToast]); }, [currentPage, pageSize, debouncedSearch, selectedEmirateFilter, selectedStatusFilter, isLoading, showToast]);
const emirateFilterOptions = React.useMemo(() => [{ label: 'All Emirates', value: '' }, ...emirateOptions], [emirateOptions]); const emirateFilterOptions = React.useMemo(() => [{ label: 'All Emirates', value: '' }, ...emirateOptions], [emirateOptions]);
@ -2072,15 +1894,49 @@ const CompanyProfile = () => {
</div> </div>
) : ( ) : (
<Table <Table
headers={headers} headers={[
'Establishment Name',
'Contact Name',
'Emirate',
'ISIC Code',
'Establishment ID',
'Products',
'Total Employees',
'Created By',
'Created On',
'Last Updated',
'Status',
'Actions',
]}
rows={displayedRows} rows={displayedRows}
columnWidths={columnWidths} columnWidths={[
250, // Establishment Name (250px)
150, // Contact Name (150px)
120, // Emirate (120px)
120, // ISIC Code (120px)
150, // Establishment ID (150px)
100, // Products (100px)
120, // Total Employees (120px)
120, // Created By (120px)
120, // Created On (120px)
120, // Last Updated (120px)
100, // Status (100px)
150, // Actions (150px)
]}
pagination={{ pagination={{
currentPage, currentPage,
onPageChange: setCurrentPage, onPageChange: (page) => {
setCurrentPage(page);
// Scroll to top when changing pages
window.scrollTo({ top: 0, behavior: 'smooth' });
},
pageSize, pageSize,
totalItems: filteredProfiles.length, totalItems: filteredProfiles.length, // Use filtered count for pagination
pageSizeOptions: [10, 20, 50, 100], pageSizeOptions: [10, 20, 50, 100],
onPageSizeChange: (size) => {
setPageSize(size);
setCurrentPage(1); // Reset to first page when changing page size
}
}} }}
/> />
)} )}