376 lines
16 KiB
TypeScript
376 lines
16 KiB
TypeScript
'use client';
|
|
import { useEffect, useState } from 'react';
|
|
import { SearchBar } from '@/components/SearchBar';
|
|
import Dropdown from '@/components/Dropdown';
|
|
import SalarieCard from '@/components/SalarieCard';
|
|
import { Salarie } from '@/types/Salarie';
|
|
import { fetchSalaries, fetchSites, fetchServices, fetchSalariesBySite, fetchSalariesByService } from '@/utils/api';
|
|
import { Site } from '@/types/Site';
|
|
import { Service } from '@/types/Service';
|
|
|
|
const Home = () => {
|
|
// states pour stocker les services, les sites et les salariés
|
|
const [sites, setSites] = useState<Site[]>([]);
|
|
const [services, setServices] = useState<Service[]>([]);
|
|
const [filteredSalaries, setFilteredSalaries] = useState<Salarie[]>([]);
|
|
|
|
// states gérer la pagination
|
|
const [currentPage, setCurrentPage] = useState<number>(1);
|
|
const [pageSize, setPageSize] = useState<number>(25);
|
|
const [totalPages, setTotalPages] = useState<number>(1);
|
|
const [totalCount, setTotalCount] = useState<number>(0);
|
|
|
|
// states pour le chargement et les erreurs
|
|
const [isLoading, setIsLoading] = useState<boolean>(false);
|
|
const [error, setError] = useState<string | null>(null);
|
|
|
|
// states pour la recherche
|
|
const [searchTerm, setSearchTerm] = useState<string>('');
|
|
|
|
// states pour les filtres actifs
|
|
const [activeFilters, setActiveFilters] = useState({
|
|
site: '',
|
|
service: ''
|
|
});
|
|
|
|
// set des sites et services uniquement au premier chargement
|
|
useEffect(() => {
|
|
async function fetchInitialData() {
|
|
setIsLoading(true);
|
|
setError(null);
|
|
try {
|
|
const [sitesResponse, servicesResponse] = await Promise.all([
|
|
fetchSites(),
|
|
fetchServices(),
|
|
]);
|
|
|
|
setSites(sitesResponse.data);
|
|
setServices(servicesResponse.data);
|
|
} catch (error) {
|
|
console.error('Erreur lors du chargement des sites et services:', error);
|
|
setError('Erreur lors du chargement des sites et services. Veuillez réessayer.');
|
|
} finally {
|
|
setIsLoading(false);
|
|
}
|
|
}
|
|
fetchInitialData();
|
|
}, []);
|
|
|
|
// chargement des salariés avec pagination et recherche
|
|
useEffect(() => {
|
|
async function fetchSalariesData() {
|
|
setIsLoading(true);
|
|
setError(null);
|
|
try {
|
|
const salariesResponse = await fetchSalaries(currentPage, pageSize, searchTerm);
|
|
|
|
setFilteredSalaries(salariesResponse.data);
|
|
setTotalPages(salariesResponse.totalPages);
|
|
setTotalCount(salariesResponse.totalCount);
|
|
} catch (error) {
|
|
console.error('Erreur lors du chargement des salariés:', error);
|
|
setError('Erreur lors du chargement des salariés. Veuillez réessayer.');
|
|
} finally {
|
|
setIsLoading(false);
|
|
}
|
|
}
|
|
|
|
// récupération des salariés si aucun filtre n'est actif
|
|
if (!activeFilters.site && !activeFilters.service) {
|
|
fetchSalariesData();
|
|
}
|
|
}, [currentPage, pageSize, searchTerm, activeFilters.site, activeFilters.service]);
|
|
|
|
|
|
const handleSearch = (query: string) => {
|
|
setSearchTerm(query);
|
|
setCurrentPage(1); // retour à la page 1 pour une d'une nouvelle recherche
|
|
|
|
// réinitialisation des filtres actifs
|
|
setActiveFilters({
|
|
site: '',
|
|
service: ''
|
|
});
|
|
};
|
|
|
|
const handleSiteFilter = async (siteId: number | '') => {
|
|
setCurrentPage(1);
|
|
setIsLoading(true);
|
|
setError(null);
|
|
|
|
setActiveFilters(prev => ({
|
|
...prev,
|
|
site: siteId ? String(siteId) : ''
|
|
}));
|
|
|
|
try {
|
|
if (siteId) {
|
|
const salariesResponse = await fetchSalariesBySite(siteId, 1, pageSize);
|
|
setFilteredSalaries(salariesResponse.data);
|
|
setTotalPages(salariesResponse.totalPages);
|
|
setTotalCount(salariesResponse.totalCount);
|
|
} else if (activeFilters.service) {
|
|
// si un service est sélectionné, garder ce filtre
|
|
const serviceId = Number(activeFilters.service);
|
|
const salariesResponse = await fetchSalariesByService(serviceId, 1, pageSize);
|
|
setFilteredSalaries(salariesResponse.data);
|
|
setTotalPages(salariesResponse.totalPages);
|
|
setTotalCount(salariesResponse.totalCount);
|
|
} else {
|
|
// aucun filtre actif, retourner aux données de base
|
|
const salariesResponse = await fetchSalaries(1, pageSize, searchTerm);
|
|
setFilteredSalaries(salariesResponse.data);
|
|
setTotalPages(salariesResponse.totalPages);
|
|
setTotalCount(salariesResponse.totalCount);
|
|
}
|
|
} catch (error) {
|
|
console.error('Erreur lors du filtrage par site:', error);
|
|
setError('Erreur lors du filtrage par site. Veuillez réessayer.');
|
|
} finally {
|
|
setIsLoading(false);
|
|
}
|
|
};
|
|
|
|
// filtre qui se fait uniquement si il n'y a que celui qui est demandé
|
|
const handleServiceFilter = async (serviceId: number | '') => {
|
|
setCurrentPage(1);
|
|
setIsLoading(true);
|
|
setError(null);
|
|
|
|
setActiveFilters(prev => ({
|
|
...prev,
|
|
service: serviceId ? String(serviceId) : ''
|
|
}));
|
|
|
|
try {
|
|
if (serviceId) {
|
|
const salariesResponse = await fetchSalariesByService(serviceId, 1, pageSize);
|
|
setFilteredSalaries(salariesResponse.data);
|
|
setTotalPages(salariesResponse.totalPages);
|
|
setTotalCount(salariesResponse.totalCount);
|
|
} else if (activeFilters.site) {
|
|
const siteId = Number(activeFilters.site);
|
|
const salariesResponse = await fetchSalariesBySite(siteId, 1, pageSize);
|
|
setFilteredSalaries(salariesResponse.data);
|
|
setTotalPages(salariesResponse.totalPages);
|
|
setTotalCount(salariesResponse.totalCount);
|
|
} else {
|
|
const salariesResponse = await fetchSalaries(1, pageSize, searchTerm);
|
|
setFilteredSalaries(salariesResponse.data);
|
|
setTotalPages(salariesResponse.totalPages);
|
|
setTotalCount(salariesResponse.totalCount);
|
|
}
|
|
} catch (error) {
|
|
console.error('Erreur lors du filtrage par service:', error);
|
|
setError('Erreur lors du filtrage par service. Veuillez réessayer.');
|
|
} finally {
|
|
setIsLoading(false);
|
|
}
|
|
};
|
|
|
|
return (
|
|
<div className="container mx-auto px-4 py-8">
|
|
|
|
{/* searchbar */}
|
|
<section className="mb-12 animate-in fade-in-50 slide-in-from-bottom-5">
|
|
<SearchBar onSearch={handleSearch} />
|
|
</section>
|
|
|
|
{/* dropdown des sites et des services */}
|
|
<section className="mb-8 grid grid-cols-1 md:grid-cols-2 gap-4 animate-in fade-in-50 slide-in-from-bottom-5 delay-100">
|
|
<Dropdown
|
|
options={sites.map((site) => ({ id: site.id, nom: site.ville }))}
|
|
onSelect={(id) => handleSiteFilter(id as number)}
|
|
label="Filtrer par Site"
|
|
/>
|
|
<Dropdown
|
|
options={services.map((service) => ({ id: service.id, nom: service.nom }))}
|
|
onSelect={(id) => handleServiceFilter(id as number)}
|
|
label="Filtrer par Service"
|
|
/>
|
|
</section>
|
|
|
|
{/* afficahage des infos */}
|
|
<section className="flex justify-between items-center mb-6">
|
|
<p className="text-sm text-muted-foreground">
|
|
{totalCount > 0 ? (
|
|
<>
|
|
<span className="font-medium">{totalCount}</span> {totalCount > 1 ? 'salariés trouvés' : 'salarié trouvé'}
|
|
{activeFilters.site && sites.length > 0 && (
|
|
<> à <span className="text-primary font-medium">
|
|
{sites.find(s => s.id === Number(activeFilters.site))?.ville}
|
|
</span></>
|
|
)}
|
|
{activeFilters.service && services.length > 0 && (
|
|
<> dans <span className="text-primary font-medium">
|
|
{services.find(s => s.id === Number(activeFilters.service))?.nom}
|
|
</span></>
|
|
)}
|
|
</>
|
|
) : !isLoading ? (
|
|
'Aucun salarié trouvé'
|
|
) : (
|
|
'Chargement des résultats...'
|
|
)}
|
|
</p>
|
|
|
|
{/* information des filtres actifs avec possibilité de les supprimer */}
|
|
{(activeFilters.site || activeFilters.service || searchTerm) && (
|
|
<div className="flex flex-wrap gap-2">
|
|
{searchTerm && (
|
|
<span className="inline-flex items-center bg-primary/10 text-primary text-xs rounded-full px-3 py-1">
|
|
Recherche: {searchTerm}
|
|
<button
|
|
onClick={() => handleSearch('')}
|
|
className="ml-2 hover:text-primary-dark"
|
|
>
|
|
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor" className="w-3 h-3">
|
|
<path strokeLinecap="round" strokeLinejoin="round" d="M6 18L18 6M6 6l12 12" />
|
|
</svg>
|
|
</button>
|
|
</span>
|
|
)}
|
|
{activeFilters.site && sites.length > 0 && (
|
|
<span className="inline-flex items-center bg-primary/10 text-primary text-xs rounded-full px-3 py-1">
|
|
Site: {sites.find(s => s.id === Number(activeFilters.site))?.ville}
|
|
<button
|
|
onClick={() => handleSiteFilter('')}
|
|
className="ml-2 hover:text-primary-dark"
|
|
>
|
|
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor" className="w-3 h-3">
|
|
<path strokeLinecap="round" strokeLinejoin="round" d="M6 18L18 6M6 6l12 12" />
|
|
</svg>
|
|
</button>
|
|
</span>
|
|
)}
|
|
{activeFilters.service && services.length > 0 && (
|
|
<span className="inline-flex items-center bg-primary/10 text-primary text-xs rounded-full px-3 py-1">
|
|
Service: {services.find(s => s.id === Number(activeFilters.service))?.nom}
|
|
<button
|
|
onClick={() => handleServiceFilter('')}
|
|
className="ml-2 hover:text-primary-dark"
|
|
>
|
|
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor" className="w-3 h-3">
|
|
<path strokeLinecap="round" strokeLinejoin="round" d="M6 18L18 6M6 6l12 12" />
|
|
</svg>
|
|
</button>
|
|
</span>
|
|
)}
|
|
</div>
|
|
)}
|
|
</section>
|
|
|
|
{/* affichage des erreurs */}
|
|
{error && (
|
|
<div className="bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded mb-8 animate-in fade-in-50">
|
|
<div className="flex items-center">
|
|
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor" className="w-5 h-5 mr-2">
|
|
<path strokeLinecap="round" strokeLinejoin="round" d="M12 9v3.75m-9.303 3.376c-.866 1.5.217 3.374 1.948 3.374h14.71c1.73 0 2.813-1.874 1.948-3.374L13.949 3.378c-.866-1.5-3.032-1.5-3.898 0L2.697 16.126zM12 15.75h.007v.008H12v-.008z" />
|
|
</svg>
|
|
<span>{error}</span>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* affichage du chargement */}
|
|
{isLoading ? (
|
|
<div className="flex justify-center items-center h-64 animate-pulse">
|
|
<div className="space-y-6 w-full max-w-md">
|
|
<div className="h-2.5 bg-muted rounded-full w-full"></div>
|
|
<div className="h-2.5 bg-muted rounded-full w-3/4"></div>
|
|
<div className="h-2.5 bg-muted rounded-full w-1/2"></div>
|
|
<div className="h-12 bg-muted rounded-lg w-full"></div>
|
|
<div className="h-12 bg-muted rounded-lg w-full"></div>
|
|
</div>
|
|
</div>
|
|
) : (
|
|
<>
|
|
{/* grille qui contient les salariés */}
|
|
{filteredSalaries.length > 0 ? (
|
|
<div className="grid gap-6 grid-cols-1 md:grid-cols-2 lg:grid-cols-3 animate-in fade-in-50">
|
|
{filteredSalaries.map((salarie, index) => (
|
|
<div key={salarie.id}
|
|
className="animate-in fade-in-50 slide-in-from-bottom-5"
|
|
style={{ animationDelay: `${index * 50}ms` }}
|
|
>
|
|
<SalarieCard salarie={salarie}/>
|
|
</div>
|
|
))}
|
|
</div>
|
|
) : (
|
|
<div className="bg-card border border-border rounded-lg p-8 text-center animate-in fade-in-50">
|
|
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor" className="w-12 h-12 mx-auto text-muted-foreground mb-4">
|
|
<path strokeLinecap="round" strokeLinejoin="round" d="M15.75 6a3.75 3.75 0 11-7.5 0 3.75 3.75 0 017.5 0zM4.501 20.118a7.5 7.5 0 0114.998 0A17.933 17.933 0 0112 21.75c-2.676 0-5.216-.584-7.499-1.632z" />
|
|
</svg>
|
|
<h3 className="text-lg font-medium mb-2">Aucun salarié trouvé</h3>
|
|
<p className="text-muted-foreground">Essayez de modifier vos critères de recherche ou de filtrage.</p>
|
|
</div>
|
|
)}
|
|
|
|
{/* pagination */}
|
|
{filteredSalaries.length > 0 && (
|
|
<div className="flex flex-col md:flex-row justify-between items-center mt-12 mb-4 space-y-4 md:space-y-0 animate-in fade-in-50 slide-in-from-bottom-5 delay-200">
|
|
|
|
{/* choix du nombre d'éléments pas page */}
|
|
<div className="md:order-1 flex items-center gap-2 w-full md:w-auto">
|
|
<label htmlFor="pageSize" className="text-sm text-muted-foreground whitespace-nowrap">
|
|
Afficher :
|
|
</label>
|
|
<select
|
|
id="pageSize"
|
|
value={pageSize}
|
|
onChange={(e) => {
|
|
setPageSize(Number(e.target.value));
|
|
setCurrentPage(1);
|
|
}}
|
|
className="input bg-card text-foreground border border-border rounded-md px-2 py-1.5 text-sm"
|
|
>
|
|
<option value={10}>10</option>
|
|
<option value={25}>25</option>
|
|
<option value={50}>50</option>
|
|
<option value={100}>100</option>
|
|
</select>
|
|
</div>
|
|
|
|
{/* navigation de pagination */}
|
|
<div className="md:order-2 flex items-center justify-center w-full md:w-auto">
|
|
<button
|
|
onClick={() => setCurrentPage((prev) => Math.max(prev - 1, 1))}
|
|
disabled={currentPage === 1}
|
|
className="button button-outline px-4 py-2 flex items-center disabled:opacity-50 disabled:cursor-not-allowed hover:scale-105 transition-transform"
|
|
>
|
|
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor" className="w-5 h-5 mr-1">
|
|
<path strokeLinecap="round" strokeLinejoin="round" d="M10.5 19.5L3 12m0 0l7.5-7.5M3 12h18" />
|
|
</svg>
|
|
Précédent
|
|
</button>
|
|
|
|
<div className="flex items-center mx-4 text-sm text-muted-foreground">
|
|
<span className="whitespace-nowrap">Page <span className="font-medium text-foreground">{currentPage}</span> sur <span className="font-medium text-foreground">{totalPages}</span></span>
|
|
</div>
|
|
|
|
<button
|
|
onClick={() => setCurrentPage((prev) => Math.min(prev + 1, totalPages))}
|
|
disabled={currentPage === totalPages}
|
|
className="button button-primary px-4 py-2 flex items-center disabled:opacity-50 disabled:cursor-not-allowed hover:scale-105 transition-transform"
|
|
>
|
|
Suivant
|
|
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor" className="w-5 h-5 ml-1">
|
|
<path strokeLinecap="round" strokeLinejoin="round" d="M13.5 4.5L21 12m0 0l-7.5 7.5M21 12H3" />
|
|
</svg>
|
|
</button>
|
|
</div>
|
|
|
|
{/* compteur du nombre résultats renvoyés */}
|
|
<div className="md:order-3 text-sm text-muted-foreground w-full md:w-auto text-center md:text-right">
|
|
<span className="whitespace-nowrap"><span className="font-medium text-foreground">{totalCount}</span> résultats</span>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</>
|
|
)}
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default Home; |