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;