changes in admin dasboard

This commit is contained in:
Malini 2025-11-09 21:10:27 +05:30
parent 546425a85c
commit b2a01bb935
10 changed files with 487 additions and 262 deletions

View File

@ -30,7 +30,7 @@ const SurveyCarousel = ({
setCurrentIndex((prev) => (prev === surveys.length - 1 ? 0 : prev + 1));
return (
<div className="bg-black rounded-lg shadow-sm ring-1 w-[1275px] ring-gray-200 h-[148px] mb-10 ml-2 ">
<div className="bg-black rounded-lg shadow-sm ring-1 w-[1275px] ring-gray-200 h-[148px] mt-[-10px] mb-10 ml-2 ">
<div className="rounded-none bg-[#F2ECCF] px-8 py-10 flex items-center justify-between transition-all duration-500">
<div className="flex-1">
<h3 className="text-[18px] leading-[28px] font-medium text-[#232528]">

View File

@ -30,9 +30,9 @@ export const WelcomeSection = ({ data = null, loading = false, error = '' }) =>
return (
<div className="min-h-[4rem] bg-gray-50">
<HeaderBar />
<div className="max-w-[1280px] mx-auto px-4 pt-4 ">
<div className="max-w-[1280px] mx-auto px-4 pt-4">
<div className="space-y-1">
<h2 className="w-[360px] text-[18px] pt-4 leading-[28px] font-medium text-[#232528]">
<h2 className="w-[360px] text-[18px] pt-2 leading-[28px] font-medium text-[#232528]">
Welcome{' '}
{(() => {
try {

View File

@ -127,9 +127,7 @@ const EstablishmentInfo = ({
React.useEffect(() => {
// Only update if we don't already have survey data
if (!surveyDataRef.current?.quarter && location.state?.survey) {
const { quarter, year, endDate } = location.state.survey;
console.log('Initial survey data:', { quarter, year, endDate });
const { quarter, year, endDate } = location.state.survey;
const newSurveyData = {
quarter: quarter || '',
year: year || '',
@ -198,8 +196,6 @@ const EstablishmentInfo = ({
localStorage.setItem('returnTo', window.location.pathname);
sessionStorage.setItem('edit_profile_from', 'EstablishmentUser');
console.log('Navigating to:', `/profile/edit/${establishmentId}`);
navigate(`/profile/edit/${establishmentId}`);
};
const handleEmployeeChange = (field) => (event) => {

View File

@ -336,7 +336,6 @@ const location = useLocation();
// Get survey data from navigation state if available
if (location.state?.survey) {
const { quarter, year, endDate } = location.state.survey;
console.log('Received survey datassdd:', { quarter, year, endDate });
setSurveyData({
quarter: quarter || '',
year: year || '',
@ -635,9 +634,7 @@ const location = useLocation();
};
const handleProductSelect = async (productId, establishmentId, id) => {
console.log('handleProductSelect called with:', { productId, establishmentId, id });
const handleProductSelect = async (productId, establishmentId, id) => {
if (!productId || !establishmentId) {
console.warn('Missing required parameters in handleProductSelect:', {
hasProductId: !!productId,

View File

@ -1,4 +1,4 @@
import React, { useState, useEffect } from 'react';
import React, { useState, useEffect, useRef } from 'react';
import AdminHeader from '@/components/admin/AdminHeader';
import StatCard from '@/components/admin/StatCard';
import SubmissionTable from '@/components/admin/SubmissionTable';
@ -29,8 +29,18 @@ const AdminDashboard = () => {
const [loading, setLoading] = useState(true);
const [selectedQuarter, setSelectedQuarter] = useState('All');
const [selectedYear, setSelectedYear] = useState('All');
const isMounted = useRef(false);
const prevParams = useRef({ quarter: null, year: null });
const fetchDashboardData = React.useCallback(async (quarter, year) => {
// Skip if params haven't changed
if (prevParams.current.quarter === quarter && prevParams.current.year === year) {
return;
}
// Update previous params
prevParams.current = { quarter, year };
const fetchDashboardData = async (quarter, year) => {
try {
setLoading(true);
const params = new URLSearchParams();
@ -44,13 +54,16 @@ const AdminDashboard = () => {
}
const response = await apiClient.get(`/admin_dashboard?${params.toString()}`);
if (!isMounted.current) return;
const data = response?.data?.summary || {};
const selectedQuarterFromApi = response?.data?.selected_quarter || quarter || 'All';
const selectedYearFromApi = response?.data?.selected_year || year || 'All';
const quarterlyWindowsData = response?.data?.quarterly_windows || {};
console.log("selectedQuarter",selectedQuarterFromApi)
console.log("selectedYear",selectedYearFromApi)
console.log("Fetching data for:", { quarter, year });
setSummary({
total_establishments: data.total_establishments || 0,
submitted: data.submitted || 0,
@ -60,30 +73,39 @@ const AdminDashboard = () => {
pending: data.pending || 0,
});
setQuarterlyWindows({
setQuarterlyWindows(prev => ({
...prev,
end_date: quarterlyWindowsData.end_date,
start_date: quarterlyWindowsData.start_date,
grace_periods_days: quarterlyWindowsData.grace_periods_days || 0
});
}));
setSelectedQuarter(selectedQuarterFromApi);
setSelectedYear(selectedYearFromApi);
// Only update these if they're different to prevent unnecessary re-renders
setSelectedQuarter(prev => prev !== selectedQuarterFromApi ? selectedQuarterFromApi : prev);
setSelectedYear(prev => prev !== selectedYearFromApi ? selectedYearFromApi : prev);
} catch (error) {
console.error('Error fetching dashboard summary:', error);
} finally {
setLoading(false);
if (isMounted.current) {
setLoading(false);
}
}
};
}, []);
useEffect(() => {
isMounted.current = true;
fetchDashboardData(selectedQuarter, selectedYear);
}, [selectedQuarter, selectedYear]);
return () => {
isMounted.current = false;
};
}, [fetchDashboardData, selectedQuarter, selectedYear]);
return (
<div className="min-h-screen bg-[#F6F5F1]">
<AdminHeader />
<div className="max-w-[1280px] mx-auto box-border px-4 py-8 flex flex-col gap-6">
<div className="flex items-center justify-between w-full max-w-[1280px] mx-auto mt-6 px-4">
<div className="max-w-[1280px] mx-auto box-border px-4 py-4 flex flex-col gap-4">
<div className="flex items-center justify-between w-full">
<h1 className="text-[18px] font-semibold text-[#232528]">
Admin Dashboard{' '}
<span className="font-normal text-[#5F646D]"> Survey Overview</span>

View File

@ -66,7 +66,7 @@ const ManageSubmissions = () => {
const [year, setYear] = React.useState('');
const [quarter, setQuarter] = React.useState('');
const [emirate, setEmirate] = React.useState('');
const [status, setStatus] = React.useState('');
const [status, setStatus] = React.useState('All');
const [selectedQuarter, setSelectedQuarter] = React.useState('Q1');
const [selectedYear, setSelectedYear] = React.useState(new Date().getFullYear().toString());
const [currentPage, setCurrentPage] = React.useState(1);
@ -303,8 +303,13 @@ const ManageSubmissions = () => {
}, []);
const yearOptions = React.useMemo(() => {
const years = ['All', ...new Set(submissions.map((i) => i.year))];
return years.sort((a, b) => {
const currentYear = new Date().getFullYear();
const years = [];
// Generate years from 2022 to current year + 1 (to include current year)
for (let y = 2022; y <= currentYear + 1; y++) {
years.push(y.toString());
}
return ['All', ...years].sort((a, b) => {
if (a === 'All') return -1;
if (b === 'All') return 1;
return b.localeCompare(a);
@ -313,7 +318,10 @@ const ManageSubmissions = () => {
const quarterOptions = React.useMemo(() => ['All', 'Q1', 'Q2', 'Q3', 'Q4'], []);
const emirateOptions = React.useMemo(() => ['All', ...new Set(submissions.map((i) => i.emirate))], [submissions]);
const statusOptions = React.useMemo(() => ['All', ...new Set(submissions.map((i) => i.status))], [submissions]);
const statusOptions = React.useMemo(() => {
const statuses = [...new Set(submissions.map((i) => i.status))];
return ['All', ...statuses];
}, [submissions]);
const filtered = React.useMemo(() => {
const term = search.trim().toLowerCase();

View File

@ -492,7 +492,6 @@ const CompanyProfile = () => {
onChange={handleFormChange('industryDescription')}
placeholder="Enter Description"
width="100%"
required
style={{ height: '72px' }}
/>
</div>
@ -585,7 +584,6 @@ const CompanyProfile = () => {
onChange={handleFormChange('contactMakaniNumber')}
placeholder="Enter Makani Number"
width="100%"
required
error={fieldErrors.contactMakaniNumber}
/>
<TextField
@ -1284,13 +1282,17 @@ const corporateFieldKeys = Object.values(contactToCorporateMap);
let creatorName = '-';
if (currentUser && item?.created_by && currentUser.id === item.created_by) {
// If current user is the creator, use their name
creatorName = currentUser.name || currentUser.username || `User ${item.created_by}`;
const name = currentUser.name || currentUser.username || `User ${item.created_by}`;
// Ensure proper spacing between first name and last initial
creatorName = name.replace(/([a-zA-Z])([A-Z])/g, '$1 $2');
} else if (item?.created_by) {
// If not the current user, try to get creator's name from the item or use ID as fallback
creatorName = item?.created_by_name ||
item?.created_by_user?.name ||
item?.creator?.name ||
`User ${item.created_by}`;
const name = item?.created_by_name ||
item?.created_by_user?.name ||
item?.creator?.name ||
`User ${item.created_by}`;
// Ensure proper spacing between first name and last initial
creatorName = name.replace(/([a-zA-Z])([A-Z])/g, '$1 $2');
}
console.log('Creator ID:', item?.created_by, 'Current User ID:', currentUser?.id);
@ -1894,7 +1896,11 @@ const requiredFields = [
return `${day}/${month}/${year}`;
};
const handleSaveForm = async () => {
const handleSaveForm = async (e) => {
// Prevent default form submission behavior
if (e) {
e.preventDefault();
}
if (!validateStepFields(activeStep)) {
return;
}
@ -2124,13 +2130,35 @@ const requiredFields = [
}
await new Promise((resolve) => requestAnimationFrame(resolve));
// For edits, update the local state immediately
if (modalMode === 'edit' && editingRow !== null) {
const updatedProfiles = [...profiles];
updatedProfiles[editingRow] = nextForm;
setProfiles(updatedProfiles);
} else {
setProfiles((prev) => [nextForm, ...prev]);
}
// For new records, wait for the API response before updating the UI
if (modalMode !== 'edit') {
if (apiResultData) {
// Use the API response data to update the state
const newProfile = mapApiEstablishmentToProfile(apiResultData);
setProfiles(prev => [newProfile, ...prev]);
}
} else {
// For edits, refresh the data to ensure consistency
try {
const response = await getEstablishments();
if (response?.data) {
const mappedProfiles = response.data.map(mapApiEstablishmentToProfile);
setProfiles(mappedProfiles);
}
} catch (error) {
console.error('Error refreshing company profiles:', error);
}
}
// Reset form and close modal
const emptyProfile = createEmptyProfile();
Object.assign(emptyProfile, computeEmploymentTotals(emptyProfile));
setForm(emptyProfile);
@ -2219,10 +2247,9 @@ const requiredFields = [
)}
<div className="px-6 py-3 flex items-center justify-between">
<div className="flex items-center gap-2">
<h3 className="text-[16px] font-medium text-[#232528]">Company Profile</h3>
<span className="text-sm bg-gray-100 text-gray-600 px-2 py-0.5 rounded-full">
{totalItems} {totalItems === 1 ? 'record' : 'records'}
</span>
<h3 className="text-[16px] font-medium text-[#232528]">
Company Profile ({totalItems})
</h3>
</div>
<div className="flex items-center gap-3">
<div className="relative min-w-[260px]">
@ -2328,7 +2355,7 @@ const requiredFields = [
window.scrollTo({ top: 0, behavior: 'smooth' });
},
pageSize,
totalItems: filteredProfiles.length, // Use filtered count for pagination
totalItems: totalItems, // Use the total count from API response
pageSizeOptions: [10, 20, 50, 100],
onPageSizeChange: (size) => {
setPageSize(size);

View File

@ -446,14 +446,13 @@ const IsicHsCodes = () => {
}, []);
const headers = [
'Code',
'HS Code',
'Product Name',
'Unit',
'Mapped Establishments',
'Created by',
'Estimated Mapped',
'Created By',
'Last Updated',
'Status',
'Actions',
'Action',
];
// Filter data based on search term
@ -495,7 +494,6 @@ const IsicHsCodes = () => {
item.estimatedMapped,
displayName,
item.updated || item.createdOn || '-',
item.status,
'actions',
];
});
@ -508,7 +506,6 @@ const IsicHsCodes = () => {
setIsLoading(true);
const productId = selected.id;
console.log(`Fetching product details for ID: ${productId}`);
const response = await productService.getProductById(productId);
console.log('Product details response:', response);
@ -769,8 +766,6 @@ const IsicHsCodes = () => {
};
const handleSaveForm = async () => {
console.log('handleSaveForm called with mode:', modalMode);
if (!form.code || !form.product) {
const errorMsg = 'Please fill in all required fields';
console.error(errorMsg);
@ -790,24 +785,16 @@ const IsicHsCodes = () => {
created_by: userProfile?.id || null,
...(form.description && { hs_description: form.description })
};
console.log('Saving product data:', JSON.stringify(productData, null, 2));
if (modalMode === 'edit' && editingRow !== null) {
const productId = rowsData[editingRow]?.id;
if (!productId) {
throw new Error('Product ID not found for editing');
}
console.log(`Updating product with ID: ${productId}`);
try {
const response = await productService.updateProduct(productId, productData);
console.log('Update response:', response);
const selectedUnit = unitOptions.find(u => u.value === form.unit);
console.log('Selected unit for update:', selectedUnit);
const response = await productService.updateProduct(productId, productData);
const selectedUnit = unitOptions.find(u => u.value === form.unit);
const updatedProduct = {
...rowsData[editingRow],
code: form.code,
@ -929,10 +916,25 @@ const IsicHsCodes = () => {
<div className="bg-white w-full rounded-lg shadow-sm ring-1 ring-[#E5E7EB]">
{/* Header */}
<div className="px-6 py-3 flex items-center justify-between">
<h3 className="text-[16px] font-medium text-[#232528]">HS Codes</h3>
<div className="flex items-center gap-4">
<div className="relative w-80">
<div className="p-4 md:px-6 md:py-3">
{/* Mobile Header */}
<div className="md:hidden mb-4">
<div className="flex justify-between items-center mb-3">
<h3 className="text-lg font-medium text-[#232528]">
HS Codes ({rowsData.length})
</h3>
<button
type="button"
className="h-10 w-10 flex items-center justify-center rounded-[6px] bg-[#92722A] text-white hover:bg-[#7a5f22]"
onClick={openAddModal}
aria-label="Add Code"
>
<img src={addIconSrc} alt="Add" className="h-5 w-5" />
</button>
</div>
{/* Search - Mobile */}
<div className="relative mb-3">
<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>
@ -944,114 +946,224 @@ const IsicHsCodes = () => {
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>
<button
className="h-10 px-4 rounded-[6px] bg-[#F7F7F7] border border-[#C3C6CB] text-sm inline-flex items-center gap-2 hover:bg-gray-50"
onClick={() => setShowImportModal(true)}
>
<img src={uploadImportIconSrc} alt="Import" className="h-5 w-5" />
<span className="font-medium text-[#232528]">Import CSV</span>
</button>
<button
className="h-10 px-4 rounded-[6px] bg-[#F7F7F7] border border-[#C3C6CB] text-sm inline-flex items-center gap-2 hover:bg-gray-50"
onClick={handleExportCSV}
>
<img src={downloadIconSrc} alt="Export" className="h-5 w-5" />
<span className="font-medium text-[#232528]">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 hover:bg-[#7a5f22]"
onClick={openAddModal}
>
<img src={addIconSrc} alt="Add" className="h-5 w-5" />
<span className="font-medium">Add Code</span>
</button>
{/* Action Buttons - Mobile */}
<div className="grid grid-cols-2 gap-2">
<button
className="h-10 px-3 rounded-[6px] bg-[#F7F7F7] border border-[#C3C6CB] text-sm inline-flex items-center justify-center gap-2 hover:bg-gray-50"
onClick={() => setShowImportModal(true)}
>
<img src={uploadImportIconSrc} alt="Import" className="h-4 w-4" />
<span className="text-xs font-medium text-[#232528]">Import</span>
</button>
<button
className="h-10 px-3 rounded-[6px] bg-[#F7F7F7] border border-[#C3C6CB] text-sm inline-flex items-center justify-center gap-2 hover:bg-gray-50"
onClick={handleExportCSV}
>
<img src={downloadIconSrc} alt="Export" className="h-4 w-4" />
<span className="text-xs font-medium text-[#232528]">Export</span>
</button>
</div>
</div>
{/* Desktop Header */}
<div className="hidden md:flex items-center justify-between">
<h3 className="text-[16px] font-medium text-[#232528]">
HS Codes ({rowsData.length})
</h3>
<div className="flex flex-wrap items-center gap-3">
<div className="relative w-60 xl:w-80">
<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 className="flex items-center gap-2">
<button
className="h-10 px-3 xl:px-4 rounded-[6px] bg-[#F7F7F7] border border-[#C3C6CB] text-sm inline-flex items-center gap-2 hover:bg-gray-50 whitespace-nowrap"
onClick={() => setShowImportModal(true)}
>
<img src={uploadImportIconSrc} alt="Import" className="h-5 w-5" />
<span className="hidden xl:inline font-medium text-[#232528]">Import CSV</span>
<span className="xl:hidden font-medium text-[#232528]">Import</span>
</button>
<button
className="h-10 px-3 xl:px-4 rounded-[6px] bg-[#F7F7F7] border border-[#C3C6CB] text-sm inline-flex items-center gap-2 hover:bg-gray-50 whitespace-nowrap"
onClick={handleExportCSV}
>
<img src={downloadIconSrc} alt="Export" className="h-5 w-5" />
<span className="hidden xl:inline font-medium text-[#232528]">Export CSV</span>
<span className="xl:hidden font-medium text-[#232528]">Export</span>
</button>
<button
type="button"
className="h-10 px-3 xl:px-4 rounded-[6px] bg-[#92722A] text-white text-sm inline-flex items-center gap-2 hover:bg-[#7a5f22] whitespace-nowrap"
onClick={openAddModal}
>
<img src={addIconSrc} alt="Add" className="h-5 w-5" />
<span className="font-medium">Add Code</span>
</button>
</div>
</div>
</div>
</div>
{/* Table */}
<Table
headers={headers}
rows={rows}
renderCell={(value, rowIndex, colIndex) => {
if (colIndex === 6) {
return (
<StatusBadge
status={value}
tone={value === 'Active' ? 'green' : 'gray'}
/>
);
}
{/* Table - Desktop */}
<div className="hidden md:block">
<Table
headers={headers}
rows={rows}
renderCell={(value, rowIndex, colIndex) => {
if (colIndex === 6) {
return (
<div className="flex items-center gap-2">
<button
type="button"
className="h-8 w-8 grid place-items-center rounded hover:bg-gray-50 cursor-pointer"
title="Edit"
aria-label="Edit code"
onClick={() => handleEdit(rowIndex)}
onMouseEnter={() => setEditingRow(rowIndex)}
onMouseLeave={() => setEditingRow(null)}
>
<img
src={editingRow === rowIndex ? pencilActiveSrc : pencilInactiveSrc}
alt="Edit"
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"
title="View"
aria-label="View code"
onClick={() => handleView(rowIndex)}
>
<img
src={eyeIconSrc}
alt="View"
className="h-4 w-4"
/>
</button>
<button
type="button"
className="h-8 w-8 flex items-center justify-center rounded hover:bg-gray-50 cursor-pointer focus:outline-none"
title="Delete"
aria-label="Delete code"
onClick={(e) => {
e.stopPropagation();
handleDelete(rowIndex);
}}
onMouseEnter={() => setDeletingRow(rowIndex)}
onMouseLeave={() => setDeletingRow(null)}
>
<img
src={deletingRow === rowIndex ? trashActiveSrc : trashInactiveSrc}
alt="Delete"
className="h-4 w-4 pointer-events-none"
/>
</button>
</div>
);
}
return value;
}}
pagination={{
currentPage,
onPageChange: setCurrentPage,
pageSize,
totalItems: filteredData.length,
}}
/>
</div>
if (colIndex === 7) {
return (
<div className="flex items-center gap-2">
<button
type="button"
className="h-8 w-8 grid place-items-center rounded hover:bg-gray-50 cursor-pointer"
title="Edit"
aria-label="Edit code"
onClick={() => handleEdit(rowIndex)}
onMouseEnter={() => setEditingRow(rowIndex)}
onMouseLeave={() => setEditingRow(null)}
>
<img
src={editingRow === rowIndex ? pencilActiveSrc : pencilInactiveSrc}
alt="Edit"
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"
title="View"
aria-label="View code"
onClick={() => handleView(rowIndex)}
>
<img
src={eyeIconSrc}
alt="View"
className="h-4 w-4"
/>
</button>
<button
type="button"
className="h-8 w-8 flex items-center justify-center rounded hover:bg-gray-50 cursor-pointer focus:outline-none"
title="Delete"
aria-label="Delete code"
onClick={(e) => {
e.stopPropagation();
console.log('Delete button clicked for row:', rowIndex);
handleDelete(rowIndex);
}}
onMouseEnter={() => setDeletingRow(rowIndex)}
onMouseLeave={() => setDeletingRow(null)}
>
<img
src={deletingRow === rowIndex ? trashActiveSrc : trashInactiveSrc}
alt="Delete"
className="h-4 w-4 pointer-events-none"
/>
</button>
{/* Mobile List View */}
<div className="md:hidden">
{rows.length === 0 ? (
<div className="p-4 text-center text-gray-500">No HS Codes found</div>
) : (
<div className="divide-y divide-gray-200">
{rows.map((row, rowIndex) => (
<div key={rowIndex} className="p-4 hover:bg-gray-50">
<div className="flex justify-between items-start">
<div>
<div className="font-medium text-gray-900">{row[1]}</div>
<div className="text-sm text-gray-500">{row[0]}</div>
<div className="mt-1 text-sm">
<span className="text-gray-500">Unit: </span>
<span className="font-medium">{row[2]}</span>
</div>
<div className="text-sm">
<span className="text-gray-500">Mapped: </span>
<span className="font-medium">{row[3]}</span>
</div>
</div>
<div className="flex items-center gap-2">
<button
type="button"
className="h-8 w-8 grid place-items-center rounded hover:bg-gray-100"
onClick={() => handleView(rowIndex)}
aria-label="View"
>
<img src={eyeIconSrc} alt="View" className="h-4 w-4" />
</button>
<button
type="button"
className="h-8 w-8 grid place-items-center rounded hover:bg-gray-100"
onClick={(e) => {
e.stopPropagation();
handleEdit(rowIndex);
}}
aria-label="Edit"
>
<img src={pencilInactiveSrc} alt="Edit" className="h-4 w-4" />
</button>
</div>
</div>
</div>
);
}
return value;
}}
pagination={{
currentPage,
onPageChange: setCurrentPage,
pageSize,
totalItems: filteredData.length,
}}
/>
))}
</div>
)}
{/* Mobile Pagination */}
{rows.length > 0 && (
<div className="px-4 py-3 flex items-center justify-between border-t border-gray-200">
<div className="flex-1 flex justify-between items-center">
<button
type="button"
onClick={() => setCurrentPage(prev => Math.max(prev - 1, 1))}
disabled={currentPage === 1}
className={`relative inline-flex items-center px-3 py-2 rounded-l-md border border-gray-300 bg-white text-sm font-medium ${currentPage === 1 ? 'text-gray-300 cursor-not-allowed' : 'text-gray-700 hover:bg-gray-50'}`}
>
Previous
</button>
<span className="text-sm text-gray-700">
Page {currentPage} of {Math.ceil(filteredData.length / pageSize)}
</span>
<button
type="button"
onClick={() => setCurrentPage(prev => Math.min(prev + 1, Math.ceil(filteredData.length / pageSize)))}
disabled={currentPage * pageSize >= filteredData.length}
className={`relative inline-flex items-center px-3 py-2 rounded-r-md border border-gray-300 bg-white text-sm font-medium ${currentPage * pageSize >= filteredData.length ? 'text-gray-300 cursor-not-allowed' : 'text-gray-700 hover:bg-gray-50'}`}
>
Next
</button>
</div>
</div>
)}
</div>
{/* Add/Edit/View Modal */}
{modalMode && (
<div className="fixed inset-0 z-50">
<div className="absolute inset-0 bg-black/40" onClick={closeModal} />
<div className="absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 w-[520px] bg-white rounded-lg shadow-xl border border-[#E5E7EB] max-h-[90vh] overflow-y-auto">
<div className="absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 w-[90%] sm:w-[520px] bg-white rounded-lg shadow-xl border border-[#E5E7EB] max-h-[90vh] overflow-y-auto">
{/* Header */}
<div className="flex items-center justify-between p-6 border-b border-[#E5E7EB]">
<h3 className="text-lg font-semibold text-[#111827]">
@ -1162,9 +1274,9 @@ const IsicHsCodes = () => {
<button
type="button"
onClick={handleSaveForm}
disabled={!form.code || !form.product || !form.unit || !form.status}
disabled={!form.code || !form.product || !form.unit}
className={`h-10 px-6 text-sm font-medium text-white rounded-md focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-[#92722A] ${
!form.code || !form.product || !form.unit || !form.status
!form.code || !form.product || !form.unit
? 'bg-gray-300 cursor-not-allowed'
: 'bg-[#92722A] hover:bg-[#7a5f22]'
}`}

View File

@ -32,7 +32,8 @@ const QuarterlyWindows = () => {
const [form, setForm] = React.useState(createEmptyQuarterForm());
const [originalForm, setOriginalForm] = React.useState(null);
const [searchTerm, setSearchTerm] = React.useState('');
const [statusFilter, setStatusFilter] = React.useState('all');
const [yearFilter, setYearFilter] = React.useState('all');
const [quarterFilter, setQuarterFilter] = React.useState('all');
const [hoveredEdit, setHoveredEdit] = React.useState(null);
const [currentPage, setCurrentPage] = React.useState(1);
const pageSize = 10;
@ -92,43 +93,44 @@ const QuarterlyWindows = () => {
fetchQuarterlyWindows();
}, []);
// Table headers with whitespace-nowrap to prevent line breaks
// Table headers with left alignment and no wrapping
const headers = [
{ name: 'Survey Name', className: 'whitespace-nowrap' },
{ name: 'Year', className: 'whitespace-nowrap' },
{ name: 'Quarter', className: 'whitespace-nowrap' },
{ name: 'Start Date', className: 'whitespace-nowrap' },
{ name: 'End Date', className: 'whitespace-nowrap' },
{ name: 'Grace Period', className: 'whitespace-nowrap' },
{ name: 'Assigned', className: 'whitespace-nowrap' },
{ name: 'Responded', className: 'whitespace-nowrap' },
{ name: 'Not Responded', className: 'whitespace-nowrap' },
{ name: 'Submission Count', className: 'whitespace-nowrap' },
{ name: 'Status', className: 'whitespace-nowrap' },
{ name: 'Actions', className: 'whitespace-nowrap' },
{ name: 'Survey Name', className: 'whitespace-nowrap text-left' },
{ name: 'Year', className: 'whitespace-nowrap text-left' },
{ name: 'Quarter', className: 'whitespace-nowrap text-left' },
{ name: 'Start Date', className: 'whitespace-nowrap text-left' },
{ name: 'End Date', className: 'whitespace-nowrap text-left' },
{ name: 'Grace Period', className: 'whitespace-nowrap text-left' },
{ name: 'Assigned', className: 'whitespace-nowrap text-left' },
{ name: 'Responded', className: 'whitespace-nowrap text-left' },
{ name: 'Not Responded', className: 'whitespace-nowrap text-left' },
{ 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 = [
550, // Survey Name
150, // Year
180, // Quarter
220, // Start Date
220, // End Date
250, // Grace Period (increased from 200 to 250)
180, // Assigned
200, // Responded
220, // Not Responded
240, // Submission Count
180, // Status
160 // Actions
];
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)
];
const filteredRows = React.useMemo(() => {
const term = searchTerm.trim().toLowerCase();
const statusValue = statusFilter.toLowerCase();
return quarterData.filter((item) => {
const matchesStatus =
statusValue === 'all' || String(item.status).toLowerCase() === statusValue;
if (!matchesStatus) return false;
// 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 = [
item.survey_name,
@ -148,7 +150,7 @@ const QuarterlyWindows = () => {
.join(' ');
return haystack.includes(term);
});
}, [quarterData, searchTerm, statusFilter]);
}, [quarterData, searchTerm, yearFilter, quarterFilter]);
const Tooltip = ({ children, text }) => (
<div className="flex items-center gap-1 group relative">
@ -181,19 +183,36 @@ const QuarterlyWindows = () => {
{item.not_responded || '0'}
</Tooltip>,
item.submissionCount,
<StatusBadge status={item.status} />,
'actions',
]);
const statusOptions = React.useMemo(
() => [
{ label: 'All', value: 'all' },
{ label: 'Active', value: 'Active' },
{ label: 'Inactive', value: 'Inactive' },
],
[]
);
// 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;
if (b === 'all') return 1;
return parseInt(b) - parseInt(a);
});
}, [quarterData]);
const quarterOptions = ['all', 'Q1', 'Q2', 'Q3', 'Q4'];
const openAddModal = () => {
setForm(createEmptyQuarterForm());
@ -370,19 +389,39 @@ const QuarterlyWindows = () => {
/>
<img src={searchIconSrc} alt="Search" className="pointer-events-none absolute left-3 top-1/2 -translate-y-1/2 h-5 w-5" />
</div>
<div className="relative w-full md:w-[134px]">
<div className="relative w-full md:w-[150px]">
<select
value={statusFilter}
onChange={(e) => setStatusFilter(e.target.value)}
value={yearFilter}
onChange={(e) => setYearFilter(e.target.value)}
className="w-full appearance-none rounded-[4px] border border-[#C3C6CB] bg-white text-sm text-[#232528] focus:outline-none cursor-pointer h-10 pl-[12px] pr-[36px] py-[10px]"
>
{statusOptions.map((option) => (
<option key={option.value} value={option.value}>
{option.label}
</option>
))}
<option value="all">All Years</option>
{yearOptions
.filter(year => year !== 'all')
.map((year) => (
<option key={year} value={year}>
{year}
</option>
))}
</select>
<img src={caretDownSrc} alt="Status" className="pointer-events-none absolute right-3 top-1/2 -translate-y-1/2 h-5 w-5" />
<img src={caretDownSrc} alt="" className="absolute right-3 top-1/2 -translate-y-1/2 w-4 h-4 pointer-events-none" />
</div>
<div className="relative w-full md:w-[130px]">
<select
value={quarterFilter}
onChange={(e) => setQuarterFilter(e.target.value)}
className="w-full appearance-none rounded-[4px] border border-[#C3C6CB] bg-white text-sm text-[#232528] focus:outline-none cursor-pointer h-10 pl-[12px] pr-[36px] py-[10px]"
>
<option value="all">All Quarters</option>
{quarterOptions
.filter(q => q !== 'all')
.map((quarter) => (
<option key={quarter} value={quarter}>
{quarter}
</option>
))}
</select>
<img src={caretDownSrc} alt="" className="absolute right-3 top-1/2 -translate-y-1/2 w-4 h-4 pointer-events-none" />
</div>
<button
type="button"
@ -443,12 +482,11 @@ const QuarterlyWindows = () => {
</div>
</div>
<div className="overflow-x-auto w-full">
<div className="w-full" style={{ minWidth: '1200px' }}>
<Table
headers={headers}
columnWidth={columnwidth}
columnWidths={columnwidth}
rows={rows}
disableHorizontalScroll={false}
renderCell={(value, rowIndex, colIndex) => {
if (colIndex === headers.length - 1) {
return (
@ -475,9 +513,18 @@ const QuarterlyWindows = () => {
}}
pagination={{
currentPage,
onPageChange: setCurrentPage,
onPageChange: (page) => {
setCurrentPage(page);
// Scroll to top when changing pages
window.scrollTo({ top: 0, behavior: 'smooth' });
},
pageSize,
totalItems: filteredRows.length,
pageSizeOptions: [10, 20, 50, 100],
onPageSizeChange: (size) => {
setPageSize(size);
setCurrentPage(1); // Reset to first page when changing page size
}
}}
/>
</div>

View File

@ -15,6 +15,7 @@ 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(() => {
@ -85,6 +86,8 @@ const UnitMaster = () => {
const pageSize = 10;
const [currentUser, setCurrentUser] = useState(null);
const [toastData, setToastData] = useState(null);
const [searchTerm, setSearchTerm] = React.useState('');
// Fetch units on component mount
useEffect(() => {
@ -110,6 +113,15 @@ const UnitMaster = () => {
}
};
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]);
useEffect(() => {
const userProfile = sessionStorage.getItem('user_profile');
if (userProfile) {
@ -124,7 +136,6 @@ const UnitMaster = () => {
'Created By',
'Created On',
'Last Updated',
'Status',
'Actions',
];
@ -148,14 +159,10 @@ const UnitMaster = () => {
item.created_by_name || currentUserName || '-',
createdDate,
updatedDate,
<StatusBadge
status={item.is_active ? 'Active' : 'Inactive'}
tone={item.is_active ? 'green' : 'gray'}
/>,
'actions',
];
});
}, [units, currentUser]);
}, [units, currentUser,filteredUnits]);
const statusOptions = React.useMemo(
() => [
@ -165,21 +172,21 @@ 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 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;
@ -266,13 +273,7 @@ const UnitMaster = () => {
});
return;
}
if (!form.status) {
setToastData({
message: 'Please select Status.',
type: 'error'
});
return;
}
// Status validation removed as per request
// Check for duplicate unit name
const nameExists = units.some(
@ -314,7 +315,7 @@ const UnitMaster = () => {
type: 'success'
});
} else {
// Create new record
//Create new record
await createUnit({
...unitData,
created_at: new Date().toISOString(),
@ -322,7 +323,7 @@ const UnitMaster = () => {
created_by_name: currentUser?.name || '',
});
// Show success message for add
// Show success message for add
setToastData({
message: 'New unit added successfully!',
type: 'success'
@ -378,11 +379,26 @@ const UnitMaster = () => {
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 (
<span className="text-gray-600 text-sm font-medium pl-1 py-1 rounded-md">
{units.length} {units.length === 1 ? 'unit': ''}
</span>
)</h3>
<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> */}
<div className="flex items-center gap-2">
<button
type="button"
@ -549,7 +565,7 @@ const UnitMaster = () => {
placeholder="Enter Description"
width="100%"
/>
<div className="w-full" ref={productDropdownRef}>
{/* <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"
@ -594,9 +610,9 @@ const UnitMaster = () => {
</ul>
</div>
)}
</div>
</div> */}
<SelectField
{/* <SelectField
label={
<>
Status <span className="text-red-500">*</span>
@ -606,7 +622,7 @@ const UnitMaster = () => {
onChange={handleFormChange('status')}
options={statusOptions}
placeholder="Select Status"
/>
/> */}
</div>
<div className="mt-6 flex items-center justify-end gap-3">