mirror of
https://git.v0id.ovh/n3wt-innov/n3wt-school.git
synced 2026-01-28 15:33:22 +00:00
feat: Réorganisation items dans la page [N3WTS-17]
This commit is contained in:
@ -2,6 +2,10 @@ import React, { useState, useEffect, useRef } from 'react';
|
||||
import {
|
||||
Edit3,
|
||||
Trash2,
|
||||
FileText,
|
||||
Star,
|
||||
ChevronDown,
|
||||
Plus
|
||||
} from 'lucide-react';
|
||||
import Modal from '@/components/Modal';
|
||||
import FormTemplateBuilder from '@/components/Form/FormTemplateBuilder';
|
||||
@ -31,12 +35,14 @@ import Loader from '@/components/Loader';
|
||||
import { useNotification } from '@/context/NotificationContext';
|
||||
import CreateDocumentModal from '@/components/Structure/Files/CreateDocumentModal';
|
||||
import FileUpload from '@/components/Form/FileUpload';
|
||||
import SectionTitle from '@/components/SectionTitle';
|
||||
import DropdownMenu from '@/components/DropdownMenu';
|
||||
|
||||
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 (selected) return 'bg-blue-200';
|
||||
return 'bg-blue-50';
|
||||
if (selected) return 'bg-emerald-100';
|
||||
return 'bg-white';
|
||||
}
|
||||
// Colonne droite : thème selon type, jamais sélectionné
|
||||
if (forceTheme) {
|
||||
@ -57,38 +63,36 @@ function SimpleList({
|
||||
minHeight = 'min-h-[200px]',
|
||||
selectable = true,
|
||||
forceTheme = false,
|
||||
headerClassName = '',
|
||||
listClassName = '',
|
||||
itemClassName = '',
|
||||
headerContent = null,
|
||||
showGroups = false,
|
||||
groupDocCount = null,
|
||||
}) {
|
||||
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'}
|
||||
px-4 py-2
|
||||
border-b border-gray-200
|
||||
bg-white
|
||||
rounded-t
|
||||
shadow-sm
|
||||
tracking-wide
|
||||
uppercase
|
||||
flex items-center
|
||||
${headerClassName}
|
||||
`}
|
||||
>
|
||||
{title}
|
||||
{headerContent ? headerContent : (
|
||||
<span className="text-base text-gray-700">{title}</span>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
<ul className="flex-1">
|
||||
<ul className={`flex-1 ${listClassName}`}>
|
||||
{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';
|
||||
@ -103,10 +107,40 @@ function SimpleList({
|
||||
selectable && idx !== items.length - 1
|
||||
? '-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 (
|
||||
<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}`}
|
||||
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={() => {
|
||||
if (!selectable || !onSelect) return;
|
||||
if (selected) {
|
||||
@ -119,8 +153,31 @@ function SimpleList({
|
||||
aria-selected={selected}
|
||||
role="option"
|
||||
>
|
||||
<span className="font-medium text-gray-800">{item.name}</span>
|
||||
{actionButtons && actionButtons(item)}
|
||||
<div className="flex flex-col">
|
||||
<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>
|
||||
);
|
||||
})
|
||||
@ -137,7 +194,7 @@ export default function FilesGroupsManagement({
|
||||
const [schoolFileMasters, setSchoolFileMasters] = useState([]);
|
||||
const [parentFiles, setParentFileMasters] = useState([]);
|
||||
const [groups, setGroups] = useState([]);
|
||||
const [selectedGroup, setSelectedGroup] = useState(null);
|
||||
|
||||
const [isModalOpen, setIsModalOpen] = useState(false);
|
||||
const [isEditing, setIsEditing] = useState(false);
|
||||
const [fileToEdit, setFileToEdit] = useState(null);
|
||||
@ -148,14 +205,35 @@ 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 [isFormBuilderOpen, setIsFormBuilderOpen] = useState(false);
|
||||
const [isFileUploadOpen, setIsFileUploadOpen] = useState(false);
|
||||
const [isParentFileModalOpen, setIsParentFileModalOpen] = useState(false);
|
||||
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) => {
|
||||
// file.groups peut contenir des IDs (number/string) ou des objets {id, name}
|
||||
@ -363,6 +441,14 @@ export default function FilesGroupsManagement({
|
||||
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) => {
|
||||
// Vérifier si des schoolFileMasters utilisent ce groupe
|
||||
const filesInGroup = schoolFileMasters.filter(
|
||||
@ -392,10 +478,8 @@ 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);
|
||||
}
|
||||
// Purger la sélection si le groupe supprimé était sélectionné
|
||||
setSelectedGroupId((prev) => (prev === groupId ? null : prev));
|
||||
setRemovePopupVisible(false);
|
||||
setIsLoading(false);
|
||||
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) => {
|
||||
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'));
|
||||
@ -445,10 +529,11 @@ export default function FilesGroupsManagement({
|
||||
return editRegistrationParentFileMaster(id, updatedFile, csrfToken)
|
||||
.then((response) => {
|
||||
logger.debug('[FilesGroupsManagement] editRegistrationParentFileMaster response:', response);
|
||||
const modifiedFile = response.data; // Extraire les données mises à jour
|
||||
const modifiedFile = response.data || response;
|
||||
setParentFileMasters((prevFiles) =>
|
||||
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);
|
||||
return modifiedFile;
|
||||
})
|
||||
@ -498,63 +583,7 @@ export default function FilesGroupsManagement({
|
||||
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.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"
|
||||
// Nouvelle aide adaptée
|
||||
const renderExplanation = () => (
|
||||
<div className="mb-4">
|
||||
<button
|
||||
@ -575,22 +604,35 @@ export default function FilesGroupsManagement({
|
||||
</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'inscription</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'une inscription élève, un seul dossier d'inscription sera rattaché à l'élève.
|
||||
<span className="font-semibold">Organisation de la page :</span>
|
||||
<br />
|
||||
<span className="text-blue-700 font-semibold">Colonne de gauche</span> : liste des dossiers d'inscription (groupes/classes).
|
||||
<br />
|
||||
<span className="text-emerald-700 font-semibold">Colonne de droite</span> : liste des documents à fournir pour l'inscription.
|
||||
</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'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>
|
||||
<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'é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'école, à remplir et/ou signer électroniquement par la famille.
|
||||
</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>
|
||||
</ul>
|
||||
<div className="mt-2 text-sm text-gray-600">
|
||||
<span className="font-semibold">Astuce :</span> Commencez toujours par créer vos dossiers d'inscription (liés à vos classes) avant d'ajouter des documents à fournir.
|
||||
<span className="font-semibold">Astuce :</span> Créez d'abord vos dossiers d'inscription avant d'ajouter des documents à fournir.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -598,42 +640,86 @@ export default function FilesGroupsManagement({
|
||||
</div>
|
||||
);
|
||||
|
||||
// Correction : définition de handleBackToCreateMenu
|
||||
const handleBackToCreateMenu = () => {
|
||||
setCreateModalKey((k) => k + 1); // force le reset du composant modal
|
||||
setIsCreateModalOpen(true);
|
||||
// Filtrage des formulaires et pièces selon le dossier sélectionné
|
||||
// Si aucun groupe sélectionné, la colonne de droite est vide
|
||||
let filteredFiles = [];
|
||||
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
|
||||
return (
|
||||
<div className="w-full">
|
||||
{/* 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>
|
||||
{/* Aide optionnelle */}
|
||||
<div className="mb-8">{renderExplanation()}</div>
|
||||
|
||||
<div className="flex gap-8">
|
||||
{/* Colonne gauche : Dossiers d'inscription */}
|
||||
<div className="w-[30%] min-w-[260px]">
|
||||
{/* 2 colonnes : groupes à gauche, documents à droite */}
|
||||
<div className="flex flex-row gap-8">
|
||||
{/* 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
|
||||
items={groups}
|
||||
selectedId={selectedGroupId}
|
||||
onSelect={setSelectedGroupId}
|
||||
onSelect={handleGroupSelect}
|
||||
getItemType={() => 'blue'}
|
||||
title="Dossiers d'inscription"
|
||||
minHeight="min-h-[240px]"
|
||||
minHeight="min-h-[60px]"
|
||||
selectable={true}
|
||||
forceTheme={false}
|
||||
groupDocCount={getGroupDocCount}
|
||||
actionButtons={(row) => (
|
||||
<div className="flex items-center gap-2">
|
||||
<button
|
||||
@ -654,130 +740,226 @@ export default function FilesGroupsManagement({
|
||||
)}
|
||||
/>
|
||||
</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>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Colonne documents (2/3) */}
|
||||
<div className="flex flex-col w-2/3">
|
||||
<div className="flex items-center mb-4">
|
||||
<SectionTitle title="Liste des documents" />
|
||||
<div className="flex-1" />
|
||||
<DropdownMenu
|
||||
buttonContent={
|
||||
<span className="flex items-center">
|
||||
<Plus className="w-5 h-5" />
|
||||
<ChevronDown className="w-4 h-4 ml-1" />
|
||||
</span>
|
||||
}
|
||||
items={[
|
||||
{
|
||||
type: 'item',
|
||||
label: (
|
||||
<span className="flex items-center">
|
||||
<Star className="w-5 h-5 mr-2 text-yellow-600" />
|
||||
Formulaire personnalisé
|
||||
</span>
|
||||
),
|
||||
onClick: () => handleDocDropdownSelect('formulaire'),
|
||||
},
|
||||
{
|
||||
type: 'item',
|
||||
label: (
|
||||
<span className="flex items-center">
|
||||
<FileText className="w-5 h-5 mr-2 text-gray-600" />
|
||||
Formulaire existant
|
||||
</span>
|
||||
),
|
||||
onClick: () => handleDocDropdownSelect('formulaire_existant'),
|
||||
},
|
||||
{
|
||||
type: 'item',
|
||||
label: (
|
||||
<span className="flex items-center">
|
||||
<Plus className="w-5 h-5 mr-2 text-orange-500" />
|
||||
Pièce à fournir
|
||||
</span>
|
||||
),
|
||||
onClick: () => handleDocDropdownSelect('parent'),
|
||||
},
|
||||
]}
|
||||
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"
|
||||
menuClassName="absolute right-0 mt-2 w-56 bg-white border border-gray-200 rounded shadow-lg z-20"
|
||||
dropdownOpen={isDocDropdownOpen}
|
||||
setDropdownOpen={setIsDocDropdownOpen}
|
||||
/>
|
||||
</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'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>
|
||||
{/* 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
|
||||
isOpen={isFileUploadModalOpen}
|
||||
setIsOpen={handleCloseFileUpload}
|
||||
title="Importer un formulaire existant"
|
||||
modalClassName="w-full max-w-md"
|
||||
isOpen={isFormBuilderOpen}
|
||||
setIsOpen={setIsFormBuilderOpen}
|
||||
title="Créer un formulaire personnalisé"
|
||||
modalClassName="w-11/12 h-5/6"
|
||||
>
|
||||
<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
|
||||
<FormTemplateBuilder
|
||||
onSave={(data) => {
|
||||
handleCreateSchoolFileMaster(data);
|
||||
setIsFormBuilderOpen(false);
|
||||
}}
|
||||
onSubmit={handleSubmitFileUpload}
|
||||
required
|
||||
enable
|
||||
groups={groups}
|
||||
isEditing={false}
|
||||
/>
|
||||
</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'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
|
||||
isOpen={isParentFileModalOpen}
|
||||
setIsOpen={(open) => {
|
||||
if (!open) closeParentFileModal();
|
||||
setIsParentFileModalOpen(open);
|
||||
if (!open) setEditingParentFile(null);
|
||||
}}
|
||||
title={editingParentFile ? 'Modifier la pièce à fournir' : 'Créer une pièce à fournir'}
|
||||
modalClassName="w-full max-w-md"
|
||||
@ -790,11 +972,11 @@ export default function FilesGroupsManagement({
|
||||
handleDelete={handleDelete}
|
||||
singleForm // affiche uniquement le formulaire, pas la liste
|
||||
initialData={editingParentFile}
|
||||
onCancel={closeParentFileModal}
|
||||
onCancel={() => setIsParentFileModalOpen(false)}
|
||||
/>
|
||||
</Modal>
|
||||
|
||||
{/* Modals et popups */}
|
||||
{/* Modals et popups pour édition */}
|
||||
<Modal
|
||||
isOpen={isModalOpen}
|
||||
setIsOpen={(isOpen) => {
|
||||
@ -802,8 +984,6 @@ 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'}
|
||||
@ -812,6 +992,7 @@ export default function FilesGroupsManagement({
|
||||
<FormTemplateBuilder
|
||||
onSave={(data) => {
|
||||
(isEditing ? handleEditSchoolFileMaster : handleCreateSchoolFileMaster)(data);
|
||||
setIsModalOpen(false);
|
||||
}}
|
||||
initialData={fileToEdit}
|
||||
groups={groups}
|
||||
@ -824,8 +1005,7 @@ export default function FilesGroupsManagement({
|
||||
setIsOpen={(isOpen) => {
|
||||
setIsGroupModalOpen(isOpen);
|
||||
if (!isOpen) {
|
||||
// Retour au menu principal de création si fermeture
|
||||
handleBackToCreateMenu();
|
||||
setGroupToEdit(null);
|
||||
}
|
||||
}}
|
||||
title={
|
||||
@ -835,6 +1015,7 @@ export default function FilesGroupsManagement({
|
||||
<RegistrationFileGroupForm
|
||||
onSubmit={(data) => {
|
||||
handleGroupSubmit(data);
|
||||
setIsGroupModalOpen(false);
|
||||
}}
|
||||
initialData={groupToEdit}
|
||||
/>
|
||||
|
||||
@ -76,7 +76,7 @@ function ParentFileForm({ initialData, groups, onSubmit, onCancel }) {
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Dossiers d&aposinscription <span className="text-red-500">*</span>
|
||||
Dossiers d'inscription <span className="text-red-500">*</span>
|
||||
</label>
|
||||
<select
|
||||
multiple
|
||||
|
||||
Reference in New Issue
Block a user