import React, { useState, useRef, useEffect, useCallback } from 'react'; import Cropper from 'react-cropper'; import 'cropperjs/dist/cropper.css'; import axios from 'axios'; import { IMaskInput } from 'react-imask'; export default function ObjetTrouveEdit(props) { const [formData, setFormData] = useState({ id: props.initialData?.id || '', title: props.initialData?.titre || '', category: props.initialData?.categoryId || '', location: props.initialData?.lieuTrouve || '', dateFound: props.initialData?.trouveAt || '', description: props.initialData?.description || '', deposeurTelephone: props.initialData?.deposeurTelephone || '', deposeurNom: props.initialData?.deposeurNom || '', deposeurDescription: props.initialData?.deposeurDescription || '', adresse: props.initialData?.adresse || '', }); const maxLengths = { title: 100, location: 100, adresse: 100, description: 500, deposeurNom: 80, deposeurDescription: 500 }; const [errors, setErrors] = useState({}); const [touched, setTouched] = useState({}); // Each slot can be: { id: 123, raw: 'file.jpg', public: 'file_p.jpg', isMasked: true, isNew: false, rawFile: null, publicFile: null } const [slots, setSlots] = useState(() => { const initial = (props.initialData?.images || []).map(img => ({ id: img.id, raw: img.raw, public: img.public, isMasked: img.isMasked, isNew: false, rawFile: null, publicFile: null, position: img.position })); // Sort by position return initial.sort((a, b) => a.position - b.position); }); const [mainImageIndex, setMainImageIndex] = useState(() => { const main = slots.findIndex(s => s.position === 0); return main !== -1 ? main : 0; }); const [dynamicFields, setDynamicFields] = useState(props.initialDynamicFields || {}); const [activeCrop, setActiveCrop] = useState(null); // { index: 0, type: 'raw' | 'public', src: 'data:...' } const cropperRef = useRef(null); const [loading, setLoading] = useState(false); const [success, setSuccess] = useState(false); const [error, setError] = useState(null); const validateField = (name, value) => { const safeValue = value || ''; if (['description', 'deposeurDescription', 'title', 'location', 'adresse', 'deposeurNom'].includes(name)) { if (safeValue && safeValue.trim().length > maxLengths[name]) { return `Ce champ ne doit pas dépasser ${maxLengths[name]} caractères.`; } } if (['title', 'location', 'description', 'deposeurDescription'].includes(name)) { if (safeValue && safeValue.trim().length > 0 && safeValue.trim().length < 3) { return 'Ce champ doit contenir au moins 3 caractères.'; } } if (name === 'deposeurTelephone') { const phoneDigits = safeValue.replace(/\D/g, ''); const unmasked = safeValue.replace(/[\s\-_]/g, ''); if (safeValue && safeValue.trim().length > 0) { if (phoneDigits.length < 12 || !/^\+221(7[05678]|30|33)\d{7}$/.test(unmasked)) { return 'Veuillez saisir le numéro de téléphone complet Sénégalais.'; } } } if (name === 'deposeurNom') { if (safeValue && safeValue.trim().length > 0 && safeValue.trim().length < 2) { return 'Le nom doit contenir au moins 2 caractères.'; } } return null; }; const handleChange = (e) => { const { name, value } = e.target; setFormData(prev => ({ ...prev, [name]: value })); const errorMsg = validateField(name, value); setErrors(prev => ({ ...prev, [name]: errorMsg })); }; const handleBlur = (e) => { const { name, value } = e.target; setTouched(prev => ({ ...prev, [name]: true })); const errorMsg = validateField(name, value); setErrors(prev => ({ ...prev, [name]: errorMsg })); }; const handleDynamicFieldChange = (fieldId, value) => { setDynamicFields(prev => ({ ...prev, [fieldId]: value })); }; const selectedCategory = props.categories?.find(c => c.id == formData.category) || null; const handleFileSelect = (index, type, e) => { if (e.target.files && e.target.files.length > 0) { const file = e.target.files[0]; const reader = new FileReader(); reader.readAsDataURL(file); reader.onload = () => { setActiveCrop({ index, type, src: reader.result }); }; } }; const handleCropExec = async () => { if (cropperRef.current && cropperRef.current.cropper) { const canvas = cropperRef.current.cropper.getCroppedCanvas({ width: 800, height: 600 }); if (canvas) { const blob = await new Promise(resolve => canvas.toBlob(resolve, 'image/jpeg', 0.9)); const dataUrl = canvas.toDataURL('image/jpeg'); setSlots(prev => { const next = [...prev]; const slot = { ...(next[activeCrop.index] || { isNew: true, position: activeCrop.index }) }; slot.rawPreview = dataUrl; slot.rawFile = blob; next[activeCrop.index] = slot; return next; }); setActiveCrop(null); } } }; const removeSlot = (index) => { if (slots.length <= 1) { setError("Il doit y avoir au moins une image."); return; } setSlots(prev => prev.filter((_, i) => i !== index)); if (mainImageIndex === index) setMainImageIndex(0); else if (mainImageIndex > index) setMainImageIndex(mainImageIndex - 1); }; const handleSubmit = async (e) => { e.preventDefault(); if (slots.length === 0) { setError("Veuillez ajouter au moins une image."); return; } setLoading(true); setError(null); const data = new FormData(); Object.keys(formData).forEach(key => data.append(key, formData[key])); data.append('mainImageIndex', mainImageIndex); data.append('dynamicFields', JSON.stringify(dynamicFields)); // Metadata about slots const slotsMetadata = slots.map((slot, index) => ({ id: slot.id || null, isNew: !!slot.isNew, hasRawFile: !!slot.rawFile, })); data.append('slotsMetadata', JSON.stringify(slotsMetadata)); slots.forEach((slot, index) => { if (slot.rawFile) data.append(`rawFiles[${index}]`, slot.rawFile, `raw-${index}.jpg`); }); try { const response = await axios.post(props.submitUrl, data); if (response.data.success) { if (e.nativeEvent.submitter?.name === 'saveAndExit') { window.location.href = props.redirectUrl; } else { setSuccess(true); } } else { setError(response.data.error || 'Une erreur est survenue'); } } catch (err) { setError(err.response?.data?.error || 'Erreur lors de la mise à jour'); } finally { setLoading(false); } }; if (success) { return (

Modifications enregistrées !

Votre signalement a été mis à jour avec succès.

Terminer
); } return (
{activeCrop && (
Recadrer l'image
)}

Modifier mon signalement

Éditez les informations et gérez les visuels publics/privés.

Tous les champs sont obligatoires, sauf indication contraire.
{error && (
{error}
)}
{errors.title &&
{errors.title}
}
= maxLengths.title ? 'text-danger font-weight-bold' : 'text-muted'}>{formData.title.length}/{maxLengths.title}
{errors.adresse &&
{errors.adresse}
}
= maxLengths.adresse ? 'text-danger font-weight-bold' : 'text-muted'}>{formData.adresse.length}/{maxLengths.adresse}
{selectedCategory?.fields?.length > 0 && (
Informations spécifiques ({selectedCategory.nom})
{selectedCategory.fields.map(field => (
handleDynamicFieldChange(field.id, e.target.value)} className="form-control" required={field.estObligatoire} />
))}
)}
{errors.location &&
{errors.location}
}
= maxLengths.location ? 'text-danger font-weight-bold' : 'text-muted'}>{formData.location.length}/{maxLengths.location}
{errors.description &&
{errors.description}
}
= maxLengths.description ? 'text-danger font-weight-bold' : 'text-muted'}>{formData.description.length}/{maxLengths.description}
{props.isAgent && (
Informations du Déposeur (Optionnel)
{ setFormData(prev => ({ ...prev, deposeurTelephone: value })); const errorMsg = validateField('deposeurTelephone', value); setErrors(prev => ({ ...prev, deposeurTelephone: errorMsg })); }} onBlur={handleBlur} className={`form-control ${errors.deposeurTelephone ? 'is-invalid' : ''}`} placeholder="+221 7X XXX XX XX" /> {errors.deposeurTelephone &&
{errors.deposeurTelephone}
}
{errors.deposeurNom &&
{errors.deposeurNom}
}
= maxLengths.deposeurNom ? 'text-danger font-weight-bold' : 'text-muted'}>{formData.deposeurNom.length}/{maxLengths.deposeurNom}
{errors.deposeurDescription &&
{errors.deposeurDescription}
}
= maxLengths.deposeurDescription ? 'text-danger font-weight-bold' : 'text-muted'}>{formData.deposeurDescription.length}/{maxLengths.deposeurDescription}
)}
Gestion des Images (Max 3)
{[0, 1, 2].map(idx => { const slot = slots[idx]; return (
{!slot ? (
document.getElementById(`upload-raw-${idx}`).click()}>
Ajouter
handleFileSelect(idx, 'raw', e)} />
) : (
{mainImageIndex === idx ? 'Principale' : `Image ${idx + 1}`} {props.initialData?.statut !== 'en attente de validation' && ( )}
{props.initialData?.statut !== 'en attente de validation' && ( <> handleFileSelect(idx, 'raw', e)} /> )}
{props.initialData?.statut !== 'en attente de validation' && mainImageIndex !== idx && ( )}
)}
); })}
Annuler
{props.initialData?.statut === 'en attente de validation' ? (
Objet en attente de validation.
) : (
)}
); }