Merge branch 'master' of https://bitbucket.org/jubilian/fcsc_ipi_frontend
This commit is contained in:
commit
6157d92d1b
@ -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={
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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">
|
||||
|
||||
@ -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
|
||||
}
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user