Files
btpxpress-frontend/app/(main)/admin/attributions/page.tsx
dahoud a8825a058b Fix: Corriger toutes les erreurs de build du frontend
- Correction des erreurs TypeScript dans userService.ts et workflowTester.ts
- Ajout des propriétés manquantes aux objets User mockés
- Conversion des dates de string vers objets Date
- Correction des appels asynchrones et des types incompatibles
- Ajout de dynamic rendering pour résoudre les erreurs useSearchParams
- Enveloppement de useSearchParams dans Suspense boundary
- Configuration de force-dynamic au niveau du layout principal

Build réussi: 126 pages générées avec succès

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-18 13:23:08 +00:00

384 lines
15 KiB
TypeScript

'use client';
export const dynamic = 'force-dynamic';
import React, { useState, useEffect, useRef } from 'react';
import { DataTable } from 'primereact/datatable';
import { Column } from 'primereact/column';
import { Button } from 'primereact/button';
import { Card } from 'primereact/card';
import { Toast } from 'primereact/toast';
import { Toolbar } from 'primereact/toolbar';
import { Dialog } from 'primereact/dialog';
import { Dropdown } from 'primereact/dropdown';
import { MultiSelect } from 'primereact/multiselect';
import { Tag } from 'primereact/tag';
import { Divider } from 'primereact/divider';
import clientService from '../../../../services/clientService';
import userService from '../../../../services/userService';
import type { Client } from '../../../../types/btp';
import type { User } from '../../../../types/auth';
interface ClientGestionnaire {
client: Client;
gestionnairePrincipal?: User;
gestionnairesSecondaires: User[];
}
const AttributionsPage = () => {
const [clients, setClients] = useState<Client[]>([]);
const [gestionnaires, setGestionnaires] = useState<User[]>([]);
const [attributions, setAttributions] = useState<ClientGestionnaire[]>([]);
const [loading, setLoading] = useState(true);
const [attributionDialog, setAttributionDialog] = useState(false);
const [selectedClient, setSelectedClient] = useState<Client | null>(null);
const [selectedGestionnairePrincipal, setSelectedGestionnairePrincipal] = useState<User | null>(null);
const [selectedGestionnairesSecondaires, setSelectedGestionnairesSecondaires] = useState<User[]>([]);
const toast = useRef<Toast>(null);
useEffect(() => {
loadData();
}, []);
const loadData = async () => {
try {
setLoading(true);
// Charger les clients
const clientsData = await clientService.getAll();
setClients(clientsData);
// Charger les gestionnaires depuis le service
const gestionnairesData = await userService.getGestionnaires();
setGestionnaires(gestionnairesData);
// Construire les attributions
const attributionsData = clientsData.map(client => ({
client,
gestionnairePrincipal: gestionnairesData.find(g => g.id === client.gestionnairePrincipalId),
gestionnairesSecondaires: gestionnairesData.filter(g =>
client.gestionnairesSecondaires?.includes(g.id)
)
}));
setAttributions(attributionsData);
} catch (error) {
console.error('Erreur lors du chargement des données:', error);
toast.current?.show({
severity: 'error',
summary: 'Erreur',
detail: 'Impossible de charger les données',
life: 3000
});
} finally {
setLoading(false);
}
};
const openAttributionDialog = (client: Client) => {
setSelectedClient(client);
// Pré-remplir les sélections actuelles
const gestionnairePrincipal = gestionnaires.find(g => g.id === client.gestionnairePrincipalId);
setSelectedGestionnairePrincipal(gestionnairePrincipal || null);
const gestionnairesSecondaires = gestionnaires.filter(g =>
client.gestionnairesSecondaires?.includes(g.id)
);
setSelectedGestionnairesSecondaires(gestionnairesSecondaires);
setAttributionDialog(true);
};
const saveAttribution = async () => {
if (!selectedClient) return;
try {
const updatedClient = {
...selectedClient,
gestionnairePrincipalId: selectedGestionnairePrincipal?.id,
gestionnairesSecondaires: selectedGestionnairesSecondaires.map(g => g.id)
};
// Mise à jour côté serveur
await clientService.update(selectedClient.id, updatedClient);
// Mettre à jour localement
const updatedClients = clients.map(c =>
c.id === selectedClient.id ? updatedClient : c
);
setClients(updatedClients);
// Mettre à jour les attributions
const updatedAttributions = attributions.map(a =>
a.client.id === selectedClient.id
? {
...a,
client: updatedClient,
gestionnairePrincipal: selectedGestionnairePrincipal || undefined,
gestionnairesSecondaires: selectedGestionnairesSecondaires
}
: a
);
setAttributions(updatedAttributions);
setAttributionDialog(false);
toast.current?.show({
severity: 'success',
summary: 'Succès',
detail: 'Attribution mise à jour',
life: 3000
});
} catch (error) {
console.error('Erreur lors de la sauvegarde:', error);
toast.current?.show({
severity: 'error',
summary: 'Erreur',
detail: 'Impossible de sauvegarder l\'attribution',
life: 3000
});
}
};
const hideDialog = () => {
setAttributionDialog(false);
setSelectedClient(null);
setSelectedGestionnairePrincipal(null);
setSelectedGestionnairesSecondaires([]);
};
const leftToolbarTemplate = () => {
return (
<div className="my-2">
<Button
label="Actualiser"
icon="pi pi-refresh"
className="mr-2"
onClick={loadData}
/>
</div>
);
};
const rightToolbarTemplate = () => {
return (
<div className="flex align-items-center">
<span className="text-color-secondary mr-2">
{attributions.filter(a => a.gestionnairePrincipal).length} / {attributions.length} clients attribués
</span>
</div>
);
};
const actionBodyTemplate = (rowData: ClientGestionnaire) => {
return (
<Button
icon="pi pi-pencil"
className="p-button-rounded p-button-text p-button-info"
tooltip="Attribuer"
onClick={() => openAttributionDialog(rowData.client)}
/>
);
};
const clientBodyTemplate = (rowData: ClientGestionnaire) => {
return (
<div className="flex flex-column">
<span className="font-medium">{`${rowData.client.prenom} ${rowData.client.nom}`}</span>
{rowData.client.entreprise && (
<span className="text-sm text-color-secondary">{rowData.client.entreprise}</span>
)}
</div>
);
};
const gestionnairePrincipalBodyTemplate = (rowData: ClientGestionnaire) => {
if (!rowData.gestionnairePrincipal) {
return (
<Tag
value="Non attribué"
severity="danger"
className="text-sm"
/>
);
}
return (
<div className="flex flex-column">
<span className="font-medium">
{`${rowData.gestionnairePrincipal.prenom} ${rowData.gestionnairePrincipal.nom}`}
</span>
<span className="text-sm text-color-secondary">
{rowData.gestionnairePrincipal.email}
</span>
</div>
);
};
const gestionnairesSecondairesBodyTemplate = (rowData: ClientGestionnaire) => {
if (rowData.gestionnairesSecondaires.length === 0) {
return <span className="text-color-secondary">Aucun</span>;
}
return (
<div className="flex flex-column gap-1">
{rowData.gestionnairesSecondaires.map(gestionnaire => (
<Tag
key={gestionnaire.id}
value={`${gestionnaire.prenom} ${gestionnaire.nom}`}
severity="info"
className="text-sm"
/>
))}
</div>
);
};
const header = (
<div className="flex flex-column md:flex-row md:justify-content-between md:align-items-center">
<h5 className="m-0">Attribution des Clients aux Gestionnaires</h5>
</div>
);
const attributionDialogFooter = (
<>
<Button
label="Annuler"
icon="pi pi-times"
text
onClick={hideDialog}
/>
<Button
label="Sauvegarder"
icon="pi pi-check"
text
onClick={saveAttribution}
/>
</>
);
return (
<div className="grid">
<div className="col-12">
<Card>
<Toast ref={toast} />
<Toolbar
className="mb-4"
left={leftToolbarTemplate}
right={rightToolbarTemplate}
/>
<DataTable
value={attributions}
dataKey="client.id"
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} attributions"
emptyMessage="Aucune attribution trouvée."
header={header}
responsiveLayout="scroll"
loading={loading}
>
<Column
header="Client"
body={clientBodyTemplate}
sortable
headerStyle={{ minWidth: '15rem' }}
/>
<Column
header="Gestionnaire Principal"
body={gestionnairePrincipalBodyTemplate}
headerStyle={{ minWidth: '15rem' }}
/>
<Column
header="Gestionnaires Secondaires"
body={gestionnairesSecondairesBodyTemplate}
headerStyle={{ minWidth: '12rem' }}
/>
<Column
body={actionBodyTemplate}
headerStyle={{ minWidth: '8rem' }}
/>
</DataTable>
<Dialog
visible={attributionDialog}
style={{ width: '600px' }}
header="Attribution des Gestionnaires"
modal
className="p-fluid"
footer={attributionDialogFooter}
onHide={hideDialog}
>
{selectedClient && (
<div>
<div className="field mb-4">
<h6 className="text-primary">Client</h6>
<div className="p-3 surface-100 border-round">
<div className="font-medium text-lg">
{`${selectedClient.prenom} ${selectedClient.nom}`}
</div>
{selectedClient.entreprise && (
<div className="text-color-secondary">
{selectedClient.entreprise}
</div>
)}
</div>
</div>
<Divider />
<div className="field">
<label htmlFor="gestionnairePrincipal">
<strong>Gestionnaire Principal</strong>
</label>
<Dropdown
id="gestionnairePrincipal"
value={selectedGestionnairePrincipal}
options={gestionnaires}
onChange={(e) => setSelectedGestionnairePrincipal(e.value)}
optionLabel="nom"
itemTemplate={(option) => option ? `${option.prenom} ${option.nom}` : ''}
placeholder="Sélectionnez un gestionnaire principal"
showClear
className="w-full"
/>
<small className="text-color-secondary">
Le gestionnaire principal est responsable de la relation client
</small>
</div>
<div className="field">
<label htmlFor="gestionnairesSecondaires">
<strong>Gestionnaires Secondaires</strong>
</label>
<MultiSelect
id="gestionnairesSecondaires"
value={selectedGestionnairesSecondaires}
options={gestionnaires.filter(g => g.id !== selectedGestionnairePrincipal?.id)}
onChange={(e) => setSelectedGestionnairesSecondaires(e.value)}
optionLabel="nom"
itemTemplate={(option) => option ? `${option.prenom} ${option.nom}` : ''}
placeholder="Sélectionnez des gestionnaires secondaires"
className="w-full"
maxSelectedLabels={3}
/>
<small className="text-color-secondary">
Les gestionnaires secondaires peuvent consulter et collaborer sur les projets
</small>
</div>
</div>
)}
</Dialog>
</Card>
</div>
</div>
);
};
export default AttributionsPage;