mirror of
https://git.v0id.ovh/n3wt-innov/n3wt-school.git
synced 2026-01-29 07:53:23 +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 {
|
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">
|
||||||
|
<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)}
|
{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'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'une inscription élève, un seul dossier d'inscription sera rattaché à l'élève.
|
<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>
|
||||||
<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>
|
</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'é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>
|
||||||
<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'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>
|
</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">
|
{/* 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
|
<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"
|
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"
|
||||||
style={{ minWidth: '320px', maxWidth: '400px' }}
|
onClick={() => setIsGroupModalOpen(true)}
|
||||||
onClick={() => setIsCreateModalOpen(true)}
|
title="Créer un nouveau dossier"
|
||||||
>
|
>
|
||||||
<span className="text-2xl font-bold">+</span>
|
<Plus className="w-5 h-5" />
|
||||||
<span>Créer un nouvel élément</span>
|
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex gap-8">
|
|
||||||
{/* Colonne gauche : Dossiers d'inscription */}
|
|
||||||
<div className="w-[30%] min-w-[260px]">
|
|
||||||
<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,40 +740,76 @@ 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">
|
||||||
|
<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>
|
</div>
|
||||||
{selectedGroupId === null ? (
|
{!selectedGroupId ? (
|
||||||
<div className="flex items-center justify-center flex-1 bg-white text-gray-400 text-center text-base">
|
<div className="flex items-center justify-center h-40 text-gray-400 text-lg italic border border-gray-200 rounded bg-white">
|
||||||
Sélectionnez un dossier pour voir les documents associés.
|
Sélectionner un dossier d'inscription
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<SimpleList
|
<SimpleList
|
||||||
key={selectedGroupId}
|
key={selectedGroupId || 'all'}
|
||||||
items={mergedDocuments}
|
items={mergedDocuments}
|
||||||
selectedId={null}
|
selectedId={null}
|
||||||
onSelect={null}
|
onSelect={null}
|
||||||
getItemType={(item) => item._type}
|
getItemType={(item) => item._type}
|
||||||
// title="Documents" // Supprimé ici, header déjà affiché au-dessus
|
|
||||||
minHeight="min-h-[240px]"
|
minHeight="min-h-[240px]"
|
||||||
selectable={false}
|
selectable={false}
|
||||||
forceTheme={true}
|
forceTheme={true}
|
||||||
|
listClassName=""
|
||||||
|
itemClassName="text-gray-800 bg-white"
|
||||||
|
title=""
|
||||||
|
headerContent={null}
|
||||||
|
showGroups={false}
|
||||||
actionButtons={(row) => (
|
actionButtons={(row) => (
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
{row._type === 'emerald' ? (
|
{row._type === 'emerald' ? (
|
||||||
@ -731,53 +853,113 @@ export default function FilesGroupsManagement({
|
|||||||
)}
|
)}
|
||||||
</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'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}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@ -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'inscription <span className="text-red-500">*</span>
|
||||||
</label>
|
</label>
|
||||||
<select
|
<select
|
||||||
multiple
|
multiple
|
||||||
|
|||||||
Reference in New Issue
Block a user