mirror of
https://git.v0id.ovh/n3wt-innov/n3wt-school.git
synced 2026-04-03 16:51:26 +00:00
feat: Gestion de l'affichage des documents validés et non validés sur la page parent [N3WTS-2]
This commit is contained in:
@ -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) {
|
||||||
|
|||||||
@ -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,19 +160,30 @@ 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) => {
|
||||||
|
const newData = {
|
||||||
...prev,
|
...prev,
|
||||||
[selectedFile.id]: true,
|
[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);
|
||||||
@ -164,7 +195,7 @@ export default function DynamicFormsList({
|
|||||||
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>
|
||||||
|
|
||||||
|
{/* Tri des templates par état */}
|
||||||
|
{(() => {
|
||||||
|
// Helper pour état
|
||||||
|
const getState = tpl => {
|
||||||
|
if (tpl.isValidated === true) return 0; // validé
|
||||||
|
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; // complété/en attente
|
||||||
|
return 2; // à compléter/refusé
|
||||||
|
};
|
||||||
|
const sortedTemplates = [...schoolFileTemplates].sort((a, b) => {
|
||||||
|
return getState(a) - getState(b);
|
||||||
|
});
|
||||||
|
return (
|
||||||
<ul className="space-y-2">
|
<ul className="space-y-2">
|
||||||
{schoolFileMasters.map((master, index) => {
|
{sortedTemplates.map((tpl, index) => {
|
||||||
const isActive = index === currentTemplateIndex;
|
const isActive = schoolFileTemplates[currentTemplateIndex]?.id === tpl.id;
|
||||||
const isCompleted = isFormCompleted(master.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 (
|
return (
|
||||||
<li
|
<li
|
||||||
key={master.id}
|
key={tpl.id}
|
||||||
className={`flex items-center cursor-pointer p-2 rounded-md transition-colors ${
|
className={`flex items-center cursor-pointer p-2 rounded-md transition-colors ${
|
||||||
isActive
|
isActive
|
||||||
? 'bg-blue-100 text-blue-700 font-semibold'
|
? `${bgClass.replace('50', '200')} ${borderClass.replace('200', '300')} ${textClass.replace('700', '900')} font-semibold`
|
||||||
: isCompleted
|
: `${bgClass} ${borderClass} ${textClass}`
|
||||||
? 'text-green-600 hover:bg-green-50'
|
|
||||||
: 'text-gray-600 hover:bg-gray-100'
|
|
||||||
}`}
|
}`}
|
||||||
onClick={() => setCurrentTemplateIndex(index)}
|
onClick={() => setCurrentTemplateIndex(schoolFileTemplates.findIndex(t => t.id === tpl.id))}
|
||||||
>
|
>
|
||||||
<span className="mr-3">
|
<span className="mr-3">{icon}</span>
|
||||||
{getFormStatusIcon(master.id, isActive)}
|
|
||||||
</span>
|
|
||||||
<div className="flex-1 min-w-0">
|
<div className="flex-1 min-w-0">
|
||||||
<div className="text-sm truncate">
|
<div className="text-sm truncate flex items-center gap-2">
|
||||||
{master.formMasterData?.title ||
|
{tpl.formMasterData?.title || tpl.title || tpl.name || 'Formulaire sans nom'}
|
||||||
master.title ||
|
<span className={`ml-2 px-2 py-0.5 rounded bg-${statusColor}-100 text-${statusColor}-700 text-xs font-semibold`}>
|
||||||
master.name ||
|
{statusLabel}
|
||||||
'Formulaire sans nom'}
|
</span>
|
||||||
</div>
|
</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">
|
<div className="text-xs text-gray-500">
|
||||||
{master.formMasterData?.fields || master.fields
|
{tpl.formMasterData?.fields || tpl.fields
|
||||||
? `${(master.formMasterData?.fields || master.fields).length} champ(s)`
|
? `${(tpl.formMasterData?.fields || tpl.fields).length} champ(s)`
|
||||||
: 'À compléter'}
|
: 'À compléter'}
|
||||||
</div>
|
</div>
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</ul>
|
</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 ||
|
|
||||||
'Formulaire sans nom'}
|
|
||||||
</h3>
|
</h3>
|
||||||
|
{/* Label d'état */}
|
||||||
|
{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">
|
||||||
|
|||||||
@ -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}
|
||||||
|
|||||||
@ -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,18 +244,9 @@ 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) */}
|
||||||
|
{index !== 0 && (
|
||||||
<span className="ml-2 flex gap-1">
|
<span className="ml-2 flex gap-1">
|
||||||
<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
|
|
||||||
${docStatuses[index] === undefined ? 'bg-gray-300 text-gray-700 border-gray-400' : 'bg-white text-gray-500 border-gray-300'}`}
|
|
||||||
aria-pressed={docStatuses[index] === undefined}
|
|
||||||
onClick={e => {
|
|
||||||
e.stopPropagation();
|
|
||||||
setDocStatuses(s => ({ ...s, [index]: undefined }));
|
|
||||||
}}
|
|
||||||
>À traiter</button>
|
|
||||||
<button
|
<button
|
||||||
type="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
|
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
|
||||||
@ -243,9 +255,8 @@ export default function ValidateSubscription({
|
|||||||
onClick={e => {
|
onClick={e => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
setDocStatuses(s => ({ ...s, [index]: 'accepted' }));
|
setDocStatuses(s => ({ ...s, [index]: 'accepted' }));
|
||||||
// Appel API pour valider le document (hors fiche élève)
|
// Appel API pour valider le document
|
||||||
if (index > 0 && handleValidateOrRefuseDoc) {
|
if (handleValidateOrRefuseDoc) {
|
||||||
// index 0 = fiche élève, ensuite school puis parent puis SEPA
|
|
||||||
let template = null;
|
let template = null;
|
||||||
let type = null;
|
let type = null;
|
||||||
if (index > 0 && index <= schoolFileTemplates.length) {
|
if (index > 0 && index <= schoolFileTemplates.length) {
|
||||||
@ -276,8 +287,8 @@ export default function ValidateSubscription({
|
|||||||
onClick={e => {
|
onClick={e => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
setDocStatuses(s => ({ ...s, [index]: 'refused' }));
|
setDocStatuses(s => ({ ...s, [index]: 'refused' }));
|
||||||
// Appel API pour refuser le document (hors fiche élève)
|
// Appel API pour refuser le document
|
||||||
if (index > 0 && handleValidateOrRefuseDoc) {
|
if (handleValidateOrRefuseDoc) {
|
||||||
let template = null;
|
let template = null;
|
||||||
let type = null;
|
let type = null;
|
||||||
if (index > 0 && index <= schoolFileTemplates.length) {
|
if (index > 0 && index <= schoolFileTemplates.length) {
|
||||||
@ -301,6 +312,7 @@ export default function ValidateSubscription({
|
|||||||
<span className="text-lg">✗</span> Refusé
|
<span className="text-lg">✗</span> Refusé
|
||||||
</button>
|
</button>
|
||||||
</span>
|
</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>
|
||||||
|
|||||||
Reference in New Issue
Block a user