Files
btpxpress-frontend/app/(main)/clients/recherche/page.tsx

492 lines
22 KiB
TypeScript
Executable File

'use client';
import React, { useState, useRef } from 'react';
import { Card } from 'primereact/card';
import { Button } from 'primereact/button';
import { InputText } from 'primereact/inputtext';
import { DataTable } from 'primereact/datatable';
import { Column } from 'primereact/column';
import { Toast } from 'primereact/toast';
import { Divider } from 'primereact/divider';
import { Tag } from 'primereact/tag';
import { Dropdown } from 'primereact/dropdown';
import { ProgressSpinner } from 'primereact/progressspinner';
import { clientService } from '../../../../services/api';
import { formatPhoneNumber, formatAddress } from '../../../../utils/formatters';
import type { Client } from '../../../../types/btp';
const RechercheClientPage = () => {
const toast = useRef<Toast>(null);
const [loading, setLoading] = useState(false);
const [clients, setClients] = useState<Client[]>([]);
const [hasSearched, setHasSearched] = useState(false);
const [searchCriteria, setSearchCriteria] = useState({
nom: '',
entreprise: '',
ville: '',
email: '',
telephone: '',
codePostal: '',
siret: '',
numeroTVA: ''
});
const [searchType, setSearchType] = useState<'simple' | 'advanced'>('simple');
const searchTypes = [
{ label: 'Recherche simple', value: 'simple' },
{ label: 'Recherche avancée', value: 'advanced' }
];
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>, field: string) => {
setSearchCriteria(prev => ({
...prev,
[field]: e.target.value
}));
};
const handleSearch = async () => {
// Vérifier qu'au moins un critère est rempli
const hasSearchCriteria = Object.values(searchCriteria).some(value => value.trim() !== '');
if (!hasSearchCriteria) {
toast.current?.show({
severity: 'warn',
summary: 'Attention',
detail: 'Veuillez saisir au moins un critère de recherche',
life: 3000
});
return;
}
setLoading(true);
try {
// Pour la démonstration, nous utilisons une recherche simple
// En production, vous pourriez avoir des endpoints spécifiques pour chaque type de recherche
let results: Client[] = [];
if (searchCriteria.nom) {
results = await clientService.searchByNom(searchCriteria.nom);
} else if (searchCriteria.entreprise) {
results = await clientService.searchByEntreprise(searchCriteria.entreprise);
} else if (searchCriteria.ville) {
results = await clientService.searchByVille(searchCriteria.ville);
} else if (searchCriteria.email) {
results = await clientService.searchByEmail(searchCriteria.email);
} else {
// Recherche générale
results = await clientService.getAll();
// Filtrer les résultats côté client pour les critères non supportés par l'API
results = results.filter(client => {
return (
(!searchCriteria.telephone || client.telephone?.includes(searchCriteria.telephone)) &&
(!searchCriteria.codePostal || client.codePostal?.includes(searchCriteria.codePostal)) &&
(!searchCriteria.siret || client.siret?.includes(searchCriteria.siret)) &&
(!searchCriteria.numeroTVA || client.numeroTVA?.includes(searchCriteria.numeroTVA))
);
});
}
setClients(results);
setHasSearched(true);
toast.current?.show({
severity: 'success',
summary: 'Recherche terminée',
detail: `${results.length} client(s) trouvé(s)`,
life: 3000
});
} catch (error) {
console.error('Erreur lors de la recherche:', error);
toast.current?.show({
severity: 'error',
summary: 'Erreur',
detail: 'Impossible d\'effectuer la recherche',
life: 3000
});
} finally {
setLoading(false);
}
};
const handleReset = () => {
setSearchCriteria({
nom: '',
entreprise: '',
ville: '',
email: '',
telephone: '',
codePostal: '',
siret: '',
numeroTVA: ''
});
setClients([]);
setHasSearched(false);
};
const exportResults = () => {
if (clients.length === 0) {
toast.current?.show({
severity: 'warn',
summary: 'Attention',
detail: 'Aucun résultat à exporter',
life: 3000
});
return;
}
// Créer un CSV simple
const headers = ['Nom', 'Prénom', 'Entreprise', 'Email', 'Téléphone', 'Ville', 'Code Postal'];
const csvContent = [
headers.join(','),
...clients.map(client => [
client.nom,
client.prenom,
client.entreprise || '',
client.email || '',
client.telephone || '',
client.ville || '',
client.codePostal || ''
].join(','))
].join('\n');
const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' });
const link = document.createElement('a');
link.href = URL.createObjectURL(blob);
link.download = `recherche_clients_${new Date().toISOString().split('T')[0]}.csv`;
link.click();
toast.current?.show({
severity: 'success',
summary: 'Export réussi',
detail: 'Les résultats ont été exportés',
life: 3000
});
};
const statusBodyTemplate = (rowData: Client) => {
return (
<Tag
value={rowData.actif ? 'Actif' : 'Inactif'}
severity={rowData.actif ? 'success' : 'danger'}
/>
);
};
const phoneBodyTemplate = (rowData: Client) => {
return rowData.telephone ? formatPhoneNumber(rowData.telephone) : '';
};
const addressBodyTemplate = (rowData: Client) => {
return formatAddress(rowData.adresse, rowData.codePostal, rowData.ville);
};
const actionBodyTemplate = (rowData: Client) => {
return (
<div className="flex gap-2">
<Button
icon="pi pi-eye"
rounded
severity="info"
size="small"
tooltip="Voir détails"
onClick={() => {
toast.current?.show({
severity: 'info',
summary: 'Info',
detail: `Affichage des détails de ${rowData.prenom} ${rowData.nom}`,
life: 3000
});
}}
/>
<Button
icon="pi pi-pencil"
rounded
severity="success"
size="small"
tooltip="Modifier"
onClick={() => {
toast.current?.show({
severity: 'info',
summary: 'Info',
detail: `Modification de ${rowData.prenom} ${rowData.nom}`,
life: 3000
});
}}
/>
</div>
);
};
return (
<div className="grid">
<div className="col-12">
<Card>
<Toast ref={toast} />
<div className="flex justify-content-between align-items-center mb-4">
<h2>Recherche de Clients</h2>
<div className="flex gap-2">
<Button
icon="pi pi-refresh"
label="Réinitialiser"
className="p-button-text"
onClick={handleReset}
/>
{hasSearched && clients.length > 0 && (
<Button
icon="pi pi-download"
label="Exporter"
className="p-button-help"
onClick={exportResults}
/>
)}
</div>
</div>
{/* Critères de recherche */}
<div className="p-fluid">
<div className="field mb-4">
<label htmlFor="searchType" className="font-bold">Type de recherche</label>
<Dropdown
id="searchType"
value={searchType}
options={searchTypes}
onChange={(e) => setSearchType(e.value)}
placeholder="Sélectionnez le type de recherche"
/>
</div>
<div className="formgrid grid">
{/* Recherche simple */}
{searchType === 'simple' && (
<>
<div className="field col-12 md:col-6">
<label htmlFor="nom" className="font-bold">Nom/Prénom</label>
<InputText
id="nom"
value={searchCriteria.nom}
onChange={(e) => handleInputChange(e, 'nom')}
placeholder="Rechercher par nom ou prénom"
/>
</div>
<div className="field col-12 md:col-6">
<label htmlFor="entreprise" className="font-bold">Entreprise</label>
<InputText
id="entreprise"
value={searchCriteria.entreprise}
onChange={(e) => handleInputChange(e, 'entreprise')}
placeholder="Nom de l'entreprise"
/>
</div>
<div className="field col-12 md:col-6">
<label htmlFor="ville" className="font-bold">Ville</label>
<InputText
id="ville"
value={searchCriteria.ville}
onChange={(e) => handleInputChange(e, 'ville')}
placeholder="Ville"
/>
</div>
<div className="field col-12 md:col-6">
<label htmlFor="email" className="font-bold">Email</label>
<InputText
id="email"
value={searchCriteria.email}
onChange={(e) => handleInputChange(e, 'email')}
placeholder="Adresse email"
/>
</div>
</>
)}
{/* Recherche avancée */}
{searchType === 'advanced' && (
<>
<div className="field col-12 md:col-4">
<label htmlFor="nom" className="font-bold">Nom/Prénom</label>
<InputText
id="nom"
value={searchCriteria.nom}
onChange={(e) => handleInputChange(e, 'nom')}
placeholder="Nom ou prénom"
/>
</div>
<div className="field col-12 md:col-4">
<label htmlFor="entreprise" className="font-bold">Entreprise</label>
<InputText
id="entreprise"
value={searchCriteria.entreprise}
onChange={(e) => handleInputChange(e, 'entreprise')}
placeholder="Nom de l'entreprise"
/>
</div>
<div className="field col-12 md:col-4">
<label htmlFor="email" className="font-bold">Email</label>
<InputText
id="email"
value={searchCriteria.email}
onChange={(e) => handleInputChange(e, 'email')}
placeholder="Adresse email"
/>
</div>
<div className="field col-12 md:col-4">
<label htmlFor="telephone" className="font-bold">Téléphone</label>
<InputText
id="telephone"
value={searchCriteria.telephone}
onChange={(e) => handleInputChange(e, 'telephone')}
placeholder="Numéro de téléphone"
/>
</div>
<div className="field col-12 md:col-4">
<label htmlFor="ville" className="font-bold">Ville</label>
<InputText
id="ville"
value={searchCriteria.ville}
onChange={(e) => handleInputChange(e, 'ville')}
placeholder="Ville"
/>
</div>
<div className="field col-12 md:col-4">
<label htmlFor="codePostal" className="font-bold">Code Postal</label>
<InputText
id="codePostal"
value={searchCriteria.codePostal}
onChange={(e) => handleInputChange(e, 'codePostal')}
placeholder="Code postal"
/>
</div>
<div className="field col-12 md:col-6">
<label htmlFor="siret" className="font-bold">SIRET</label>
<InputText
id="siret"
value={searchCriteria.siret}
onChange={(e) => handleInputChange(e, 'siret')}
placeholder="Numéro SIRET"
/>
</div>
<div className="field col-12 md:col-6">
<label htmlFor="numeroTVA" className="font-bold">Numéro TVA</label>
<InputText
id="numeroTVA"
value={searchCriteria.numeroTVA}
onChange={(e) => handleInputChange(e, 'numeroTVA')}
placeholder="Numéro TVA"
/>
</div>
</>
)}
</div>
<div className="flex justify-content-center gap-2 mt-4">
<Button
label="Rechercher"
icon="pi pi-search"
onClick={handleSearch}
loading={loading}
/>
<Button
label="Réinitialiser"
icon="pi pi-refresh"
className="p-button-text"
onClick={handleReset}
/>
</div>
</div>
<Divider />
{/* Résultats */}
{loading && (
<div className="flex justify-content-center py-4">
<ProgressSpinner />
</div>
)}
{hasSearched && !loading && (
<>
<div className="flex justify-content-between align-items-center mb-3">
<h3>Résultats de la recherche</h3>
<span className="text-600">
{clients.length} client(s) trouvé(s)
</span>
</div>
<DataTable
value={clients}
paginator
rows={10}
rowsPerPageOptions={[5, 10, 25]}
className="datatable-responsive"
paginatorTemplate="FirstPageLink PrevPageLink PageLinks NextPageLink LastPageLink CurrentPageReport RowsPerPageDropdown"
currentPageReportTemplate="Affichage de {first} à {last} sur {totalRecords} clients"
emptyMessage="Aucun client trouvé avec ces critères."
responsiveLayout="scroll"
>
<Column
field="nom"
header="Nom"
sortable
body={(rowData) => `${rowData.prenom} ${rowData.nom}`}
headerStyle={{ minWidth: '12rem' }}
/>
<Column
field="entreprise"
header="Entreprise"
sortable
headerStyle={{ minWidth: '10rem' }}
/>
<Column
field="email"
header="Email"
sortable
headerStyle={{ minWidth: '12rem' }}
/>
<Column
field="telephone"
header="Téléphone"
body={phoneBodyTemplate}
headerStyle={{ minWidth: '10rem' }}
/>
<Column
field="ville"
header="Ville"
sortable
headerStyle={{ minWidth: '8rem' }}
/>
<Column
field="actif"
header="Statut"
body={statusBodyTemplate}
sortable
headerStyle={{ minWidth: '8rem' }}
/>
<Column
body={actionBodyTemplate}
headerStyle={{ minWidth: '8rem' }}
/>
</DataTable>
</>
)}
</Card>
</div>
</div>
);
};
export default RechercheClientPage;