feat: Gestion de l'affichage des documents validés et non validés sur la page parent [N3WTS-2]

This commit is contained in:
N3WT DE COMPET
2026-02-22 18:34:00 +01:00
parent 8fd1b62ec0
commit 4f7d7d0024
4 changed files with 342 additions and 200 deletions

View File

@ -10,10 +10,16 @@ export default function FileUpload({
required, required,
errorMsg, errorMsg,
enable = true, // Nouvelle prop pour activer/désactiver le champ enable = true, // Nouvelle prop pour activer/désactiver le champ
key,
}) { }) {
const [localFileName, setLocalFileName] = useState(uploadedFileName || ''); const [localFileName, setLocalFileName] = useState(uploadedFileName || '');
const fileInputRef = useRef(null); const fileInputRef = useRef(null);
// Réinitialise localFileName à chaque changement de key (id template)
React.useEffect(() => {
setLocalFileName(uploadedFileName || '');
}, [key, uploadedFileName]);
const handleFileChange = (e) => { const handleFileChange = (e) => {
const file = e.target.files[0]; const file = e.target.files[0];
if (file) { if (file) {

View File

@ -2,20 +2,20 @@
import React, { useState, useEffect } from 'react'; import React, { useState, useEffect } from 'react';
import FormRenderer from '@/components/Form/FormRenderer'; import FormRenderer from '@/components/Form/FormRenderer';
import FileUpload from '@/components/Form/FileUpload'; import FileUpload from '@/components/Form/FileUpload';
import { CheckCircle, Hourglass, FileText, Download, Upload } from 'lucide-react'; import { CheckCircle, Hourglass, FileText, Download, Upload, XCircle } from 'lucide-react';
import logger from '@/utils/logger'; import logger from '@/utils/logger';
import { BASE_URL } from '@/utils/Url'; import { BASE_URL } from '@/utils/Url';
/** /**
* Composant pour afficher et gérer les formulaires dynamiques d'inscription * Composant pour afficher et gérer les formulaires dynamiques d'inscription
* @param {Array} schoolFileMasters - Liste des formulaires maîtres * @param {Array} schoolFileTemplates - Liste des templates de formulaires
* @param {Object} existingResponses - Réponses déjà sauvegardées * @param {Object} existingResponses - Réponses déjà sauvegardées
* @param {Function} onFormSubmit - Callback appelé quand un formulaire est soumis * @param {Function} onFormSubmit - Callback appelé quand un formulaire est soumis
* @param {Boolean} enable - Si les formulaires sont modifiables * @param {Boolean} enable - Si les formulaires sont modifiables
* @param {Function} onFileUpload - Callback appelé quand un fichier est sélectionné * @param {Function} onFileUpload - Callback appelé quand un fichier est sélectionné
*/ */
export default function DynamicFormsList({ export default function DynamicFormsList({
schoolFileMasters, schoolFileTemplates,
existingResponses = {}, existingResponses = {},
onFormSubmit, onFormSubmit,
enable = true, enable = true,
@ -23,47 +23,77 @@ export default function DynamicFormsList({
onFileUpload, // nouvelle prop pour gérer l'upload (à passer depuis le parent) onFileUpload, // nouvelle prop pour gérer l'upload (à passer depuis le parent)
}) { }) {
const [currentTemplateIndex, setCurrentTemplateIndex] = useState(0); const [currentTemplateIndex, setCurrentTemplateIndex] = useState(0);
// Remet formsValidation à false et formsData à undefined lors de la sélection d'un document refusé
useEffect(() => {
const currentTemplate = schoolFileTemplates[currentTemplateIndex];
if (
currentTemplate &&
currentTemplate.isValidated === false &&
formsValidation[currentTemplate.id] !== true
) {
setFormsValidation((prev) => {
const newValidation = { ...prev };
newValidation[currentTemplate.id] = false;
return newValidation;
});
setFormsData((prev) => {
const newData = { ...prev };
delete newData[currentTemplate.id];
return newData;
});
}
}, [currentTemplateIndex, schoolFileTemplates, formsValidation]);
const [formsData, setFormsData] = useState({}); const [formsData, setFormsData] = useState({});
const [formsValidation, setFormsValidation] = useState({}); const [formsValidation, setFormsValidation] = useState({});
const fileInputRefs = React.useRef({}); const fileInputRefs = React.useRef({});
// Initialiser les données avec les réponses existantes // Initialiser les données avec les réponses existantes
useEffect(() => { useEffect(() => {
if (existingResponses && Object.keys(existingResponses).length > 0) { // Initialisation complète de formsValidation et formsData pour chaque template
setFormsData(existingResponses); if (schoolFileTemplates && schoolFileTemplates.length > 0) {
// Initialiser formsData pour chaque template (avec données existantes ou objet vide)
// Marquer les formulaires avec réponses comme valides const dataState = {};
const validationState = {}; schoolFileTemplates.forEach((tpl) => {
Object.keys(existingResponses).forEach((formId) => {
if ( if (
existingResponses[formId] && existingResponses &&
Object.keys(existingResponses[formId]).length > 0 existingResponses[tpl.id] &&
Object.keys(existingResponses[tpl.id]).length > 0
) { ) {
validationState[formId] = true; dataState[tpl.id] = existingResponses[tpl.id];
} else {
dataState[tpl.id] = {};
}
});
setFormsData(dataState);
// Initialiser formsValidation pour chaque template
const validationState = {};
schoolFileTemplates.forEach((tpl) => {
if (
existingResponses &&
existingResponses[tpl.id] &&
Object.keys(existingResponses[tpl.id]).length > 0
) {
validationState[tpl.id] = true;
} else {
validationState[tpl.id] = false;
} }
}); });
setFormsValidation(validationState); setFormsValidation(validationState);
} }
}, [existingResponses]); }, [existingResponses]);
// Debug: Log des formulaires maîtres reçus
useEffect(() => {
logger.debug(
'DynamicFormsList - Formulaires maîtres reçus:',
schoolFileMasters
);
}, [schoolFileMasters]);
// Mettre à jour la validation globale quand la validation des formulaires change // Mettre à jour la validation globale quand la validation des formulaires change
useEffect(() => { useEffect(() => {
const allFormsValid = schoolFileMasters.every( // Un document est considéré comme "validé" s'il est validé par l'école OU complété localement OU déjà existant
(master, index) => formsValidation[master.id] === true const allFormsValid = schoolFileTemplates.every(
tpl => tpl.isValidated === true ||
(formsData[tpl.id] && Object.keys(formsData[tpl.id]).length > 0) ||
(existingResponses[tpl.id] && Object.keys(existingResponses[tpl.id]).length > 0)
); );
if (onValidationChange) { onValidationChange(allFormsValid);
onValidationChange(allFormsValid); }, [formsData, formsValidation, existingResponses, schoolFileTemplates, onValidationChange]);
}
}, [formsValidation, schoolFileMasters, onValidationChange]);
/** /**
* Gère la soumission d'un formulaire individuel * Gère la soumission d'un formulaire individuel
@ -90,7 +120,7 @@ export default function DynamicFormsList({
} }
// Passer au formulaire suivant si disponible // Passer au formulaire suivant si disponible
if (currentTemplateIndex < schoolFileMasters.length - 1) { if (currentTemplateIndex < schoolFileTemplates.length - 1) {
setCurrentTemplateIndex(currentTemplateIndex + 1); setCurrentTemplateIndex(currentTemplateIndex + 1);
} }
@ -100,16 +130,6 @@ export default function DynamicFormsList({
} }
}; };
/**
* Gère les changements de validation d'un formulaire
*/
const handleFormValidationChange = (isValid, templateId) => {
setFormsValidation((prev) => ({
...prev,
[templateId]: isValid,
}));
};
/** /**
* Vérifie si un formulaire est complété * Vérifie si un formulaire est complété
*/ */
@ -140,31 +160,42 @@ export default function DynamicFormsList({
* Obtient le formulaire actuel à afficher * Obtient le formulaire actuel à afficher
*/ */
const getCurrentTemplate = () => { const getCurrentTemplate = () => {
return schoolFileMasters[currentTemplateIndex]; return schoolFileTemplates[currentTemplateIndex];
}; };
// Handler d'upload pour formulaire existant // Handler d'upload pour formulaire existant
const handleUpload = async (file, selectedFile) => { const handleUpload = async (file, selectedFile) => {
if (!file || !selectedFile) return; if (!file || !selectedFile) return;
try { try {
const templateId = currentTemplate.id;
if (onFileUpload) { if (onFileUpload) {
await onFileUpload(file, selectedFile); await onFileUpload(file, selectedFile);
setFormsValidation((prev) => ({ setFormsData((prev) => {
...prev, const newData = {
[selectedFile.id]: true, ...prev,
})); [templateId]: { uploaded: true, fileName: file.name },
};
return newData;
});
setFormsValidation((prev) => {
const newValidation = {
...prev,
[templateId]: true,
};
return newValidation;
});
} }
} catch (error) { } catch (error) {
logger.error('Erreur lors de l\'upload du fichier :', error); logger.error('Erreur lors de l\'upload du fichier :', error);
} }
}; };
const isDynamicForm = (template) => const isDynamicForm = (template) =>
template.formTemplateData && template.formTemplateData &&
Array.isArray(template.formTemplateData.fields) && Array.isArray(template.formTemplateData.fields) &&
template.formTemplateData.fields.length > 0; template.formTemplateData.fields.length > 0;
if (!schoolFileMasters || schoolFileMasters.length === 0) { if (!schoolFileTemplates || schoolFileTemplates.length === 0) {
return ( return (
<div className="text-center py-8"> <div className="text-center py-8">
<FileText className="w-16 h-16 text-gray-300 mx-auto mb-4" /> <FileText className="w-16 h-16 text-gray-300 mx-auto mb-4" />
@ -173,8 +204,6 @@ export default function DynamicFormsList({
); );
} }
const currentTemplate = getCurrentTemplate();
return ( return (
<div className="mt-8 mb-4 w-full mx-auto flex gap-8"> <div className="mt-8 mb-4 w-full mx-auto flex gap-8">
{/* Liste des formulaires */} {/* Liste des formulaires */}
@ -183,85 +212,172 @@ export default function DynamicFormsList({
Formulaires à compléter Formulaires à compléter
</h3> </h3>
<div className="text-sm text-gray-600 mb-4"> <div className="text-sm text-gray-600 mb-4">
{/* Compteur x/y : inclut les documents validés */}
{ {
Object.keys(formsValidation).filter((id) => formsValidation[id]) schoolFileTemplates.filter(tpl => {
.length // Validé ou complété localement
}{' '} return tpl.isValidated === true ||
/ {schoolFileMasters.length} complétés (formsData[tpl.id] && Object.keys(formsData[tpl.id]).length > 0) ||
(existingResponses[tpl.id] && Object.keys(existingResponses[tpl.id]).length > 0);
}).length
}
{' / '}
{schoolFileTemplates.length} complétés
</div> </div>
<ul className="space-y-2"> {/* Tri des templates par état */}
{schoolFileMasters.map((master, index) => { {(() => {
const isActive = index === currentTemplateIndex; // Helper pour état
const isCompleted = isFormCompleted(master.id); const getState = tpl => {
if (tpl.isValidated === true) return 0; // validé
return ( const isCompletedLocally = !!(
<li (formsData[tpl.id] && Object.keys(formsData[tpl.id]).length > 0) ||
key={master.id} (existingResponses[tpl.id] && Object.keys(existingResponses[tpl.id]).length > 0)
className={`flex items-center cursor-pointer p-2 rounded-md transition-colors ${
isActive
? 'bg-blue-100 text-blue-700 font-semibold'
: isCompleted
? 'text-green-600 hover:bg-green-50'
: 'text-gray-600 hover:bg-gray-100'
}`}
onClick={() => setCurrentTemplateIndex(index)}
>
<span className="mr-3">
{getFormStatusIcon(master.id, isActive)}
</span>
<div className="flex-1 min-w-0">
<div className="text-sm truncate">
{master.formMasterData?.title ||
master.title ||
master.name ||
'Formulaire sans nom'}
</div>
{isCompleted ? (
<div className="text-xs text-green-600">
Complété -{' '}
{
Object.keys(
formsData[master.id] ||
existingResponses[master.id] ||
{}
).length
}{' '}
réponse(s)
</div>
) : (
<div className="text-xs text-gray-500">
{master.formMasterData?.fields || master.fields
? `${(master.formMasterData?.fields || master.fields).length} champ(s)`
: 'À compléter'}
</div>
)}
</div>
</li>
); );
})} if (isCompletedLocally) return 1; // complété/en attente
</ul> return 2; // à compléter/refusé
};
const sortedTemplates = [...schoolFileTemplates].sort((a, b) => {
return getState(a) - getState(b);
});
return (
<ul className="space-y-2">
{sortedTemplates.map((tpl, index) => {
const isActive = schoolFileTemplates[currentTemplateIndex]?.id === tpl.id;
const isValidated = typeof tpl.isValidated === 'boolean' ? tpl.isValidated : undefined;
const isCompletedLocally = !!(
(formsData[tpl.id] && Object.keys(formsData[tpl.id]).length > 0) ||
(existingResponses[tpl.id] && Object.keys(existingResponses[tpl.id]).length > 0)
);
// Statut d'affichage
let statusLabel = '';
let statusColor = '';
let icon = null;
let bgClass = '';
let borderClass = '';
let textClass = '';
let canEdit = true;
if (isValidated === true) {
statusLabel = 'Validé';
statusColor = 'emerald';
icon = <CheckCircle className="w-5 h-5 text-emerald-600" />;
bgClass = 'bg-emerald-50';
borderClass = 'border border-emerald-200';
textClass = 'text-emerald-700';
bgClass = isActive ? 'bg-emerald-200' : bgClass;
borderClass = isActive ? 'border border-emerald-300' : borderClass;
textClass = isActive ? 'text-emerald-900 font-semibold' : textClass;
canEdit = false;
} else if (isValidated === false) {
if (isCompletedLocally) {
statusLabel = 'Complété';
statusColor = 'orange';
icon = <Hourglass className="w-5 h-5 text-orange-400" />;
bgClass = isActive ? 'bg-orange-200' : 'bg-orange-50';
borderClass = isActive ? 'border border-orange-300' : 'border border-orange-200';
textClass = isActive ? 'text-orange-900 font-semibold' : 'text-orange-700';
canEdit = true;
} else {
statusLabel = 'Refusé';
statusColor = 'red';
icon = <XCircle className="w-5 h-5 text-red-500" />;
bgClass = isActive ? 'bg-red-200' : 'bg-red-50';
borderClass = isActive ? 'border border-red-300' : 'border border-red-200';
textClass = isActive ? 'text-red-900 font-semibold' : 'text-red-700';
canEdit = true;
}
} else {
if (isCompletedLocally) {
statusLabel = 'Complété';
statusColor = 'orange';
icon = <Hourglass className="w-5 h-5 text-orange-400" />;
bgClass = isActive ? 'bg-orange-200' : 'bg-orange-50';
borderClass = isActive ? 'border border-orange-300' : 'border border-orange-200';
textClass = isActive ? 'text-orange-900 font-semibold' : 'text-orange-700';
canEdit = true;
} else {
statusLabel = 'À compléter';
statusColor = 'gray';
icon = <Hourglass className="w-5 h-5 text-gray-400" />;
bgClass = isActive ? 'bg-gray-200' : '';
borderClass = isActive ? 'border border-gray-300' : '';
textClass = isActive ? 'text-gray-900 font-semibold' : 'text-gray-600';
canEdit = true;
}
}
return (
<li
key={tpl.id}
className={`flex items-center cursor-pointer p-2 rounded-md transition-colors ${
isActive
? `${bgClass.replace('50', '200')} ${borderClass.replace('200', '300')} ${textClass.replace('700', '900')} font-semibold`
: `${bgClass} ${borderClass} ${textClass}`
}`}
onClick={() => setCurrentTemplateIndex(schoolFileTemplates.findIndex(t => t.id === tpl.id))}
>
<span className="mr-3">{icon}</span>
<div className="flex-1 min-w-0">
<div className="text-sm truncate flex items-center gap-2">
{tpl.formMasterData?.title || tpl.title || tpl.name || 'Formulaire sans nom'}
<span className={`ml-2 px-2 py-0.5 rounded bg-${statusColor}-100 text-${statusColor}-700 text-xs font-semibold`}>
{statusLabel}
</span>
</div>
<div className="text-xs text-gray-500">
{tpl.formMasterData?.fields || tpl.fields
? `${(tpl.formMasterData?.fields || tpl.fields).length} champ(s)`
: 'À compléter'}
</div>
</div>
</li>
);
})}
</ul>
);
})()}
</div> </div>
{/* Affichage du formulaire actuel */}
<div className="w-3/4"> <div className="w-3/4">
{currentTemplate && ( {currentTemplate && (
<div className="bg-white p-6 rounded-lg shadow-sm border border-gray-200"> <div className="bg-white p-6 rounded-lg shadow-sm border border-gray-200">
<div className="mb-6"> <div className="mb-6">
<h3 className="text-xl font-semibold text-gray-800 mb-2"> <div className="flex items-center gap-3 mb-2">
{currentTemplate.formTemplateData?.title || <h3 className="text-xl font-semibold text-gray-800">
currentTemplate.title || {currentTemplate.name}
currentTemplate.name || </h3>
'Formulaire sans nom'} {/* Label d'état */}
</h3> {currentTemplate.isValidated === true ? (
<span className="px-2 py-0.5 rounded bg-emerald-100 text-emerald-700 text-sm font-semibold">Validé</span>
) : ((formsData[currentTemplate.id] && Object.keys(formsData[currentTemplate.id]).length > 0) ||
(existingResponses[currentTemplate.id] && Object.keys(existingResponses[currentTemplate.id]).length > 0)) ? (
<span className="px-2 py-0.5 rounded bg-orange-100 text-orange-700 text-sm font-semibold">Complété</span>
) : (
<span className="px-2 py-0.5 rounded bg-red-100 text-red-700 text-sm font-semibold">Refusé</span>
)}
</div>
<p className="text-sm text-gray-600"> <p className="text-sm text-gray-600">
{currentTemplate.formTemplateData?.description || {currentTemplate.formTemplateData?.description ||
currentTemplate.description || currentTemplate.description || ''}
'Veuillez compléter ce formulaire pour continuer votre inscription.'}
</p> </p>
<div className="text-xs text-gray-500 mt-1"> <div className="text-xs text-gray-500 mt-1">
Formulaire {currentTemplateIndex + 1} sur{' '} Formulaire {(() => {
{schoolFileMasters.length} // Trouver l'index du template courant dans la liste triée
const getState = tpl => {
if (tpl.isValidated === true) return 0;
const isCompletedLocally = !!(
(formsData[tpl.id] && Object.keys(formsData[tpl.id]).length > 0) ||
(existingResponses[tpl.id] && Object.keys(existingResponses[tpl.id]).length > 0)
);
if (isCompletedLocally) return 1;
return 2;
};
const sortedTemplates = [...schoolFileTemplates].sort((a, b) => getState(a) - getState(b));
const idx = sortedTemplates.findIndex(tpl => tpl.id === currentTemplate.id);
return idx + 1;
})()} sur {schoolFileTemplates.length}
</div> </div>
</div> </div>
@ -286,16 +402,21 @@ export default function DynamicFormsList({
onFormSubmit={(formData) => onFormSubmit={(formData) =>
handleFormSubmit(formData, currentTemplate.id) handleFormSubmit(formData, currentTemplate.id)
} }
// Désactive le bouton suivant si le template est validé
enable={currentTemplate.isValidated !== true}
/> />
) : ( ) : (
// Formulaire existant (PDF, image, etc.) // Formulaire existant (PDF, image, etc.)
<div className="flex flex-col items-center gap-6"> <div className="flex flex-col items-center gap-6">
<div className="flex flex-col items-center gap-2"> <div className="flex flex-col items-center gap-2">
<FileText className="w-16 h-16 text-gray-400" /> {currentTemplate.file && currentTemplate.isValidated === true ? (
<div className="text-lg font-semibold text-gray-700"> <iframe
{currentTemplate.name} src={`${BASE_URL}${currentTemplate.file}`}
</div> title={currentTemplate.name}
{currentTemplate.file && ( className="w-full"
style={{ height: '600px', border: 'none' }}
/>
) : currentTemplate.file && (
<a <a
href={`${BASE_URL}${currentTemplate.file}`} href={`${BASE_URL}${currentTemplate.file}`}
target="_blank" target="_blank"
@ -308,21 +429,24 @@ export default function DynamicFormsList({
</a> </a>
)} )}
</div> </div>
{enable && ( {/* Upload désactivé si validé par l'école */}
{enable && currentTemplate.isValidated !== true && (
<FileUpload <FileUpload
selectionMessage="Sélectionnez le fichier du document" key={currentTemplate.id}
selectionMessage={'Sélectionnez le fichier du document'}
onFileSelect={(file) => handleUpload(file, currentTemplate)} onFileSelect={(file) => handleUpload(file, currentTemplate)}
required required
enable enable={true}
/> />
)} )}
{/* Le label d'état est maintenant dans l'en-tête */}
</div> </div>
)} )}
</div> </div>
)} )}
{/* Message de fin */} {/* Message de fin */}
{currentTemplateIndex >= schoolFileMasters.length && ( {currentTemplateIndex >= schoolFileTemplates.length && (
<div className="text-center py-8"> <div className="text-center py-8">
<CheckCircle className="w-16 h-16 text-green-600 mx-auto mb-4" /> <CheckCircle className="w-16 h-16 text-green-600 mx-auto mb-4" />
<h3 className="text-lg font-semibold text-green-600 mb-2"> <h3 className="text-lg font-semibold text-green-600 mb-2">

View File

@ -799,7 +799,7 @@ export default function InscriptionFormShared({
{/* Page 5 : Formulaires dynamiques d'inscription */} {/* Page 5 : Formulaires dynamiques d'inscription */}
{currentPage === 5 && ( {currentPage === 5 && (
<DynamicFormsList <DynamicFormsList
schoolFileMasters={schoolFileTemplates} schoolFileTemplates={schoolFileTemplates}
existingResponses={formResponses} existingResponses={formResponses}
onFormSubmit={handleDynamicFormSubmit} onFormSubmit={handleDynamicFormSubmit}
onValidationChange={handleDynamicFormsValidationChange} onValidationChange={handleDynamicFormsValidationChange}

View File

@ -33,6 +33,27 @@ export default function ValidateSubscription({
const [isPageValid, setIsPageValid] = useState(false); const [isPageValid, setIsPageValid] = useState(false);
// Pour la validation/refus des documents // Pour la validation/refus des documents
const [docStatuses, setDocStatuses] = useState({}); // {index: 'accepted'|'refused'} const [docStatuses, setDocStatuses] = useState({}); // {index: 'accepted'|'refused'}
// Met à jour docStatuses selon isValidated des templates récupérés
useEffect(() => {
// On construit la map index -> status à partir des templates
const newStatuses = {};
// Fiche élève (pas de validation individuelle)
newStatuses[0] = undefined;
// School templates
schoolFileTemplates.forEach((tpl, i) => {
if (typeof tpl.isValidated === 'boolean') {
newStatuses[1 + i] = tpl.isValidated ? 'accepted' : 'refused';
}
});
// Parent templates
parentFileTemplates.forEach((tpl, i) => {
if (typeof tpl.isValidated === 'boolean') {
newStatuses[1 + schoolFileTemplates.length + i] = tpl.isValidated ? 'accepted' : 'refused';
}
});
setDocStatuses(s => ({ ...s, ...newStatuses }));
}, [schoolFileTemplates, parentFileTemplates]);
const [showRefusedPopup, setShowRefusedPopup] = useState(false); const [showRefusedPopup, setShowRefusedPopup] = useState(false);
// Affiche la popup de confirmation finale (tous docs validés et classe sélectionnée) // Affiche la popup de confirmation finale (tous docs validés et classe sélectionnée)
@ -155,8 +176,8 @@ export default function ValidateSubscription({
.map((doc, idx) => ({ ...doc, idx })) .map((doc, idx) => ({ ...doc, idx }))
.filter((doc, idx) => docStatuses[idx] === 'refused'); .filter((doc, idx) => docStatuses[idx] === 'refused');
// Récupère la liste des documents à cocher (inclut la fiche élève) // Récupère la liste des documents à cocher (hors fiche élève)
const docIndexes = allTemplates.map((_, idx) => idx); const docIndexes = allTemplates.map((_, idx) => idx).filter(idx => idx !== 0);
const allChecked = docIndexes.length > 0 && docIndexes.every(idx => docStatuses[idx] === 'accepted' || docStatuses[idx] === 'refused'); const allChecked = docIndexes.length > 0 && docIndexes.every(idx => docStatuses[idx] === 'accepted' || docStatuses[idx] === 'refused');
const allValidated = docIndexes.length > 0 && docIndexes.every(idx => docStatuses[idx] === 'accepted'); const allValidated = docIndexes.length > 0 && docIndexes.every(idx => docStatuses[idx] === 'accepted');
const hasRefused = docIndexes.some(idx => docStatuses[idx] === 'refused'); const hasRefused = docIndexes.some(idx => docStatuses[idx] === 'refused');
@ -223,84 +244,75 @@ export default function ValidateSubscription({
)} )}
</span> </span>
<span className="flex-1">{template.name}</span> <span className="flex-1">{template.name}</span>
{/* 3 boutons côte à côte : À traiter / Validé / Refusé (pour tous les documents, y compris fiche élève) */} {/* 2 boutons : Validé / Refusé (sauf fiche élève) */}
<span className="ml-2 flex gap-1"> {index !== 0 && (
<button <span className="ml-2 flex gap-1">
type="button" <button
className={`px-2 py-1 rounded-full text-xs font-medium border transition-colors focus:outline-none focus:ring-2 focus:ring-blue-400 type="button"
${docStatuses[index] === undefined ? 'bg-gray-300 text-gray-700 border-gray-400' : 'bg-white text-gray-500 border-gray-300'}`} className={`px-2 py-1 rounded-full text-xs font-medium border transition-colors focus:outline-none focus:ring-2 focus:ring-blue-400 flex items-center gap-1
aria-pressed={docStatuses[index] === undefined} ${docStatuses[index] === 'accepted' ? 'bg-emerald-500 text-white border-emerald-500' : 'bg-white text-emerald-600 border-emerald-300'}`}
onClick={e => { aria-pressed={docStatuses[index] === 'accepted'}
e.stopPropagation(); onClick={e => {
setDocStatuses(s => ({ ...s, [index]: undefined })); e.stopPropagation();
}} setDocStatuses(s => ({ ...s, [index]: 'accepted' }));
>À traiter</button> // Appel API pour valider le document
<button if (handleValidateOrRefuseDoc) {
type="button" let template = null;
className={`px-2 py-1 rounded-full text-xs font-medium border transition-colors focus:outline-none focus:ring-2 focus:ring-blue-400 flex items-center gap-1 let type = null;
${docStatuses[index] === 'accepted' ? 'bg-emerald-500 text-white border-emerald-500' : 'bg-white text-emerald-600 border-emerald-300'}`} if (index > 0 && index <= schoolFileTemplates.length) {
aria-pressed={docStatuses[index] === 'accepted'} template = schoolFileTemplates[index - 1];
onClick={e => { type = 'school';
e.stopPropagation(); } else if (index > schoolFileTemplates.length && index <= schoolFileTemplates.length + parentFileTemplates.length) {
setDocStatuses(s => ({ ...s, [index]: 'accepted' })); template = parentFileTemplates[index - 1 - schoolFileTemplates.length];
// Appel API pour valider le document (hors fiche élève) type = 'parent';
if (index > 0 && handleValidateOrRefuseDoc) { }
// index 0 = fiche élève, ensuite school puis parent puis SEPA if (template && template.id) {
let template = null; handleValidateOrRefuseDoc({
let type = null; templateId: template.id,
if (index > 0 && index <= schoolFileTemplates.length) { type,
template = schoolFileTemplates[index - 1]; validated: true,
type = 'school'; csrfToken,
} else if (index > schoolFileTemplates.length && index <= schoolFileTemplates.length + parentFileTemplates.length) { });
template = parentFileTemplates[index - 1 - schoolFileTemplates.length]; }
type = 'parent';
} }
if (template && template.id) { }}
handleValidateOrRefuseDoc({ >
templateId: template.id, <span className="text-lg"></span> Validé
type, </button>
validated: true, <button
csrfToken, type="button"
}); className={`px-2 py-1 rounded-full text-xs font-medium border transition-colors focus:outline-none focus:ring-2 focus:ring-blue-400 flex items-center gap-1
${docStatuses[index] === 'refused' ? 'bg-red-500 text-white border-red-500' : 'bg-white text-red-600 border-red-300'}`}
aria-pressed={docStatuses[index] === 'refused'}
onClick={e => {
e.stopPropagation();
setDocStatuses(s => ({ ...s, [index]: 'refused' }));
// Appel API pour refuser le document
if (handleValidateOrRefuseDoc) {
let template = null;
let type = null;
if (index > 0 && index <= schoolFileTemplates.length) {
template = schoolFileTemplates[index - 1];
type = 'school';
} else if (index > schoolFileTemplates.length && index <= schoolFileTemplates.length + parentFileTemplates.length) {
template = parentFileTemplates[index - 1 - schoolFileTemplates.length];
type = 'parent';
}
if (template && template.id) {
handleValidateOrRefuseDoc({
templateId: template.id,
type,
validated: false,
csrfToken,
});
}
} }
} }}
}} >
> <span className="text-lg"></span> Refusé
<span className="text-lg"></span> Validé </button>
</button> </span>
<button )}
type="button"
className={`px-2 py-1 rounded-full text-xs font-medium border transition-colors focus:outline-none focus:ring-2 focus:ring-blue-400 flex items-center gap-1
${docStatuses[index] === 'refused' ? 'bg-red-500 text-white border-red-500' : 'bg-white text-red-600 border-red-300'}`}
aria-pressed={docStatuses[index] === 'refused'}
onClick={e => {
e.stopPropagation();
setDocStatuses(s => ({ ...s, [index]: 'refused' }));
// Appel API pour refuser le document (hors fiche élève)
if (index > 0 && handleValidateOrRefuseDoc) {
let template = null;
let type = null;
if (index > 0 && index <= schoolFileTemplates.length) {
template = schoolFileTemplates[index - 1];
type = 'school';
} else if (index > schoolFileTemplates.length && index <= schoolFileTemplates.length + parentFileTemplates.length) {
template = parentFileTemplates[index - 1 - schoolFileTemplates.length];
type = 'parent';
}
if (template && template.id) {
handleValidateOrRefuseDoc({
templateId: template.id,
type,
validated: false,
csrfToken,
});
}
}
}}
>
<span className="text-lg"></span> Refusé
</button>
</span>
</li> </li>
))} ))}
</ul> </ul>
@ -377,7 +389,7 @@ export default function ValidateSubscription({
<span> <span>
{`Le dossier d'inscription de ${firstName} ${lastName} va être refusé. Un email sera envoyé au responsable à l'adresse : `} {`Le dossier d'inscription de ${firstName} ${lastName} va être refusé. Un email sera envoyé au responsable à l'adresse : `}
<span className="font-semibold text-blue-700">{email}</span> <span className="font-semibold text-blue-700">{email}</span>
{` avec la liste des documents non validés :`} {' avec la liste des documents non validés :'}
<ul className="list-disc ml-6 mt-2"> <ul className="list-disc ml-6 mt-2">
{refusedDocs.map(doc => ( {refusedDocs.map(doc => (
<li key={doc.idx}>{doc.name}</li> <li key={doc.idx}>{doc.name}</li>