Files
btpxpress-frontend/app/(main)/annuaire/page.tsx
2025-10-13 05:29:32 +02:00

518 lines
23 KiB
TypeScript

'use client';
import React, { useState, useRef, useEffect } from 'react';
import { DataView } from 'primereact/dataview';
import { Card } from 'primereact/card';
import { Button } from 'primereact/button';
import { Tag } from 'primereact/tag';
import { Rating } from 'primereact/rating';
import { InputText } from 'primereact/inputtext';
import { Dropdown } from 'primereact/dropdown';
import { MultiSelect } from 'primereact/multiselect';
import { Slider } from 'primereact/slider';
import { Toast } from 'primereact/toast';
import { Badge } from 'primereact/badge';
import { Avatar } from 'primereact/avatar';
import { Chip } from 'primereact/chip';
import { Divider } from 'primereact/divider';
interface EntrepriseProfile {
id: string;
nomCommercial: string;
description: string;
slogan?: string;
logoUrl?: string;
secteurActivite: string;
specialites: string[];
certifications: string[];
ville: string;
departement: string;
region: string;
noteGlobale: number;
nombreAvis: number;
nombreProjetsRealises: number;
nombreClientsServis: number;
certifie: boolean;
typeAbonnement: 'GRATUIT' | 'PREMIUM' | 'ENTERPRISE';
zonesIntervention: string[];
siteWeb?: string;
emailContact: string;
telephoneCommercial?: string;
photosRealisations?: string[];
budgetMinProjet?: number;
budgetMaxProjet?: number;
delaiMoyenIntervention?: number;
derniereActivite: string;
}
const AnnuaireProfessionnel = () => {
const [entreprises, setEntreprises] = useState<EntrepriseProfile[]>([]);
const [filteredEntreprises, setFilteredEntreprises] = useState<EntrepriseProfile[]>([]);
const [loading, setLoading] = useState(false);
const [globalFilter, setGlobalFilter] = useState('');
// Filtres
const [regionFilter, setRegionFilter] = useState('');
const [secteurFilter, setSecteurFilter] = useState('');
const [specialitesFilter, setSpecialitesFilter] = useState<string[]>([]);
const [noteMinFilter, setNoteMinFilter] = useState(0);
const [certifiesUniquement, setCertifiesUniquement] = useState(false);
const [budgetRange, setBudgetRange] = useState<[number, number]>([0, 1000000]);
const [layout, setLayout] = useState<'grid' | 'list'>('grid');
const [first, setFirst] = useState(0);
const [rows, setRows] = useState(12);
const toast = useRef<Toast>(null);
const regions = [
{ label: 'Toutes les régions', value: '' },
{ label: 'Île-de-France', value: 'IDF' },
{ label: 'Auvergne-Rhône-Alpes', value: 'ARA' },
{ label: 'Nouvelle-Aquitaine', value: 'NA' },
{ label: 'Occitanie', value: 'OCC' },
{ label: 'Hauts-de-France', value: 'HDF' },
{ label: 'Grand Est', value: 'GE' },
{ label: 'Provence-Alpes-Côte d\'Azur', value: 'PACA' },
{ label: 'Pays de la Loire', value: 'PDL' },
{ label: 'Bretagne', value: 'BRE' },
{ label: 'Normandie', value: 'NOR' }
];
const secteurs = [
{ label: 'Tous secteurs', value: '' },
{ label: 'Gros œuvre', value: 'GROS_OEUVRE' },
{ label: 'Second œuvre', value: 'SECOND_OEUVRE' },
{ label: 'Génie civil', value: 'GENIE_CIVIL' },
{ label: 'Rénovation', value: 'RENOVATION' },
{ label: 'Aménagement', value: 'AMENAGEMENT' }
];
const specialitesDisponibles = [
{ label: 'Maçonnerie', value: 'MACONNERIE' },
{ label: 'Charpenterie', value: 'CHARPENTERIE' },
{ label: 'Couverture', value: 'COUVERTURE' },
{ label: 'Plomberie', value: 'PLOMBERIE' },
{ label: 'Électricité', value: 'ELECTRICITE' },
{ label: 'Carrelage', value: 'CARRELAGE' },
{ label: 'Peinture', value: 'PEINTURE' },
{ label: 'Menuiserie', value: 'MENUISERIE' },
{ label: 'Terrassement', value: 'TERRASSEMENT' },
{ label: 'Étanchéité', value: 'ETANCHEITE' }
];
useEffect(() => {
loadEntreprises();
}, []);
useEffect(() => {
applyFilters();
}, [entreprises, globalFilter, regionFilter, secteurFilter, specialitesFilter, noteMinFilter, certifiesUniquement, budgetRange]);
const loadEntreprises = async () => {
setLoading(true);
try {
// TODO: Remplacer par un appel API réel
// const data = await annuaireService.getEntreprises();
// Afficher une liste vide plutôt que des données fictives
const data: EntrepriseProfile[] = [];
setEntreprises(data);
} catch (error) {
toast.current?.show({
severity: 'error',
summary: 'Erreur',
detail: 'Impossible de charger l\'annuaire',
life: 3000
});
} finally {
setLoading(false);
}
};
const applyFilters = () => {
let filtered = [...entreprises];
// Filtre global (recherche textuelle)
if (globalFilter) {
filtered = filtered.filter(entreprise =>
entreprise.nomCommercial.toLowerCase().includes(globalFilter.toLowerCase()) ||
entreprise.description.toLowerCase().includes(globalFilter.toLowerCase()) ||
entreprise.ville.toLowerCase().includes(globalFilter.toLowerCase()) ||
entreprise.specialites.some(s => s.toLowerCase().includes(globalFilter.toLowerCase()))
);
}
// Filtre par région
if (regionFilter) {
filtered = filtered.filter(entreprise => entreprise.region === regionFilter);
}
// Filtre par secteur
if (secteurFilter) {
filtered = filtered.filter(entreprise => entreprise.secteurActivite === secteurFilter);
}
// Filtre par spécialités
if (specialitesFilter.length > 0) {
filtered = filtered.filter(entreprise =>
specialitesFilter.some(spec => entreprise.specialites.includes(spec))
);
}
// Filtre par note minimale
if (noteMinFilter > 0) {
filtered = filtered.filter(entreprise => entreprise.noteGlobale >= noteMinFilter);
}
// Filtre certifiés uniquement
if (certifiesUniquement) {
filtered = filtered.filter(entreprise => entreprise.certifie);
}
// Filtre par budget
if (budgetRange[0] > 0 || budgetRange[1] < 1000000) {
filtered = filtered.filter(entreprise =>
entreprise.budgetMinProjet && entreprise.budgetMaxProjet &&
entreprise.budgetMaxProjet >= budgetRange[0] && entreprise.budgetMinProjet <= budgetRange[1]
);
}
setFilteredEntreprises(filtered);
setFirst(0); // Reset pagination
};
const getAbonnementSeverity = (type: string) => {
switch (type) {
case 'ENTERPRISE': return 'info';
case 'PREMIUM': return 'warning';
default: return 'info';
}
};
const getAbonnementLabel = (type: string) => {
switch (type) {
case 'ENTERPRISE': return 'Enterprise';
case 'PREMIUM': return 'Premium';
default: return 'Standard';
}
};
const contacterEntreprise = (entreprise: EntrepriseProfile) => {
// Logique de contact à implémenter
toast.current?.show({
severity: 'info',
summary: 'Contact',
detail: `Contactez ${entreprise.nomCommercial} via ${entreprise.emailContact}`,
life: 3000
});
};
const voirProfil = (entreprise: EntrepriseProfile) => {
// Navigation vers le profil détaillé
console.log('Voir profil:', entreprise);
};
const itemTemplate = (entreprise: EntrepriseProfile) => {
if (layout === 'list') {
return (
<div className="col-12">
<Card className="border-1 surface-border border-round m-2">
<div className="flex flex-column xl:flex-row xl:align-items-start p-4 gap-4">
<div className="flex align-items-center xl:align-items-start flex-column sm:flex-row gap-4">
<Avatar
image={entreprise.logoUrl}
icon="pi pi-building"
size="xlarge"
shape="circle"
className="bg-primary"
/>
<div className="flex flex-column align-items-center sm:align-items-start gap-3">
<div className="text-2xl font-bold text-900">
{entreprise.nomCommercial}
{entreprise.certifie && (
<Badge value="Certifiée" severity="success" className="ml-2" />
)}
</div>
<div className="flex align-items-center gap-3">
<Rating value={entreprise.noteGlobale} readOnly cancel={false} />
<span className="text-500">({entreprise.nombreAvis} avis)</span>
</div>
<div className="flex align-items-center gap-2">
<Tag value={secteurs.find(s => s.value === entreprise.secteurActivite)?.label || entreprise.secteurActivite}
severity="info" />
<Tag value={getAbonnementLabel(entreprise.typeAbonnement)}
severity={getAbonnementSeverity(entreprise.typeAbonnement)} />
</div>
</div>
</div>
<div className="flex flex-column align-items-end">
<span className="text-xl font-semibold text-900 mb-2">{entreprise.ville}</span>
<div className="flex flex-wrap gap-1 mb-3">
{entreprise.specialites.slice(0, 3).map((spec, index) => (
<Chip key={index} label={spec} className="mr-1 mb-1" />
))}
{entreprise.specialites.length > 3 && (
<Chip label={`+${entreprise.specialites.length - 3}`} className="bg-primary" />
)}
</div>
<div className="flex gap-2">
<Button
icon="pi pi-eye"
label="Voir profil"
className="p-button-outlined"
onClick={() => voirProfil(entreprise)}
/>
<Button
icon="pi pi-envelope"
label="Contacter"
onClick={() => contacterEntreprise(entreprise)}
/>
</div>
</div>
</div>
<div className="surface-100 p-3 border-round">
<p className="text-700 line-height-3 m-0">
{entreprise.description}
</p>
</div>
</Card>
</div>
);
}
return (
<div className="col-12 sm:col-6 lg:col-4 xl:col-3">
<Card className="border-1 surface-border border-round m-2 h-full">
<div className="flex flex-column h-full">
<div className="text-center mb-3">
<Avatar
image={entreprise.logoUrl}
icon="pi pi-building"
size="xlarge"
shape="circle"
className="bg-primary mb-3"
/>
<div className="text-xl font-bold text-900 mb-1">
{entreprise.nomCommercial}
</div>
<div className="flex justify-content-center align-items-center gap-2 mb-2">
<Rating value={entreprise.noteGlobale} readOnly cancel={false} />
<span className="text-500 text-sm">({entreprise.nombreAvis})</span>
</div>
<div className="flex justify-content-center gap-1 mb-2">
<Tag value={getAbonnementLabel(entreprise.typeAbonnement)}
severity={getAbonnementSeverity(entreprise.typeAbonnement)} />
{entreprise.certifie && (
<Tag value="Certifiée" severity="success" icon="pi pi-check" />
)}
</div>
</div>
<div className="flex-grow-1 mb-3">
<div className="text-center mb-2">
<i className="pi pi-map-marker text-primary mr-1"></i>
<span className="text-600">{entreprise.ville}</span>
</div>
<div className="text-center mb-3">
<Tag value={secteurs.find(s => s.value === entreprise.secteurActivite)?.label || entreprise.secteurActivite}
severity="info" className="mb-2" />
</div>
<div className="flex flex-wrap justify-content-center gap-1 mb-3">
{entreprise.specialites.slice(0, 2).map((spec, index) => (
<Chip key={index} label={spec} className="text-xs" />
))}
{entreprise.specialites.length > 2 && (
<Chip label={`+${entreprise.specialites.length - 2}`} className="bg-primary text-xs" />
)}
</div>
<p className="text-600 text-sm line-height-3">
{entreprise.description.substring(0, 120)}...
</p>
<div className="text-center text-500 text-sm">
<div><strong>{entreprise.nombreProjetsRealises}</strong> projets réalisés</div>
<div><strong>{entreprise.nombreClientsServis}</strong> clients servis</div>
</div>
</div>
<div className="flex gap-2">
<Button
icon="pi pi-eye"
className="p-button-outlined flex-1"
onClick={() => voirProfil(entreprise)}
tooltip="Voir le profil"
/>
<Button
icon="pi pi-envelope"
className="flex-1"
onClick={() => contacterEntreprise(entreprise)}
tooltip="Contacter"
/>
</div>
</div>
</Card>
</div>
);
};
const header = () => {
return (
<div className="grid grid-nogutter">
<div className="col-12 md:col-6">
<div className="flex flex-column md:flex-row gap-3">
<span className="p-input-icon-left">
<i className="pi pi-search" />
<InputText
type="search"
value={globalFilter}
onChange={(e) => setGlobalFilter(e.target.value)}
placeholder="Rechercher une entreprise..."
className="w-full"
/>
</span>
</div>
</div>
<div className="col-12 md:col-6">
<div className="flex justify-content-center md:justify-content-end gap-2">
<Button
icon="pi pi-th-large"
className={`p-button-outlined ${layout === 'grid' ? 'p-button-info' : ''}`}
onClick={() => setLayout('grid')}
tooltip="Vue grille"
/>
<Button
icon="pi pi-bars"
className={`p-button-outlined ${layout === 'list' ? 'p-button-info' : ''}`}
onClick={() => setLayout('list')}
tooltip="Vue liste"
/>
</div>
</div>
</div>
);
};
return (
<div className="grid">
<Toast ref={toast} />
<div className="col-12">
<h2>Annuaire Professionnel BTP</h2>
<p className="text-600">Découvrez notre réseau d'entreprises BTP certifiées et trouvez le partenaire idéal pour vos projets.</p>
<Divider />
{/* Filtres */}
<Card title="Filtres de recherche" className="mb-4">
<div className="grid">
<div className="col-12 md:col-6 lg:col-3">
<label htmlFor="region" className="block text-900 font-medium mb-2">Région</label>
<Dropdown
id="region"
value={regionFilter}
options={regions}
onChange={(e) => setRegionFilter(e.value)}
placeholder="Toutes les régions"
className="w-full"
/>
</div>
<div className="col-12 md:col-6 lg:col-3">
<label htmlFor="secteur" className="block text-900 font-medium mb-2">Secteur d'activité</label>
<Dropdown
id="secteur"
value={secteurFilter}
options={secteurs}
onChange={(e) => setSecteurFilter(e.value)}
placeholder="Tous secteurs"
className="w-full"
/>
</div>
<div className="col-12 md:col-6 lg:col-3">
<label htmlFor="specialites" className="block text-900 font-medium mb-2">Spécialités</label>
<MultiSelect
id="specialites"
value={specialitesFilter}
options={specialitesDisponibles}
onChange={(e) => setSpecialitesFilter(e.value)}
placeholder="Sélectionner spécialités"
maxSelectedLabels={2}
className="w-full"
/>
</div>
<div className="col-12 md:col-6 lg:col-3">
<label htmlFor="note" className="block text-900 font-medium mb-2">Note minimale: {noteMinFilter}/5</label>
<Slider
id="note"
value={noteMinFilter}
onChange={(e) => setNoteMinFilter(e.value as number)}
min={0}
max={5}
step={0.5}
className="w-full"
/>
</div>
</div>
<div className="grid mt-3">
<div className="col-12 md:col-6">
<div className="field-checkbox">
<input
type="checkbox"
id="certifies"
checked={certifiesUniquement}
onChange={(e) => setCertifiesUniquement(e.target.checked)}
/>
<label htmlFor="certifies" className="ml-2">Entreprises certifiées uniquement</label>
</div>
</div>
<div className="col-12 md:col-6">
<label className="block text-900 font-medium mb-2">
Budget projet: {budgetRange[0].toLocaleString()} - {budgetRange[1].toLocaleString()}
</label>
<Slider
value={budgetRange}
onChange={(e) => setBudgetRange(e.value as [number, number])}
range
min={0}
max={1000000}
step={10000}
className="w-full"
/>
</div>
</div>
</Card>
<DataView
value={filteredEntreprises}
itemTemplate={itemTemplate}
layout={layout}
header={header()}
paginator
rows={rows}
first={first}
onPage={(e) => setFirst(e.first)}
paginatorTemplate="FirstPageLink PrevPageLink PageLinks NextPageLink LastPageLink RowsPerPageDropdown"
rowsPerPageOptions={[12, 24, 48]}
loading={loading}
emptyMessage="Aucune entreprise ne correspond à vos critères."
paginatorLeft={
<span className="text-600">
{filteredEntreprises.length} entreprise{filteredEntreprises.length !== 1 ? 's' : ''} trouvée{filteredEntreprises.length !== 1 ? 's' : ''}
</span>
}
/>
</div>
</div>
);
};
export default AnnuaireProfessionnel;