- 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>
359 lines
15 KiB
TypeScript
359 lines
15 KiB
TypeScript
'use client';
|
|
export const dynamic = 'force-dynamic';
|
|
|
|
|
|
import React, { useState, useRef } from 'react';
|
|
import { useRouter } from 'next/navigation';
|
|
import { Card } from 'primereact/card';
|
|
import { Button } from 'primereact/button';
|
|
import { InputText } from 'primereact/inputtext';
|
|
import { InputTextarea } from 'primereact/inputtextarea';
|
|
import { Toast } from 'primereact/toast';
|
|
import { Message } from 'primereact/message';
|
|
import { Divider } from 'primereact/divider';
|
|
import { Checkbox } from 'primereact/checkbox';
|
|
import { clientService } from '../../../../services/api';
|
|
import type { Client } from '../../../../types/btp';
|
|
|
|
const NouveauClientPage = () => {
|
|
const router = useRouter();
|
|
const toast = useRef<Toast>(null);
|
|
const [loading, setLoading] = useState(false);
|
|
const [submitted, setSubmitted] = useState(false);
|
|
|
|
const [client, setClient] = useState<Client>({
|
|
id: '',
|
|
nom: '',
|
|
prenom: '',
|
|
entreprise: '',
|
|
email: '',
|
|
telephone: '',
|
|
adresse: '',
|
|
codePostal: '',
|
|
ville: '',
|
|
numeroTVA: '',
|
|
siret: '',
|
|
actif: true,
|
|
dateCreation: new Date().toISOString(),
|
|
dateModification: new Date().toISOString()
|
|
});
|
|
|
|
const [errors, setErrors] = useState<Record<string, string>>({});
|
|
|
|
const validateForm = () => {
|
|
const newErrors: Record<string, string> = {};
|
|
|
|
if (!client.nom.trim()) {
|
|
newErrors.nom = 'Le nom est obligatoire';
|
|
}
|
|
|
|
if (!client.prenom.trim()) {
|
|
newErrors.prenom = 'Le prénom est obligatoire';
|
|
}
|
|
|
|
if (client.email && !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(client.email)) {
|
|
newErrors.email = 'Format d\'email invalide';
|
|
}
|
|
|
|
if (client.codePostal && !/^\d{5}$/.test(client.codePostal)) {
|
|
newErrors.codePostal = 'Le code postal doit contenir 5 chiffres';
|
|
}
|
|
|
|
if (client.siret && !/^\d{14}$/.test(client.siret.replace(/\s/g, ''))) {
|
|
newErrors.siret = 'Le SIRET doit contenir 14 chiffres';
|
|
}
|
|
|
|
if (client.numeroTVA && !/^[A-Z]{2}\d{11}$/.test(client.numeroTVA.replace(/\s/g, ''))) {
|
|
newErrors.numeroTVA = 'Format de TVA invalide (ex: FR12345678901)';
|
|
}
|
|
|
|
setErrors(newErrors);
|
|
return Object.keys(newErrors).length === 0;
|
|
};
|
|
|
|
const handleSubmit = async (e: React.FormEvent) => {
|
|
e.preventDefault();
|
|
setSubmitted(true);
|
|
|
|
if (!validateForm()) {
|
|
toast.current?.show({
|
|
severity: 'error',
|
|
summary: 'Erreur',
|
|
detail: 'Veuillez corriger les erreurs du formulaire',
|
|
life: 3000
|
|
});
|
|
return;
|
|
}
|
|
|
|
setLoading(true);
|
|
try {
|
|
await clientService.create(client);
|
|
toast.current?.show({
|
|
severity: 'success',
|
|
summary: 'Succès',
|
|
detail: 'Client créé avec succès',
|
|
life: 3000
|
|
});
|
|
|
|
setTimeout(() => {
|
|
router.push('/clients');
|
|
}, 1000);
|
|
} catch (error: any) {
|
|
console.error('Erreur lors de la création:', error);
|
|
|
|
// Extraire le message d'erreur du backend
|
|
let errorMessage = 'Impossible de créer le client';
|
|
if (error.response?.data?.message) {
|
|
errorMessage = error.response.data.message;
|
|
} else if (error.response?.data?.error) {
|
|
errorMessage = error.response.data.error;
|
|
} else if (error.response?.data) {
|
|
errorMessage = JSON.stringify(error.response.data);
|
|
} else if (error.response?.status === 400) {
|
|
errorMessage = 'Données invalides. Vérifiez que tous les champs obligatoires sont remplis correctement.';
|
|
}
|
|
|
|
toast.current?.show({
|
|
severity: 'error',
|
|
summary: 'Erreur',
|
|
detail: errorMessage,
|
|
life: 5000
|
|
});
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
const handleCancel = () => {
|
|
router.push('/clients');
|
|
};
|
|
|
|
const onInputChange = (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>, name: string) => {
|
|
const val = (e.target && e.target.value) || '';
|
|
let _client = { ...client };
|
|
(_client as any)[name] = val;
|
|
setClient(_client);
|
|
|
|
// Clear error when user starts typing
|
|
if (errors[name]) {
|
|
const newErrors = { ...errors };
|
|
delete newErrors[name];
|
|
setErrors(newErrors);
|
|
}
|
|
};
|
|
|
|
const onCheckboxChange = (e: any) => {
|
|
setClient(prev => ({ ...prev, actif: e.checked }));
|
|
};
|
|
|
|
return (
|
|
<div className="grid">
|
|
<div className="col-12">
|
|
<Card>
|
|
<Toast ref={toast} />
|
|
|
|
<div className="flex justify-content-between align-items-center mb-4">
|
|
<h2>Nouveau Client</h2>
|
|
<Button
|
|
icon="pi pi-arrow-left"
|
|
label="Retour"
|
|
className="p-button-text"
|
|
onClick={handleCancel}
|
|
/>
|
|
</div>
|
|
|
|
<form onSubmit={handleSubmit} className="p-fluid">
|
|
<div className="formgrid grid">
|
|
{/* Informations personnelles */}
|
|
<div className="col-12">
|
|
<h3 className="text-primary">Informations personnelles</h3>
|
|
<Divider />
|
|
</div>
|
|
|
|
<div className="field col-12 md:col-6">
|
|
<label htmlFor="nom" className="font-bold">
|
|
Nom <span className="text-red-500">*</span>
|
|
</label>
|
|
<InputText
|
|
id="nom"
|
|
value={client.nom}
|
|
onChange={(e) => onInputChange(e, 'nom')}
|
|
className={errors.nom ? 'p-invalid' : ''}
|
|
placeholder="Nom du client"
|
|
/>
|
|
{errors.nom && <small className="p-error">{errors.nom}</small>}
|
|
</div>
|
|
|
|
<div className="field col-12 md:col-6">
|
|
<label htmlFor="prenom" className="font-bold">
|
|
Prénom <span className="text-red-500">*</span>
|
|
</label>
|
|
<InputText
|
|
id="prenom"
|
|
value={client.prenom}
|
|
onChange={(e) => onInputChange(e, 'prenom')}
|
|
className={errors.prenom ? 'p-invalid' : ''}
|
|
placeholder="Prénom du client"
|
|
/>
|
|
{errors.prenom && <small className="p-error">{errors.prenom}</small>}
|
|
</div>
|
|
|
|
<div className="field col-12">
|
|
<label htmlFor="entreprise" className="font-bold">Entreprise</label>
|
|
<InputText
|
|
id="entreprise"
|
|
value={client.entreprise}
|
|
onChange={(e) => onInputChange(e, 'entreprise')}
|
|
placeholder="Nom de l'entreprise"
|
|
/>
|
|
</div>
|
|
|
|
{/* Contact */}
|
|
<div className="col-12">
|
|
<h3 className="text-primary">Contact</h3>
|
|
<Divider />
|
|
</div>
|
|
|
|
<div className="field col-12 md:col-6">
|
|
<label htmlFor="email" className="font-bold">Email</label>
|
|
<InputText
|
|
id="email"
|
|
value={client.email}
|
|
onChange={(e) => onInputChange(e, 'email')}
|
|
type="email"
|
|
className={errors.email ? 'p-invalid' : ''}
|
|
placeholder="email@exemple.com"
|
|
/>
|
|
{errors.email && <small className="p-error">{errors.email}</small>}
|
|
</div>
|
|
|
|
<div className="field col-12 md:col-6">
|
|
<label htmlFor="telephone" className="font-bold">Téléphone</label>
|
|
<InputText
|
|
id="telephone"
|
|
value={client.telephone}
|
|
onChange={(e) => onInputChange(e, 'telephone')}
|
|
placeholder="01 23 45 67 89"
|
|
/>
|
|
</div>
|
|
|
|
{/* Adresse */}
|
|
<div className="col-12">
|
|
<h3 className="text-primary">Adresse</h3>
|
|
<Divider />
|
|
</div>
|
|
|
|
<div className="field col-12">
|
|
<label htmlFor="adresse" className="font-bold">Adresse</label>
|
|
<InputTextarea
|
|
id="adresse"
|
|
value={client.adresse}
|
|
onChange={(e) => onInputChange(e, 'adresse')}
|
|
rows={3}
|
|
placeholder="Adresse complète"
|
|
/>
|
|
</div>
|
|
|
|
<div className="field col-12 md:col-6">
|
|
<label htmlFor="codePostal" className="font-bold">Code Postal</label>
|
|
<InputText
|
|
id="codePostal"
|
|
value={client.codePostal}
|
|
onChange={(e) => onInputChange(e, 'codePostal')}
|
|
className={errors.codePostal ? 'p-invalid' : ''}
|
|
placeholder="75000"
|
|
/>
|
|
{errors.codePostal && <small className="p-error">{errors.codePostal}</small>}
|
|
</div>
|
|
|
|
<div className="field col-12 md:col-6">
|
|
<label htmlFor="ville" className="font-bold">Ville</label>
|
|
<InputText
|
|
id="ville"
|
|
value={client.ville}
|
|
onChange={(e) => onInputChange(e, 'ville')}
|
|
placeholder="Ville"
|
|
/>
|
|
</div>
|
|
|
|
{/* Informations légales */}
|
|
<div className="col-12">
|
|
<h3 className="text-primary">Informations légales</h3>
|
|
<Divider />
|
|
</div>
|
|
|
|
<div className="field col-12 md:col-6">
|
|
<label htmlFor="numeroTVA" className="font-bold">Numéro TVA</label>
|
|
<InputText
|
|
id="numeroTVA"
|
|
value={client.numeroTVA}
|
|
onChange={(e) => onInputChange(e, 'numeroTVA')}
|
|
className={errors.numeroTVA ? 'p-invalid' : ''}
|
|
placeholder="FR12345678901"
|
|
/>
|
|
{errors.numeroTVA && <small className="p-error">{errors.numeroTVA}</small>}
|
|
</div>
|
|
|
|
<div className="field col-12 md:col-6">
|
|
<label htmlFor="siret" className="font-bold">SIRET</label>
|
|
<InputText
|
|
id="siret"
|
|
value={client.siret}
|
|
onChange={(e) => onInputChange(e, 'siret')}
|
|
className={errors.siret ? 'p-invalid' : ''}
|
|
placeholder="12345678901234"
|
|
/>
|
|
{errors.siret && <small className="p-error">{errors.siret}</small>}
|
|
</div>
|
|
|
|
{/* Statut */}
|
|
<div className="col-12">
|
|
<h3 className="text-primary">Statut</h3>
|
|
<Divider />
|
|
</div>
|
|
|
|
<div className="field col-12">
|
|
<div className="flex align-items-center">
|
|
<Checkbox
|
|
id="actif"
|
|
checked={client.actif}
|
|
onChange={onCheckboxChange}
|
|
/>
|
|
<label htmlFor="actif" className="ml-2 font-bold">
|
|
Client actif
|
|
</label>
|
|
</div>
|
|
<small className="text-600">
|
|
Un client inactif n'apparaîtra pas dans les listes de sélection
|
|
</small>
|
|
</div>
|
|
|
|
{/* Boutons */}
|
|
<div className="col-12">
|
|
<Divider />
|
|
<div className="flex justify-content-end gap-2">
|
|
<Button
|
|
label="Annuler"
|
|
icon="pi pi-times"
|
|
className="p-button-text"
|
|
onClick={handleCancel}
|
|
disabled={loading}
|
|
/>
|
|
<Button
|
|
type="submit"
|
|
label="Créer le client"
|
|
icon="pi pi-check"
|
|
loading={loading}
|
|
disabled={loading}
|
|
/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</form>
|
|
</Card>
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default NouveauClientPage; |