feat: Réorganisation items dans la page [N3WTS-17]

This commit is contained in:
N3WT DE COMPET
2026-01-05 14:56:36 +01:00
parent a034149eae
commit 8549699dec
2 changed files with 418 additions and 237 deletions

View File

@ -2,6 +2,10 @@ import React, { useState, useEffect, useRef } from 'react';
import { import {
Edit3, Edit3,
Trash2, Trash2,
FileText,
Star,
ChevronDown,
Plus
} from 'lucide-react'; } from 'lucide-react';
import Modal from '@/components/Modal'; import Modal from '@/components/Modal';
import FormTemplateBuilder from '@/components/Form/FormTemplateBuilder'; import FormTemplateBuilder from '@/components/Form/FormTemplateBuilder';
@ -31,12 +35,14 @@ import Loader from '@/components/Loader';
import { useNotification } from '@/context/NotificationContext'; import { useNotification } from '@/context/NotificationContext';
import CreateDocumentModal from '@/components/Structure/Files/CreateDocumentModal'; import CreateDocumentModal from '@/components/Structure/Files/CreateDocumentModal';
import FileUpload from '@/components/Form/FileUpload'; import FileUpload from '@/components/Form/FileUpload';
import SectionTitle from '@/components/SectionTitle';
import DropdownMenu from '@/components/DropdownMenu';
function getItemBgColor(type, selected, forceTheme = false) { function getItemBgColor(type, selected, forceTheme = false) {
// Colonne gauche : bleu, sélectionné plus soutenu // Colonne gauche : blanc si rien n'est sélectionné, emerald si sélectionné
if (type === 'blue') { if (type === 'blue') {
if (selected) return 'bg-blue-200'; if (selected) return 'bg-emerald-100';
return 'bg-blue-50'; return 'bg-white';
} }
// Colonne droite : thème selon type, jamais sélectionné // Colonne droite : thème selon type, jamais sélectionné
if (forceTheme) { if (forceTheme) {
@ -57,38 +63,36 @@ function SimpleList({
minHeight = 'min-h-[200px]', minHeight = 'min-h-[200px]',
selectable = true, selectable = true,
forceTheme = false, forceTheme = false,
headerClassName = '',
listClassName = '',
itemClassName = '',
headerContent = null,
showGroups = false,
groupDocCount = null,
}) { }) {
return ( return (
<div className={`rounded border border-gray-200 bg-white ${minHeight} flex flex-col`}> <div className={`rounded border border-gray-200 bg-white ${minHeight} flex flex-col`}>
{title && ( {title && (
<div <div
className={` className={`
px-4 py-3 px-4 py-2
font-bold text-base border-b border-gray-200
border-b border-gray-300 bg-white
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 rounded-t
shadow-sm
tracking-wide
uppercase
flex items-center flex items-center
${headerClassName}
`} `}
> >
{title} {headerContent ? headerContent : (
<span className="text-base text-gray-700">{title}</span>
)}
</div> </div>
)} )}
<ul className="flex-1"> <ul className={`flex-1 ${listClassName}`}>
{items.length === 0 ? ( {items.length === 0 ? (
<li className="py-4 text-center text-gray-400">Aucun élément</li> <li className="py-4 text-center text-gray-400">Aucun élément</li>
) : ( ) : (
items.map((item, idx) => { 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 key = `${item.id}-${idx}`;
const selected = selectedId === item.id; const selected = selectedId === item.id;
const itemType = getItemType ? getItemType(item) : 'gray'; const itemType = getItemType ? getItemType(item) : 'gray';
@ -103,10 +107,40 @@ function SimpleList({
selectable && idx !== items.length - 1 selectable && idx !== items.length - 1
? '-mb-[1px]' ? '-mb-[1px]'
: ''; : '';
let description = '';
if (typeof item.description === 'string' && item.description.trim()) {
description = item.description;
} else if (
item._type === 'emerald' &&
item.formMasterData &&
typeof item.formMasterData.description === 'string' &&
item.formMasterData.description.trim()
) {
description = item.formMasterData.description;
} else {
description = 'aucune description fournie';
}
const groupsLabel =
showGroups && Array.isArray(item.groups) && item.groups.length > 0
? item.groups.map(g => g.name).join(', ')
: null;
const docCount = groupDocCount && typeof groupDocCount === 'function'
? groupDocCount(item)
: null;
const showCustomForm =
item._type === 'emerald' &&
Array.isArray(item.formMasterData?.fields) &&
item.formMasterData.fields.length > 0;
const showRequired =
item._type === 'orange' && item.is_required;
// Correction du bug liseré : appliquer un z-index élevé au premier item sélectionné
const extraZ = selected && idx === 0 ? 'z-20 relative' : '';
return ( return (
<li <li
key={key} 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}`} className={`flex items-center justify-between px-4 py-3 transition ${bgColor} ${selected && selectable ? 'ring-2 ring-emerald-400' : ''} ${selectable ? 'cursor-pointer' : ''} ${zIndex} ${marginFix} ${extraZ} ${typeof itemClassName === 'function' ? itemClassName(item) : itemClassName}`}
onClick={() => { onClick={() => {
if (!selectable || !onSelect) return; if (!selectable || !onSelect) return;
if (selected) { if (selected) {
@ -119,8 +153,31 @@ function SimpleList({
aria-selected={selected} aria-selected={selected}
role="option" role="option"
> >
<span className="font-medium text-gray-800">{item.name}</span> <div className="flex flex-col">
{actionButtons && actionButtons(item)} <span className="text-gray-800">{item.name}</span>
<span className="text-xs text-gray-400 mt-1 italic">
{description}
</span>
</div>
<div className="flex items-center gap-2">
{docCount !== null && (
<span className="text-xs text-blue-700 font-semibold mr-2">{docCount} document{docCount > 1 ? 's' : ''}</span>
)}
{showCustomForm && (
<span className="inline-flex items-center px-2 py-0.5 rounded text-xs font-bold border border-yellow-600 bg-yellow-400 text-yellow-900 mr-1">
Formulaire personnalisé
</span>
)}
{showRequired && (
<span className="inline-flex items-center px-2 py-0.5 rounded text-xs font-bold border border-orange-500 bg-orange-100 text-orange-700 mr-1">
Obligatoire
</span>
)}
{showGroups && groupsLabel && (
<span className="text-xs text-gray-500 mr-2">{groupsLabel}</span>
)}
{actionButtons && actionButtons(item)}
</div>
</li> </li>
); );
}) })
@ -137,7 +194,7 @@ export default function FilesGroupsManagement({
const [schoolFileMasters, setSchoolFileMasters] = useState([]); const [schoolFileMasters, setSchoolFileMasters] = useState([]);
const [parentFiles, setParentFileMasters] = useState([]); const [parentFiles, setParentFileMasters] = useState([]);
const [groups, setGroups] = useState([]); const [groups, setGroups] = useState([]);
const [selectedGroup, setSelectedGroup] = useState(null);
const [isModalOpen, setIsModalOpen] = useState(false); const [isModalOpen, setIsModalOpen] = useState(false);
const [isEditing, setIsEditing] = useState(false); const [isEditing, setIsEditing] = useState(false);
const [fileToEdit, setFileToEdit] = useState(null); const [fileToEdit, setFileToEdit] = useState(null);
@ -148,14 +205,35 @@ export default function FilesGroupsManagement({
const [removePopupMessage, setRemovePopupMessage] = useState(''); const [removePopupMessage, setRemovePopupMessage] = useState('');
const [removePopupOnConfirm, setRemovePopupOnConfirm] = useState(() => {}); const [removePopupOnConfirm, setRemovePopupOnConfirm] = useState(() => {});
const [isLoading, setIsLoading] = useState(false); const [isLoading, setIsLoading] = useState(false);
const [isCreateModalOpen, setIsCreateModalOpen] = useState(false);
const [createModalKey, setCreateModalKey] = useState(0);
const [selectedGroupId, setSelectedGroupId] = useState(null); const [selectedGroupId, setSelectedGroupId] = useState(null);
const [showHelp, setShowHelp] = useState(false); const [showHelp, setShowHelp] = useState(false);
const { showNotification } = useNotification(); const { showNotification } = useNotification();
const [isFormBuilderOpen, setIsFormBuilderOpen] = useState(false);
const [isFileUploadOpen, setIsFileUploadOpen] = useState(false);
const [isParentFileModalOpen, setIsParentFileModalOpen] = useState(false); const [isParentFileModalOpen, setIsParentFileModalOpen] = useState(false);
const [editingParentFile, setEditingParentFile] = useState(null); const [editingParentFile, setEditingParentFile] = useState(null);
const [isFileUploadModalOpen, setIsFileUploadModalOpen] = useState(false);
// Pour la popup "Télécharger un document existant"
const [isFileUploadPopupOpen, setIsFileUploadPopupOpen] = useState(false);
// Dropdown pour création de document
const [isDocDropdownOpen, setIsDocDropdownOpen] = useState(false);
// Handler pour le dropdown document (ouvre la bonne modale)
const handleDocDropdownSelect = (type) => {
setIsDocDropdownOpen(false);
if (type === 'formulaire') {
setIsFormBuilderOpen(true);
setIsEditing(false);
setFileToEdit(null);
} else if (type === 'formulaire_existant') {
setIsFileUploadPopupOpen(true);
setFileToEdit({});
} else if (type === 'parent') {
setEditingParentFile(null);
setIsParentFileModalOpen(true);
}
};
const transformFileData = (file, groups) => { const transformFileData = (file, groups) => {
// file.groups peut contenir des IDs (number/string) ou des objets {id, name} // file.groups peut contenir des IDs (number/string) ou des objets {id, name}
@ -363,6 +441,14 @@ export default function FilesGroupsManagement({
setIsGroupModalOpen(true); setIsGroupModalOpen(true);
}; };
// Handler pour sélectionner/désélectionner un groupe (simple)
const handleGroupSelect = (groupId) => {
setSelectedGroupId((prev) => (prev === groupId ? null : groupId));
};
// Handler pour tout désélectionner
const clearGroupSelection = () => setSelectedGroupId(null);
const handleGroupDelete = (groupId) => { const handleGroupDelete = (groupId) => {
// Vérifier si des schoolFileMasters utilisent ce groupe // Vérifier si des schoolFileMasters utilisent ce groupe
const filesInGroup = schoolFileMasters.filter( const filesInGroup = schoolFileMasters.filter(
@ -392,10 +478,8 @@ export default function FilesGroupsManagement({
throw new Error('Erreur lors de la suppression du groupe.'); throw new Error('Erreur lors de la suppression du groupe.');
} }
setGroups(groups.filter((group) => group.id !== groupId)); setGroups(groups.filter((group) => group.id !== groupId));
// Si le groupe supprimé était sélectionné, on désélectionne // Purger la sélection si le groupe supprimé était sélectionné
if (String(selectedGroupId) === String(groupId)) { setSelectedGroupId((prev) => (prev === groupId ? null : prev));
setSelectedGroupId(null);
}
setRemovePopupVisible(false); setRemovePopupVisible(false);
setIsLoading(false); setIsLoading(false);
showNotification('Groupe supprimé avec succès.', 'success', 'Succès'); showNotification('Groupe supprimé avec succès.', 'success', 'Succès');
@ -434,9 +518,9 @@ export default function FilesGroupsManagement({
}); });
}; };
// Correction du bug : ne pas supprimer l'élément lors de l'édition d'un doc parent
const handleEdit = (id, updatedFile) => { const handleEdit = (id, updatedFile) => {
logger.debug('[FilesGroupsManagement] handleEdit called with:', 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) { if (typeof updatedFile !== 'object' || updatedFile === null) {
logger.error('[FilesGroupsManagement] handleEdit: updatedFile is not an object', updatedFile); logger.error('[FilesGroupsManagement] handleEdit: updatedFile is not an object', updatedFile);
return Promise.reject(new Error('updatedFile is not an object')); return Promise.reject(new Error('updatedFile is not an object'));
@ -445,10 +529,11 @@ export default function FilesGroupsManagement({
return editRegistrationParentFileMaster(id, updatedFile, csrfToken) return editRegistrationParentFileMaster(id, updatedFile, csrfToken)
.then((response) => { .then((response) => {
logger.debug('[FilesGroupsManagement] editRegistrationParentFileMaster response:', response); logger.debug('[FilesGroupsManagement] editRegistrationParentFileMaster response:', response);
const modifiedFile = response.data; // Extraire les données mises à jour const modifiedFile = response.data || response;
setParentFileMasters((prevFiles) => setParentFileMasters((prevFiles) =>
prevFiles.map((file) => (file.id === id ? modifiedFile : file)) prevFiles.map((file) => (file.id === id ? modifiedFile : file))
); );
// Correction : ne pas filtrer/supprimer l'élément, juste le remplacer
logger.debug('Document parent mis à jour avec succès:', modifiedFile); logger.debug('Document parent mis à jour avec succès:', modifiedFile);
return modifiedFile; return modifiedFile;
}) })
@ -498,63 +583,7 @@ export default function FilesGroupsManagement({
setIsParentFileModalOpen(false); setIsParentFileModalOpen(false);
}; };
// Ouvre le menu de choix pour formulaire d'école // Nouvelle aide adaptée
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.map(String).includes(String(selectedGroupId))
);
});
// 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' })),
];
// Nouvelle explication: adaptée au contexte "dossier lié à une classe/groupe de classes"
const renderExplanation = () => ( const renderExplanation = () => (
<div className="mb-4"> <div className="mb-4">
<button <button
@ -575,22 +604,35 @@ export default function FilesGroupsManagement({
</h2> </h2>
<div className="text-gray-700 space-y-2"> <div className="text-gray-700 space-y-2">
<p> <p>
<span className="font-semibold">1. Créez un ou plusieurs <span className="text-blue-700">dossiers d&apos;inscription</span></span> :<br /> <span className="font-semibold">Organisation de la page :</span>
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. <br />
<span className="text-blue-700 font-semibold">Colonne de gauche</span> : liste des dossiers d&apos;inscription (groupes/classes).
<br />
<span className="text-emerald-700 font-semibold">Colonne de droite</span> : liste des documents à fournir pour l&apos;inscription.
</p> </p>
<p> <p>
<span className="font-semibold">2. Pour chaque dossier, ajoutez des documents à fournir :</span> <span className="font-semibold">Ajout de dossiers :</span>
<br />
Cliquez sur le bouton <span className="inline-flex items-center px-2 py-0.5 rounded text-xs font-bold bg-emerald-500 text-white border border-emerald-600">+</span> à droite de la liste pour créer un nouveau dossier d&apos;inscription.
</p>
<p>
<span className="font-semibold">Ajout de documents :</span>
<br />
Cliquez sur le bouton <span className="inline-flex items-center px-2 py-0.5 rounded text-xs font-bold bg-emerald-500 text-white border border-emerald-600">+</span> à droite de la liste des documents pour ajouter :
</p> </p>
<ul className="list-disc list-inside ml-6"> <ul className="list-disc list-inside ml-6">
<li> <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.). <span className="text-yellow-700 font-semibold">Formulaire personnalisé</span> : créé dynamiquement par l&apos;école, à remplir et/ou signer électroniquement par la famille.
</li> </li>
<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). <span className="text-black font-semibold">Formulaire existant</span> : importez un PDF ou autre document à faire remplir.
</li>
<li>
<span className="text-orange-700 font-semibold">Pièce à fournir</span> : document à déposer par la famille (ex : RIB, justificatif de domicile).
</li> </li>
</ul> </ul>
<div className="mt-2 text-sm text-gray-600"> <div className="mt-2 text-sm text-gray-600">
<span className="font-semibold">Astuce :</span> Commencez toujours par créer vos dossiers d&apos;inscription (liés à vos classes) avant d&apos;ajouter des documents à fournir. <span className="font-semibold">Astuce :</span> Créez d&apos;abord vos dossiers d&apos;inscription avant d&apos;ajouter des documents à fournir.
</div> </div>
</div> </div>
</div> </div>
@ -598,42 +640,86 @@ export default function FilesGroupsManagement({
</div> </div>
); );
// Correction : définition de handleBackToCreateMenu // Filtrage des formulaires et pièces selon le dossier sélectionné
const handleBackToCreateMenu = () => { // Si aucun groupe sélectionné, la colonne de droite est vide
setCreateModalKey((k) => k + 1); // force le reset du composant modal let filteredFiles = [];
setIsCreateModalOpen(true); let filteredParentFiles = [];
if (selectedGroupId) {
filteredFiles = schoolFileMasters
.map((file) => transformFileData(file, groups))
.filter(
(file) =>
file.groups &&
file.groups.some((group) => group.id === selectedGroupId)
);
filteredParentFiles = parentFiles.filter(
(file) =>
file.groups &&
file.groups.some((gid) =>
(typeof gid === 'object' ? gid.id : gid) === selectedGroupId
)
);
}
const mergedDocuments =
selectedGroupId
? [
...filteredFiles.map((doc) => ({ ...doc, _type: 'emerald' })),
...filteredParentFiles.map((doc) => ({ ...doc, _type: 'orange' })),
]
: [];
// Calcul du nombre de documents par groupe
const getGroupDocCount = (group) => {
const groupId = group.id;
let count = 0;
// Documents école
count += schoolFileMasters.filter(
(file) =>
Array.isArray(file.groups)
? file.groups.some((g) => (typeof g === 'object' ? g.id : g) === groupId)
: false
).length;
// Pièces à fournir
count += parentFiles.filter(
(file) =>
Array.isArray(file.groups)
? file.groups.some((g) => (typeof g === 'object' ? g.id : g) === groupId)
: false
).length;
return count;
}; };
// Nouvelle disposition: sections côte à côte alignées // Nouvelle disposition: sections côte à côte alignées
return ( return (
<div className="w-full"> <div className="w-full">
{/* Aide optionnelle + bouton global de création */} {/* Aide optionnelle */}
<div className="mb-8"> <div className="mb-8">{renderExplanation()}</div>
{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"> {/* 2 colonnes : groupes à gauche, documents à droite */}
{/* Colonne gauche : Dossiers d'inscription */} <div className="flex flex-row gap-8">
<div className="w-[30%] min-w-[260px]"> {/* Colonne groupes (1/3) */}
<div className="flex flex-col w-1/3 min-w-[320px] max-w-md">
<div className="flex items-center mb-4">
<SectionTitle title="Liste des dossiers d'inscriptions" />
<div className="flex-1" />
<button
className="flex items-center justify-center bg-emerald-500 hover:bg-emerald-600 text-white px-3 py-2 rounded-lg shadow transition text-base font-semibold"
onClick={() => setIsGroupModalOpen(true)}
title="Créer un nouveau dossier"
>
<Plus className="w-5 h-5" />
</button>
</div>
<SimpleList <SimpleList
items={groups} items={groups}
selectedId={selectedGroupId} selectedId={selectedGroupId}
onSelect={setSelectedGroupId} onSelect={handleGroupSelect}
getItemType={() => 'blue'} getItemType={() => 'blue'}
title="Dossiers d'inscription" minHeight="min-h-[60px]"
minHeight="min-h-[240px]"
selectable={true} selectable={true}
forceTheme={false} forceTheme={false}
groupDocCount={getGroupDocCount}
actionButtons={(row) => ( actionButtons={(row) => (
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<button <button
@ -654,130 +740,226 @@ export default function FilesGroupsManagement({
)} )}
/> />
</div> </div>
{/* Colonne droite : Documents (fusion des formulaires et pièces à fournir) */}
<div className="w-[70%] min-w-[320px]"> {/* Colonne documents (2/3) */}
<div className="rounded border border-gray-200 bg-white min-h-[240px] flex flex-col"> <div className="flex flex-col w-2/3">
<div <div className="flex items-center mb-4">
className={` <SectionTitle title="Liste des documents" />
px-4 py-3 <div className="flex-1" />
font-bold text-base <DropdownMenu
border-b border-gray-300 buttonContent={
bg-gradient-to-r <span className="flex items-center">
from-emerald-100 to-orange-50 text-emerald-800 <Plus className="w-5 h-5" />
rounded-t <ChevronDown className="w-4 h-4 ml-1" />
shadow-sm </span>
tracking-wide }
uppercase items={[
flex items-center {
`} type: 'item',
> label: (
Documents <span className="flex items-center">
</div> <Star className="w-5 h-5 mr-2 text-yellow-600" />
{selectedGroupId === null ? ( Formulaire personnalisé
<div className="flex items-center justify-center flex-1 bg-white text-gray-400 text-center text-base"> </span>
Sélectionnez un dossier pour voir les documents associés. ),
</div> onClick: () => handleDocDropdownSelect('formulaire'),
) : ( },
<SimpleList {
key={selectedGroupId} type: 'item',
items={mergedDocuments} label: (
selectedId={null} <span className="flex items-center">
onSelect={null} <FileText className="w-5 h-5 mr-2 text-gray-600" />
getItemType={(item) => item._type} Formulaire existant
// title="Documents" // Supprimé ici, header déjà affiché au-dessus </span>
minHeight="min-h-[240px]" ),
selectable={false} onClick: () => handleDocDropdownSelect('formulaire_existant'),
forceTheme={true} },
actionButtons={(row) => ( {
<div className="flex items-center gap-2"> type: 'item',
{row._type === 'emerald' ? ( label: (
<> <span className="flex items-center">
<button <Plus className="w-5 h-5 mr-2 text-orange-500" />
onClick={(e) => { e.stopPropagation(); editTemplateMaster(row); }} Pièce à fournir
className="text-blue-500 hover:text-blue-700" </span>
title="Modifier" ),
> onClick: () => handleDocDropdownSelect('parent'),
<Edit3 className="w-5 h-5" /> },
</button> ]}
<button buttonClassName="flex items-center justify-center bg-emerald-500 hover:bg-emerald-600 text-white px-3 py-2 rounded-lg shadow transition text-base font-semibold"
onClick={(e) => { e.stopPropagation(); deleteTemplateMaster(row); }} menuClassName="absolute right-0 mt-2 w-56 bg-white border border-gray-200 rounded shadow-lg z-20"
className="text-red-500 hover:text-red-700" dropdownOpen={isDocDropdownOpen}
title="Supprimer" setDropdownOpen={setIsDocDropdownOpen}
> />
<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>
{!selectedGroupId ? (
<div className="flex items-center justify-center h-40 text-gray-400 text-lg italic border border-gray-200 rounded bg-white">
Sélectionner un dossier d&apos;inscription
</div>
) : (
<SimpleList
key={selectedGroupId || 'all'}
items={mergedDocuments}
selectedId={null}
onSelect={null}
getItemType={(item) => item._type}
minHeight="min-h-[240px]"
selectable={false}
forceTheme={true}
listClassName=""
itemClassName="text-gray-800 bg-white"
title=""
headerContent={null}
showGroups={false}
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> </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 */} {/* Modals pour création/édition */}
<Modal <Modal
isOpen={isFileUploadModalOpen} isOpen={isFormBuilderOpen}
setIsOpen={handleCloseFileUpload} setIsOpen={setIsFormBuilderOpen}
title="Importer un formulaire existant" title="Créer un formulaire personnalisé"
modalClassName="w-full max-w-md" modalClassName="w-11/12 h-5/6"
> >
<FileUpload <FormTemplateBuilder
selectionMessage="Sélectionnez le fichier du formulaire" onSave={(data) => {
onFileSelect={(file) => { handleCreateSchoolFileMaster(data);
// Ici, il faut gérer le nom et les groupes (à adapter selon vos besoins UI) setIsFormBuilderOpen(false);
// Par exemple, ouvrir un petit formulaire pour compléter les infos puis submit
}} }}
onSubmit={handleSubmitFileUpload} groups={groups}
required isEditing={false}
enable
/> />
</Modal> </Modal>
{/* Modal création/édition pièce à fournir */} {/* Popup pour téléchargement d'un document existant */}
<Modal
isOpen={isFileUploadPopupOpen}
setIsOpen={setIsFileUploadPopupOpen}
title="Télécharger un document existant"
modalClassName="w-full max-w-md"
>
<form
className="flex flex-col gap-4"
onSubmit={e => {
e.preventDefault();
if (!fileToEdit?.name || !fileToEdit?.group_ids || !fileToEdit?.file) return;
handleCreateSchoolFileMaster({
name: fileToEdit.name,
group_ids: fileToEdit.group_ids,
file: fileToEdit.file,
});
setIsFileUploadPopupOpen(false);
setFileToEdit(null);
}}
>
<input
type="text"
className="border rounded px-3 py-2"
placeholder="Nom du document"
value={fileToEdit?.name || ''}
onChange={e => setFileToEdit({ ...fileToEdit, name: e.target.value })}
required
/>
<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={fileToEdit?.group_ids?.includes(group.id) || false}
onChange={(e) => {
let group_ids = fileToEdit?.group_ids || [];
if (e.target.checked) {
group_ids = [...group_ids, group.id];
} else {
group_ids = group_ids.filter((id) => id !== group.id);
}
setFileToEdit({ ...fileToEdit, group_ids });
}}
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 document"
onFileSelect={file =>
setFileToEdit({ ...fileToEdit, file })
}
required
enable
/>
<button
type="submit"
className="bg-emerald-600 text-white px-4 py-2 rounded font-bold mt-2"
disabled={
!fileToEdit?.name ||
!fileToEdit?.group_ids ||
fileToEdit.group_ids.length === 0 ||
!fileToEdit?.file
}
>
Créer le document
</button>
</form>
</Modal>
<Modal <Modal
isOpen={isParentFileModalOpen} isOpen={isParentFileModalOpen}
setIsOpen={(open) => { setIsOpen={(open) => {
if (!open) closeParentFileModal(); setIsParentFileModalOpen(open);
if (!open) setEditingParentFile(null);
}} }}
title={editingParentFile ? 'Modifier la pièce à fournir' : 'Créer une pièce à fournir'} title={editingParentFile ? 'Modifier la pièce à fournir' : 'Créer une pièce à fournir'}
modalClassName="w-full max-w-md" modalClassName="w-full max-w-md"
@ -790,11 +972,11 @@ export default function FilesGroupsManagement({
handleDelete={handleDelete} handleDelete={handleDelete}
singleForm // affiche uniquement le formulaire, pas la liste singleForm // affiche uniquement le formulaire, pas la liste
initialData={editingParentFile} initialData={editingParentFile}
onCancel={closeParentFileModal} onCancel={() => setIsParentFileModalOpen(false)}
/> />
</Modal> </Modal>
{/* Modals et popups */} {/* Modals et popups pour édition */}
<Modal <Modal
isOpen={isModalOpen} isOpen={isModalOpen}
setIsOpen={(isOpen) => { setIsOpen={(isOpen) => {
@ -802,8 +984,6 @@ export default function FilesGroupsManagement({
if (!isOpen) { if (!isOpen) {
setFileToEdit(null); setFileToEdit(null);
setIsEditing(false); setIsEditing(false);
// Retour au menu principal de création si fermeture
handleBackToCreateMenu();
} }
}} }}
title={isEditing ? 'Modification du formulaire' : 'Créer un formulaire'} title={isEditing ? 'Modification du formulaire' : 'Créer un formulaire'}
@ -812,6 +992,7 @@ export default function FilesGroupsManagement({
<FormTemplateBuilder <FormTemplateBuilder
onSave={(data) => { onSave={(data) => {
(isEditing ? handleEditSchoolFileMaster : handleCreateSchoolFileMaster)(data); (isEditing ? handleEditSchoolFileMaster : handleCreateSchoolFileMaster)(data);
setIsModalOpen(false);
}} }}
initialData={fileToEdit} initialData={fileToEdit}
groups={groups} groups={groups}
@ -824,8 +1005,7 @@ export default function FilesGroupsManagement({
setIsOpen={(isOpen) => { setIsOpen={(isOpen) => {
setIsGroupModalOpen(isOpen); setIsGroupModalOpen(isOpen);
if (!isOpen) { if (!isOpen) {
// Retour au menu principal de création si fermeture setGroupToEdit(null);
handleBackToCreateMenu();
} }
}} }}
title={ title={
@ -835,6 +1015,7 @@ export default function FilesGroupsManagement({
<RegistrationFileGroupForm <RegistrationFileGroupForm
onSubmit={(data) => { onSubmit={(data) => {
handleGroupSubmit(data); handleGroupSubmit(data);
setIsGroupModalOpen(false);
}} }}
initialData={groupToEdit} initialData={groupToEdit}
/> />

View File

@ -76,7 +76,7 @@ function ParentFileForm({ initialData, groups, onSubmit, onCancel }) {
</div> </div>
<div> <div>
<label className="block text-sm font-medium text-gray-700 mb-1"> <label className="block text-sm font-medium text-gray-700 mb-1">
Dossiers d&aposinscription <span className="text-red-500">*</span> Dossiers d&apos;inscription <span className="text-red-500">*</span>
</label> </label>
<select <select
multiple multiple