PROBLÈME RÉSOLU: - Erreur "Code already used" répétée dans les logs Keycloak - Boucle infinie de tentatives d'échange du code d'autorisation OAuth - Utilisateurs bloqués à la connexion CORRECTIONS APPLIQUÉES: 1. Ajout de useRef pour protéger contre les exécutions multiples - hasExchanged.current: Flag pour prévenir les réexécutions - isProcessing.current: Protection pendant le traitement 2. Modification des dépendances useEffect - AVANT: [searchParams, router] → exécution à chaque changement - APRÈS: [] → exécution unique au montage du composant 3. Amélioration du logging - Console logs pour debug OAuth flow - Messages emoji pour faciliter le suivi 4. Nettoyage de l'URL - window.history.replaceState() pour retirer les paramètres OAuth - Évite les re-renders causés par les paramètres dans l'URL 5. Gestion d'erreurs améliorée - Capture des erreurs JSON du serveur - Messages d'erreur plus explicites FICHIERS AJOUTÉS: - app/(main)/aide/* - 4 pages du module Aide (documentation, tutoriels, support) - app/(main)/messages/* - 4 pages du module Messages (inbox, envoyés, archives) - app/auth/callback/page.tsx.backup - Sauvegarde avant modification IMPACT: ✅ Un seul échange de code par authentification ✅ Plus d'erreur "Code already used" ✅ Connexion fluide et sans boucle ✅ Logs propres et lisibles 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
543 lines
17 KiB
TypeScript
543 lines
17 KiB
TypeScript
'use client';
|
|
export const dynamic = 'force-dynamic';
|
|
|
|
import React, { useState, useEffect } from 'react';
|
|
import { Card } from 'primereact/card';
|
|
import { Button } from 'primereact/button';
|
|
import { DataTable } from 'primereact/datatable';
|
|
import { Column } from 'primereact/column';
|
|
import { Dialog } from 'primereact/dialog';
|
|
import { InputText } from 'primereact/inputtext';
|
|
import { InputTextarea } from 'primereact/inputtextarea';
|
|
import { Dropdown } from 'primereact/dropdown';
|
|
import { Toast } from 'primereact/toast';
|
|
import { ConfirmDialog } from 'primereact/confirmdialog';
|
|
import { Tag } from 'primereact/tag';
|
|
import { Toolbar } from 'primereact/toolbar';
|
|
import { useRef } from 'react';
|
|
import { fournisseurService } from '@/services/fournisseurService';
|
|
|
|
interface Fournisseur {
|
|
id: string;
|
|
nom: string;
|
|
contact: string;
|
|
telephone: string;
|
|
email: string;
|
|
adresse: string;
|
|
ville: string;
|
|
codePostal: string;
|
|
pays: string;
|
|
siret?: string;
|
|
tva?: string;
|
|
conditionsPaiement: string;
|
|
delaiLivraison: number;
|
|
note?: string;
|
|
actif: boolean;
|
|
dateCreation: string;
|
|
dateModification: string;
|
|
}
|
|
|
|
interface FournisseurFormData {
|
|
nom: string;
|
|
contact: string;
|
|
telephone: string;
|
|
email: string;
|
|
adresse: string;
|
|
ville: string;
|
|
codePostal: string;
|
|
pays: string;
|
|
siret: string;
|
|
tva: string;
|
|
conditionsPaiement: string;
|
|
delaiLivraison: number;
|
|
note: string;
|
|
}
|
|
|
|
const FournisseursPage = () => {
|
|
const [fournisseurs, setFournisseurs] = useState<Fournisseur[]>([]);
|
|
const [loading, setLoading] = useState(true);
|
|
const [showDialog, setShowDialog] = useState(false);
|
|
const [editingFournisseur, setEditingFournisseur] = useState<Fournisseur | null>(null);
|
|
const [isSubmitting, setIsSubmitting] = useState(false);
|
|
const [globalFilter, setGlobalFilter] = useState('');
|
|
const [selectedFournisseurs, setSelectedFournisseurs] = useState<Fournisseur[]>([]);
|
|
const toast = useRef<Toast>(null);
|
|
|
|
const [formData, setFormData] = useState<FournisseurFormData>({
|
|
nom: '',
|
|
contact: '',
|
|
telephone: '',
|
|
email: '',
|
|
adresse: '',
|
|
ville: '',
|
|
codePostal: '',
|
|
pays: 'France',
|
|
siret: '',
|
|
tva: '',
|
|
conditionsPaiement: '30 jours',
|
|
delaiLivraison: 7,
|
|
note: ''
|
|
});
|
|
|
|
const conditionsPaiementOptions = [
|
|
{ label: 'Comptant', value: 'Comptant' },
|
|
{ label: '30 jours', value: '30 jours' },
|
|
{ label: '45 jours', value: '45 jours' },
|
|
{ label: '60 jours', value: '60 jours' },
|
|
{ label: '90 jours', value: '90 jours' }
|
|
];
|
|
|
|
const paysOptions = [
|
|
{ label: 'France', value: 'France' },
|
|
{ label: 'Belgique', value: 'Belgique' },
|
|
{ label: 'Suisse', value: 'Suisse' },
|
|
{ label: 'Allemagne', value: 'Allemagne' },
|
|
{ label: 'Espagne', value: 'Espagne' },
|
|
{ label: 'Italie', value: 'Italie' }
|
|
];
|
|
|
|
useEffect(() => {
|
|
loadFournisseurs();
|
|
}, []);
|
|
|
|
const loadFournisseurs = async () => {
|
|
try {
|
|
setLoading(true);
|
|
const data = await fournisseurService.getAllFournisseurs();
|
|
setFournisseurs(data);
|
|
} catch (error) {
|
|
console.error('Erreur lors du chargement des fournisseurs:', error);
|
|
toast.current?.show({
|
|
severity: 'error',
|
|
summary: 'Erreur',
|
|
detail: 'Impossible de charger les fournisseurs'
|
|
});
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
const handleSubmit = async (e: React.FormEvent) => {
|
|
e.preventDefault();
|
|
|
|
try {
|
|
setIsSubmitting(true);
|
|
|
|
// Validation des champs obligatoires
|
|
if (!formData.nom || !formData.contact || !formData.email) {
|
|
toast.current?.show({
|
|
severity: 'warn',
|
|
summary: 'Validation',
|
|
detail: 'Veuillez remplir tous les champs obligatoires'
|
|
});
|
|
return;
|
|
}
|
|
|
|
// Conversion des données du formulaire vers le format API
|
|
const apiData = {
|
|
nom: formData.nom,
|
|
contact: formData.contact,
|
|
telephone: formData.telephone,
|
|
email: formData.email,
|
|
adresse: formData.adresse,
|
|
ville: formData.ville,
|
|
codePostal: formData.codePostal,
|
|
pays: formData.pays,
|
|
siret: formData.siret,
|
|
tva: formData.tva,
|
|
conditionsPaiement: formData.conditionsPaiement,
|
|
delaiLivraison: formData.delaiLivraison,
|
|
note: formData.note
|
|
};
|
|
|
|
if (editingFournisseur) {
|
|
await fournisseurService.updateFournisseur(editingFournisseur.id, apiData);
|
|
toast.current?.show({
|
|
severity: 'success',
|
|
summary: 'Succès',
|
|
detail: 'Fournisseur mis à jour avec succès'
|
|
});
|
|
} else {
|
|
await fournisseurService.createFournisseur(apiData);
|
|
toast.current?.show({
|
|
severity: 'success',
|
|
summary: 'Succès',
|
|
detail: 'Fournisseur créé avec succès'
|
|
});
|
|
}
|
|
|
|
await loadFournisseurs();
|
|
setShowDialog(false);
|
|
resetForm();
|
|
} catch (error) {
|
|
console.error('Erreur lors de la sauvegarde:', error);
|
|
toast.current?.show({
|
|
severity: 'error',
|
|
summary: 'Erreur',
|
|
detail: 'Erreur lors de la sauvegarde du fournisseur'
|
|
});
|
|
} finally {
|
|
setIsSubmitting(false);
|
|
}
|
|
};
|
|
|
|
const resetForm = () => {
|
|
setFormData({
|
|
nom: '',
|
|
contact: '',
|
|
telephone: '',
|
|
email: '',
|
|
adresse: '',
|
|
ville: '',
|
|
codePostal: '',
|
|
pays: 'France',
|
|
siret: '',
|
|
tva: '',
|
|
conditionsPaiement: '30 jours',
|
|
delaiLivraison: 7,
|
|
note: ''
|
|
});
|
|
setEditingFournisseur(null);
|
|
};
|
|
|
|
const openDialog = (fournisseur?: Fournisseur) => {
|
|
if (fournisseur) {
|
|
setEditingFournisseur(fournisseur);
|
|
setFormData({
|
|
nom: fournisseur.nom,
|
|
contact: fournisseur.contact,
|
|
telephone: fournisseur.telephone,
|
|
email: fournisseur.email,
|
|
adresse: fournisseur.adresse,
|
|
ville: fournisseur.ville,
|
|
codePostal: fournisseur.codePostal,
|
|
pays: fournisseur.pays,
|
|
siret: fournisseur.siret || '',
|
|
tva: fournisseur.tva || '',
|
|
conditionsPaiement: fournisseur.conditionsPaiement,
|
|
delaiLivraison: fournisseur.delaiLivraison,
|
|
note: fournisseur.note || ''
|
|
});
|
|
} else {
|
|
resetForm();
|
|
}
|
|
setShowDialog(true);
|
|
};
|
|
|
|
const handleDelete = async (fournisseur: Fournisseur) => {
|
|
try {
|
|
await fournisseurService.deleteFournisseur(fournisseur.id);
|
|
toast.current?.show({
|
|
severity: 'success',
|
|
summary: 'Succès',
|
|
detail: 'Fournisseur supprimé avec succès'
|
|
});
|
|
await loadFournisseurs();
|
|
} catch (error) {
|
|
console.error('Erreur lors de la suppression:', error);
|
|
toast.current?.show({
|
|
severity: 'error',
|
|
summary: 'Erreur',
|
|
detail: 'Erreur lors de la suppression du fournisseur'
|
|
});
|
|
}
|
|
};
|
|
|
|
const handleDeleteSelected = async () => {
|
|
try {
|
|
for (const fournisseur of selectedFournisseurs) {
|
|
await fournisseurService.deleteFournisseur(fournisseur.id);
|
|
}
|
|
toast.current?.show({
|
|
severity: 'success',
|
|
summary: 'Succès',
|
|
detail: `${selectedFournisseurs.length} fournisseur(s) supprimé(s) avec succès`
|
|
});
|
|
setSelectedFournisseurs([]);
|
|
await loadFournisseurs();
|
|
} catch (error) {
|
|
console.error('Erreur lors de la suppression:', error);
|
|
toast.current?.show({
|
|
severity: 'error',
|
|
summary: 'Erreur',
|
|
detail: 'Erreur lors de la suppression des fournisseurs'
|
|
});
|
|
}
|
|
};
|
|
|
|
const statusBodyTemplate = (fournisseur: Fournisseur) => {
|
|
return (
|
|
<Tag
|
|
value={fournisseur.actif ? 'Actif' : 'Inactif'}
|
|
severity={fournisseur.actif ? 'success' : 'danger'}
|
|
/>
|
|
);
|
|
};
|
|
|
|
const actionBodyTemplate = (fournisseur: Fournisseur) => {
|
|
return (
|
|
<div className="flex gap-2">
|
|
<Button
|
|
icon="pi pi-pencil"
|
|
className="p-button-rounded p-button-text p-button-plain"
|
|
onClick={() => openDialog(fournisseur)}
|
|
tooltip="Modifier"
|
|
/>
|
|
<Button
|
|
icon="pi pi-trash"
|
|
className="p-button-rounded p-button-text p-button-danger"
|
|
onClick={() => handleDelete(fournisseur)}
|
|
tooltip="Supprimer"
|
|
/>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
const leftToolbarTemplate = () => {
|
|
return (
|
|
<div className="flex flex-wrap gap-2">
|
|
<Button
|
|
label="Nouveau"
|
|
icon="pi pi-plus"
|
|
className="p-button-success"
|
|
onClick={() => openDialog()}
|
|
/>
|
|
{selectedFournisseurs.length > 0 && (
|
|
<Button
|
|
label="Supprimer"
|
|
icon="pi pi-trash"
|
|
className="p-button-danger"
|
|
onClick={handleDeleteSelected}
|
|
/>
|
|
)}
|
|
</div>
|
|
);
|
|
};
|
|
|
|
const rightToolbarTemplate = () => {
|
|
return (
|
|
<div className="flex align-items-center gap-2">
|
|
<span className="p-input-icon-left">
|
|
<i className="pi pi-search" />
|
|
<InputText
|
|
value={globalFilter}
|
|
onChange={(e) => setGlobalFilter(e.target.value)}
|
|
placeholder="Rechercher..."
|
|
/>
|
|
</span>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
return (
|
|
<div className="grid">
|
|
<Toast ref={toast} />
|
|
<ConfirmDialog />
|
|
|
|
<div className="col-12">
|
|
<Card title="Gestion des Fournisseurs">
|
|
<Toolbar
|
|
left={leftToolbarTemplate}
|
|
right={rightToolbarTemplate}
|
|
className="mb-4"
|
|
/>
|
|
|
|
<DataTable
|
|
value={fournisseurs}
|
|
loading={loading}
|
|
paginator
|
|
rows={10}
|
|
rowsPerPageOptions={[5, 10, 25]}
|
|
globalFilter={globalFilter}
|
|
selection={selectedFournisseurs}
|
|
onSelectionChange={(e) => setSelectedFournisseurs(e.value)}
|
|
dataKey="id"
|
|
emptyMessage="Aucun fournisseur trouvé"
|
|
className="p-datatable-sm"
|
|
>
|
|
<Column selectionMode="multiple" headerStyle={{ width: '3rem' }} />
|
|
<Column field="nom" header="Nom" sortable />
|
|
<Column field="contact" header="Contact" sortable />
|
|
<Column field="telephone" header="Téléphone" />
|
|
<Column field="email" header="Email" />
|
|
<Column field="ville" header="Ville" sortable />
|
|
<Column field="conditionsPaiement" header="Conditions" />
|
|
<Column field="delaiLivraison" header="Délai (jours)" sortable />
|
|
<Column field="actif" header="Statut" body={statusBodyTemplate} />
|
|
<Column body={actionBodyTemplate} headerStyle={{ width: '8rem' }} />
|
|
</DataTable>
|
|
</Card>
|
|
</div>
|
|
|
|
<Dialog
|
|
visible={showDialog}
|
|
style={{ width: '50vw' }}
|
|
header={editingFournisseur ? 'Modifier le fournisseur' : 'Nouveau fournisseur'}
|
|
modal
|
|
onHide={() => setShowDialog(false)}
|
|
>
|
|
<form onSubmit={handleSubmit} className="p-fluid">
|
|
<div className="grid">
|
|
<div className="col-12 md:col-6">
|
|
<div className="field">
|
|
<label htmlFor="nom">Nom *</label>
|
|
<InputText
|
|
id="nom"
|
|
value={formData.nom}
|
|
onChange={(e) => setFormData({ ...formData, nom: e.target.value })}
|
|
required
|
|
/>
|
|
</div>
|
|
</div>
|
|
<div className="col-12 md:col-6">
|
|
<div className="field">
|
|
<label htmlFor="contact">Contact *</label>
|
|
<InputText
|
|
id="contact"
|
|
value={formData.contact}
|
|
onChange={(e) => setFormData({ ...formData, contact: e.target.value })}
|
|
required
|
|
/>
|
|
</div>
|
|
</div>
|
|
<div className="col-12 md:col-6">
|
|
<div className="field">
|
|
<label htmlFor="telephone">Téléphone</label>
|
|
<InputText
|
|
id="telephone"
|
|
value={formData.telephone}
|
|
onChange={(e) => setFormData({ ...formData, telephone: e.target.value })}
|
|
/>
|
|
</div>
|
|
</div>
|
|
<div className="col-12 md:col-6">
|
|
<div className="field">
|
|
<label htmlFor="email">Email *</label>
|
|
<InputText
|
|
id="email"
|
|
type="email"
|
|
value={formData.email}
|
|
onChange={(e) => setFormData({ ...formData, email: e.target.value })}
|
|
required
|
|
/>
|
|
</div>
|
|
</div>
|
|
<div className="col-12">
|
|
<div className="field">
|
|
<label htmlFor="adresse">Adresse</label>
|
|
<InputTextarea
|
|
id="adresse"
|
|
value={formData.adresse}
|
|
onChange={(e) => setFormData({ ...formData, adresse: e.target.value })}
|
|
rows={2}
|
|
/>
|
|
</div>
|
|
</div>
|
|
<div className="col-12 md:col-4">
|
|
<div className="field">
|
|
<label htmlFor="ville">Ville</label>
|
|
<InputText
|
|
id="ville"
|
|
value={formData.ville}
|
|
onChange={(e) => setFormData({ ...formData, ville: e.target.value })}
|
|
/>
|
|
</div>
|
|
</div>
|
|
<div className="col-12 md:col-4">
|
|
<div className="field">
|
|
<label htmlFor="codePostal">Code postal</label>
|
|
<InputText
|
|
id="codePostal"
|
|
value={formData.codePostal}
|
|
onChange={(e) => setFormData({ ...formData, codePostal: e.target.value })}
|
|
/>
|
|
</div>
|
|
</div>
|
|
<div className="col-12 md:col-4">
|
|
<div className="field">
|
|
<label htmlFor="pays">Pays</label>
|
|
<Dropdown
|
|
id="pays"
|
|
value={formData.pays}
|
|
options={paysOptions}
|
|
onChange={(e) => setFormData({ ...formData, pays: e.value })}
|
|
/>
|
|
</div>
|
|
</div>
|
|
<div className="col-12 md:col-6">
|
|
<div className="field">
|
|
<label htmlFor="siret">SIRET</label>
|
|
<InputText
|
|
id="siret"
|
|
value={formData.siret}
|
|
onChange={(e) => setFormData({ ...formData, siret: e.target.value })}
|
|
/>
|
|
</div>
|
|
</div>
|
|
<div className="col-12 md:col-6">
|
|
<div className="field">
|
|
<label htmlFor="tva">N° TVA</label>
|
|
<InputText
|
|
id="tva"
|
|
value={formData.tva}
|
|
onChange={(e) => setFormData({ ...formData, tva: e.target.value })}
|
|
/>
|
|
</div>
|
|
</div>
|
|
<div className="col-12 md:col-6">
|
|
<div className="field">
|
|
<label htmlFor="conditionsPaiement">Conditions de paiement</label>
|
|
<Dropdown
|
|
id="conditionsPaiement"
|
|
value={formData.conditionsPaiement}
|
|
options={conditionsPaiementOptions}
|
|
onChange={(e) => setFormData({ ...formData, conditionsPaiement: e.value })}
|
|
/>
|
|
</div>
|
|
</div>
|
|
<div className="col-12 md:col-6">
|
|
<div className="field">
|
|
<label htmlFor="delaiLivraison">Délai de livraison (jours)</label>
|
|
<InputText
|
|
id="delaiLivraison"
|
|
type="number"
|
|
value={formData.delaiLivraison}
|
|
onChange={(e) => setFormData({ ...formData, delaiLivraison: parseInt(e.target.value) || 0 })}
|
|
/>
|
|
</div>
|
|
</div>
|
|
<div className="col-12">
|
|
<div className="field">
|
|
<label htmlFor="note">Note</label>
|
|
<InputTextarea
|
|
id="note"
|
|
value={formData.note}
|
|
onChange={(e) => setFormData({ ...formData, note: e.target.value })}
|
|
rows={3}
|
|
/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="flex justify-content-end gap-2 mt-4">
|
|
<Button
|
|
type="button"
|
|
label="Annuler"
|
|
icon="pi pi-times"
|
|
className="p-button-text"
|
|
onClick={() => setShowDialog(false)}
|
|
/>
|
|
<Button
|
|
type="submit"
|
|
label={editingFournisseur ? 'Modifier' : 'Créer'}
|
|
icon="pi pi-check"
|
|
loading={isSubmitting}
|
|
/>
|
|
</div>
|
|
</form>
|
|
</Dialog>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default FournisseursPage; |