Initial commit
This commit is contained in:
490
app/(main)/clients/recherche/page.tsx
Normal file
490
app/(main)/clients/recherche/page.tsx
Normal file
@@ -0,0 +1,490 @@
|
||||
'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;
|
||||
Reference in New Issue
Block a user