feat: Changement du rendu de la page des documents + gestion des

formulaires d'école déjà existants [N3WTS-17]
This commit is contained in:
N3WT DE COMPET
2026-01-03 17:49:25 +01:00
parent 2dc0dfa268
commit 12f5fc7aa9
17 changed files with 1622 additions and 423 deletions

View File

@ -0,0 +1,209 @@
import React, { useState } from 'react';
import Modal from '@/components/Modal';
import { FolderPlus, FileText, FilePlus2, ArrowLeft, Settings2, Upload as UploadIcon } from 'lucide-react';
import FormTemplateBuilder from '@/components/Form/FormTemplateBuilder';
import FileUpload from '@/components/Form/FileUpload';
export default function CreateDocumentModal({
isOpen,
onClose,
onCreateGroup,
onCreateParentFile,
onCreateSchoolFileMaster,
groups = [],
}) {
const [step, setStep] = useState('main'); // main | choose_form | form_builder | file_upload
const [fileName, setFileName] = useState('');
const [selectedGroupsFileUpload, setSelectedGroupsFileUpload] = useState([]);
const [uploadedFile, setUploadedFile] = useState(null);
React.useEffect(() => {
if (!isOpen) {
setStep('main');
setFileName('');
setSelectedGroupsFileUpload([]);
setUploadedFile(null);
}
}, [isOpen]);
// Handler pour chaque type
const handleSelect = (type) => {
if (type === 'groupe') {
setStep('main');
onCreateGroup();
onClose();
}
if (type === 'formulaire') {
setStep('choose_form');
}
if (type === 'parent') {
setStep('main');
onCreateParentFile();
onClose();
}
};
// Retour au menu principal
const handleBack = () => setStep('main');
// Submit pour formulaire existant
const handleFileUploadSubmit = (e) => {
e.preventDefault();
if (!fileName || selectedGroupsFileUpload.length === 0 || !uploadedFile) return;
onCreateSchoolFileMaster({
name: fileName,
group_ids: selectedGroupsFileUpload,
file: uploadedFile,
});
onClose();
};
return (
<Modal
isOpen={isOpen}
setIsOpen={onClose}
title="Créer un document"
modalClassName="w-full max-w-md"
>
{step === 'main' && (
<div className="flex flex-col gap-6 py-4">
<button
className="flex items-center gap-3 px-4 py-3 rounded-lg bg-blue-50 hover:bg-blue-100 border border-blue-200 transition"
onClick={() => handleSelect('groupe')}
>
<FolderPlus className="w-6 h-6 text-blue-600" />
<span className="font-semibold text-blue-800">Dossier d&aposinscription</span>
</button>
<button
className="flex items-center gap-3 px-4 py-3 rounded-lg bg-emerald-50 hover:bg-emerald-100 border border-emerald-200 transition"
onClick={() => handleSelect('formulaire')}
>
<FileText className="w-6 h-6 text-emerald-600" />
<span className="font-semibold text-emerald-800">Formulaire scolaire</span>
</button>
<button
className="flex items-center gap-3 px-4 py-3 rounded-lg bg-orange-50 hover:bg-orange-100 border border-orange-200 transition"
onClick={() => handleSelect('parent')}
>
<FilePlus2 className="w-6 h-6 text-orange-500" />
<span className="font-semibold text-orange-700">Pièce à fournir</span>
</button>
</div>
)}
{step === 'choose_form' && (
<div className="flex flex-col gap-4 py-4">
<button
className="flex items-center gap-3 px-4 py-3 rounded-lg bg-emerald-100 hover:bg-emerald-200 border border-emerald-300 transition"
onClick={() => setStep('form_builder')}
>
<Settings2 className="w-6 h-6 text-emerald-700" />
<span className="font-semibold text-emerald-900">Formulaire personnalisé</span>
</button>
<button
className="flex items-center gap-3 px-4 py-3 rounded-lg bg-gray-100 hover:bg-gray-200 border border-gray-300 transition"
onClick={() => setStep('file_upload')}
>
<UploadIcon className="w-6 h-6 text-gray-700" />
<span className="font-semibold text-gray-900">Importer un formulaire existant</span>
</button>
<button
className="flex items-center gap-2 text-gray-600 hover:text-gray-900 mt-2"
onClick={handleBack}
>
<ArrowLeft className="w-5 h-5" />
<span>Retour</span>
</button>
</div>
)}
{step === 'form_builder' && (
<div>
<button
className="flex items-center gap-2 text-gray-600 hover:text-gray-900 mb-2"
onClick={handleBack}
>
<ArrowLeft className="w-5 h-5" />
<span>Retour</span>
</button>
<FormTemplateBuilder
onSave={(data) => {
onCreateSchoolFileMaster(data);
onClose();
}}
groups={groups}
isEditing={false}
/>
</div>
)}
{step === 'file_upload' && (
<div>
<button
className="flex items-center gap-2 text-gray-600 hover:text-gray-900 mb-2"
onClick={handleBack}
>
<ArrowLeft className="w-5 h-5" />
<span>Retour</span>
</button>
<form className="flex flex-col gap-4" onSubmit={handleFileUploadSubmit}>
<input
type="text"
className="border rounded px-3 py-2"
placeholder="Nom du formulaire"
value={fileName}
onChange={e => setFileName(e.target.value)}
required
/>
{/* Sélecteur de groupes à cocher */}
<div className="space-y-2">
<label className="block text-sm font-medium text-gray-700">
Groupes d&apos;inscription <span className="text-red-500">*</span>
</label>
<div className="space-y-2 max-h-32 overflow-y-auto border rounded-md p-2">
{groups && groups.length > 0 ? (
groups.map((group) => (
<label key={group.id} className="flex items-center">
<input
type="checkbox"
checked={selectedGroupsFileUpload.includes(group.id)}
onChange={(e) => {
if (e.target.checked) {
setSelectedGroupsFileUpload([
...selectedGroupsFileUpload,
group.id,
]);
} else {
setSelectedGroupsFileUpload(
selectedGroupsFileUpload.filter((id) => id !== group.id)
);
}
}}
className="mr-2 text-blue-600"
/>
<span className="text-sm">{group.name}</span>
</label>
))
) : (
<p className="text-gray-500 text-sm">
Aucun groupe disponible
</p>
)}
</div>
</div>
<FileUpload
selectionMessage="Sélectionnez le fichier du formulaire"
onFileSelect={setUploadedFile}
required
enable
/>
<button
type="submit"
className="bg-emerald-600 text-white px-4 py-2 rounded font-bold mt-2"
disabled={!fileName || selectedGroupsFileUpload.length === 0 || !uploadedFile}
>
Créer le formulaire
</button>
</form>
</div>
)}
</Modal>
);
}

View File

@ -10,8 +10,8 @@ import { useEstablishment } from '@/context/EstablishmentContext';
import Popup from '@/components/Popup';
export default function FileUploadDocuSeal({
handleCreateTemplateMaster,
handleEditTemplateMaster,
handleCreateSchoolFileMaster,
handleEditSchoolFileMaster,
fileToEdit = null,
onSuccess,
}) {
@ -75,7 +75,7 @@ export default function FileUploadDocuSeal({
const is_required = data.fields.length > 0;
if (fileToEdit) {
logger.debug('Modification du template master:', templateMaster?.id);
handleEditTemplateMaster({
handleEditSchoolFileMaster({
name: uploadedFileName,
group_ids: selectedGroups.map((group) => group.id),
id: templateMaster?.id,
@ -83,7 +83,7 @@ export default function FileUploadDocuSeal({
});
} else {
logger.debug('Création du template master:', templateMaster?.id);
handleCreateTemplateMaster({
handleCreateSchoolFileMaster({
name: uploadedFileName,
group_ids: selectedGroups.map((group) => group.id),
id: templateMaster?.id,

View File

@ -1,16 +1,10 @@
import React, { useState, useEffect } from 'react';
import React, { useState, useEffect, useRef } from 'react';
import {
Download,
Edit3,
Trash2,
FolderPlus,
FileText,
AlertTriangle,
} from 'lucide-react';
import Modal from '@/components/Modal';
import Table from '@/components/Table';
import FormTemplateBuilder from '@/components/Form/FormTemplateBuilder';
import { BASE_URL } from '@/utils/Url';
import {
// GET
fetchRegistrationFileGroups,
@ -31,13 +25,110 @@ import {
} from '@/app/actions/registerFileGroupAction';
import RegistrationFileGroupForm from '@/components/Structure/Files/RegistrationFileGroupForm';
import logger from '@/utils/logger';
import ParentFilesSection from '@/components/Structure/Files/ParentFilesSection';
import SectionHeader from '@/components/SectionHeader';
import ParentFiles from './ParentFiles';
import Popup from '@/components/Popup';
import Loader from '@/components/Loader';
import { useNotification } from '@/context/NotificationContext';
import AlertMessage from '@/components/AlertMessage';
import FileUploadDocuSeal from '@/components/Structure/Files/FileUploadDocuSeal';
import CreateDocumentModal from '@/components/Structure/Files/CreateDocumentModal';
import FileUpload from '@/components/Form/FileUpload';
function getItemBgColor(type, selected, forceTheme = false) {
// Colonne gauche : bleu, sélectionné plus soutenu
if (type === 'blue') {
if (selected) return 'bg-blue-200';
return 'bg-blue-50';
}
// Colonne droite : thème selon type, jamais sélectionné
if (forceTheme) {
if (type === 'emerald') return 'bg-emerald-50';
if (type === 'orange') return 'bg-orange-50';
return 'bg-gray-50';
}
return 'bg-white';
}
function SimpleList({
items,
onSelect,
selectedId,
actionButtons,
getItemType,
title,
minHeight = 'min-h-[200px]',
selectable = true,
forceTheme = false,
}) {
return (
<div className={`rounded border border-gray-200 bg-white ${minHeight} flex flex-col`}>
{title && (
<div
className={`
px-4 py-3
font-bold text-base
border-b border-gray-300
bg-gradient-to-r
${title === "Dossiers d'inscription"
? 'from-blue-100 to-blue-50 text-blue-800'
: title === 'Documents'
? 'from-emerald-100 to-orange-50 text-emerald-800'
: 'from-gray-100 to-white text-gray-800'}
rounded-t
shadow-sm
tracking-wide
uppercase
flex items-center
`}
>
{title}
</div>
)}
<ul className="flex-1">
{items.length === 0 ? (
<li className="py-4 text-center text-gray-400">Aucun élément</li>
) : (
items.map((item, idx) => {
// Correction : clé unique et stable même si plusieurs items ont le même id
// On concatène l'id et l'index pour garantir l'unicité
const key = `${item.id}-${idx}`;
const selected = selectedId === item.id;
const itemType = getItemType ? getItemType(item) : 'gray';
const bgColor = getItemBgColor(itemType, selected, forceTheme);
const zIndex =
selectable && selected
? 'z-10 relative'
: selectable
? 'z-0 relative'
: '';
const marginFix =
selectable && idx !== items.length - 1
? '-mb-[1px]'
: '';
return (
<li
key={key}
className={`flex items-center justify-between px-4 py-3 transition ${bgColor} ${selected && selectable ? 'ring-2 ring-blue-400' : ''} ${selectable ? 'cursor-pointer' : ''} ${zIndex} ${marginFix}`}
onClick={() => {
if (!selectable || !onSelect) return;
if (selected) {
onSelect(null);
} else {
onSelect(item.id);
}
}}
tabIndex={0}
aria-selected={selected}
role="option"
>
<span className="font-medium text-gray-800">{item.name}</span>
{actionButtons && actionButtons(item)}
</li>
);
})
)}
</ul>
</div>
);
}
export default function FilesGroupsManagement({
csrfToken,
@ -57,22 +148,34 @@ export default function FilesGroupsManagement({
const [removePopupMessage, setRemovePopupMessage] = useState('');
const [removePopupOnConfirm, setRemovePopupOnConfirm] = useState(() => {});
const [isLoading, setIsLoading] = useState(false);
const [isCreateModalOpen, setIsCreateModalOpen] = useState(false);
const [createModalKey, setCreateModalKey] = useState(0);
const [selectedGroupId, setSelectedGroupId] = useState(null);
const [showHelp, setShowHelp] = useState(false);
const { showNotification } = useNotification();
const [isParentFileModalOpen, setIsParentFileModalOpen] = useState(false);
const [editingParentFile, setEditingParentFile] = useState(null);
const [isFileUploadModalOpen, setIsFileUploadModalOpen] = useState(false);
const transformFileData = (file, groups) => {
const groupInfos = file.groups.map(
(groupId) =>
groups.find((g) => g.id === groupId) || {
id: groupId,
name: 'Groupe inconnu',
}
);
// file.groups peut contenir des IDs (number/string) ou des objets {id, name}
const groupInfos = (file.groups || []).map((group) => {
if (typeof group === 'object' && group !== null && 'id' in group) {
// Déjà un objet groupe
const found = groups.find((g) => g.id === group.id);
return found || group;
} else {
// C'est un ID
return groups.find((g) => g.id === group) || { id: group, name: 'Groupe inconnu' };
}
});
return {
...file,
groups: groupInfos,
};
};
// Ne pas transformer ici, stocker la donnée brute
useEffect(() => {
if (selectedEstablishmentId) {
Promise.all([
@ -83,11 +186,7 @@ export default function FilesGroupsManagement({
.then(([dataSchoolFileMasters, groupsData, dataParentFileMasters]) => {
setGroups(groupsData);
setParentFileMasters(dataParentFileMasters);
// Transformer chaque fichier pour inclure les informations complètes du groupe
const transformedFiles = dataSchoolFileMasters.map((file) =>
transformFileData(file, groupsData)
);
setSchoolFileMasters(transformedFiles);
setSchoolFileMasters(dataSchoolFileMasters); // donnée brute
})
.catch((err) => {
logger.debug(err.message);
@ -148,19 +247,22 @@ export default function FilesGroupsManagement({
setIsModalOpen(true);
};
const handleCreateTemplateMaster = ({ name, group_ids, formMasterData }) => {
const data = {
name: name,
const handleCreateSchoolFileMaster = ({ name, group_ids, formMasterData, file }) => {
// Toujours envoyer en FormData, même sans fichier
const dataToSend = new FormData();
const jsonData = {
name,
groups: group_ids,
formMasterData: formMasterData, // Envoyer directement l'objet
formMasterData,
};
logger.debug(data);
dataToSend.append('data', JSON.stringify(jsonData));
if (file) {
dataToSend.append('file', file, file.path || file.name);
}
createRegistrationSchoolFileMaster(data, csrfToken)
createRegistrationSchoolFileMaster(dataToSend, csrfToken)
.then((data) => {
// Transformer le nouveau fichier avec les informations du groupe
const transformedFile = transformFileData(data, groups);
setSchoolFileMasters((prevFiles) => [...prevFiles, transformedFile]);
setSchoolFileMasters((prevFiles) => [...prevFiles, data]);
setIsModalOpen(false);
showNotification(
`Le formulaire "${name}" a été créé avec succès.`,
@ -178,7 +280,7 @@ export default function FilesGroupsManagement({
});
};
const handleEditTemplateMaster = ({
const handleEditSchoolFileMaster = ({
name,
group_ids,
formMasterData,
@ -193,10 +295,8 @@ export default function FilesGroupsManagement({
editRegistrationSchoolFileMaster(id, data, csrfToken)
.then((data) => {
// Transformer le fichier mis à jour avec les informations du groupe
const transformedFile = transformFileData(data, groups);
setSchoolFileMasters((prevFichiers) =>
prevFichiers.map((f) => (f.id === id ? transformedFile : f))
prevFichiers.map((f) => (f.id === id ? data : f))
);
setIsModalOpen(false);
showNotification(
@ -292,6 +392,10 @@ export default function FilesGroupsManagement({
throw new Error('Erreur lors de la suppression du groupe.');
}
setGroups(groups.filter((group) => group.id !== groupId));
// Si le groupe supprimé était sélectionné, on désélectionne
if (String(selectedGroupId) === String(groupId)) {
setSelectedGroupId(null);
}
setRemovePopupVisible(false);
setIsLoading(false);
showNotification('Groupe supprimé avec succès.', 'success', 'Succès');
@ -331,15 +435,22 @@ export default function FilesGroupsManagement({
};
const handleEdit = (id, updatedFile) => {
logger.debug('[FilesGroupsManagement] handleEdit called with:', id, updatedFile);
// Correction : vérifier si updatedFile est bien un objet (et pas juste un id)
if (typeof updatedFile !== 'object' || updatedFile === null) {
logger.error('[FilesGroupsManagement] handleEdit: updatedFile is not an object', updatedFile);
return Promise.reject(new Error('updatedFile is not an object'));
}
logger.debug('[FilesGroupsManagement] handleEdit payload:', JSON.stringify(updatedFile));
return editRegistrationParentFileMaster(id, updatedFile, csrfToken)
.then((response) => {
logger.debug('[FilesGroupsManagement] editRegistrationParentFileMaster response:', response);
const modifiedFile = response.data; // Extraire les données mises à jour
// Mettre à jour la liste des fichiers parents
setParentFileMasters((prevFiles) =>
prevFiles.map((file) => (file.id === id ? modifiedFile : file))
);
logger.debug('Document parent mis à jour avec succès:', modifiedFile);
return modifiedFile; // Retourner le fichier mis à jour
return modifiedFile;
})
.catch((error) => {
logger.error(
@ -369,77 +480,321 @@ export default function FilesGroupsManagement({
});
};
const filteredFiles = schoolFileMasters.filter((file) => {
if (!selectedGroup) return true;
// Ouvre la modale de création d'une pièce à fournir
const openParentFileModal = () => {
setEditingParentFile(null);
setIsParentFileModalOpen(true);
};
// Ouvre la modale d'édition d'une pièce à fournir
const openEditParentFileModal = (file) => {
setEditingParentFile(file);
setIsParentFileModalOpen(true);
};
// Ferme la modale de pièce à fournir
const closeParentFileModal = () => {
setEditingParentFile(null);
setIsParentFileModalOpen(false);
};
// Ouvre le menu de choix pour formulaire d'école
const handleCreateFormMenu = () => {
setIsCreateModalOpen(false);
setTimeout(() => setIsCreateModalOpen(true), 0); // force le reset du menu
};
// Ouvre la modale FormTemplateBuilder
const handleOpenFormTemplateBuilder = () => {
setIsModalOpen(true);
setIsEditing(false);
setFileToEdit(null);
};
// Ouvre la modale FileUpload
const handleOpenFileUpload = () => {
setIsFileUploadModalOpen(true);
};
// Ferme la modale FileUpload
const handleCloseFileUpload = () => {
setIsFileUploadModalOpen(false);
};
// Soumission du formulaire existant (upload)
const handleSubmitFileUpload = ({ name, group_ids, file }) => {
// On centralise la logique dans handleCreateSchoolFileMaster
handleCreateSchoolFileMaster({ name, group_ids, file });
setIsFileUploadModalOpen(false);
};
// Filtrage des formulaires et pièces selon le dossier sélectionné
// Appliquer la transformation à la volée ici et comparer en string
const filteredFiles = schoolFileMasters
.map((file) => transformFileData(file, groups))
.filter((file) => {
if (!selectedGroupId) return false;
return (
file.groups &&
file.groups.some((group) => String(group.id) === String(selectedGroupId))
);
});
const filteredParentFiles = parentFiles.filter((file) => {
if (!selectedGroupId) return false;
return (
file.groups &&
file.groups.some((group) => group.id === parseInt(selectedGroup))
file.groups.map(String).includes(String(selectedGroupId))
);
});
const columnsFiles = [
{ name: 'Nom du formulaire', transform: (row) => row.name },
{
name: "Dossiers d'inscription",
transform: (row) =>
row.groups && row.groups.length > 0
? row.groups.map((group) => group.name).join(', ')
: 'Aucun',
},
{
name: 'Actions',
transform: (row) => (
<div className="flex items-center justify-center gap-2">
<button
onClick={() => editTemplateMaster(row)}
className="text-blue-500 hover:text-blue-700"
title="Modifier le formulaire"
>
<Edit3 className="w-5 h-5" />
</button>
<button
onClick={() => deleteTemplateMaster(row)}
className="text-red-500 hover:text-red-700"
title="Supprimer le formulaire"
>
<Trash2 className="w-5 h-5" />
</button>
</div>
),
},
// Fusion des deux types de documents pour la colonne de droite
const mergedDocuments = [
...filteredFiles.map((doc) => ({ ...doc, _type: 'emerald' })),
...filteredParentFiles.map((doc) => ({ ...doc, _type: 'orange' })),
];
const columnsGroups = [
{ name: 'Nom du dossier', transform: (row) => row.name },
{ name: 'Description', transform: (row) => row.description },
{
name: 'Actions',
transform: (row) => (
<div className="flex items-center justify-center gap-2">
<button
onClick={() => handleGroupEdit(row)}
className="text-blue-500 hover:text-blue-700"
>
<Edit3 className="w-5 h-5" />
</button>
<button
onClick={() => handleGroupDelete(row.id)}
className="text-red-500 hover:text-red-700"
>
<Trash2 className="w-5 h-5" />
</button>
// Nouvelle explication: adaptée au contexte "dossier lié à une classe/groupe de classes"
const renderExplanation = () => (
<div className="mb-4">
<button
className="flex items-center gap-2 text-emerald-700 hover:text-emerald-900 font-medium mb-2"
onClick={() => setShowHelp((v) => !v)}
aria-expanded={showHelp}
aria-controls="aide-inscription"
>
<span className="underline">{showHelp ? 'Masquer' : 'Afficher'} laide</span>
<svg className={`w-4 h-4 transition-transform ${showHelp ? 'rotate-180' : ''}`} fill="none" stroke="currentColor" strokeWidth="2" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" d="M19 9l-7 7-7-7" />
</svg>
</button>
{showHelp && (
<div id="aide-inscription" className="p-4 bg-blue-50 border border-blue-200 rounded mb-4">
<h2 className="text-lg font-bold mb-2">
Gestion des dossiers et documents d&apos;inscription
</h2>
<div className="text-gray-700 space-y-2">
<p>
<span className="font-semibold">1. Créez un ou plusieurs <span className="text-blue-700">dossiers d&aposinscription</span></span> :<br />
Chaque dossier correspond à une classe ou un groupe de classes (ex : <span className="italic">Dossier Maternelles</span>, <span className="italic">Dossier Élémentaires</span>). Lors de la création d&apos;une inscription élève, un seul dossier d&apos;inscription sera rattaché à l&apos;élève.
</p>
<p>
<span className="font-semibold">2. Pour chaque dossier, ajoutez des documents à fournir :</span>
</p>
<ul className="list-disc list-inside ml-6">
<li>
<span className="text-emerald-700 font-semibold">Formulaires personnalisés</span> : créés dynamiquement par l&aposécole, à remplir et/ou signer électroniquement par la famille (ex : autorisation de sortie, fiche sanitaire, etc.).
</li>
<li>
<span className="text-orange-700 font-semibold">Pièces à fournir</span> : documents à déposer par la famille (ex : RIB, justificatif de domicile, ou formulaire PDF à télécharger, remplir puis ré-uploader).
</li>
</ul>
<div className="mt-2 text-sm text-gray-600">
<span className="font-semibold">Astuce :</span> Commencez toujours par créer vos dossiers d&aposinscription (liés à vos classes) avant d&aposajouter des documents à fournir.
</div>
</div>
</div>
),
},
];
)}
</div>
);
if (isLoading) {
return <Loader />;
}
// Correction : définition de handleBackToCreateMenu
const handleBackToCreateMenu = () => {
setCreateModalKey((k) => k + 1); // force le reset du composant modal
setIsCreateModalOpen(true);
};
// Nouvelle disposition: sections côte à côte alignées
return (
<div className="w-full">
{/* Modal pour les formulaires */}
{/* Aide optionnelle + bouton global de création */}
<div className="mb-8">
{renderExplanation()}
<div className="flex justify-center">
<button
className="flex items-center justify-center gap-3 bg-emerald-500 hover:bg-emerald-600 text-white px-8 py-4 rounded-xl shadow transition text-lg font-bold"
style={{ minWidth: '320px', maxWidth: '400px' }}
onClick={() => setIsCreateModalOpen(true)}
>
<span className="text-2xl font-bold">+</span>
<span>Créer un nouvel élément</span>
</button>
</div>
</div>
<div className="flex gap-8">
{/* Colonne gauche : Dossiers d'inscription */}
<div className="w-[30%] min-w-[260px]">
<SimpleList
items={groups}
selectedId={selectedGroupId}
onSelect={setSelectedGroupId}
getItemType={() => 'blue'}
title="Dossiers d'inscription"
minHeight="min-h-[240px]"
selectable={true}
forceTheme={false}
actionButtons={(row) => (
<div className="flex items-center gap-2">
<button
onClick={(e) => { e.stopPropagation(); handleGroupEdit(row); }}
className="text-blue-500 hover:text-blue-700"
title="Modifier"
>
<Edit3 className="w-5 h-5" />
</button>
<button
onClick={(e) => { e.stopPropagation(); handleGroupDelete(row.id); }}
className="text-red-500 hover:text-red-700"
title="Supprimer"
>
<Trash2 className="w-5 h-5" />
</button>
</div>
)}
/>
</div>
{/* Colonne droite : Documents (fusion des formulaires et pièces à fournir) */}
<div className="w-[70%] min-w-[320px]">
<div className="rounded border border-gray-200 bg-white min-h-[240px] flex flex-col">
<div
className={`
px-4 py-3
font-bold text-base
border-b border-gray-300
bg-gradient-to-r
from-emerald-100 to-orange-50 text-emerald-800
rounded-t
shadow-sm
tracking-wide
uppercase
flex items-center
`}
>
Documents
</div>
{selectedGroupId === null ? (
<div className="flex items-center justify-center flex-1 bg-white text-gray-400 text-center text-base">
Sélectionnez un dossier pour voir les documents associés.
</div>
) : (
<SimpleList
key={selectedGroupId}
items={mergedDocuments}
selectedId={null}
onSelect={null}
getItemType={(item) => item._type}
// title="Documents" // Supprimé ici, header déjà affiché au-dessus
minHeight="min-h-[240px]"
selectable={false}
forceTheme={true}
actionButtons={(row) => (
<div className="flex items-center gap-2">
{row._type === 'emerald' ? (
<>
<button
onClick={(e) => { e.stopPropagation(); editTemplateMaster(row); }}
className="text-blue-500 hover:text-blue-700"
title="Modifier"
>
<Edit3 className="w-5 h-5" />
</button>
<button
onClick={(e) => { e.stopPropagation(); deleteTemplateMaster(row); }}
className="text-red-500 hover:text-red-700"
title="Supprimer"
>
<Trash2 className="w-5 h-5" />
</button>
</>
) : (
<>
<button
onClick={(e) => { e.stopPropagation(); openEditParentFileModal(row); }}
className="text-blue-500 hover:text-blue-700"
title="Modifier"
>
<Edit3 className="w-5 h-5" />
</button>
<button
onClick={(e) => { e.stopPropagation(); handleDelete(row.id); }}
className="text-red-500 hover:text-red-700"
title="Supprimer"
>
<Trash2 className="w-5 h-5" />
</button>
</>
)}
</div>
)}
/>
)}
</div>
</div>
</div>
{/* Modal de création centralisée */}
<CreateDocumentModal
key={createModalKey}
isOpen={isCreateModalOpen}
onClose={() => setIsCreateModalOpen(false)}
onCreateGroup={() => {
setIsGroupModalOpen(true);
setIsCreateModalOpen(false);
}}
onCreateForm={() => {
setIsCreateModalOpen(false);
setTimeout(() => setIsModalOpen(true), 0);
}}
onCreateParentFile={() => {
openParentFileModal();
setIsCreateModalOpen(false);
}}
onBack={handleBackToCreateMenu}
onCreateSchoolFileMaster={handleCreateSchoolFileMaster}
groups={groups}
/>
{/* Modal pour upload de formulaire existant */}
<Modal
isOpen={isFileUploadModalOpen}
setIsOpen={handleCloseFileUpload}
title="Importer un formulaire existant"
modalClassName="w-full max-w-md"
>
<FileUpload
selectionMessage="Sélectionnez le fichier du formulaire"
onFileSelect={(file) => {
// Ici, il faut gérer le nom et les groupes (à adapter selon vos besoins UI)
// Par exemple, ouvrir un petit formulaire pour compléter les infos puis submit
}}
onSubmit={handleSubmitFileUpload}
required
enable
/>
</Modal>
{/* Modal création/édition pièce à fournir */}
<Modal
isOpen={isParentFileModalOpen}
setIsOpen={(open) => {
if (!open) closeParentFileModal();
}}
title={editingParentFile ? 'Modifier la pièce à fournir' : 'Créer une pièce à fournir'}
modalClassName="w-full max-w-md"
>
<ParentFiles
parentFiles={parentFiles}
groups={groups}
handleCreate={handleCreate}
handleEdit={handleEdit}
handleDelete={handleDelete}
singleForm // affiche uniquement le formulaire, pas la liste
initialData={editingParentFile}
onCancel={closeParentFileModal}
/>
</Modal>
{/* Modals et popups */}
<Modal
isOpen={isModalOpen}
setIsOpen={(isOpen) => {
@ -447,100 +802,51 @@ export default function FilesGroupsManagement({
if (!isOpen) {
setFileToEdit(null);
setIsEditing(false);
// Retour au menu principal de création si fermeture
handleBackToCreateMenu();
}
}}
title={isEditing ? 'Modification du formulaire' : 'Créer un formulaire'}
modalClassName="w-11/12 h-5/6"
>
<FormTemplateBuilder
onSave={
isEditing ? handleEditTemplateMaster : handleCreateTemplateMaster
}
onSave={(data) => {
(isEditing ? handleEditSchoolFileMaster : handleCreateSchoolFileMaster)(data);
}}
initialData={fileToEdit}
groups={groups}
isEditing={isEditing}
/>
</Modal>
{/* Modal pour les groupes */}
<Modal
isOpen={isGroupModalOpen}
setIsOpen={setIsGroupModalOpen}
setIsOpen={(isOpen) => {
setIsGroupModalOpen(isOpen);
if (!isOpen) {
// Retour au menu principal de création si fermeture
handleBackToCreateMenu();
}
}}
title={
groupToEdit ? 'Modifier le dossier' : "Création d'un nouveau dossier"
}
>
<RegistrationFileGroupForm
onSubmit={handleGroupSubmit}
onSubmit={(data) => {
handleGroupSubmit(data);
}}
initialData={groupToEdit}
/>
</Modal>
{/* Section Groupes de fichiers */}
<div className="mt-8 w-3/5">
<SectionHeader
icon={FolderPlus}
title="Dossiers d'inscriptions"
description="Gérez les dossiers d'inscription pour organiser vos documents."
button={true}
buttonOpeningModal={true}
onClick={() => setIsGroupModalOpen(true)}
/>
<Table
data={groups}
columns={columnsGroups}
emptyMessage={
<AlertMessage
type="warning"
title="Aucun dossier d'inscription enregistré"
message="Veuillez procéder à la création d'un nouveau dossier d'inscription"
/>
}
/>
</div>
{/* Section Formulaires */}
<div className="mt-12 mb-4 w-3/5">
<SectionHeader
icon={FileText}
title="Formulaires personnalisés"
description="Créez et gérez vos formulaires d'inscription personnalisés."
button={true}
buttonOpeningModal={true}
onClick={() => {
setIsModalOpen(true);
setIsEditing(false);
setFileToEdit(null);
}}
/>
<Table
data={filteredFiles}
columns={columnsFiles}
emptyMessage={
<AlertMessage
type="warning"
title="Aucun formulaire enregistré"
message="Veuillez procéder à la création d'un nouveau formulaire d'inscription"
/>
}
/>
</div>
{/* Section Pièces à fournir */}
<ParentFilesSection
parentFiles={parentFiles}
setParentFileMasters={setParentFileMasters}
groups={groups}
handleCreate={handleCreate}
handleEdit={handleEdit}
handleDelete={handleDelete}
/>
<Popup
isOpen={removePopupVisible}
message={removePopupMessage}
onConfirm={removePopupOnConfirm}
onCancel={() => setRemovePopupVisible(false)}
/>
{isLoading && <Loader />}
</div>
);
}

View File

@ -0,0 +1,267 @@
import React, { useState, useEffect } from 'react';
import Modal from '@/components/Modal';
import logger from '@/utils/logger';
import { Edit3, Trash2, Plus } from 'lucide-react';
function ParentFileForm({ initialData, groups, onSubmit, onCancel }) {
const [name, setName] = useState(initialData?.name || '');
const [description, setDescription] = useState(initialData?.description || '');
// Correction : s'assurer que selectedGroups ne contient que des IDs uniques
const [selectedGroups, setSelectedGroups] = useState(
Array.isArray(initialData?.groups)
? Array.from(
new Set(
initialData.groups.map(g => (typeof g === 'object' && g !== null && 'id' in g ? g.id : g))
)
)
: []
);
const [isRequired, setIsRequired] = useState(initialData?.is_required || false);
useEffect(() => {
if (initialData) {
setName(initialData.name || '');
setDescription(initialData.description || '');
setSelectedGroups(
Array.isArray(initialData.groups)
? Array.from(
new Set(
initialData.groups.map(g => (typeof g === 'object' && g !== null && 'id' in g ? g.id : g))
)
)
: []
);
setIsRequired(initialData.is_required || false);
}
}, [initialData]);
const handleSubmit = (e) => {
e.preventDefault();
if (!name || selectedGroups.length === 0) return;
const data = {
name,
description,
groups: selectedGroups,
is_required: isRequired,
id: initialData?.id,
};
logger.debug('[ParentFileForm] handleSubmit data:', data);
onSubmit(data);
};
return (
<form onSubmit={handleSubmit} className="space-y-4 py-2">
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Nom de la pièce <span className="text-red-500">*</span>
</label>
<input
type="text"
value={name}
required
onChange={e => setName(e.target.value)}
className="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-1 focus:ring-orange-500 focus:border-orange-500"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Description
</label>
<textarea
value={description}
onChange={e => setDescription(e.target.value)}
rows={2}
className="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-1 focus:ring-orange-500 focus:border-orange-500"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Dossiers d&aposinscription <span className="text-red-500">*</span>
</label>
<select
multiple
value={selectedGroups}
onChange={e =>
setSelectedGroups(
Array.from(new Set(Array.from(e.target.selectedOptions, opt => Number(opt.value))))
)
}
className="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-1 focus:ring-orange-500 focus:border-orange-500"
required
>
{groups.map(group => (
<option key={`group-option-${group.id}`} value={group.id}>
{group.name}
</option>
))}
</select>
</div>
<div className="flex items-center gap-2">
<input
type="checkbox"
id="is_required"
checked={isRequired}
onChange={e => setIsRequired(e.target.checked)}
className="mr-2"
/>
<label htmlFor="is_required" className="text-sm text-gray-700">
Obligatoire
</label>
</div>
<div className="flex justify-end gap-2">
<button
type="button"
className="px-4 py-2 rounded-md bg-gray-200 text-gray-700 hover:bg-gray-300"
onClick={onCancel}
>
Annuler
</button>
<button
type="submit"
className="bg-orange-600 text-white px-4 py-2 rounded-md hover:bg-orange-700 focus:outline-none focus:ring-2 focus:ring-orange-500 focus:ring-offset-2"
disabled={!name || selectedGroups.length === 0}
>
{initialData?.id ? 'Modifier' : 'Créer'}
</button>
</div>
</form>
);
}
export default function ParentFiles({
parentFiles,
groups,
handleCreate,
handleEdit,
handleDelete,
singleForm = false,
initialData = null,
onCancel,
}) {
const [isModalOpen, setIsModalOpen] = useState(singleForm);
const [editingFile, setEditingFile] = useState(initialData);
useEffect(() => {
if (singleForm) {
setIsModalOpen(true);
setEditingFile(initialData);
}
}, [singleForm, initialData]);
const openCreateModal = () => {
setEditingFile(null);
setIsModalOpen(true);
};
const openEditModal = (file) => {
setEditingFile(file);
setIsModalOpen(true);
};
const closeModal = () => {
setEditingFile(null);
setIsModalOpen(false);
if (onCancel) onCancel();
};
const handleFormSubmit = (data) => {
logger.debug('[ParentFiles] handleFormSubmit data:', data);
if (editingFile && editingFile.id) {
logger.debug('[ParentFiles] handleEdit called with:', data.id, data);
handleEdit(data.id, data).then(closeModal);
} else {
logger.debug('[ParentFiles] handleCreate called with:', data);
handleCreate(data).then(closeModal);
}
};
if (singleForm) {
return (
<ParentFileForm
initialData={editingFile}
groups={groups}
onSubmit={handleFormSubmit}
onCancel={closeModal}
/>
);
}
return (
<div className="w-full">
<div className="flex items-center justify-between mb-4">
<h2 className="text-lg font-bold text-orange-700">Pièces à fournir</h2>
<button
className="flex items-center gap-2 bg-orange-500 hover:bg-orange-600 text-white px-4 py-2 rounded shadow"
onClick={openCreateModal}
>
<Plus className="w-5 h-5" />
<span>Ajouter une pièce</span>
</button>
</div>
<table className="min-w-full border border-gray-200 rounded bg-white">
<thead>
<tr className="bg-orange-50">
<th className="px-3 py-2 text-left text-xs font-medium text-orange-700 border-b">Nom</th>
<th className="px-3 py-2 text-left text-xs font-medium text-orange-700 border-b">Description</th>
<th className="px-3 py-2 text-left text-xs font-medium text-orange-700 border-b">Dossiers</th>
<th className="px-3 py-2 text-center text-xs font-medium text-orange-700 border-b">Obligatoire</th>
<th className="px-3 py-2 text-center text-xs font-medium text-orange-700 border-b">Actions</th>
</tr>
</thead>
<tbody>
{parentFiles.length === 0 ? (
<tr>
<td colSpan={5} className="text-center py-6 text-gray-400">Aucune pièce à fournir</td>
</tr>
) : (
parentFiles.map((file) => (
<tr key={file.id} className="hover:bg-orange-50">
<td className="px-3 py-2 border-b">{file.name}</td>
<td className="px-3 py-2 border-b">{file.description}</td>
<td className="px-3 py-2 border-b">
{(file.groups || []).map(
gid => groups.find(g => g.id === gid)?.name || gid
).join(', ')}
</td>
<td className="px-3 py-2 border-b text-center">
{file.is_required ? (
<span className="bg-green-100 text-green-700 px-2 py-1 rounded text-xs font-semibold">Oui</span>
) : (
<span className="bg-gray-100 text-gray-600 px-2 py-1 rounded text-xs">Non</span>
)}
</td>
<td className="px-3 py-2 border-b text-center">
<button
className="text-blue-500 hover:text-blue-700 mr-2"
onClick={() => openEditModal(file)}
>
<Edit3 className="w-5 h-5" />
</button>
<button
className="text-red-500 hover:text-red-700"
onClick={() => handleDelete(file.id)}
>
<Trash2 className="w-5 h-5" />
</button>
</td>
</tr>
))
)}
</tbody>
</table>
<Modal
isOpen={isModalOpen}
setIsOpen={closeModal}
title={editingFile ? 'Modifier la pièce à fournir' : 'Créer une pièce à fournir'}
modalClassName="w-full max-w-md"
>
<ParentFileForm
initialData={editingFile}
groups={groups}
onSubmit={handleFormSubmit}
onCancel={closeModal}
/>
</Modal>
</div>
);
}

View File

@ -1,16 +1,13 @@
import React, { useState } from 'react';
import { Plus, Edit3, Trash2, Check, X, FileText } from 'lucide-react';
import Table from '@/components/Table';
import InputText from '@/components/Form/InputText';
import MultiSelect from '@/components/Form/MultiSelect';
import Popup from '@/components/Popup';
import logger from '@/utils/logger';
import { createRegistrationParentFileTemplate } from '@/app/actions/registerFileGroupAction';
import { useCsrfToken } from '@/context/CsrfContext';
import SectionHeader from '@/components/SectionHeader';
import ToggleSwitch from '@/components/Form/ToggleSwitch';
import { useNotification } from '@/context/NotificationContext';
import AlertMessage from '@/components/AlertMessage';
import Popup from '@/components/Popup';
import InputText from '@/components/Form/InputText';
import MultiSelect from '@/components/Form/MultiSelect';
import ToggleSwitch from '@/components/Form/ToggleSwitch';
export default function ParentFilesSection({
parentFiles,
@ -18,6 +15,11 @@ export default function ParentFilesSection({
handleCreate,
handleEdit,
handleDelete,
hideCreateButton = false,
tableContainerClass = '',
headerClassName = '',
TableComponent,
SectionHeaderComponent,
}) {
const [editingDocumentId, setEditingDocumentId] = useState(null);
const [formData, setFormData] = useState(null);
@ -325,27 +327,30 @@ export default function ParentFilesSection({
},
];
// Ajout : écouteur d'event global pour déclencher la création depuis la popup centrale
React.useEffect(() => {
if (!hideCreateButton) return;
const handler = () => handleAddEmptyRequiredDocument();
window.addEventListener('parentFilesSection:create', handler);
return () => window.removeEventListener('parentFilesSection:create', handler);
}, [hideCreateButton]);
const Table = TableComponent || ((props) => <div />); // fallback
const SectionHeader = SectionHeaderComponent || ((props) => <div />);
return (
<div className="mt-12 w-4/5">
<div className={`w-full h-full flex flex-col ${tableContainerClass}`}>
<SectionHeader
icon={FileText}
title="Pièces à fournir"
description="Configurez la liste des documents que les parents doivent fournir."
button={true}
onClick={handleAddEmptyRequiredDocument}
className={headerClassName}
/>
<Table
data={
editingDocumentId === 'new' ? [formData, ...parentFiles] : parentFiles
}
columns={columnsRequiredDocuments}
emptyMessage={
<AlertMessage
type="warning"
title="Aucune pièce à fournir enregistrée"
message="Veuillez procéder à la création de nouvelles pièces à fournir par les parents"
/>
}
emptyMessage="Aucune pièce à fournir enregistrée"
/>
<Popup
isOpen={removePopupVisible}