Files
n3wt-school/Front-End/src/components/Structure/Files/FilesGroupsManagement.js
2026-04-05 16:06:04 +02:00

1489 lines
53 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import React, { useState, useEffect } from 'react';
import { useRouter } from 'next/navigation';
import { Edit, Trash2, FileText, Star, ChevronDown, Plus } from 'lucide-react';
import Modal from '@/components/Modal';
import {
// GET
fetchRegistrationFileGroups,
fetchRegistrationSchoolFileMasters,
fetchRegistrationParentFileMasters,
// POST
createRegistrationFileGroup,
createRegistrationSchoolFileMaster,
createRegistrationParentFileMaster,
// PUT
editRegistrationFileGroup,
editRegistrationSchoolFileMaster,
editRegistrationParentFileMaster,
// DELETE
deleteRegistrationFileGroup,
deleteRegistrationSchoolFileMaster,
deleteRegistrationParentFileMaster,
} from '@/app/actions/registerFileGroupAction';
import RegistrationFileGroupForm from '@/components/Structure/Files/RegistrationFileGroupForm';
import logger from '@/utils/logger';
import Popup from '@/components/Popup';
import Loader from '@/components/Loader';
import { useNotification } from '@/context/NotificationContext';
import FileUpload from '@/components/Form/FileUpload';
import SectionTitle from '@/components/SectionTitle';
import DropdownMenu from '@/components/DropdownMenu';
import CheckBox from '@/components/Form/CheckBox';
import Button from '@/components/Form/Button';
import InputText from '@/components/Form/InputText';
import { getSecureFileUrl } from '@/utils/fileUrl';
import { FE_ADMIN_STRUCTURE_FORM_BUILDER_URL } from '@/utils/Url';
function getItemBgColor(type, selected, forceTheme = false) {
// Colonne gauche : blanc si rien n'est sélectionné, emerald si sélectionné
if (type === 'blue') {
if (selected) return 'bg-primary/10';
return 'bg-white';
}
// Colonne droite : thème selon type, jamais sélectionné
if (forceTheme) {
if (type === 'emerald') return 'bg-primary/5';
if (type === 'orange') return 'bg-orange-50';
return 'bg-gray-50';
}
return 'bg-white';
}
function SimpleList({
items,
onSelect,
selectedId,
actionButtons,
getItemType,
title,
minHeight = 'min-h-[200px]',
selectable = true,
forceTheme = false,
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-2
border-b border-gray-200
bg-white
rounded-t
flex items-center
${headerClassName}
`}
>
{headerContent ? (
headerContent
) : (
<span className="text-base text-gray-700">{title}</span>
)}
</div>
)}
<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) => {
const key = `${item.id}-${idx}`;
const selected = selectedId === item.id;
const itemType = getItemType ? getItemType(item) : 'gray';
const bgColor = getItemBgColor(itemType, selected, forceTheme);
const zIndex =
selectable && selected
? 'z-10 relative'
: selectable
? 'z-0 relative'
: '';
const marginFix =
selectable && idx !== items.length - 1 ? '-mb-[1px]' : '';
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={`w-full flex items-center justify-between px-4 py-3 transition ${bgColor} ${selected && selectable ? 'ring-2 ring-tertiary' : ''} ${selectable ? 'cursor-pointer' : ''} ${zIndex} ${marginFix} ${extraZ} ${typeof itemClassName === 'function' ? itemClassName(item) : itemClassName}`}
onClick={() => {
if (!selectable || !onSelect) return;
if (selected) {
onSelect(null);
} else {
onSelect(item.id);
}
}}
tabIndex={0}
aria-selected={selected}
role="option"
>
<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>
);
})
)}
</ul>
</div>
);
}
export default function FilesGroupsManagement({
csrfToken,
selectedEstablishmentId,
profileRole,
}) {
const [schoolFileMasters, setSchoolFileMasters] = useState([]);
const [parentFiles, setParentFileMasters] = useState([]);
const [groups, setGroups] = useState([]);
const router = useRouter();
const [isEditing, setIsEditing] = useState(false);
const [fileToEdit, setFileToEdit] = useState(null);
const [isGroupModalOpen, setIsGroupModalOpen] = useState(false);
const [groupToEdit, setGroupToEdit] = useState(null);
const [removePopupVisible, setRemovePopupVisible] = useState(false);
const [removePopupMessage, setRemovePopupMessage] = useState('');
const [removePopupOnConfirm, setRemovePopupOnConfirm] = useState(() => {});
const [isLoading, setIsLoading] = useState(false);
const [selectedGroupId, setSelectedGroupId] = useState(null);
const [showHelp, setShowHelp] = useState(false);
const { showNotification } = useNotification();
const [isParentFileModalOpen, setIsParentFileModalOpen] = useState(false);
const [editingParentFile, setEditingParentFile] = useState(null);
// 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') {
const groupParam = selectedGroupId ? `?groupId=${selectedGroupId}` : '';
router.push(`${FE_ADMIN_STRUCTURE_FORM_BUILDER_URL}${groupParam}`);
} 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}
const groupInfos = (file.groups || []).map((group) => {
if (typeof group === 'object' && group !== null && 'id' in group) {
// Déjà un objet groupe
const found = groups.find((g) => g.id === group.id);
return found || group;
} else {
// C'est un ID
return (
groups.find((g) => g.id === group) || {
id: group,
name: 'Groupe inconnu',
}
);
}
});
return {
...file,
groups: groupInfos,
};
};
// Ne pas transformer ici, stocker la donnée brute
useEffect(() => {
if (selectedEstablishmentId) {
Promise.all([
fetchRegistrationSchoolFileMasters(selectedEstablishmentId),
fetchRegistrationFileGroups(selectedEstablishmentId),
fetchRegistrationParentFileMasters(selectedEstablishmentId),
])
.then(([dataSchoolFileMasters, groupsData, dataParentFileMasters]) => {
setGroups(groupsData);
setParentFileMasters(dataParentFileMasters);
setSchoolFileMasters(dataSchoolFileMasters); // donnée brute
})
.catch((err) => {
logger.debug(err.message);
});
}
}, [selectedEstablishmentId]);
const deleteTemplateMaster = (templateMaster) => {
setRemovePopupVisible(true);
setRemovePopupMessage(
`Attention ! \nVous êtes sur le point de supprimer le formulaire "${templateMaster.name}".\nÊtes-vous sûr(e) de vouloir poursuivre l'opération ?`
);
setRemovePopupOnConfirm(() => () => {
setIsLoading(true);
// Supprimer le formulaire de la base de données
deleteRegistrationSchoolFileMaster(templateMaster.id, csrfToken)
.then((response) => {
if (response.ok) {
setSchoolFileMasters(
schoolFileMasters.filter(
(fichier) => fichier.id !== templateMaster.id
)
);
showNotification(
`Le formulaire "${templateMaster.name}" a été correctement supprimé.`,
'success',
'Succès'
);
setRemovePopupVisible(false);
} else {
showNotification(
`Erreur lors de la suppression du formulaire "${templateMaster.name}".`,
'error',
'Erreur'
);
setRemovePopupVisible(false);
}
})
.catch((error) => {
logger.error('Error deleting file from database:', error);
showNotification(
`Erreur lors de la suppression du formulaire "${templateMaster.name}".`,
'error',
'Erreur'
);
setRemovePopupVisible(false);
})
.finally(() => {
setIsLoading(false);
});
});
};
const editTemplateMaster = (file) => {
const isDynamic =
file.formMasterData &&
Array.isArray(file.formMasterData.fields) &&
file.formMasterData.fields.length > 0;
if (isDynamic) {
router.push(`${FE_ADMIN_STRUCTURE_FORM_BUILDER_URL}?id=${file.id}`);
} else {
setFileToEdit(file);
setIsEditing(true);
setIsFileUploadPopupOpen(true);
}
};
const handleCreateSchoolFileMaster = (
{
name,
group_ids,
formMasterData,
file,
requires_electronic_signature,
},
onCreated
) => {
// Toujours envoyer en FormData, même sans fichier
const dataToSend = new FormData();
const jsonData = {
name,
groups: group_ids,
formMasterData,
establishment: selectedEstablishmentId,
requires_electronic_signature: requires_electronic_signature || false,
};
dataToSend.append('data', JSON.stringify(jsonData));
if (file) {
// Récupérer l'extension du fichier d'origine
let extension = '';
if (file.name && file.name.lastIndexOf('.') !== -1) {
extension = file.name.substring(file.name.lastIndexOf('.'));
}
// Nettoyer le nom saisi pour le fichier (éviter les caractères spéciaux)
const cleanName = (name || 'document')
.replace(/[^a-zA-Z0-9_\-]/g, '_')
.replace(/_+/g, '_')
.replace(/^_+|_+$/g, '');
// Générer le nom de fichier final
const finalFileName = `${cleanName}${extension}`;
dataToSend.append('file', file, finalFileName);
}
createRegistrationSchoolFileMaster(dataToSend, csrfToken)
.then((data) => {
setSchoolFileMasters((prevFiles) => [...prevFiles, data]);
showNotification(
`Le formulaire "${name}" a été créé avec succès.`,
'success',
'Succès'
);
if (onCreated) onCreated(data);
})
.catch((error) => {
logger.error('Error creating form:', error);
showNotification(
'Erreur lors de la création du formulaire',
'error',
'Erreur'
);
});
};
const handleEditSchoolFileMaster = ({
name,
group_ids,
formMasterData,
id,
file,
requires_electronic_signature,
}) => {
// Correction : normaliser group_ids pour ne garder que les IDs (number/string)
let normalizedGroupIds = [];
if (Array.isArray(group_ids)) {
normalizedGroupIds = group_ids.map((g) =>
typeof g === 'object' && g !== null && 'id' in g ? g.id : g
);
}
const dataToSend = new FormData();
const jsonData = {
name: name,
groups: normalizedGroupIds,
formMasterData: formMasterData,
establishment: selectedEstablishmentId,
requires_electronic_signature: requires_electronic_signature || false,
};
dataToSend.append('data', JSON.stringify(jsonData));
// Cas 1 : Nouveau fichier sélectionné (File/Blob)
if (file instanceof File || file instanceof Blob) {
let extension = '';
if (file.name && file.name.lastIndexOf('.') !== -1) {
extension = file.name.substring(file.name.lastIndexOf('.'));
}
const cleanName = (name || 'document')
.replace(/[^a-zA-Z0-9_\-]/g, '_')
.replace(/_+/g, '_')
.replace(/^_+|_+$/g, '');
const finalFileName = `${cleanName}${extension}`;
dataToSend.append('file', file, finalFileName);
}
// Cas 2 : Pas de nouveau fichier, mais le nom a changé → renvoyer le fichier existant avec le nouveau nom
else if (typeof file === 'string' && file) {
// Extraire l'extension du path existant
let extension = '';
const lastDot = file.lastIndexOf('.');
if (lastDot !== -1) {
extension = file.substring(lastDot);
}
const cleanName = (name || 'document')
.replace(/[^a-zA-Z0-9_\-]/g, '_')
.replace(/_+/g, '_')
.replace(/^_+|_+$/g, '');
const finalFileName = `${cleanName}${extension}`;
// Correction : il faut récupérer le fichier à l'URL d'origine, pas à la nouvelle URL renommée
// On utilise le path original (file) pour le fetch, pas le chemin avec le nouveau nom
fetch(getSecureFileUrl(file))
.then((response) => {
if (!response.ok) throw new Error('Fichier distant introuvable');
return response.blob();
})
.then((blob) => {
dataToSend.append('file', blob, finalFileName);
editRegistrationSchoolFileMaster(id, dataToSend, csrfToken)
.then((data) => {
setSchoolFileMasters((prevFichiers) =>
prevFichiers.map((f) => (f.id === id ? data : f))
);
showNotification(
`Le formulaire "${name}" a été modifié avec succès.`,
'success',
'Succès'
);
})
.catch((error) => {
logger.error('Error editing form:', error);
showNotification(
'Erreur lors de la modification du formulaire',
'error',
'Erreur'
);
});
})
.catch((error) => {
logger.error(
'Erreur lors de la récupération du fichier existant pour renommage:',
error
);
showNotification(
'Erreur lors de la récupération du fichier existant pour renommage',
'error',
'Erreur'
);
});
return; // On sort ici car l'appel API est fait dans le fetch
}
// Cas standard (nouveau fichier ou pas de renommage)
editRegistrationSchoolFileMaster(id, dataToSend, csrfToken)
.then((data) => {
setSchoolFileMasters((prevFichiers) =>
prevFichiers.map((f) => (f.id === id ? data : f))
);
showNotification(
`Le formulaire "${name}" a été modifié avec succès.`,
'success',
'Succès'
);
})
.catch((error) => {
logger.error('Error editing form:', error);
showNotification(
'Erreur lors de la modification du formulaire',
'error',
'Erreur'
);
});
};
const handleGroupSubmit = (groupData) => {
if (groupToEdit) {
editRegistrationFileGroup(groupToEdit.id, groupData, csrfToken)
.then((updatedGroup) => {
setGroups(
groups.map((group) =>
group.id === groupToEdit.id ? updatedGroup : group
)
);
setGroupToEdit(null);
setIsGroupModalOpen(false);
})
.catch((error) => {
logger.error('Error handling group:', error);
showNotification(
"Erreur lors de l'opération sur le groupe",
'error',
'Erreur'
);
});
} else {
// Ajouter l'établissement sélectionné lors de la création d'un nouveau groupe
const newGroupData = {
...groupData,
establishment: selectedEstablishmentId,
};
createRegistrationFileGroup(newGroupData, csrfToken)
.then((newGroup) => {
setGroups([...groups, newGroup]);
setIsGroupModalOpen(false);
})
.catch((error) => {
logger.error('Error handling group:', error);
showNotification(
"Erreur lors de l'opération sur le groupe",
'error',
'Erreur'
);
});
}
};
const handleGroupEdit = (group) => {
setGroupToEdit(group);
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(
(file) => file.group && file.group.id === groupId
);
if (filesInGroup.length > 0) {
showNotification(
"Impossible de supprimer ce groupe car il contient déjà des formulaires. Veuillez d'abord retirer tous les formules de ce groupe.",
'error',
'Erreur'
);
return;
}
setRemovePopupVisible(true);
setRemovePopupMessage(
'Attentions ! \nÊtes-vous sûr de vouloir supprimer ce groupe ?'
);
setRemovePopupOnConfirm(() => () => {
setIsLoading(true);
deleteRegistrationFileGroup(groupId, csrfToken)
.then((response) => {
if (response.status === 409) {
throw new Error('Ce groupe est lié à des inscriptions existantes.');
}
if (!response.ok) {
throw new Error('Erreur lors de la suppression du groupe.');
}
setGroups(groups.filter((group) => group.id !== groupId));
// 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');
})
.catch((error) => {
logger.error('Error deleting group:', error);
setRemovePopupVisible(false);
setIsLoading(false);
showNotification(
error.message ||
"Erreur lors de la suppression du groupe. Vérifiez qu'aucune inscription n'utilise ce groupe.",
'error',
'Erreur'
);
});
});
};
const handleCreate = (newParentFile) => {
return createRegistrationParentFileMaster(newParentFile, csrfToken)
.then((response) => {
const createdFile = response;
// Ajouter le nouveau fichier parent à la liste existante
setParentFileMasters((prevFiles) => [...prevFiles, createdFile]);
logger.debug('Document parent créé avec succès:', createdFile);
return createdFile;
})
.catch((error) => {
logger.error('Erreur lors de la création du document parent:', error);
showNotification(
'Une erreur est survenue lors de la création du document parent.',
'error',
'Erreur'
);
throw error;
});
};
// 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
);
if (typeof updatedFile !== 'object' || updatedFile === null) {
logger.error(
'[FilesGroupsManagement] handleEdit: updatedFile is not an object',
updatedFile
);
return Promise.reject(new Error('updatedFile is not an object'));
}
logger.debug(
'[FilesGroupsManagement] handleEdit payload:',
JSON.stringify(updatedFile)
);
return editRegistrationParentFileMaster(id, updatedFile, csrfToken)
.then((response) => {
logger.debug(
'[FilesGroupsManagement] editRegistrationParentFileMaster response:',
response
);
const modifiedFile = response.data || 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;
})
.catch((error) => {
logger.error(
'Erreur lors de la modification du document parent:',
error
);
showNotification(
'Une erreur est survenue lors de la modification du document parent.',
'error',
'Erreur'
);
throw error;
});
};
const handleDelete = (id) => {
// Vérification avant suppression : afficher une popup de confirmation
setRemovePopupMessage(
"Attention !\nVous êtes sur le point de supprimer la pièce à fournir.\nÊtes-vous sûr(e) de vouloir poursuivre l'opération ?"
);
setRemovePopupOnConfirm(() => () => {
deleteRegistrationParentFileMaster(id, csrfToken)
.then(() => {
setParentFileMasters((prevFiles) =>
prevFiles.filter((file) => file.id !== id)
);
logger.debug('Document parent supprimé avec succès:', id);
showNotification(
'La pièce à fournir a été supprimée avec succès.',
'success',
'Succès'
);
setRemovePopupVisible(false);
})
.catch((error) => {
logger.error(
'Erreur lors de la suppression du fichier parent:',
error
);
showNotification(
'Erreur lors de la suppression de la pièce à fournir.',
'error',
'Erreur'
);
setRemovePopupVisible(false);
});
});
setRemovePopupVisible(true);
};
// Ouvre la modale de création d'une pièce à fournir
const openParentFileModal = () => {
setEditingParentFile(null);
setIsParentFileModalOpen(true);
};
// Ouvre la modale d'édition d'une pièce à fournir
const openEditParentFileModal = (file) => {
setEditingParentFile(file);
setIsParentFileModalOpen(true);
setIsEditing(true);
};
// Ferme la modale de pièce à fournir
const closeParentFileModal = () => {
setEditingParentFile(null);
setIsParentFileModalOpen(false);
};
// Nouvelle aide adaptée
const renderExplanation = () => (
<div className="mb-4">
<button
className="flex items-center gap-2 text-secondary hover:text-secondary font-medium mb-2"
onClick={() => setShowHelp((v) => !v)}
aria-expanded={showHelp}
aria-controls="aide-inscription"
>
<span className="underline">
{showHelp ? 'Masquer' : 'Afficher'} laide
</span>
<svg
className={`w-4 h-4 transition-transform ${showHelp ? 'rotate-180' : ''}`}
fill="none"
stroke="currentColor"
strokeWidth="2"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M19 9l-7 7-7-7"
/>
</svg>
</button>
{showHelp && (
<div
id="aide-inscription"
className="p-4 bg-blue-50 border border-blue-200 rounded mb-4"
>
<h2 className="font-headline text-lg font-bold mb-2">
Gestion des dossiers et documents d&apos;inscription
</h2>
<div className="text-gray-700 space-y-2">
<p>
<span className="font-semibold">Organisation de la page :</span>
<br />
<span className="text-blue-700 font-semibold">
Colonne de gauche
</span>{' '}
: liste des dossiers d&apos;inscription (groupes/classes).
<br />
<span className="text-secondary font-semibold">
Colonne de droite
</span>{' '}
: liste des documents à fournir pour l&apos;inscription.
</p>
<p>
<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-primary text-white border border-primary">
+
</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-primary text-white border border-primary">
+
</span>{' '}
à droite de la liste des documents pour ajouter :
</p>
<ul className="list-disc list-inside ml-6">
<li>
<span className="text-black font-semibold">
Formulaire existant
</span>{' '}
: importez un PDF ou autre document à faire remplir. Vous pouvez
activer la signature électronique.
</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> Créez d&apos;abord
vos dossiers d&apos;inscription avant d&apos;ajouter des documents
à fournir.
</div>
</div>
</div>
)}
</div>
);
// 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;
};
return (
<div className="w-full">
{/* Aide optionnelle */}
<div className="mb-8">{renderExplanation()}</div>
{/* 2 colonnes : groupes à gauche, documents à droite */}
<div className="flex flex-col xl:flex-row gap-8">
{/* Colonne groupes (plein écran mobile/tablette, 1/3 desktop) */}
<div className="flex flex-col w-full xl:w-1/3 xl:min-w-[320px] xl:max-w-md">
<div className="flex items-center mb-4">
<SectionTitle title="Liste des dossiers d'inscriptions" />
<div className="flex-1" />
{profileRole !== 0 && (
<button
className="flex items-center justify-center bg-primary hover:bg-primary 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={handleGroupSelect}
getItemType={() => 'blue'}
minHeight="min-h-[60px]"
selectable={true}
forceTheme={false}
groupDocCount={getGroupDocCount}
actionButtons={(row) => (
<div className="flex items-center gap-2">
<button
onClick={(e) => {
e.stopPropagation();
handleGroupEdit(row);
}}
className="p-2 rounded-full hover:bg-gray-100 transition"
title="Modifier"
>
<span title="Editer le dossier">
<Edit className="w-5 h-5 text-blue-500 hover:text-blue-700" />
</span>
</button>
<button
onClick={(e) => {
e.stopPropagation();
handleGroupDelete(row.id);
}}
className="p-2 rounded-full hover:bg-gray-100 transition"
title="Supprimer"
>
<span title="Supprimer le dossier">
<Trash2 className="w-5 h-5 text-red-500 hover:text-red-700" />
</span>
</button>
</div>
)}
/>
</div>
{/* Colonne documents (plein écran mobile/tablette, 2/3 desktop) */}
<div className="flex flex-col w-full xl:flex-1">
<div className="flex items-center mb-4">
<SectionTitle title="Liste des documents" />
<div className="flex-1" />
{profileRole !== 0 && (
<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">
<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-primary hover:bg-primary 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&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">
<button
onClick={(e) => {
e.stopPropagation();
if (row._type === 'emerald') {
editTemplateMaster(row);
} else {
openEditParentFileModal(row);
}
}}
className="p-2 rounded-full hover:bg-gray-100 transition"
title="Modifier"
>
<span title="Editer le document">
<Edit className="w-5 h-5 text-blue-500 hover:text-blue-700" />
</span>
</button>
<button
onClick={(e) => {
e.stopPropagation();
if (row._type === 'emerald') {
deleteTemplateMaster(row);
} else {
handleDelete(row.id);
}
}}
className="p-2 rounded-full hover:bg-gray-100 transition"
title="Supprimer"
>
<span title="Supprimer le document">
<Trash2 className="w-5 h-5 text-red-500 hover:text-red-700" />
</span>
</button>
</div>
)}
/>
)}
</div>
</div>
{/* Popup pour création/édition d'un groupe de documents */}
<Modal
isOpen={isGroupModalOpen}
setIsOpen={(isOpen) => {
setIsGroupModalOpen(isOpen);
if (!isOpen) {
setGroupToEdit(null);
}
}}
modalClassName="max-w-md sm:max-w-md sm:min-w-0"
title={
groupToEdit ? 'Modifier le dossier' : "Création d'un nouveau dossier"
}
>
<div className="w-full max-w-md max-h-[90vh] overflow-y-auto">
<RegistrationFileGroupForm
onSubmit={(data) => {
handleGroupSubmit(data);
setIsGroupModalOpen(false);
}}
initialData={groupToEdit}
/>
</div>
</Modal>
{/* Popup pour création/édition d'un formulaire d'école déjà existant */}
<Modal
isOpen={isFileUploadPopupOpen}
setIsOpen={setIsFileUploadPopupOpen}
modalClassName="max-w-md sm:max-w-md sm:min-w-0"
title={
fileToEdit && fileToEdit.id
? 'Modifier le document existant'
: 'Télécharger un document existant'
}
>
<div className="w-full max-h-[90vh] overflow-y-auto">
{fileToEdit && fileToEdit.id ? (
<form
className="flex flex-col gap-4 w-full"
onSubmit={(e) => {
e.preventDefault();
if (
!fileToEdit?.name ||
!fileToEdit?.groups ||
fileToEdit.groups.length === 0 ||
!fileToEdit?.file
)
return;
if (isEditing) {
handleEditSchoolFileMaster({
id: fileToEdit.id,
name: fileToEdit.name,
group_ids: fileToEdit.groups,
file: fileToEdit.file,
formMasterData: fileToEdit.formMasterData,
requires_electronic_signature:
fileToEdit.requires_electronic_signature || false,
});
} else {
handleCreateSchoolFileMaster({
name: fileToEdit.name,
group_ids: fileToEdit.groups,
file: fileToEdit.file,
requires_electronic_signature:
fileToEdit.requires_electronic_signature || false,
});
}
setIsFileUploadPopupOpen(false);
setFileToEdit(null);
}}
>
<InputText
label="Nom du document"
name="name"
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="flex flex-wrap gap-4 max-h-32 overflow-y-auto border rounded-md p-2">
{groups && groups.length > 0 ? (
groups.map((group) => {
const selectedGroupIds = (fileToEdit?.groups || []).map(
(g) =>
typeof g === 'object' && g !== null && 'id' in g
? g.id
: g
);
return (
<CheckBox
key={group.id}
item={{ id: group.id }}
formData={{
groups: selectedGroupIds,
}}
handleChange={() => {
let group_ids = selectedGroupIds;
if (group_ids.includes(group.id)) {
group_ids = group_ids.filter(
(id) => id !== group.id
);
} else {
group_ids = [...group_ids, group.id];
}
setFileToEdit({ ...fileToEdit, groups: group_ids });
}}
fieldName="groups"
itemLabelFunc={() => group.name}
/>
);
})
) : (
<p className="text-gray-500 text-sm">
Aucun groupe disponible
</p>
)}
</div>
</div>
{/* Label document sélectionné sans icône œil */}
{fileToEdit?.file && (
<div className="flex items-center gap-2 mb-2">
<FileText className="w-5 h-5 text-gray-600" />
<span className="text-sm truncate">
{fileToEdit.file.name ||
fileToEdit.file.path ||
'Document sélectionné'}
</span>
</div>
)}
<FileUpload
selectionMessage="Sélectionnez le fichier du document"
onFileSelect={(file) => setFileToEdit({ ...fileToEdit, file })}
required
enable
/>
<CheckBox
item={{ id: 'signature' }}
formData={{
requires_electronic_signature:
fileToEdit?.requires_electronic_signature || false,
}}
handleChange={() =>
setFileToEdit({
...fileToEdit,
requires_electronic_signature:
!fileToEdit?.requires_electronic_signature,
})
}
fieldName="requires_electronic_signature"
itemLabelFunc={() => 'À signer électroniquement'}
/>
<Button
primary
type="submit"
text="Enregistrer"
className="mt-2"
disabled={
!fileToEdit?.name ||
!fileToEdit?.groups ||
fileToEdit.groups.length === 0 ||
!fileToEdit?.file
}
/>
</form>
) : (
<form
className="flex flex-col gap-4 w-full"
onSubmit={(e) => {
e.preventDefault();
if (
!fileToEdit?.name ||
!fileToEdit?.groups ||
fileToEdit.groups.length === 0 ||
!fileToEdit?.file
)
return;
handleCreateSchoolFileMaster({
name: fileToEdit.name,
group_ids: fileToEdit.groups,
file: fileToEdit.file,
requires_electronic_signature:
fileToEdit.requires_electronic_signature || false,
});
setIsFileUploadPopupOpen(false);
setFileToEdit(null);
}}
>
<InputText
label="Nom du document"
name="name"
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="flex flex-wrap gap-4 max-h-32 overflow-y-auto border rounded-md p-2">
{groups && groups.length > 0 ? (
groups.map((group) => {
const selectedGroupIds = (fileToEdit?.groups || []).map(
(g) =>
typeof g === 'object' && g !== null && 'id' in g
? g.id
: g
);
return (
<CheckBox
key={group.id}
item={{ id: group.id }}
formData={{
groups: selectedGroupIds,
}}
handleChange={() => {
let group_ids = selectedGroupIds;
if (group_ids.includes(group.id)) {
group_ids = group_ids.filter(
(id) => id !== group.id
);
} else {
group_ids = [...group_ids, group.id];
}
setFileToEdit({ ...fileToEdit, groups: group_ids });
}}
fieldName="groups"
itemLabelFunc={() => group.name}
/>
);
})
) : (
<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
/>
<CheckBox
item={{ id: 'signature' }}
formData={{
requires_electronic_signature:
fileToEdit?.requires_electronic_signature || false,
}}
handleChange={() =>
setFileToEdit({
...fileToEdit,
requires_electronic_signature:
!fileToEdit?.requires_electronic_signature,
})
}
fieldName="requires_electronic_signature"
itemLabelFunc={() => 'À signer électroniquement'}
/>
<Button
primary
type="submit"
text="Enregistrer"
className="mt-2"
disabled={
!fileToEdit?.name ||
!fileToEdit?.groups ||
fileToEdit.groups.length === 0 ||
!fileToEdit?.file
}
/>
</form>
)}
</div>
</Modal>
{/* Popup pour création/édition d'un document parent */}
<Modal
isOpen={isParentFileModalOpen}
setIsOpen={(open) => {
setIsParentFileModalOpen(open);
if (!open) setEditingParentFile(null);
}}
modalClassName="max-w-md sm:max-w-md sm:min-w-0"
title={
editingParentFile && editingParentFile.id
? 'Modifier la pièce à fournir'
: 'Créer une pièce à fournir'
}
>
<div className="w-full max-w-md max-h-[90vh] overflow-y-auto">
<form
className="flex flex-col gap-4"
onSubmit={(e) => {
e.preventDefault();
if (
!editingParentFile?.name ||
!editingParentFile?.groups ||
editingParentFile.groups.length === 0
)
return;
const payload = {
name: editingParentFile.name,
description: editingParentFile.description || '',
groups: editingParentFile.groups,
is_required: !!editingParentFile.is_required,
};
if (editingParentFile?.id) {
handleEdit(editingParentFile.id, payload);
} else {
handleCreate(payload);
}
setIsParentFileModalOpen(false);
setEditingParentFile(null);
}}
>
<InputText
label="Nom de la pièce à fournir"
name="name"
value={editingParentFile?.name || ''}
onChange={(e) =>
setEditingParentFile({
...editingParentFile,
name: e.target.value,
})
}
required
/>
<InputText
label="Description"
name="description"
value={editingParentFile?.description || ''}
onChange={(e) =>
setEditingParentFile({
...editingParentFile,
description: e.target.value,
})
}
required={false}
/>
<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="flex flex-wrap gap-4 max-h-32 overflow-y-auto border rounded-md p-2">
{groups && groups.length > 0 ? (
groups.map((group) => {
const selectedGroupIds = (
editingParentFile?.groups || []
).map((g) =>
typeof g === 'object' && g !== null && 'id' in g
? g.id
: g
);
return (
<CheckBox
key={group.id}
item={{ id: group.id }}
formData={{
groups: selectedGroupIds,
}}
handleChange={() => {
let group_ids = selectedGroupIds;
if (group_ids.includes(group.id)) {
group_ids = group_ids.filter(
(id) => id !== group.id
);
} else {
group_ids = [...group_ids, group.id];
}
setEditingParentFile({
...editingParentFile,
groups: group_ids,
});
}}
fieldName="groups"
itemLabelFunc={() => group.name}
/>
);
})
) : (
<p className="text-gray-500 text-sm">
Aucun groupe disponible
</p>
)}
</div>
</div>
<div className="flex items-center gap-2 mt-2">
<CheckBox
item={{ id: 'is_required' }}
formData={{ is_required: !!editingParentFile?.is_required }}
handleChange={() =>
setEditingParentFile({
...editingParentFile,
is_required: !editingParentFile?.is_required,
})
}
fieldName="is_required"
itemLabelFunc={() => 'Requis'}
/>
</div>
<Button
primary
type="submit"
text="Enregistrer"
className="mt-2"
disabled={
!editingParentFile?.name ||
!editingParentFile?.groups ||
editingParentFile.groups.length === 0
}
/>
</form>
</div>
</Modal>
<Popup
isOpen={removePopupVisible}
message={removePopupMessage}
onConfirm={removePopupOnConfirm}
onCancel={() => setRemovePopupVisible(false)}
/>
{isLoading && <Loader />}
</div>
);
}