import React, { useState, useRef, useEffect } from 'react'; import { createPortal } from 'react-dom'; import axios from 'axios'; import Cropper from 'react-cropper'; import 'cropperjs/dist/cropper.css'; import { IMaskInput } from 'react-imask'; export default function RestitutionModal({ reclamationId, submitUrl, rejectUrl, objetTitre, objetImage, objetReference, csrfToken, showLabel = false, label = '', customClass = '', isDisabled = false, mode = 'automatic' // 'automatic' ou 'manual' }) { const [isOpen, setIsOpen] = useState(false); const [step, setStep] = useState(1); // 1 = formulaire, 2 = succès // État du formulaire const [formData, setFormData] = useState({ commentaire: '', nomReclamant: '', telephoneReclamant: '', canalReclamation: 'appel', attestationHonneur: false }); // Validation const [errors, setErrors] = useState({}); const [touched, setTouched] = useState({}); const maxLengths = { commentaire: 500, nomReclamant: 80, }; // Médias const [photoFile, setPhotoFile] = useState(null); // Blob recadré final const [photoPreview, setPhotoPreview] = useState(null); // Data URL pour l'aperçu const [tempPhotoUrl, setTempPhotoUrl] = useState(null); // Data URL brute pour le Cropper const [isCropping, setIsCropping] = useState(false); const cropperRef = useRef(null); const fileInputRef = useRef(null); const [isSubmitting, setIsSubmitting] = useState(false); const [apiError, setApiError] = useState(null); const toggleModal = () => { if (!isOpen && isDisabled) return; setIsOpen(!isOpen); if (isOpen) { resetFormState(); } }; const resetFormState = () => { setStep(1); setFormData({ commentaire: '', nomReclamant: '', telephoneReclamant: '', canalReclamation: 'app', attestationHonneur: false }); setErrors({}); setTouched({}); setPhotoFile(null); setPhotoPreview(null); setTempPhotoUrl(null); setIsCropping(false); setApiError(null); }; const validateField = (name, value) => { if (mode === 'manual') { if (name === 'nomReclamant') { if (!value || value.trim().length === 0) return 'Le nom est requis.'; const words = value.trim().split(/\s+/); if (words.length < 2) return 'Veuillez saisir votre nom complet (Prénom et Nom).'; if (value.trim().length < 2) return 'Le nom doit contenir au moins 2 caractères.'; if (value.trim().length > maxLengths.nomReclamant) return `Maximum ${maxLengths.nomReclamant} caractères.`; } if (name === 'telephoneReclamant') { if (!value || value.trim().length === 0) return 'Le téléphone est requis.'; // Regex basique pour +221 7X XXX XX XX (IMask format) if (!/^\+221\s7\d\s\d{3}\s\d{2}\s\d{2}$/.test(value)) return 'Veuillez saisir un numéro complet.'; } } if (name === 'commentaire') { if (value && value.length > maxLengths.commentaire) return `Maximum ${maxLengths.commentaire} caractères.`; } return null; }; const handleChange = (e) => { const { name, value } = e.target; setFormData(prev => ({ ...prev, [name]: value })); if (touched[name]) { setErrors(prev => ({ ...prev, [name]: validateField(name, value) })); } }; const handleBlur = (e) => { const { name, value } = e.target; setTouched(prev => ({ ...prev, [name]: true })); setErrors(prev => ({ ...prev, [name]: validateField(name, value) })); }; const handlePhotoChange = (e) => { const file = e.target.files[0]; if (!file) return; const reader = new FileReader(); reader.onload = () => { setTempPhotoUrl(reader.result); setIsCropping(true); }; reader.readAsDataURL(file); }; const handleCrop = async () => { if (cropperRef.current && cropperRef.current.cropper) { const canvas = cropperRef.current.cropper.getCroppedCanvas({ width: 1200, height: 320 }); if (canvas) { const blob = await new Promise(resolve => canvas.toBlob(resolve, 'image/jpeg', 0.9)); const dataUrl = canvas.toDataURL('image/jpeg'); setPhotoFile(blob); setPhotoPreview(dataUrl); setIsCropping(false); setTempPhotoUrl(null); } } }; const cancelCrop = () => { setIsCropping(false); setTempPhotoUrl(null); if (fileInputRef.current) fileInputRef.current.value = ''; }; const isFormValid = () => { const hasPhoto = photoFile !== null; if (!hasPhoto && !formData.attestationHonneur) return false; const baseErrors = Object.values(errors).some(error => error !== null); if (baseErrors) return false; if (mode === 'manual') { const words = formData.nomReclamant.trim().split(/\s+/); return words.length >= 2 && /^\+221\s7\d\s\d{3}\s\d{2}\s\d{2}$/.test(formData.telephoneReclamant); } return true; }; const handleSubmit = async (e) => { e.preventDefault(); if (!isFormValid()) return; setIsSubmitting(true); setApiError(null); const body = new FormData(); if (photoFile) { body.append('photo', photoFile, 'preuve-restitution.jpg'); } body.append('attestationHonneur', formData.attestationHonneur); body.append('commentaire', formData.commentaire || ''); body.append('_token', csrfToken); if (mode === 'manual' || formData.nomReclamant) { body.append('nomReclamant', formData.nomReclamant || ''); body.append('telephoneReclamant', formData.telephoneReclamant || ''); body.append('canalReclamation', formData.canalReclamation || 'appel'); } try { const response = await axios.post(submitUrl, body, { headers: { 'Content-Type': 'multipart/form-data' } }); if (response.data.success) { setStep(2); setTimeout(() => window.location.reload(), 2500); } else { setApiError(response.data.message || 'Une erreur est survenue.'); } } catch (err) { setApiError(err.response?.data?.message || 'Erreur de connexion au serveur.'); } finally { setIsSubmitting(false); } }; const modalContent = (
{ if (e.target === e.currentTarget) toggleModal(); }} >
{/* Header */}
{mode === 'manual' ? 'Restitution manuelle (hors plateforme)' : 'Confirmer la restitution'}
{/* Body */}
{step === 2 ? ( /* Succès */

Restitution confirmée !

La preuve de restitution a été enregistrée.
{objetTitre} est maintenant marqué comme restitué.

Redirection en cours…

) : (
{/* Sidebar: objet */}
{objetImage ? ( {objetTitre} ) : (
)}
{objetTitre}
REF: {objetReference}
Important
En confirmant la restitution, vous certifiez avoir remis l'objet au propriétaire légitime. Cette action est définitive.
{/* Formulaire */}
{apiError && (
{apiError}
)} {mode === 'manual' && (
Infos du réclamant
{errors.nomReclamant &&
{errors.nomReclamant}
}
{ setFormData(prev => ({ ...prev, telephoneReclamant: value })); if (touched.telephoneReclamant) { setErrors(prev => ({ ...prev, telephoneReclamant: validateField('telephoneReclamant', value) })); } }} onBlur={() => handleBlur({ target: { name: 'telephoneReclamant', value: formData.telephoneReclamant } })} className={`form-control ${errors.telephoneReclamant ? 'is-invalid' : ''}`} placeholder="+221 7X XXX XX XX" /> {errors.telephoneReclamant &&
{errors.telephoneReclamant}
}
)} {/* Photo de preuve */}
{isCropping ? (
) : ( <>

{formData.attestationHonneur ? "Même avec l'attestation, une photo est fortement recommandée pour sécuriser la preuve." : "Preuve de remise de l'objet (reçu, photo, etc.). Obligatoire sans attestation."}

fileInputRef.current.click()} style={{ border: '2px dashed #28a745', borderRadius: '14px', padding: '16px', cursor: 'pointer', background: photoPreview ? '#f0fdf4' : '#fafafa', transition: 'all 0.2s', minHeight: '130px', display: 'flex', alignItems: 'center', justifyContent: 'center', flexDirection: 'column', }} > {photoPreview ? ( Prévisualisation ) : ( <> Cliquez pour choisir une photo )}
)} {photoFile && !isCropping && (
Photo prête
)}
{/* Attestation sur l'honneur */}
setFormData({ ...formData, attestationHonneur: e.target.checked })} />
{/* Commentaire */}