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>
}
/>
<Route path="/admin/configuration">
{/* <Route path="/admin/configuration">
<Route
index
element={
@ -235,7 +235,14 @@ function App() {
</RequireRole>
}
/>
</Route>
</Route> */}
<Route path="/admin/configuration/*" element={
<RequireRole allowedRoles={['Admin', 'EstablishmentUser']}>
<Configuration />
</RequireRole>
} />
{/* /> */}
{/* </Route> */}
<Route
path="/admin/users"
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]';
return (
<div className={`rounded-lg bg-white ${className}`}>
<div className={`overflow-hidden rounded-lg bg-white ${className}`}>
{beforeHeader}
<div
className="w-full overflow-x-auto px-6 pt-2 pb-6"
style={{ scrollbarWidth: 'thin' }}
<div
className={`${disableHorizontalScroll ? 'overflow-x-hidden' : 'overflow-x-auto'} px-6 pt-2 pb-6`}
style={disableHorizontalScroll ? undefined : { scrollbarWidth: 'thin' }}
>
<table
className={`min-w-full w-auto table-fixed${
separated ? ` border-separate ${rowGapClass}` : ' divide-y divide-[#C3C6CB]'
} border border-[#C3C6CB] rounded-2xl`}
className={`min-w-[720px] w-full table-fixed rounded-2xl px-4 py-4${
separated ? `border-separate ${rowGapClass}` : 'divide-y'
} border border-[#C3C6CB] rounded-2xl divide-[#C3C6CB]`}
>
<thead className={headerClassName || 'bg-[#F2ECCF] rounded-2xl'}>
<tr>

View File

@ -33,20 +33,20 @@ const Configuration = () => {
}, [active, navigate]);
// Update active tab based on URL
useEffect(() => {
const pathToTab = {
'quarterly': 'Quarterly Windows',
'hscodes': 'HS Codes',
'unitmaster': 'Unit Master',
'admin-users': 'Admin Users',
'profile': 'Company Profile'
};
useEffect(() => {
const pathToTab = {
'quarterly': 'Quarterly Windows',
'hscodes': 'HS Codes',
'unitmaster': 'Unit Master',
'admin-users': 'Admin Users',
'profile': 'Company Profile'
};
const path = location.pathname.split('/').pop();
if (pathToTab[path] && pathToTab[path] !== active) {
setActive(pathToTab[path]);
}
}, [location.pathname]);
const path = location.pathname.split('/').pop();
if (pathToTab[path] && pathToTab[path] !== active) {
setActive(pathToTab[path]);
}
}, [location.pathname]);
const Breadcrumbs = (
<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 [selectedStatusFilter, setSelectedStatusFilter] = React.useState('');
const [debouncedSearch, setDebouncedSearch] = React.useState('');
const [sortConfig, setSortConfig] = React.useState({ field: 'establishmentName', direction: 'asc' });
const [activeStep, setActiveStep] = React.useState(0);
const [confirmError, setConfirmError] = React.useState('');
const [passwordError, setPasswordError] = React.useState('');
@ -167,6 +166,7 @@ const CompanyProfile = () => {
const initialSnapshotRef = React.useRef(createEmptyProfile());
const toastTimeoutRef = React.useRef(null);
const [isDeletingApi, setIsDeletingApi] = React.useState(false);
const [totalItems, setTotalItems] = React.useState(0);
const showToast = React.useCallback((type, message) => {
if (!message) return;
@ -812,112 +812,7 @@ const CompanyProfile = () => {
setActiveStep((prev) => Math.min(prev + 1, formSteps.length - 1));
}, [activeStep, form.userProfilePassword, form.userProfileConfirmPassword, formSteps.length, validateStepFields]);
const handleSort = React.useCallback(
(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 handleToggleStatus = (item) => {
const updatedStatus = item.status === 'Active' ? 'Inactive' : 'Active';
// Optional: call your API to update backend
@ -947,103 +842,30 @@ const CompanyProfile = () => {
.toLowerCase()
.includes(normalized)
);
}, [profiles, debouncedSearch]);
}, [profiles, debouncedSearch, selectedStatusFilter]);
const sortedProfiles = React.useMemo(() => {
const parseDateValue = (value) => {
if (!value) return 0;
if (value instanceof Date) return value.getTime();
if (typeof value === 'number') return value;
if (typeof value === 'string') {
const parts = value.split(/[\/-]/).map((part) => part.trim());
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;
// Render status badge with appropriate styling
const renderStatusBadge = (status) => {
const statusMap = {
active: { text: 'Active', bgColor: 'bg-[#F3FAF4]', textColor: 'text-[#2F663C]' },
inactive: { text: 'Inactive', bgColor: 'bg-[#FEF2F2]', textColor: 'text-[#B91C1C]' },
pending: { text: 'Pending', bgColor: 'bg-[#FFFBEB]', textColor: 'text-[#B45309]' },
// Add more status mappings as needed
};
if (!sortConfig?.field) {
return filteredProfiles;
}
const statusConfig = statusMap[status?.toLowerCase()] || { text: status, bgColor: 'bg-gray-100', textColor: 'text-gray-800' };
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 (
<span
className={`${baseClasses} ${isActiveStatus ? activeClasses : inactiveClasses}`}
aria-label={`Status: ${label}`}
>
{label}
<span className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium ${statusConfig.bgColor} ${statusConfig.textColor}`}>
{statusConfig.text}
</span>
);
}, []);
};
// Apply pagination to sorted profiles
const paginatedProfiles = React.useMemo(() => {
const startIndex = (currentPage - 1) * pageSize;
const endIndex = startIndex + pageSize;
return sortedProfiles.slice(startIndex, endIndex);
}, [sortedProfiles, currentPage, pageSize]);
// 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 isEditing =
@ -1326,37 +1148,39 @@ const CompanyProfile = () => {
let cancelled = false;
const controller = new AbortController();
const loadProfiles = async () => {
setIsLoading(true);
const params = {
page: exportInProgressRef.current ? 1 : currentPage,
limit: exportInProgressRef.current ? pageSize : pageSize,
search: debouncedSearch || undefined,
emirateId: selectedEmirateFilter || undefined,
isicCode: 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 });
const payload = response?.data ?? response;
const records = Array.isArray(payload?.data) ? payload.data : Array.isArray(payload) ? payload : [];
if (!cancelled) {
setProfiles(records.map(mapApiEstablishmentToProfile));
}
} catch (error) {
if (cancelled) return;
if (error.name === 'CanceledError' || error.name === 'AbortError') return;
console.error('Failed to fetch establishments list.', error);
const message = error?.response?.data?.message || error?.message || 'Unable to load establishments. Using default data.';
showToast('error', message);
} finally {
if (!cancelled) {
setIsLoading(false);
}
}
};
const loadProfiles = async () => {
setIsLoading(true);
const params = {
page: currentPage,
limit: pageSize,
search: debouncedSearch || undefined,
emirateId: selectedEmirateFilter || undefined,
status: selectedStatusFilter || undefined,
};
try {
const response = await fetchEstablishments({ params, signal: controller.signal });
const payload = response?.data ?? response;
const records = Array.isArray(payload?.data) ? payload.data : Array.isArray(payload) ? payload : [];
if (!cancelled) {
setProfiles(records.map(mapApiEstablishmentToProfile));
// Update total items from API response or use records length as fallback
const apiTotal = payload?.total || payload?.count || records.length;
setTotalItems(apiTotal);
}
} catch (error) {
if (cancelled) return;
if (error.name === 'CanceledError' || error.name === 'AbortError') return;
console.error('Failed to fetch establishments list.', error);
const message = error?.response?.data?.message || error?.message || 'Unable to load establishments.';
showToast('error', message);
} finally {
if (!cancelled) {
setIsLoading(false);
}
}
};
if (!exportInProgressRef.current) {
loadProfiles();
@ -1367,7 +1191,7 @@ const CompanyProfile = () => {
controller.abort();
setIsLoading(false);
};
}, [currentPage, pageSize, debouncedSearch, sortConfig, selectedEmirateFilter, selectedStatusFilter, mapApiEstablishmentToProfile, showToast]);
}, [currentPage, pageSize, debouncedSearch, selectedEmirateFilter, selectedStatusFilter, mapApiEstablishmentToProfile, showToast]);
const handleExport = React.useCallback(async () => {
if (isLoading) return;
@ -1380,8 +1204,6 @@ const CompanyProfile = () => {
search: debouncedSearch || undefined,
emirateId: selectedEmirateFilter || undefined,
status: selectedStatusFilter || undefined,
sortBy: sortConfig.field === 'establishmentName' ? 'factory_name' : sortConfig.field,
sortOrder: sortConfig.direction?.toUpperCase(),
export: 'excel',
},
responseType: 'blob',
@ -1410,7 +1232,7 @@ const CompanyProfile = () => {
} finally {
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]);
@ -2072,15 +1894,49 @@ const CompanyProfile = () => {
</div>
) : (
<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}
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={{
currentPage,
onPageChange: setCurrentPage,
onPageChange: (page) => {
setCurrentPage(page);
// Scroll to top when changing pages
window.scrollTo({ top: 0, behavior: 'smooth' });
},
pageSize,
totalItems: filteredProfiles.length,
totalItems: filteredProfiles.length, // Use filtered count for pagination
pageSizeOptions: [10, 20, 50, 100],
onPageSizeChange: (size) => {
setPageSize(size);
setCurrentPage(1); // Reset to first page when changing page size
}
}}
/>
)}