Merge remote-tracking branch 'origin/WIP_Inscriptions' into develop

This commit is contained in:
Luc SORIGNET
2025-04-25 10:55:54 +02:00
39 changed files with 2400 additions and 1610 deletions

View File

@ -1,105 +0,0 @@
import React, { useState, useEffect } from 'react';
import ToggleSwitch from '@/components/ToggleSwitch'; // Import du composant ToggleSwitch
import DraggableFileUpload from './DraggableFileUpload';
import { fetchRegistrationFileGroups } from '@/app/actions/registerFileGroupAction';
import { useEstablishment } from '@/context/EstablishmentContext';
export default function FileUpload({ onFileUpload, fileToEdit = null }) {
const [fileName, setFileName] = useState('');
const [file, setFile] = useState(null);
const [isRequired, setIsRequired] = useState(false); // État pour le toggle isRequired
const [order, setOrder] = useState(0);
const [groups, setGroups] = useState([]);
const [selectedGroup, setSelectedGroup] = useState('');
const { selectedEstablishmentId } = useEstablishment();
useEffect(() => {
fetchRegistrationFileGroups(selectedEstablishmentId).then((data) =>
setGroups(data)
);
if (fileToEdit) {
setFileName(fileToEdit.name || '');
setIsRequired(fileToEdit.is_required || false);
setOrder(fileToEdit.fusion_order || 0);
setSelectedGroup(fileToEdit.group_id || '');
}
}, [fileToEdit]);
const handleFileNameChange = (event) => {
setFileName(event.target.value);
};
const handleUpload = () => {
onFileUpload({
file,
name: fileName,
is_required: isRequired,
order: parseInt(order, 10),
groupId: selectedGroup || null,
});
setFile(null);
setFileName('');
setIsRequired(false);
setOrder(0);
setSelectedGroup('');
};
return (
<div>
<DraggableFileUpload
fileName={fileName}
onFileSelect={(selectedFile) => {
setFile(selectedFile);
setFileName(selectedFile.name.replace(/\.[^/.]+$/, ''));
}}
/>
<div className="flex mt-2">
<input
type="text"
placeholder="Nom du fichier"
value={fileName}
onChange={handleFileNameChange}
className="flex-grow p-2 border border-gray-200 rounded-md"
/>
<input
type="number"
value={order}
onChange={(e) => setOrder(e.target.value)}
placeholder="Ordre de fusion"
className="p-2 border border-gray-200 rounded-md ml-2 w-20"
/>
<button
onClick={handleUpload}
className={`p-2 rounded-md shadow transition duration-200 ml-2 ${fileName !== '' ? 'bg-emerald-600 text-white hover:bg-emerald-900' : 'bg-gray-300 text-gray-500 cursor-not-allowed'}`}
disabled={fileName === ''}
>
Ajouter
</button>
</div>
<div className="flex items-center mt-4">
<ToggleSwitch
label="Fichier à remplir obligatoirement"
checked={isRequired}
onChange={() => setIsRequired(!isRequired)}
/>
</div>
<div className="mt-4">
<label className="block mb-2">Groupe</label>
<select
value={selectedGroup}
onChange={(e) => setSelectedGroup(e.target.value)}
className="w-full border rounded p-2"
>
<option value="">Aucun groupe</option>
{groups.map((group) => (
<option key={group.id} value={group.id}>
{group.name}
</option>
))}
</select>
</div>
</div>
);
}

View File

@ -0,0 +1,60 @@
import React, { useState, useRef } from 'react';
import { CloudUpload } from 'lucide-react';
import logger from '@/utils/logger';
export default function FileUpload({ selectionMessage, onFileSelect, uploadedFileName }) {
const [localFileName, setLocalFileName] = useState(uploadedFileName || '');
const fileInputRef = useRef(null); // Utilisation de useRef pour cibler l'input
const handleFileChange = (e) => {
const file = e.target.files[0];
if (file) {
setLocalFileName(file.name);
logger.debug('Fichier sélectionné:', file.name);
onFileSelect(file); // Appelle la fonction passée en prop
}
};
const handleFileDrop = (e) => {
e.preventDefault();
const file = e.dataTransfer.files[0];
if (file) {
setLocalFileName(file.name);
logger.debug('Fichier déposé:', file.name);
onFileSelect(file); // Appelle la fonction passée en prop
}
};
return (
<div className="border p-4 rounded-md shadow-md">
<h3 className="text-lg font-semibold mb-4">{`${selectionMessage}`}</h3>
<div
className="border-2 border-dashed border-gray-500 p-6 rounded-lg flex flex-col items-center justify-center cursor-pointer hover:border-emerald-500"
onClick={() => fileInputRef.current.click()} // Utilisation de la référence pour ouvrir l'explorateur
onDragOver={(e) => e.preventDefault()}
onDrop={handleFileDrop}
>
<CloudUpload className="w-12 h-12 text-emerald-500 mb-4" /> {/* Icône de cloud */}
<input
type="file"
accept=".pdf"
onChange={handleFileChange}
className="hidden"
ref={fileInputRef} // Attachement de la référence
/>
<label htmlFor="fileInput" className="text-center text-gray-500">
<p className="text-lg font-semibold text-gray-800">Déposez votre fichier ici</p>
<p className="text-sm text-gray-500 mt-2">ou cliquez pour sélectionner un fichier PDF</p>
</label>
</div>
{localFileName && (
<div className="mt-4 flex items-center space-x-4 bg-gray-100 p-3 rounded-md shadow-sm">
<CloudUpload className="w-6 h-6 text-emerald-500" />
<p className="text-sm font-medium text-gray-800">
<span className="font-semibold">{localFileName}</span>
</p>
</div>
)}
</div>
);
}

View File

@ -11,6 +11,11 @@ export default function InputPhone({
className,
required,
}) {
const handlePhoneChange = (phone) => {
// Appeler onChange avec un objet personnalisé
onChange({ target: { name, value: phone } });
};
return (
<div className={`${className}`}>
<label htmlFor={name} className="block text-sm font-medium text-gray-700">
@ -21,7 +26,7 @@ export default function InputPhone({
<PhoneInput
defaultCountry="fr"
value={value}
onChange={(phone) => onChange(phone)}
onChange={handlePhoneChange}
inputProps={{
name: name,
required: required,

View File

@ -1,19 +1,244 @@
import React from 'react';
import React, { useState } from 'react';
import Table from '@/components/Table';
import FileUpload from '@/components/FileUpload';
import { Upload, Eye, Trash2, FileText } from 'lucide-react';
import { BASE_URL } from '@/utils/Url';
import Popup from '@/components/Popup';
import logger from '@/utils/logger';
export default function FilesToUpload({
parentFileTemplates,
uploadedFiles,
onFileUpload,
onFileDelete,
}) {
const [selectedFile, setSelectedFile] = useState(null); // État pour le fichier sélectionné
const [actionType, setActionType] = useState(null);
const [popupVisible, setPopupVisible] = useState(false);
const [popupMessage, setPopupMessage] = useState('');
const [removePopupVisible, setRemovePopupVisible] = useState(false);
const [removePopupMessage, setRemovePopupMessage] = useState('');
const [removePopupOnConfirm, setRemovePopupOnConfirm] = useState(() => {});
// Vérification si un fichier est déjà uploadé
const isFileUploaded = (file) => {
return file && file.fileName; // Si `fileName` est défini, le fichier est considéré comme téléversé
};
// Récupération d'un fichier uploadé
const getUploadedFile = (templateId) => {
return uploadedFiles.find(
(file) => parseInt(file.id) === templateId && file.fileName
);
};
const handleUpload = (file, selectedFile) => {
if (!file || !selectedFile) {
logger.error('Données manquantes pour le téléversement.');
return;
}
// Appeler la fonction de téléversement passée en prop
onFileUpload(file, selectedFile)
.then((response) => {
// Mettre à jour uploadedFiles avec les nouvelles données
const updatedFiles = uploadedFiles.map((f) =>
f.id === selectedFile.id
? {
...f,
fileName: response.data.fileName,
file: response.data.file_url,
}
: f
);
// Si le fichier n'existe pas encore, l'ajouter
if (!updatedFiles.find((f) => f.id === selectedFile.id)) {
updatedFiles.push({
id: selectedFile.id,
fileName: response.data.fileName,
file: response.data.file_url,
});
}
})
.catch((error) => {
logger.error('Erreur lors du téléversement du fichier :', error);
});
// Mettre à jour l'état local
setSelectedFile(null);
setActionType(null); // Réinitialiser l'action après l'upload
};
// Définition des colonnes
const columns = [
{ name: 'Nom du fichier', transform: (row) => row.master_name },
{
name: 'Description du fichier',
transform: (row) => row.master_description,
},
{
name: 'Statut',
transform: (row) => {
const uploadedFile = getUploadedFile(row.id);
return (
<span
className={`px-2 py-1 rounded-md text-sm font-medium ${
isFileUploaded(uploadedFile)
? 'bg-green-50 text-green-600'
: 'bg-orange-50 text-orange-600'
}`}
>
{isFileUploaded(uploadedFile) ? 'Chargé' : 'A ajouter'}
</span>
);
},
},
{
name: 'Actions',
transform: (row) => {
const uploadedFile = getUploadedFile(row.id);
return (
<div className="flex items-center justify-center gap-4">
{uploadedFile && (
<>
<button
className={`flex items-center justify-center w-8 h-8 rounded-full ${
actionType === 'view' && selectedFile?.id === row.id
? 'bg-blue-100 text-blue-600 ring-3 ring-blue-500'
: 'text-blue-500 hover:text-blue-700'
}`}
onClick={() => {
if (actionType === 'view' && selectedFile?.id === row.id) {
setSelectedFile(null);
setActionType(null);
} else {
const uploadedFile = getUploadedFile(row.id);
setSelectedFile(uploadedFile || row); // Utiliser les données mises à jour
setActionType('view');
}
}}
type="button"
>
<Eye className="w-5 h-5" />
</button>
<button
className="flex items-center justify-center w-8 h-8 rounded-full text-red-500 hover:text-red-700"
onClick={() => {
setRemovePopupVisible(true);
setRemovePopupMessage(
`Êtes-vous sûr(e) de vouloir supprimer le fichier "${row.master_name}" ?`
);
setRemovePopupOnConfirm(() => () => {
onFileDelete(row.id)
.then(() => {
setPopupMessage(
`Le fichier "${row.master_name}" a été supprimé avec succès.`
);
setPopupVisible(true);
setRemovePopupVisible(false);
})
.catch((error) => {
logger.error(
'Erreur lors de la suppression du fichier :',
error
);
setPopupMessage(
`Erreur lors de la suppression du fichier "${row.master_name}".`
);
setPopupVisible(true);
setRemovePopupVisible(false);
});
setActionType(null);
setSelectedFile(null);
});
}}
type="button"
>
<Trash2 className="w-5 h-5" />
</button>
</>
)}
{!uploadedFile && (
<button
className={`flex items-center justify-center w-8 h-8 rounded-full ${
actionType === 'upload' && selectedFile?.id === row.id
? 'bg-emerald-100 text-emerald-600 ring-3 ring-emerald-500'
: 'text-emerald-500 hover:text-emerald-700'
}`}
onClick={() => {
if (actionType === 'upload' && selectedFile?.id === row.id) {
setSelectedFile(null);
setActionType(null);
} else {
setSelectedFile(row);
setActionType('upload');
}
}}
type="button"
>
<Upload className="w-5 h-5" />
</button>
)}
</div>
);
},
},
];
export default function FilesToUpload({ fileTemplates, columns }) {
return (
<div className="bg-white p-6 rounded-lg shadow-sm border border-gray-200">
<h2 className="text-xl font-bold mb-4 text-gray-800">
Fichiers à uploader
</h2>
<Table
data={fileTemplates}
columns={columns}
itemsPerPage={5}
currentPage={1}
totalPages={1}
onPageChange={() => {}}
<div className="mt-8 mb-4 w-3/5">
<div className="flex items-center justify-between mb-6">
<div className="flex items-center space-x-4">
<div className="bg-emerald-100 p-3 rounded-full shadow-md">
<FileText className="w-8 h-8 text-emerald-600" />
</div>
<div>
<h2 className="text-2xl font-bold text-gray-800">
Pièces à fournir
</h2>
<p className="text-sm text-gray-500 italic">
Ajoutez les documents pour compléter votre inscription
</p>
</div>
</div>
</div>
<Table data={parentFileTemplates} columns={columns} />
{selectedFile && (
<div className="mt-4">
{actionType === 'view' && selectedFile.fileName ? (
<iframe
src={`${BASE_URL}/${selectedFile.fileName}`}
title="Document Viewer"
className="w-full"
style={{
height: '75vh',
border: 'none',
}}
/>
) : actionType === 'upload' ? (
<FileUpload
selectionMessage={`Téléversez le fichier ${selectedFile.master_name}`}
onFileSelect={(file) => handleUpload(file, selectedFile)}
uploadedFileName={selectedFile.fileName || ''}
/>
) : null}
</div>
)}
<Popup
visible={popupVisible}
message={popupMessage}
onConfirm={() => setPopupVisible(false)}
onCancel={() => setPopupVisible(false)}
uniqueConfirmButton={true}
/>
<Popup
visible={removePopupVisible}
message={removePopupMessage}
onConfirm={removePopupOnConfirm}
onCancel={() => setRemovePopupVisible(false)}
/>
</div>
);

View File

@ -1,12 +1,5 @@
import { useState, useEffect, useRef } from 'react';
import {
User,
Mail,
Phone,
UserCheck,
DollarSign,
Percent,
} from 'lucide-react';
import { User, Mail } from 'lucide-react';
import InputTextIcon from '@/components/InputTextIcon';
import ToggleSwitch from '@/components/ToggleSwitch';
import Button from '@/components/Button';
@ -150,7 +143,7 @@ const InscriptionForm = ({
};
const handleChange = (e) => {
const { name, value, type } = e.target;
const { name, value } = e.target;
setFormData((prevState) => ({
...prevState,
[name]: value,
@ -444,7 +437,7 @@ const InscriptionForm = ({
/>
<InputPhone
name="guardianPhone"
label={t('Numéro de téléphone (optionnel)')}
label="Numéro de téléphone (optionnel)"
value={formData.guardianPhone}
onChange={handleChange}
className="w-full mt-4"

View File

@ -5,23 +5,19 @@ import Button from '@/components/Button';
import DjangoCSRFToken from '@/components/DjangoCSRFToken';
import {
fetchRegisterForm,
fetchTemplatesFromRegistrationFiles,
fetchSchoolFileTemplatesFromRegistrationFiles,
fetchParentFileTemplatesFromRegistrationFiles,
} from '@/app/actions/subscriptionAction';
import {
downloadTemplate,
createRegistrationTemplates,
editRegistrationTemplates,
deleteRegistrationTemplates,
editRegistrationSchoolFileTemplates,
editRegistrationParentFileTemplates,
} from '@/app/actions/registerFileGroupAction';
import {
fetchRegistrationPaymentModes,
fetchTuitionPaymentModes,
} from '@/app/actions/schoolAction';
import { Download, Upload, Trash2, Eye } from 'lucide-react';
import { BASE_URL } from '@/utils/Url';
import DraggableFileUpload from '@/components/DraggableFileUpload';
import Modal from '@/components/Modal';
import FileStatusLabel from '@/components/FileStatusLabel';
import logger from '@/utils/logger';
import StudentInfoForm, {
validateStudentInfo,
@ -42,7 +38,6 @@ export default function InscriptionFormShared({
csrfToken,
selectedEstablishmentId,
onSubmit,
cancelUrl,
errors = {}, // Nouvelle prop pour les erreurs
}) {
// États pour gérer les données du formulaire
@ -69,13 +64,8 @@ export default function InscriptionFormShared({
// États pour la gestion des fichiers
const [uploadedFiles, setUploadedFiles] = useState([]);
const [fileTemplates, setFileTemplates] = useState([]);
const [fileGroup, setFileGroup] = useState(null);
const [fileName, setFileName] = useState('');
const [file, setFile] = useState('');
const [showUploadModal, setShowUploadModal] = useState(false);
const [currentTemplateId, setCurrentTemplateId] = useState(null);
const [schoolFileTemplates, setSchoolFileTemplates] = useState([]);
const [parentFileTemplates, setParentFileTemplates] = useState([]);
const [currentPage, setCurrentPage] = useState(1);
const isCurrentPageValid = () => {
@ -110,7 +100,6 @@ export default function InscriptionFormShared({
totalTuitionFees: data?.totalTuitionFees,
});
setGuardians(data?.student?.guardians || []);
setUploadedFiles(data.registration_files || []);
});
setIsLoading(false);
@ -118,12 +107,23 @@ export default function InscriptionFormShared({
}, [studentId]);
useEffect(() => {
fetchTemplatesFromRegistrationFiles(studentId).then((data) => {
setFileTemplates(data);
fetchSchoolFileTemplatesFromRegistrationFiles(studentId).then((data) => {
setSchoolFileTemplates(data);
});
fetchParentFileTemplatesFromRegistrationFiles(studentId).then((data) => {
setParentFileTemplates(data);
// Initialiser uploadedFiles avec uniquement les fichiers dont `file` n'est pas null
const filteredFiles = data
.filter((item) => item.file !== null)
.map((item) => ({
id: item.id,
fileName: item.file,
}));
setUploadedFiles(filteredFiles);
});
}, []);
useEffect(() => {
if (selectedEstablishmentId) {
// Fetch data for registration payment modes
handleRegistrationPaymentModes();
@ -164,71 +164,89 @@ export default function InscriptionFormShared({
setFormData((prev) => ({ ...prev, [field]: value }));
};
// Gestion du téléversement de fichiers
const handleFileUpload = async (file, fileName) => {
if (!file || !currentTemplateId || !formData.id) {
logger.error('Missing required data for upload');
const handleFileUpload = (file, selectedFile) => {
if (!file || !selectedFile) {
logger.error('Données manquantes pour le téléversement.');
return Promise.reject(
new Error('Données manquantes pour le téléversement.')
);
}
const updateData = new FormData();
updateData.append('file', file);
return editRegistrationParentFileTemplates(
selectedFile.id,
updateData,
csrfToken
)
.then((response) => {
logger.debug('Template mis à jour avec succès :', response);
setUploadedFiles((prev) => {
const updatedFiles = prev.map((uploadedFile) =>
uploadedFile.id === selectedFile.id
? { ...uploadedFile, fileName: response.data.file } // Met à jour le fichier téléversé
: uploadedFile
);
// Si le fichier n'existe pas encore, l'ajouter
if (!updatedFiles.find((file) => file.id === selectedFile.id)) {
updatedFiles.push({
id: selectedFile.id,
fileName: response.data.file,
});
}
return updatedFiles;
});
return response; // Retourner la réponse pour signaler le succès
})
.catch((error) => {
logger.error('Erreur lors de la mise à jour du fichier :', error);
throw error; // Relancer l'erreur pour que l'appelant puisse la capturer
});
};
const handleDeleteFile = (templateId) => {
const fileToDelete = uploadedFiles.find(
(file) => parseInt(file.id) === templateId && file.fileName
);
if (!fileToDelete) {
logger.error('Aucun fichier trouvé pour suppression.');
return;
}
const data = new FormData();
data.append('file', file);
data.append('name', fileName);
data.append('template', currentTemplateId);
data.append('register_form', formData.id);
// Créer un FormData avec un champ vide pour "file"
const updateData = new FormData();
updateData.append('file', ''); // Envoyer chaine vide pour indiquer qu'aucun fichier n'est uploadé
try {
const response = await createRegistrationTemplates(data, csrfToken);
if (response) {
setUploadedFiles((prev) => {
const newFiles = prev.filter(
(f) => parseInt(f.template) !== currentTemplateId
);
return [
...newFiles,
{
name: fileName,
template: currentTemplateId,
file: response.file,
},
];
});
return editRegistrationParentFileTemplates(
templateId,
updateData,
csrfToken
)
.then((response) => {
logger.debug('Fichier supprimé avec succès dans la base :', response);
// Rafraîchir les données du formulaire pour avoir les fichiers à jour
if (studentId) {
fetchRegisterForm(studentId).then((data) => {
setUploadedFiles(data.registration_files || []);
});
}
}
} catch (error) {
logger.error('Error uploading file:', error);
}
};
// Vérification si un fichier est déjà uploadé
const isFileUploaded = (templateId) => {
return uploadedFiles.find((template) => template.template === templateId);
};
// Récupération d'un fichier uploadé
const getUploadedFile = (templateId) => {
return uploadedFiles.find((file) => parseInt(file.template) === templateId);
};
// Suppression d'un fichier
const handleDeleteFile = async (templateId) => {
const fileToDelete = getUploadedFile(templateId);
if (!fileToDelete) return;
try {
await deleteRegistrationTemplates(fileToDelete.id, csrfToken);
setUploadedFiles((prev) =>
prev.filter((f) => parseInt(f.template) !== templateId)
);
} catch (error) {
logger.error('Error deleting file:', error);
}
// Mettre à jour l'état local pour refléter la suppression
setUploadedFiles((prev) =>
prev.map((uploadedFile) =>
uploadedFile.id === templateId
? { ...uploadedFile, fileName: null, fileUrl: null } // Réinitialiser les champs liés au fichier
: uploadedFile
)
);
return response;
})
.catch((error) => {
logger.error(
'Erreur lors de la suppression du fichier dans la base :',
error
);
throw error;
});
};
// Soumission du formulaire
@ -268,90 +286,12 @@ export default function InscriptionFormShared({
setCurrentPage(currentPage - 1);
};
const requiredFileTemplates = fileTemplates;
// Configuration des colonnes pour le tableau des fichiers
const columns = [
{ name: 'Nom du fichier', transform: (row) => row.name },
{
name: 'Fichier à Remplir',
transform: (row) => (row.is_required ? 'Oui' : 'Non'),
},
{
name: 'Fichier de référence',
transform: (row) =>
row.file && (
<div className="flex items-center justify-center gap-2">
{' '}
<a
href={`${BASE_URL}${row.file}`}
target="_blank"
className="text-blue-500 hover:text-blue-700"
>
<Download size={16} />
</a>{' '}
</div>
),
},
{
name: 'Statut',
transform: (row) =>
row.is_required && (
<FileStatusLabel
status={isFileUploaded(row.id) ? 'sent' : 'pending'}
/>
),
},
{
name: 'Actions',
transform: (row) => {
if (!row.is_required) return null;
const uploadedFile = getUploadedFile(row.id);
if (uploadedFile) {
return (
<div className="flex items-center justify-center gap-2">
<a
href={`${BASE_URL}${uploadedFile.file}`}
target="_blank"
className="text-blue-500 hover:text-blue-700"
>
<Eye size={16} />
</a>
<button
className="text-red-500 hover:text-red-700"
onClick={() => handleDeleteFile(row.id)}
type="button"
>
<Trash2 size={16} />
</button>
</div>
);
}
return (
<button
className="text-emerald-500 hover:text-emerald-700"
type="button"
onClick={() => {
setCurrentTemplateId(row.id);
setShowUploadModal(true);
}}
>
<Upload size={16} />
</button>
);
},
},
];
// Affichage du loader pendant le chargement
if (isLoading) return <Loader />;
// Rendu du composant
return (
<div className="max-w-4xl mx-auto p-6">
<div className="mx-auto p-6">
<form onSubmit={handleSubmit} className="space-y-8">
<DjangoCSRFToken csrfToken={csrfToken} />
{/* Page 1 : Informations de l'élève et Responsables */}
@ -368,84 +308,84 @@ export default function InscriptionFormShared({
)}
{/* Pages suivantes : Section Fichiers d'inscription */}
{currentPage > 1 && currentPage <= requiredFileTemplates.length + 1 && (
<div className="bg-white p-6 rounded-lg shadow-sm border border-gray-200">
{/* Titre du document */}
<div className="mb-4">
<h2 className="text-lg font-semibold text-gray-800">
{requiredFileTemplates[currentPage - 2].name ||
'Document sans nom'}
</h2>
<p className="text-sm text-gray-500">
{requiredFileTemplates[currentPage - 2].description ||
'Aucune description disponible pour ce document.'}
</p>
{currentPage > 1 && currentPage <= schoolFileTemplates.length + 1 && (
<div className="mt-8 mb-4 w-3/5">
<div className="bg-white p-6 rounded-lg shadow-sm border border-gray-200">
{/* Titre du document */}
<div className="mb-4">
<h2 className="text-lg font-semibold text-gray-800">
{schoolFileTemplates[currentPage - 2].name ||
'Document sans nom'}
</h2>
<p className="text-sm text-gray-500">
{schoolFileTemplates[currentPage - 2].description ||
'Aucune description disponible pour ce document.'}
</p>
</div>
{/* Affichage du formulaire ou du document */}
{schoolFileTemplates[currentPage - 2].file === null ? (
<DocusealForm
id="docusealForm"
src={
'https://docuseal.com/s/' +
schoolFileTemplates[currentPage - 2].slug
}
withDownloadButton={false}
onComplete={() => {
downloadTemplate(schoolFileTemplates[currentPage - 2].slug)
.then((data) => fetch(data))
.then((response) => response.blob())
.then((blob) => {
const file = new File(
[blob],
`${schoolFileTemplates[currentPage - 2].name}.pdf`,
{ type: blob.type }
);
const updateData = new FormData();
updateData.append('file', file);
return editRegistrationSchoolFileTemplates(
schoolFileTemplates[currentPage - 2].id,
updateData,
csrfToken
);
})
.then((data) => {
logger.debug('EDIT TEMPLATE : ', data);
})
.catch((error) => {
logger.error('error editing template : ', error);
});
}}
/>
) : (
<iframe
src={`${BASE_URL}/${schoolFileTemplates[currentPage - 2].file}`}
title="Document Viewer"
className="w-full"
style={{
height: '75vh', // Ajuster la hauteur à 75% de la fenêtre
border: 'none',
}}
/>
)}
</div>
{/* Affichage du formulaire ou du document */}
{requiredFileTemplates[currentPage - 2].file === '' ? (
<DocusealForm
id="docusealForm"
src={
'https://docuseal.com/s/' +
requiredFileTemplates[currentPage - 2].slug
}
withDownloadButton={false}
onComplete={() => {
downloadTemplate(requiredFileTemplates[currentPage - 2].slug)
.then((data) => fetch(data))
.then((response) => response.blob())
.then((blob) => {
const file = new File(
[blob],
`${requiredFileTemplates[currentPage - 2].name}.pdf`,
{ type: blob.type }
);
const updateData = new FormData();
updateData.append('file', file);
return editRegistrationTemplates(
requiredFileTemplates[currentPage - 2].id,
updateData,
csrfToken
);
})
.then((data) => {
logger.debug('EDIT TEMPLATE : ', data);
})
.catch((error) => {
logger.error('error editing template : ', error);
});
}}
/>
) : (
<iframe
src={`${BASE_URL}/${requiredFileTemplates[currentPage - 2].file}`}
title="Document Viewer"
className="w-full"
style={{
height: '75vh', // Ajuster la hauteur à 75% de la fenêtre
border: 'none',
}}
/>
)}
</div>
)}
{/* Dernière page : Section Fichiers parents */}
{currentPage === requiredFileTemplates.length + 2 && (
<>
<FilesToUpload
fileTemplates={fileTemplates.filter(
(template) => !template.is_required
)}
columns={columns}
/>
</>
{currentPage === schoolFileTemplates.length + 2 && (
<FilesToUpload
parentFileTemplates={parentFileTemplates}
uploadedFiles={uploadedFiles}
onFileUpload={handleFileUpload}
onFileDelete={handleDeleteFile}
/>
)}
{/* Boutons de contrôle */}
<div className="flex justify-end space-x-4">
<div className="flex justify-center space-x-4">
<Button
text="Sauvegarder"
onClick={handleSave}
@ -462,7 +402,7 @@ export default function InscriptionFormShared({
}}
/>
)}
{currentPage < requiredFileTemplates.length + 2 && (
{currentPage < schoolFileTemplates.length + 2 && (
<Button
text="Suivant"
onClick={(e) => {
@ -479,57 +419,11 @@ export default function InscriptionFormShared({
name="Next"
/>
)}
{currentPage === requiredFileTemplates.length + 2 && (
{currentPage === schoolFileTemplates.length + 2 && (
<Button type="submit" text="Valider" primary />
)}
</div>
</form>
{fileTemplates.length > 0 && (
<Modal
isOpen={showUploadModal}
setIsOpen={setShowUploadModal}
title="Téléverser un fichier"
ContentComponent={() => (
<>
<DraggableFileUpload
className="w-full"
fileName={fileName}
onFileSelect={(selectedFile) => {
if (selectedFile) {
setFile(selectedFile);
setFileName(selectedFile.name);
}
}}
/>
<div className="mt-4 flex justify-center space-x-4">
<Button
text="Annuler"
onClick={() => {
setShowUploadModal(false);
setCurrentTemplateId(null);
setFile(null);
setFileName('');
}}
/>
<Button
text="Valider"
onClick={() => {
if (file && fileName) {
handleFileUpload(file, fileName);
setShowUploadModal(false);
setCurrentTemplateId(null);
setFile(null);
setFileName('');
}
}}
primary={true}
disabled={!file || !fileName}
/>
</div>
</>
)}
/>
)}
</div>
);
}

View File

@ -1,25 +1,35 @@
'use client';
import React, { useState, useEffect } from 'react';
import { DocusealBuilder } from '@docuseal/react';
import Button from '@/components/Button';
import ToggleSwitch from '@/components/ToggleSwitch'; // Import du composant ToggleSwitch
import { BASE_URL } from '@/utils/Url';
import { generateToken } from '@/app/actions/registerFileGroupAction';
import {
fetchSchoolFileTemplatesFromRegistrationFiles,
fetchParentFileTemplatesFromRegistrationFiles,
} from '@/app/actions/subscriptionAction';
import logger from '@/utils/logger';
import { GraduationCap, CloudUpload } from 'lucide-react';
import { GraduationCap } from 'lucide-react';
import FileUpload from '@/components/FileUpload';
import SectionHeader from '@/components/SectionHeader';
export default function ValidateSubscription({
studentId,
firstName,
lastName,
paymentMode,
paymentSepa,
file,
onAccept,
}) {
const [token, setToken] = useState(null);
const [uploadedFileName, setUploadedFileName] = useState('');
const [selectedFile, setSelectedFile] = useState(null); // Nouvel état pour le fichier sélectionné
const [pdfUrl, setPdfUrl] = useState(`${BASE_URL}/${file}`);
const [isSepa, setIsSepa] = useState(paymentMode === '1'); // Vérifie si le mode de paiement est SEPA
const [currentPage, setCurrentPage] = useState(1); // Gestion des pages
const [isSepa, setIsSepa] = useState(paymentSepa); // Vérifie si le mode de paiement est SEPA
const [currentPage, setCurrentPage] = useState(1); // Gestion des étapes
const [schoolFileTemplates, setSchoolFileTemplates] = useState([]); // Stocke les fichiers schoolFileTemplates
const [parentFileTemplates, setParentFileTemplates] = useState([]); // Stocke les fichiers parentFileTemplates
const [mergeDocuments, setMergeDocuments] = useState(false); // État pour activer/désactiver la fusion des documents
useEffect(() => {
if (isSepa) {
@ -33,38 +43,67 @@ export default function ValidateSubscription({
}
}, [isSepa]);
const handleUpload = (detail) => {
logger.debug('Uploaded file detail:', detail);
setUploadedFileName(detail.name);
};
useEffect(() => {
// Récupérer les fichiers schoolFileTemplates pour l'étudiant
fetchSchoolFileTemplatesFromRegistrationFiles(studentId)
.then((data) => {
setSchoolFileTemplates(data);
logger.debug('Fichiers schoolFileTemplates récupérés:', data);
})
.catch((error) =>
logger.error(
'Erreur lors de la récupération des schoolFileTemplates:',
error
)
);
// Récupérer les fichiers parentFileTemplates pour l'étudiant
fetchParentFileTemplatesFromRegistrationFiles(studentId)
.then((data) => {
setParentFileTemplates(data);
logger.debug('Fichiers parentFileTemplates récupérés:', data);
})
.catch((error) =>
logger.error(
'Erreur lors de la récupération des parentFileTemplates:',
error
)
);
}, [studentId]);
const handleAccept = () => {
const fileInput = document.getElementById('fileInput'); // Récupère l'élément input
const file = fileInput?.files[0]; // Récupère le fichier sélectionné
if (!file) {
if (!selectedFile && isSepa) {
logger.error('Aucun fichier sélectionné pour le champ SEPA.');
return;
}
// Ajouter le paramètre fusion dans l'URL
const fusionParam = mergeDocuments ? 'true' : 'false';
const data = {
status: 7,
sepa_file: file,
sepa_file: selectedFile, // Utilise le fichier sélectionné depuis l'état
fusionParam: fusionParam,
};
// Appeler la fonction passée par le parent pour mettre à jour le RF
onAccept(data);
};
const handleRefuse = () => {
logger.debug("Dossier refusé pour l'étudiant:", studentId);
// Logique pour refuser l'inscription
const handleToggleMergeDocuments = () => {
// Inverser l'état de mergeDocuments
setMergeDocuments((prevState) => !prevState);
};
const isValidateButtonDisabled = isSepa && !uploadedFileName;
const goToNextPage = () => {
if (currentPage < (isSepa ? 2 : 1)) {
const totalPages =
1 +
schoolFileTemplates.length +
parentFileTemplates.length +
(isSepa ? 1 : 0);
if (currentPage < totalPages) {
setCurrentPage(currentPage + 1);
}
};
@ -75,93 +114,102 @@ export default function ValidateSubscription({
}
};
const totalPages =
1 +
schoolFileTemplates.length +
parentFileTemplates.length +
(isSepa ? 1 : 0);
const renderContent = () => {
if (currentPage === 1) {
// Page 1 : Afficher le PDF principal
return (
<iframe
src={pdfUrl}
title="Aperçu du PDF"
className="w-full h-[900px] border rounded-lg"
style={{
transform: 'scale(0.95)', // Dézoom léger pour une meilleure vue
transformOrigin: 'top center',
border: 'none',
}}
/>
);
} else if (
currentPage > 1 &&
currentPage <= 1 + schoolFileTemplates.length
) {
// Pages des schoolFileTemplates
const index = currentPage - 2; // Décalage pour correspondre à l'index du tableau
return (
<iframe
src={`${BASE_URL}/${schoolFileTemplates[index]?.file}`}
title={`Document ${index + 1}`}
className="w-full h-[900px] border rounded-lg"
style={{
transform: 'scale(0.95)', // Dézoom léger pour une meilleure vue
transformOrigin: 'top center',
border: 'none',
}}
/>
);
} else if (
currentPage > 1 + schoolFileTemplates.length &&
currentPage <= 1 + schoolFileTemplates.length + parentFileTemplates.length
) {
// Pages des parentFileTemplates
const index = currentPage - 2 - schoolFileTemplates.length; // Décalage pour correspondre à l'index du tableau
return (
<iframe
src={`${BASE_URL}/${parentFileTemplates[index]?.file}`}
title={`Document Parent ${index + 1}`}
className="w-full h-[900px] border rounded-lg"
style={{
transform: 'scale(0.95)', // Dézoom léger pour une meilleure vue
transformOrigin: 'top center',
border: 'none',
}}
/>
);
} else if (currentPage === totalPages && isSepa) {
// Dernière page : Mandat SEPA
return (
<FileUpload
selectionMessage="Sélectionnez un mandat de prélèvement SEPA"
onFileSelect={(file) => {
setUploadedFileName(file.name); // Stocke uniquement le nom du fichier
setSelectedFile(file); // Stocke le fichier dans l'état
logger.debug('Fichier sélectionné:', file.name);
}}
uploadedFileName={uploadedFileName}
/>
);
}
return null;
};
return (
<div className="p-8 space-y-6 bg-gray-50 rounded-lg shadow-lg">
{/* Titre */}
<div className="flex items-center space-x-4">
<div className="bg-emerald-100 p-3 rounded-full shadow-md">
<GraduationCap className="w-8 h-8 text-emerald-600" />
</div>
<div>
<h1 className="text-3xl font-bold text-gray-800">
Dossier scolaire de{' '}
<span className="text-emerald-600">
{firstName} {lastName}
</span>
</h1>
<p className="text-sm text-gray-500 italic">
Année scolaire {new Date().getFullYear()}-
{new Date().getFullYear() + 1}
</p>
</div>
</div>
<div className="space-y-6 p-6">
<SectionHeader
icon={GraduationCap}
title={`Dossier scolaire de ${firstName} ${lastName}`}
description={`Année scolaire ${new Date().getFullYear()}-${new Date().getFullYear() + 1}`}
/>
{/* Contenu principal */}
{currentPage === 1 && (
<div className="border p-6 rounded-lg shadow-md bg-white flex justify-center items-center">
<iframe
src={pdfUrl}
title="Aperçu du PDF"
className="w-full h-[900px] border rounded-lg"
style={{
transform: 'scale(0.95)', // Dézoom léger pour une meilleure vue
transformOrigin: 'top center',
border: 'none',
}}
/>
</div>
)}
<div className="p-6 items-center">{renderContent()}</div>
{currentPage === 2 && isSepa && (
<div className="border p-4 rounded-md shadow-md">
<h3 className="text-lg font-semibold mb-4">
Sélection du mandat de pélèvement SEPA
</h3>
<div
className="border-2 border-dashed border-gray-500 p-6 rounded-lg flex flex-col items-center justify-center cursor-pointer hover:border-emerald-500"
onClick={() => document.getElementById('fileInput').click()} // Ouvre l'explorateur de fichiers au clic
onDragOver={(e) => e.preventDefault()}
onDrop={(e) => {
e.preventDefault();
const file = e.dataTransfer.files[0];
if (file) {
setUploadedFileName(file.name); // Stocke uniquement le nom du fichier
logger.debug('Fichier déposé:', file.name);
}
}}
>
<CloudUpload className="w-12 h-12 text-emerald-500 mb-4" />{' '}
{/* Icône de cloud */}
<input
type="file"
accept=".pdf"
onChange={(e) => {
const file = e.target.files[0];
if (file) {
setUploadedFileName(file.name); // Stocke uniquement le nom du fichier
logger.debug('Fichier sélectionné:', file.name);
}
}}
className="hidden"
id="fileInput"
/>
<label htmlFor="fileInput" className="text-center text-gray-500">
<p className="text-lg font-semibold text-gray-800">
Déposez votre fichier ici
</p>
<p className="text-sm text-gray-500 mt-2">
ou cliquez pour sélectionner un fichier PDF
</p>
</label>
</div>
{uploadedFileName && (
<div className="mt-4 flex items-center space-x-4 bg-gray-100 p-3 rounded-md shadow-sm">
<CloudUpload className="w-6 h-6 text-emerald-500" />
<p className="text-sm font-medium text-gray-800">
<span className="font-semibold">{uploadedFileName}</span>
</p>
</div>
)}
{/* Option de fusion des documents (affichée uniquement sur la dernière page) */}
{currentPage === totalPages && (
<div className="flex items-center justify-between mt-6">
<span className="text-gray-700">
Fusionner les documents en un seul fichier PDF
</span>
<ToggleSwitch
label="Fusionner"
checked={mergeDocuments}
onChange={handleToggleMergeDocuments} // Appeler la fonction pour inverser l'état
/>
</div>
)}
@ -174,7 +222,7 @@ export default function ValidateSubscription({
className="bg-gray-300 text-gray-700 hover:bg-gray-400 px-6 py-2"
/>
)}
{currentPage < (isSepa ? 2 : 1) && (
{currentPage < totalPages && (
<Button
text="Suivant"
onClick={goToNextPage}
@ -182,7 +230,7 @@ export default function ValidateSubscription({
className="bg-emerald-500 text-white hover:bg-emerald-600 px-6 py-2"
/>
)}
{currentPage === (isSepa ? 2 : 1) && (
{currentPage === totalPages && (
<Button
text="Valider"
onClick={handleAccept}

View File

@ -0,0 +1,44 @@
import React from 'react';
import { Plus } from 'lucide-react';
const SectionHeader = ({
icon: Icon,
discountStyle = false,
title,
description,
button = false,
buttonOpeningModal = false,
onClick = null
}) => {
return (
<div className="flex items-center justify-between mb-6">
<div className="flex items-center space-x-4">
<div className={`${discountStyle ? "bg-yellow-100" : "bg-emerald-100"} p-3 rounded-full shadow-md`}>
<Icon
className={discountStyle ?
"w-8 h-8 text-yellow-600" :
"w-8 h-8 text-emerald-600"
}
/>
</div>
<div>
<h2 className="text-2xl font-bold text-gray-800">{title}</h2>
<p className="text-sm text-gray-500 italic">{description}</p>
</div>
</div>
{button && onClick && (
<button
onClick={onClick}
className={buttonOpeningModal ?
"flex items-center bg-emerald-200 text-emerald-700 p-2 rounded-full shadow-sm hover:bg-emerald-300" :
"text-emerald-500 hover:bg-emerald-200 rounded-full p-2"
}
>
<Plus className="w-6 h-6" />
</button>
)}
</div>
);
};
export default SectionHeader;

View File

@ -9,7 +9,11 @@ const SidebarTabs = ({ tabs }) => {
{tabs.map((tab) => (
<button
key={tab.id}
className={`flex-1 p-4 ${activeTab === tab.id ? 'border-b-2 border-emerald-500 text-emerald-500' : 'text-gray-500 hover:text-emerald-500'}`}
className={`flex-1 p-4 ${
activeTab === tab.id
? 'border-b-2 border-emerald-500 text-emerald-500'
: 'text-gray-500 hover:text-emerald-500'
}`}
onClick={() => setActiveTab(tab.id)}
>
{tab.label}

View File

@ -1,13 +1,4 @@
import {
Trash2,
Edit3,
Plus,
ZoomIn,
Users,
Check,
X,
Hand,
} from 'lucide-react';
import { Trash2, Edit3, ZoomIn, Users, Check, X, Hand } from 'lucide-react';
import React, { useState, useEffect } from 'react';
import Table from '@/components/Table';
import Popup from '@/components/Popup';
@ -21,6 +12,7 @@ import { HTML5Backend } from 'react-dnd-html5-backend';
import { ESTABLISHMENT_ID } from '@/utils/Url';
import logger from '@/utils/logger';
import ClasseDetails from '@/components/ClasseDetails';
import SectionHeader from '@/components/SectionHeader';
const ItemTypes = {
TEACHER: 'teacher',
@ -553,19 +545,13 @@ const ClassesSection = ({
return (
<DndProvider backend={HTML5Backend}>
<div className="space-y-4">
<div className="flex justify-between items-center">
<div className="flex items-center mb-4">
<Users className="w-6 h-6 text-emerald-500 mr-2" />
<h2 className="text-xl font-semibold">Classes</h2>
</div>
<button
type="button"
onClick={handleAddClass}
className="text-emerald-500 hover:text-emerald-700"
>
<Plus className="w-5 h-5" />
</button>
</div>
<SectionHeader
icon={Users}
title="Liste des classes"
description="Gérez les classes de votre école"
button={true}
onClick={handleAddClass}
/>
<Table
data={newClass ? [newClass, ...classes] : classes}
columns={columns}

View File

@ -1,4 +1,4 @@
import { Plus, Trash2, Edit3, Check, X, BookOpen } from 'lucide-react';
import { Trash2, Edit3, Check, X, BookOpen } from 'lucide-react';
import { useState } from 'react';
import Table from '@/components/Table';
import Popup from '@/components/Popup';
@ -6,7 +6,9 @@ import InputTextWithColorIcon from '@/components/InputTextWithColorIcon';
import { DndProvider } from 'react-dnd';
import { HTML5Backend } from 'react-dnd-html5-backend';
import SpecialityItem from '@/components/Structure/Configuration/SpecialityItem';
import { useEstablishment } from '@/context/EstablishmentContext';
import logger from '@/utils/logger';
import SectionHeader from '@/components/SectionHeader';
const SpecialitiesSection = ({
specialities,
@ -26,6 +28,8 @@ const SpecialitiesSection = ({
const [removePopupMessage, setRemovePopupMessage] = useState('');
const [removePopupOnConfirm, setRemovePopupOnConfirm] = useState(() => {});
const { selectedEstablishmentId } = useEstablishment();
// Récupération des messages d'erreur
const getError = (field) => {
return localErrors?.[field]?.[0];
@ -49,7 +53,13 @@ const SpecialitiesSection = ({
const handleSaveNewSpeciality = () => {
if (newSpeciality.name) {
handleCreate(newSpeciality)
// Ajouter l'ID de l'établissement à la nouvelle spécialité
const specialityData = {
...newSpeciality,
establishment: selectedEstablishmentId, // Inclure l'ID de l'établissement
};
handleCreate(specialityData)
.then((createdSpeciality) => {
setSpecialities([createdSpeciality, ...specialities]);
setNewSpeciality(null);
@ -234,19 +244,13 @@ const SpecialitiesSection = ({
return (
<DndProvider backend={HTML5Backend}>
<div className="space-y-4">
<div className="flex justify-between items-center">
<div className="flex items-center mb-4">
<BookOpen className="w-6 h-6 text-emerald-500 mr-2" />
<h2 className="text-xl font-semibold">Spécialités</h2>
</div>
<button
type="button"
onClick={handleAddSpeciality}
className="text-emerald-500 hover:text-emerald-700"
>
<Plus className="w-5 h-5" />
</button>
</div>
<SectionHeader
icon={BookOpen}
title="Liste des spécialités"
description="Gérez les spécialités de votre école"
button={true}
onClick={handleAddSpeciality}
/>
<Table
data={newSpeciality ? [newSpeciality, ...specialities] : specialities}
columns={columns}

View File

@ -22,9 +22,9 @@ const StructureManagement = ({
handleDelete,
}) => {
return (
<div className="max-w-8xl mx-auto p-4 mt-6 space-y-8">
<div className="w-full mx-auto mt-6">
<ClassesProvider>
<div className="w-2/5 p-4 bg-white rounded-lg shadow-md">
<div className="mt-8 w-2/5">
<SpecialitiesSection
specialities={specialities}
setSpecialities={setSpecialities}
@ -48,7 +48,7 @@ const StructureManagement = ({
}
/>
</div>
<div className="w-4/5 p-4 bg-white rounded-lg shadow-md">
<div className="w-4/5 mt-12">
<TeachersSection
teachers={teachers}
setTeachers={setTeachers}
@ -70,7 +70,7 @@ const StructureManagement = ({
}
/>
</div>
<div className="w-full p-4 bg-white rounded-lg shadow-md">
<div className="w-full mt-12">
<ClassesSection
classes={classes}
setClasses={setClasses}

View File

@ -1,18 +1,8 @@
import React, { useState, useEffect } from 'react';
import {
Plus,
Edit3,
Trash2,
GraduationCap,
Check,
X,
Hand,
Search,
} from 'lucide-react';
import { Edit3, Trash2, GraduationCap, Check, X, Hand } from 'lucide-react';
import Table from '@/components/Table';
import Popup from '@/components/Popup';
import ToggleSwitch from '@/components/ToggleSwitch';
import { createProfile, updateProfile } from '@/app/actions/authAction';
import { useCsrfToken } from '@/context/CsrfContext';
import { DndProvider, useDrag, useDrop } from 'react-dnd';
import { HTML5Backend } from 'react-dnd-html5-backend';
@ -20,8 +10,8 @@ import InputText from '@/components/InputText';
import SpecialityItem from '@/components/Structure/Configuration/SpecialityItem';
import TeacherItem from './TeacherItem';
import logger from '@/utils/logger';
import { fetchProfiles } from '@/app/actions/authAction';
import { useEstablishment } from '@/context/EstablishmentContext';
import SectionHeader from '@/components/SectionHeader';
const ItemTypes = {
SPECIALITY: 'speciality',
@ -578,19 +568,13 @@ const TeachersSection = ({
return (
<DndProvider backend={HTML5Backend}>
<div className="space-y-4">
<div className="flex justify-between items-center">
<div className="flex items-center mb-4">
<GraduationCap className="w-6 h-6 text-emerald-500 mr-2" />
<h2 className="text-xl font-semibold">Enseignants</h2>
</div>
<button
type="button"
onClick={handleAddTeacher}
className="text-emerald-500 hover:text-emerald-700"
>
<Plus className="w-5 h-5" />
</button>
</div>
<SectionHeader
icon={GraduationCap}
title="Liste des enseignants.es"
description="Gérez les enseignants.es de votre école"
button={true}
onClick={handleAddTeacher}
/>
<Table
data={newTeacher ? [newTeacher, ...teachers] : teachers}
columns={columns}

View File

@ -1,31 +1,22 @@
import React, { useState, useEffect } from 'react';
import ToggleSwitch from '@/components/ToggleSwitch'; // Import du composant ToggleSwitch
import {
fetchRegistrationFileGroups,
createRegistrationTemplates,
createRegistrationSchoolFileTemplate,
cloneTemplate,
generateToken,
} from '@/app/actions/registerFileGroupAction';
import { DocusealBuilder } from '@docuseal/react';
import logger from '@/utils/logger';
import {
BE_DOCUSEAL_GET_JWT,
BASE_URL,
FE_API_DOCUSEAL_GENERATE_TOKEN,
} from '@/utils/Url';
import Button from '@/components/Button'; // Import du composant Button
import MultiSelect from '@/components/MultiSelect'; // Import du composant MultiSelect
import { useCsrfToken } from '@/context/CsrfContext';
import { useEstablishment } from '@/context/EstablishmentContext';
export default function FileUpload({
export default function FileUploadDocuSeal({
handleCreateTemplateMaster,
handleEditTemplateMaster,
fileToEdit = null,
onSuccess,
}) {
const [isRequired, setIsRequired] = useState(false); // État pour le toggle isRequired
const [order, setOrder] = useState(0);
const [groups, setGroups] = useState([]);
const [token, setToken] = useState(null);
const [templateMaster, setTemplateMaster] = useState(null);
@ -61,10 +52,6 @@ export default function FileUpload({
);
}, [fileToEdit]);
const handleFileNameChange = (event) => {
setUploadedFileName(event.target.value);
};
const handleGroupChange = (selectedGroups) => {
setSelectedGroups(selectedGroups);
@ -120,7 +107,7 @@ export default function FileUpload({
logger.debug('creation du clone avec required : ', is_required);
cloneTemplate(templateMaster?.id, guardian.email, is_required)
.then((clonedDocument) => {
// Sauvegarde des templates clonés dans la base de données
// Sauvegarde des schoolFileTemplates clonés dans la base de données
const data = {
name: `${uploadedFileName}_${guardian.first_name}_${guardian.last_name}`,
slug: clonedDocument.slug,
@ -128,7 +115,7 @@ export default function FileUpload({
master: templateMaster?.id,
registration_form: guardian.registration_form,
};
createRegistrationTemplates(data, csrfToken)
createRegistrationSchoolFileTemplate(data, csrfToken)
.then((response) => {
logger.debug('Template enregistré avec succès:', response);
onSuccess();

View File

@ -1,36 +1,40 @@
import React, { useState, useEffect } from 'react';
import {
Plus,
Download,
Edit3,
Trash2,
FolderPlus,
Signature,
} from 'lucide-react';
import { Download, Edit3, Trash2, FolderPlus, Signature } from 'lucide-react';
import Modal from '@/components/Modal';
import Table from '@/components/Table';
import FileUpload from '@/components/Structure/Files/FileUpload';
import FileUploadDocuSeal from '@/components/Structure/Files/FileUploadDocuSeal';
import { BASE_URL } from '@/utils/Url';
import {
// GET
fetchRegistrationFileGroups,
fetchRegistrationSchoolFileMasters,
fetchRegistrationSchoolFileTemplates,
fetchRegistrationParentFileMasters,
// POST
createRegistrationFileGroup,
deleteRegistrationFileGroup,
createRegistrationSchoolFileMaster,
createRegistrationParentFileMaster,
// PUT
editRegistrationFileGroup,
fetchRegistrationTemplateMaster,
createRegistrationTemplateMaster,
editRegistrationTemplateMaster,
deleteRegistrationTemplateMaster,
fetchRegistrationTemplates,
editRegistrationSchoolFileMaster,
editRegistrationParentFileMaster,
// DELETE
deleteRegistrationFileGroup,
deleteRegistrationSchoolFileMaster,
deleteRegistrationParentFileMaster,
} from '@/app/actions/registerFileGroupAction';
import RegistrationFileGroupForm from '@/components/Structure/Files/RegistrationFileGroupForm';
import logger from '@/utils/logger';
import ParentFilesSection from '@/components/Structure/Files/ParentFilesSection';
import SectionHeader from '@/components/SectionHeader';
export default function FilesGroupsManagement({
csrfToken,
selectedEstablishmentId,
}) {
const [templateMasters, setTemplateMasters] = useState([]);
const [templates, setTemplates] = useState([]);
const [schoolFileMasters, setSchoolFileMasters] = useState([]);
const [schoolFileTemplates, setSchoolFileTemplates] = useState([]);
const [parentFiles, setParentFileMasters] = useState([]);
const [groups, setGroups] = useState([]);
const [selectedGroup, setSelectedGroup] = useState(null);
const [isModalOpen, setIsModalOpen] = useState(false);
@ -39,6 +43,10 @@ export default function FilesGroupsManagement({
const [isGroupModalOpen, setIsGroupModalOpen] = useState(false);
const [groupToEdit, setGroupToEdit] = useState(null);
const [reloadTemplates, setReloadTemplates] = useState(false);
const [editingDocumentId, setEditingDocumentId] = useState(null);
const [formData, setFormData] = useState({});
const [uploadedFileName, setUploadedFileName] = useState('');
const handleReloadTemplates = () => {
setReloadTemplates(true);
@ -61,19 +69,28 @@ export default function FilesGroupsManagement({
useEffect(() => {
if (selectedEstablishmentId) {
Promise.all([
fetchRegistrationTemplateMaster(),
fetchRegistrationSchoolFileMasters(),
fetchRegistrationFileGroups(selectedEstablishmentId),
fetchRegistrationTemplates(),
fetchRegistrationSchoolFileTemplates(),
fetchRegistrationParentFileMasters(),
])
.then(([filesTemplateMasters, groupsData, filesTemplates]) => {
setGroups(groupsData);
setTemplates(filesTemplates);
// Transformer chaque fichier pour inclure les informations complètes du groupe
const transformedFiles = filesTemplateMasters.map((file) =>
transformFileData(file, groupsData)
);
setTemplateMasters(transformedFiles);
})
.then(
([
dataSchoolFileMasters,
groupsData,
dataSchoolFileTemplates,
dataParentFileMasters,
]) => {
setGroups(groupsData);
setSchoolFileTemplates(dataSchoolFileTemplates);
setParentFileMasters(dataParentFileMasters);
// Transformer chaque fichier pour inclure les informations complètes du groupe
const transformedFiles = dataSchoolFileMasters.map((file) =>
transformFileData(file, groupsData)
);
setSchoolFileMasters(transformedFiles);
}
)
.catch((err) => {
console.log(err.message);
})
@ -85,7 +102,7 @@ export default function FilesGroupsManagement({
const deleteTemplateMaster = (templateMaster) => {
// Supprimer les clones associés via l'API DocuSeal
const removeClonesPromises = templates
const removeClonesPromises = schoolFileTemplates
.filter((template) => template.master === templateMaster.id)
.map((template) => removeTemplate(template.id));
@ -100,11 +117,11 @@ export default function FilesGroupsManagement({
logger.debug('Master et clones supprimés avec succès de DocuSeal.');
// Supprimer le template master de la base de données
deleteRegistrationTemplateMaster(templateMaster.id, csrfToken)
deleteRegistrationSchoolFileMaster(templateMaster.id, csrfToken)
.then((response) => {
if (response.ok) {
setTemplateMasters(
templateMasters.filter(
setSchoolFileMasters(
schoolFileMasters.filter(
(fichier) => fichier.id !== templateMaster.id
)
);
@ -175,11 +192,11 @@ export default function FilesGroupsManagement({
};
logger.debug(data);
createRegistrationTemplateMaster(data, csrfToken)
createRegistrationSchoolFileMaster(data, csrfToken)
.then((data) => {
// Transformer le nouveau fichier avec les informations du groupe
const transformedFile = transformFileData(data, groups);
setTemplateMasters((prevFiles) => [...prevFiles, transformedFile]);
setSchoolFileMasters((prevFiles) => [...prevFiles, transformedFile]);
setIsModalOpen(false);
})
.catch((error) => {
@ -196,11 +213,11 @@ export default function FilesGroupsManagement({
};
logger.debug(data);
editRegistrationTemplateMaster(id, data, csrfToken)
editRegistrationSchoolFileMaster(id, data, csrfToken)
.then((data) => {
// Transformer le fichier mis à jour avec les informations du groupe
const transformedFile = transformFileData(data, groups);
setTemplateMasters((prevFichiers) =>
setSchoolFileMasters((prevFichiers) =>
prevFichiers.map((f) => (f.id === id ? transformedFile : f))
);
setIsModalOpen(false);
@ -228,7 +245,13 @@ export default function FilesGroupsManagement({
alert("Erreur lors de l'opération sur le groupe");
});
} else {
createRegistrationFileGroup(groupData, csrfToken)
// 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);
@ -246,13 +269,13 @@ export default function FilesGroupsManagement({
};
const handleGroupDelete = (groupId) => {
// Vérifier si des templateMasters utilisent ce groupe
const filesInGroup = templateMasters.filter(
// Vérifier si des schoolFileMasters utilisent ce groupe
const filesInGroup = schoolFileMasters.filter(
(file) => file.group && file.group.id === groupId
);
if (filesInGroup.length > 0) {
alert(
"Impossible de supprimer ce groupe car il contient des templateMasters. Veuillez d'abord retirer tous les templateMasters de ce groupe."
"Impossible de supprimer ce groupe car il contient des schoolFileMasters. Veuillez d'abord retirer tous les schoolFileMasters de ce groupe."
);
return;
}
@ -279,7 +302,62 @@ export default function FilesGroupsManagement({
}
};
const filteredFiles = templateMasters.filter((file) => {
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);
alert(
'Une erreur est survenue lors de la création du document parent.'
);
throw error;
});
};
const handleEdit = (id, updatedFile) => {
return editRegistrationParentFileMaster(id, updatedFile, csrfToken)
.then((response) => {
const modifiedFile = response.data; // Extraire les données mises à jour
// Mettre à jour la liste des fichiers parents
setParentFileMasters((prevFiles) =>
prevFiles.map((file) => (file.id === id ? modifiedFile : file))
);
logger.debug('Document parent mis à jour avec succès:', modifiedFile);
return modifiedFile; // Retourner le fichier mis à jour
})
.catch((error) => {
logger.error(
'Erreur lors de la modification du document parent:',
error
);
alert(
'Une erreur est survenue lors de la modification du document parent.'
);
throw error;
});
};
const handleDelete = (id) => {
return deleteRegistrationParentFileMaster(id, csrfToken)
.then(() => {
// Mettre à jour la liste des fichiers parents en supprimant l'élément correspondant
setParentFileMasters((prevFiles) =>
prevFiles.filter((file) => file.id !== id)
);
logger.debug('Document parent supprimé avec succès:', id);
})
.catch((error) => {
logger.error('Erreur lors de la suppression du fichier parent:', error);
});
};
const filteredFiles = schoolFileMasters.filter((file) => {
if (!selectedGroup) return true;
return (
file.groups &&
@ -288,9 +366,9 @@ export default function FilesGroupsManagement({
});
const columnsFiles = [
{ name: 'Nom du fichier', transform: (row) => row.name },
{ name: 'Nom du formulaire', transform: (row) => row.name },
{
name: 'Groupes',
name: "Dossiers d'inscription",
transform: (row) =>
row.groups && row.groups.length > 0
? row.groups.map((group) => group.name).join(', ')
@ -327,7 +405,7 @@ export default function FilesGroupsManagement({
];
const columnsGroups = [
{ name: 'Nom du groupe', transform: (row) => row.name },
{ name: 'Nom du dossier', transform: (row) => row.name },
{ name: 'Description', transform: (row) => row.description },
{
name: 'Actions',
@ -351,7 +429,8 @@ export default function FilesGroupsManagement({
];
return (
<div>
<div className="w-full mx-auto mt-6">
{/* Modal pour les fichiers */}
<Modal
isOpen={isModalOpen}
setIsOpen={(isOpen) => {
@ -362,7 +441,7 @@ export default function FilesGroupsManagement({
}}
title={isEditing ? 'Modification du document' : 'Ajouter un document'}
ContentComponent={() => (
<FileUpload
<FileUploadDocuSeal
handleCreateTemplateMaster={handleCreateTemplateMaster}
handleEditTemplateMaster={handleEditTemplateMaster}
fileToEdit={fileToEdit}
@ -371,13 +450,15 @@ export default function FilesGroupsManagement({
)}
modalClassName="w-4/5 h-4/5"
/>
{/* Modal pour les groupes */}
<Modal
isOpen={isGroupModalOpen}
setIsOpen={setIsGroupModalOpen}
title={
groupToEdit
? 'Modifier le groupe'
: 'Ajouter un groupe de templateMasters'
: 'Ajouter un groupe de schoolFileMasters'
}
ContentComponent={() => (
<RegistrationFileGroupForm
@ -386,61 +467,45 @@ export default function FilesGroupsManagement({
/>
)}
/>
<div className="mt-8 mb-4">
<div className="flex justify-between items-center mb-4">
<h2 className="text-xl font-bold">Groupes de fichiers</h2>
<button
onClick={() => setIsGroupModalOpen(true)}
className="flex items-center bg-blue-600 text-white p-2 rounded-full shadow hover:bg-blue-900 transition duration-200"
>
<FolderPlus className="w-5 h-5" />
</button>
</div>
<Table
data={groups}
columns={columnsGroups}
itemsPerPage={5}
currentPage={1}
totalPages={Math.ceil(groups.length / 5)}
{/* Section Groupes de fichiers */}
<div className="mt-8 w-3/5">
<SectionHeader
icon={FolderPlus}
title="Dossiers d'inscriptions"
description="Gérez les dossiers d'inscription pour organiser vos documents."
button={true}
buttonOpeningModal={true}
onClick={() => setIsGroupModalOpen(true)}
/>
<Table data={groups} columns={columnsGroups} />
</div>
{groups.length > 0 && (
<div className="mt-8">
<div className="flex justify-between items-center mb-4">
<h2 className="text-xl font-bold">Fichiers</h2>
<div className="flex items-center gap-4">
<select
className="border rounded p-2"
value={selectedGroup || ''}
onChange={(e) => setSelectedGroup(e.target.value)}
>
<option value="">Tous les groupes</option>
{groups.map((group) => (
<option key={group.id} value={group.id}>
{group.name}
</option>
))}
</select>
<button
onClick={() => {
setIsModalOpen(true);
setIsEditing(false);
}}
className="flex items-center bg-emerald-600 text-white p-2 rounded-full shadow hover:bg-emerald-900 transition duration-200"
>
<Plus className="w-5 h-5" />
</button>
</div>
</div>
<Table
data={filteredFiles}
columns={columnsFiles}
itemsPerPage={10}
currentPage={1}
totalPages={Math.ceil(filteredFiles.length / 10)}
/>
</div>
)}
{/* Section Fichiers */}
<div className="mt-12 mb-4 w-3/5">
<SectionHeader
icon={Signature}
title="Formulaires à remplir"
description="Gérez les formulaires nécessitant une signature électronique."
button={true}
buttonOpeningModal={true}
onClick={() => {
setIsModalOpen(true);
setIsEditing(false);
}}
/>
<Table data={filteredFiles} columns={columnsFiles} />
</div>
{/* Section Pièces à fournir */}
<ParentFilesSection
parentFiles={parentFiles}
setParentFileMasters={setParentFileMasters}
groups={groups}
handleCreate={handleCreate}
handleEdit={handleEdit}
handleDelete={handleDelete}
/>
</div>
);
}

View File

@ -1,343 +0,0 @@
import React, { useState, useEffect } from 'react';
import { Plus, Download, Edit, Trash2, FolderPlus, Signature } from 'lucide-react';
import Modal from '@/components/Modal';
import Table from '@/components/Table';
import FileUpload from '@/components/FileUpload';
import { formatDate } from '@/utils/Date';
import { BASE_URL } from '@/utils/Url';
import {
fetchRegisterFormFileTemplate,
createRegistrationFormFileTemplate,
editRegistrationFormFileTemplate,
deleteRegisterFormFileTemplate,
getRegisterFormFileTemplate
} from '@/app/actions/subscriptionAction';
import {
fetchRegistrationFileGroups,
createRegistrationFileGroup,
deleteRegistrationFileGroup,
editRegistrationFileGroup
} from '@/app/actions/registerFileGroupAction';
import RegistrationFileGroupForm from '@/components/RegistrationFileGroupForm';
import { useEstablishment } from '@/context/EstablishmentContext';
export default function FilesManagement({ csrfToken }) {
const [fichiers, setFichiers] = 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);
const [isGroupModalOpen, setIsGroupModalOpen] = useState(false);
const [groupToEdit, setGroupToEdit] = useState(null);
const { selectedEstablishmentId } = useEstablishment();
// Fonction pour transformer les données des fichiers avec les informations complètes du groupe
const transformFileData = (file, groups) => {
if (!file.group) return file;
const groupInfo = groups.find(g => g.id === file.group);
return {
...file,
group: groupInfo || { id: file.group, name: 'Groupe inconnu' }
};
};
useEffect(() => {
Promise.all([
fetchRegisterFormFileTemplate(),
fetchRegistrationFileGroups(selectedEstablishmentId)
]).then(([filesData, groupsData]) => {
setGroups(groupsData);
// Sélectionner automatiquement le premier groupe s'il existe
if (groupsData.length > 0) {
setSelectedGroup(groupsData[0].id.toString());
}
// Transformer chaque fichier pour inclure les informations complètes du groupe
const transformedFiles = filesData.map(file => transformFileData(file, groupsData));
setFichiers(transformedFiles);
}).catch(err => {
console.log(err.message);
});
}, []);
const handleFileDelete = (fileId) => {
deleteRegisterFormFileTemplate(fileId, csrfToken)
.then(response => {
if (response.ok) {
setFichiers(fichiers.filter(fichier => fichier.id !== fileId));
alert('Fichier supprimé avec succès.');
} else {
alert('Erreur lors de la suppression du fichier.');
}
})
.catch(error => {
console.error('Error deleting file:', error);
alert('Erreur lors de la suppression du fichier.');
});
};
const handleFileEdit = (file) => {
setIsEditing(true);
setFileToEdit(file);
setIsModalOpen(true);
};
const handleFileUpload = ({file, name, is_required, order, groupId}) => {
if (!name) {
alert('Veuillez entrer un nom de fichier.');
return;
}
const formData = new FormData();
if(file) {
formData.append('file', file);
}
formData.append('name', name);
formData.append('is_required', is_required);
formData.append('order', order);
// Modification ici : vérifier si groupId existe et n'est pas vide
if (groupId && groupId !== '') {
formData.append('group', groupId); // Notez que le nom du champ est 'group' et non 'group_id'
}
if (isEditing && fileToEdit) {
editRegistrationFormFileTemplate(fileToEdit.id, formData, csrfToken)
.then(data => {
// Transformer le fichier mis à jour avec les informations du groupe
const transformedFile = transformFileData(data, groups);
setFichiers(prevFichiers =>
prevFichiers.map(f => f.id === fileToEdit.id ? transformedFile : f)
);
setIsModalOpen(false);
setFileToEdit(null);
setIsEditing(false);
})
.catch(error => {
console.error('Error editing file:', error);
alert('Erreur lors de la modification du fichier');
});
} else {
createRegistrationFormFileTemplate(formData, csrfToken)
.then(data => {
// Transformer le nouveau fichier avec les informations du groupe
const transformedFile = transformFileData(data, groups);
setFichiers(prevFiles => [...prevFiles, transformedFile]);
setIsModalOpen(false);
})
.catch(error => {
console.error('Error uploading file:', error);
});
}
};
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 => {
console.error('Error handling group:', error);
alert('Erreur lors de l\'opération sur le groupe');
});
} else {
createRegistrationFileGroup(groupData, csrfToken)
.then(newGroup => {
setGroups([...groups, newGroup]);
setIsGroupModalOpen(false);
})
.catch(error => {
console.error('Error handling group:', error);
alert('Erreur lors de l\'opération sur le groupe');
});
}
};
const handleGroupEdit = (group) => {
setGroupToEdit(group);
setIsGroupModalOpen(true);
};
const handleGroupDelete = (groupId) => {
// Vérifier si des fichiers utilisent ce groupe
const filesInGroup = fichiers.filter(file => file.group && file.group.id === groupId);
if (filesInGroup.length > 0) {
alert('Impossible de supprimer ce groupe car il contient des fichiers. Veuillez d\'abord retirer tous les fichiers de ce groupe.');
return;
}
if (window.confirm('Êtes-vous sûr de vouloir supprimer ce groupe ?')) {
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));
alert('Groupe supprimé avec succès.');
})
.catch(error => {
console.error('Error deleting group:', error);
alert(error.message || 'Erreur lors de la suppression du groupe. Vérifiez qu\'aucune inscription n\'utilise ce groupe.');
});
}
};
// Ajouter cette fonction de filtrage
const filteredFiles = fichiers.filter(file => {
if (!selectedGroup) return true;
return file.group && file.group.id === parseInt(selectedGroup);
});
const columnsFiles = [
{ name: 'Nom du fichier', transform: (row) => row.name },
{ name: 'Groupe', transform: (row) => row.group ? row.group.name : 'Aucun' },
{ name: 'Date de création', transform: (row) => formatDate(new Date (row.date_added),"DD/MM/YYYY hh:mm:ss") },
{ name: 'Fichier Obligatoire', transform: (row) => row.is_required ? 'Oui' : 'Non' },
{ name: 'Ordre de fusion', transform: (row) => row.order },
{ name: 'Actions', transform: (row) => (
<div className="flex items-center justify-center gap-2">
{row.file && (
<a href={`${BASE_URL}${row.file}`} target='_blank' className="text-blue-500 hover:text-blue-700">
<Download size={16} />
</a>
)}
<button onClick={() => handleFileEdit(row)} className="text-blue-500 hover:text-blue-700">
<Edit size={16} />
</button>
<button onClick={() => handleFileDelete(row.id)} className="text-red-500 hover:text-red-700">
<Trash2 size={16} />
</button>
<button onClick={() => handleSignatureRequest(row)} className="text-green-500 hover:text-green-700">
<Signature size={16} />
</button>
</div>
)}
];
const columnsGroups = [
{ name: 'Nom du groupe', transform: (row) => row.name },
{ name: 'Description', transform: (row) => row.description },
{ name: 'Actions', transform: (row) => (
<div className="flex items-center justify-center gap-2">
<button onClick={() => handleGroupEdit(row)} className="text-blue-500 hover:text-blue-700">
<Edit size={16} />
</button>
<button onClick={() => handleGroupDelete(row.id)} className="text-red-500 hover:text-red-700">
<Trash2 size={16} />
</button>
</div>
)}
];
// Fonction pour gérer la demande de signature
const handleSignatureRequest = (file) => {
const formData = new FormData();
formData.append('file', file);
console.log('Demande de signature pour le fichier :', file);
fetch('http://localhost:8080:/DocuSeal/generateToken', {
method: 'POST',
headers: {
'Authorization': 'Bearer NFPZy6BBGvYs1BwTuXMQ3XAu5N1kLFiXWftGQhkiz2A',
},
body: formData,
})
.then((response) => {
if (!response.ok) {
throw new Error('Erreur lors du téléversement du document : ' + response.statusText);
}
return response.json();
})
.then((data) => {
const documentId = data.documentId;
console.log('Document téléversé avec succès, ID :', documentId);
onUpload(documentId);
});
.catch((error) => console.error(error));
};
return (
<div>
<Modal
isOpen={isModalOpen}
setIsOpen={setIsModalOpen}
title={isEditing ? 'Modifier un fichier' : 'Ajouter un fichier'}
ContentComponent={() => (
<FileUpload
onFileUpload={handleFileUpload}
fileToEdit={fileToEdit}
/>
)}
/>
<Modal
isOpen={isGroupModalOpen}
setIsOpen={setIsGroupModalOpen}
title={groupToEdit ? "Modifier le groupe" : "Ajouter un groupe de fichiers"}
ContentComponent={() => (
<RegistrationFileGroupForm
onSubmit={handleGroupSubmit}
initialData={groupToEdit}
/>
)}
/>
<div className="mt-8 mb-4">
<div className="flex justify-between items-center mb-4">
<h2 className="text-xl font-bold">Groupes de fichiers</h2>
<button
onClick={() => setIsGroupModalOpen(true)}
className="flex items-center bg-blue-600 text-white p-2 rounded-full shadow hover:bg-blue-900 transition duration-200"
>
<FolderPlus className="w-5 h-5" />
</button>
</div>
<Table
data={groups}
columns={columnsGroups}
itemsPerPage={5}
currentPage={1}
totalPages={Math.ceil(groups.length / 5)}
/>
</div>
{groups.length > 0 && (
<div className="mt-8">
<div className="flex justify-between items-center mb-4">
<h2 className="text-xl font-bold">Fichiers</h2>
<div className="flex items-center gap-4">
<select
className="border rounded p-2"
value={selectedGroup || ''}
onChange={(e) => setSelectedGroup(e.target.value)}
>
<option value="">Tous les groupes</option>
{groups.map(group => (
<option key={group.id} value={group.id}>{group.name}</option>
))}
</select>
<button
onClick={() => { setIsModalOpen(true); setIsEditing(false); }}
className="flex items-center bg-emerald-600 text-white p-2 rounded-full shadow hover:bg-emerald-900 transition duration-200"
>
<Plus className="w-5 h-5" />
</button>
</div>
</div>
<Table
data={filteredFiles}
columns={columnsFiles}
itemsPerPage={10}
currentPage={1}
totalPages={Math.ceil(filteredFiles.length / 10)}
/>
</div>
)}
</div>
);
}

View File

@ -0,0 +1,273 @@
import React, { useState } from 'react';
import { Plus, Edit3, Trash2, Check, X, FileText } from 'lucide-react';
import Table from '@/components/Table';
import InputText from '@/components/InputText';
import MultiSelect from '@/components/MultiSelect';
import Popup from '@/components/Popup';
import logger from '@/utils/logger';
import { createRegistrationParentFileTemplate } from '@/app/actions/registerFileGroupAction';
import { useCsrfToken } from '@/context/CsrfContext';
import SectionHeader from '@/components/SectionHeader';
export default function ParentFilesSection({ parentFiles, groups, handleCreate, handleEdit, handleDelete }) {
const [editingDocumentId, setEditingDocumentId] = useState(null);
const [formData, setFormData] = useState(null);
const [selectedGroups, setSelectedGroups] = useState([]); // Gestion des groupes sélectionnés
const [guardianDetails, setGuardianDetails] = useState([]);
const [popupVisible, setPopupVisible] = useState(false);
const [popupMessage, setPopupMessage] = useState("");
const [removePopupVisible, setRemovePopupVisible] = useState(false);
const [removePopupMessage, setRemovePopupMessage] = useState("");
const [removePopupOnConfirm, setRemovePopupOnConfirm] = useState(() => {});
const csrfToken = useCsrfToken();
const handleAddEmptyRequiredDocument = () => {
setEditingDocumentId('new');
setFormData({ name: '', description: '', groups: [] });
setSelectedGroups([]); // Réinitialiser les groupes sélectionnés
};
const handleEditDocument = (document) => {
setEditingDocumentId(document.id);
setFormData(document);
const initialSelectedGroups = document.groups.map((groupId) =>
groups.find((group) => group.id === groupId)
);
setSelectedGroups(initialSelectedGroups);
};
const handleSaveDocument = () => {
if (!formData.name) {
alert('Le nom de la pièce est requis.');
return;
}
const updatedFormData = {
...formData,
groups: selectedGroups.map((group) => group.id),
};
if (editingDocumentId === 'new') {
handleCreate(updatedFormData).then((createdDocument) => {
setEditingDocumentId(null);
setFormData(null);
setSelectedGroups([]);
guardianDetails.forEach((guardian, index) => {
// Création des templates
const data = {
master: createdDocument?.id,
registration_form: guardian.registration_form
};
console.log(guardian)
createRegistrationParentFileTemplate(data, csrfToken)
.then(response => {
logger.debug('Template enregistré avec succès:', response);
})
.catch(error => {
logger.error('Erreur lors de l\'enregistrement du template:', error);
});
});
});
} else {
handleEdit(editingDocumentId, updatedFormData).then(() => {
setEditingDocumentId(null);
setFormData(null);
setSelectedGroups([]);
});
}
};
const handleRemoveDocument = (id) => {
return handleDelete(id)
.then(() => {
setEditingDocumentId(null);
setFormData(null);
setSelectedGroups([]);
})
.catch((error) => {
logger.error(error);
});
};
const handleCancelEdit = () => {
setEditingDocumentId(null);
setFormData(null);
setSelectedGroups([]);
};
const handleGroupChange = (selected) => {
setSelectedGroups(selected);
console.log('selected : ', selected)
// Extraire les guardians associés aux register_forms des groupes sélectionnés
const details = selected.flatMap(group =>
group.registration_forms.flatMap(form =>
form.guardians.map(guardian => ({
email: guardian.associated_profile_email,
last_name: form.last_name, // Extraire depuis form
first_name: form.first_name, // Extraire depuis form
registration_form: form.student_id // Utiliser student_id comme ID du register_form
}))
)
);
console.log("Guardians associés : ", details);
setGuardianDetails(details); // Mettre à jour la variable d'état avec les détails des guardians
};
const renderRequiredDocumentCell = (document, column) => {
const isEditing = editingDocumentId === document.id || (editingDocumentId === 'new' && !document.id);
if (isEditing) {
switch (column) {
case 'Nom de la pièce':
return (
<InputText
name="name"
value={formData.name}
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
placeholder="Nom de la pièce"
className="w-full"
/>
);
case 'Description':
return (
<InputText
name="description"
value={formData.description}
onChange={(e) => setFormData({ ...formData, description: e.target.value })}
placeholder="Description"
className="w-full"
/>
);
case 'Dossiers d\'inscription':
return (
<MultiSelect
name="groups"
label="Sélection de groupes de fichiers"
options={groups}
selectedOptions={selectedGroups}
onChange={handleGroupChange}
errorMsg={null}
/>
);
case 'Actions':
return (
<div className="flex justify-center space-x-2">
<button
type="button"
onClick={handleSaveDocument}
className="text-green-500 hover:text-green-700"
>
<Check className="w-5 h-5" />
</button>
<button
type="button"
onClick={handleCancelEdit}
className="text-red-500 hover:text-red-700"
>
<X className="w-5 h-5" />
</button>
</div>
);
default:
return null;
}
} else {
switch (column) {
case 'Nom de la pièce':
return <span>{document.name}</span>;
case 'Description':
return <span>{document.description}</span>;
case 'Dossiers d\'inscription':
return (
<span>
{document.groups
.map((groupId) => groups.find((group) => group.id === groupId)?.name || 'Dossiers d\'inscription inconnu')
.join(', ')}
</span>
);
case 'Actions':
return (
<div className="flex justify-center space-x-2">
<button
type="button"
onClick={() => handleEditDocument(document)}
className="text-blue-500 hover:text-blue-700"
>
<Edit3 className="w-5 h-5" />
</button>
<button
type="button"
onClick={() => {
setRemovePopupVisible(true);
setRemovePopupMessage(
`Attentions ! \nVous êtes sur le point de supprimer le document "${document.name}".\nÊtes-vous sûr(e) de vouloir poursuivre l'opération ?`
);
setRemovePopupOnConfirm(() => () => {
handleRemoveDocument(document.id)
.then(() => {
setPopupMessage(`Le document "${document.name}" a été correctement supprimé.`);
setPopupVisible(true);
setRemovePopupVisible(false);
})
.catch((error) => {
logger.error('Erreur lors de la suppression du document:', error);
setPopupMessage(`Erreur lors de la suppression du document "${document.name}".`);
setPopupVisible(true);
setRemovePopupVisible(false);
});
});
}}
className="text-red-500 hover:text-red-700"
>
<Trash2 className="w-5 h-5" />
</button>
</div>
);
default:
return null;
}
}
};
const columnsRequiredDocuments = [
{ name: 'Nom de la pièce', transform: (row) => renderRequiredDocumentCell(row, 'Nom de la pièce') },
{ name: 'Description', transform: (row) => renderRequiredDocumentCell(row, 'Description') },
{ name: 'Dossiers d\'inscription', transform: (row) => renderRequiredDocumentCell(row, 'Dossiers d\'inscription') },
{ name: 'Actions', transform: (row) => renderRequiredDocumentCell(row, 'Actions') },
];
return (
<div className="mt-12 w-4/5">
<SectionHeader
icon={FileText}
title="Pièces à fournir"
description="Configurez la liste des documents que les parents doivent fournir."
button={true}
onClick={handleAddEmptyRequiredDocument}
/>
<Table
data={editingDocumentId === 'new' ? [formData, ...parentFiles] : parentFiles}
columns={columnsRequiredDocuments}
/>
<Popup
visible={popupVisible}
message={popupMessage}
onConfirm={() => setPopupVisible(false)}
onCancel={() => setPopupVisible(false)}
uniqueConfirmButton={true}
/>
<Popup
visible={removePopupVisible}
message={removePopupMessage}
onConfirm={removePopupOnConfirm}
onCancel={() => setRemovePopupVisible(false)}
/>
</div>
);
}

View File

@ -1,20 +1,12 @@
import React, { useState } from 'react';
import {
Plus,
Trash2,
Edit3,
Check,
X,
Percent,
EuroIcon,
Tag,
} from 'lucide-react';
import { Trash2, Edit3, Check, X, Percent, EuroIcon, Tag } from 'lucide-react';
import Table from '@/components/Table';
import Popup from '@/components/Popup';
import CheckBox from '@/components/CheckBox';
import InputText from '@/components/InputText';
import logger from '@/utils/logger';
import { ESTABLISHMENT_ID } from '@/utils/Url';
import SectionHeader from '@/components/SectionHeader';
const DiscountsSection = ({
discounts,
@ -347,22 +339,15 @@ const DiscountsSection = ({
];
return (
<div className="space-y-4">
{!subscriptionMode && (
<div className="flex justify-between items-center">
<div className="flex items-center mb-4">
<Tag className="w-6 h-6 text-emerald-500 mr-2" />
<h2 className="text-xl font-semibold">Liste des réductions</h2>
</div>
<button
type="button"
onClick={handleAddDiscount}
className="text-emerald-500 hover:text-emerald-700"
>
<Plus className="w-5 h-5" />
</button>
</div>
)}
<div className="space-y-4 mt-8">
<SectionHeader
icon={Tag}
discountStyle={true}
title={`${type == 0 ? "Liste des réductions sur les frais d'inscription" : 'Liste des réductions sur les frais de scolarité'}`}
description={`${subscriptionMode ? 'Sélectionnez' : 'Gérez'} ${type == 0 ? " vos réductions sur les frais d'inscription" : ' vos réductions sur les frais de scolarité'}`}
button={!subscriptionMode}
onClick={handleAddDiscount}
/>
<Table
data={newDiscount ? [newDiscount, ...discounts] : discounts}
columns={columns}

View File

@ -1,4 +1,4 @@
import React, { useState } from 'react';
import React from 'react';
import FeesSection from '@/components/Structure/Tarification/FeesSection';
import DiscountsSection from '@/components/Structure/Tarification/DiscountsSection';
import PaymentPlanSelector from '@/components/PaymentPlanSelector';
@ -50,185 +50,181 @@ const FeesManagement = ({
};
return (
<div className="w-full mx-auto p-2 mt-6 space-y-6">
<div className="bg-white p-2 rounded-lg shadow-md">
<h2 className="text-2xl font-semibold mb-4">
Frais d&apos;inscription
</h2>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div className="col-span-1 p-6 rounded-lg shadow-inner mt-4">
<FeesSection
fees={registrationFees}
setFees={setRegistrationFees}
discounts={registrationDiscounts}
handleCreate={(newData) =>
handleCreate(
`${BE_SCHOOL_FEES_URL}`,
newData,
setRegistrationFees
)
}
handleEdit={(id, updatedData) =>
handleEdit(
`${BE_SCHOOL_FEES_URL}`,
id,
updatedData,
setRegistrationFees
)
}
handleDelete={(id) =>
handleDelete(`${BE_SCHOOL_FEES_URL}`, id, setRegistrationFees)
}
type={0}
/>
</div>
<div className="col-span-1 p-6 rounded-lg shadow-inner mt-4">
<DiscountsSection
discounts={registrationDiscounts}
setDiscounts={setRegistrationDiscounts}
handleCreate={(newData) =>
handleCreate(
`${BE_SCHOOL_DISCOUNTS_URL}`,
newData,
setRegistrationDiscounts
)
}
handleEdit={(id, updatedData) =>
handleEdit(
`${BE_SCHOOL_DISCOUNTS_URL}`,
id,
updatedData,
setRegistrationDiscounts
)
}
handleDelete={(id) =>
handleDelete(
`${BE_SCHOOL_DISCOUNTS_URL}`,
id,
setRegistrationDiscounts
)
}
onDiscountDelete={(id) => handleDiscountDelete(id, 0)}
type={0}
/>
</div>
<div className="col-span-1 p-6 rounded-lg shadow-inner mt-4">
<PaymentPlanSelector
paymentPlans={registrationPaymentPlans}
setPaymentPlans={setRegistrationPaymentPlans}
handleEdit={(id, updatedData) =>
handleEdit(
`${BE_SCHOOL_PAYMENT_PLANS_URL}`,
id,
updatedData,
setRegistrationPaymentPlans
)
}
type={0}
/>
</div>
<div className="col-span-1 p-6 rounded-lg shadow-inner mt-4">
<PaymentModeSelector
paymentModes={registrationPaymentModes}
setPaymentModes={setRegistrationPaymentModes}
handleEdit={(id, updatedData) =>
handleEdit(
`${BE_SCHOOL_PAYMENT_MODES_URL}`,
id,
updatedData,
setRegistrationPaymentModes
)
}
type={0}
/>
</div>
<div className="w-full mx-auto mt-6">
<div className="w-4/5 mx-auto flex items-center mt-8">
<hr className="flex-grow border-t-2 border-gray-300" />
<span className="mx-4 text-gray-600 font-semibold">
Frais d'inscription
</span>
<hr className="flex-grow border-t-2 border-gray-300" />
</div>
<div className="mt-8 w-4/5">
<FeesSection
fees={registrationFees}
setFees={setRegistrationFees}
discounts={registrationDiscounts}
handleCreate={(newData) =>
handleCreate(`${BE_SCHOOL_FEES_URL}`, newData, setRegistrationFees)
}
handleEdit={(id, updatedData) =>
handleEdit(
`${BE_SCHOOL_FEES_URL}`,
id,
updatedData,
setRegistrationFees
)
}
handleDelete={(id) =>
handleDelete(`${BE_SCHOOL_FEES_URL}`, id, setRegistrationFees)
}
type={0}
/>
</div>
<div className="mt-12 w-4/5">
<DiscountsSection
discounts={registrationDiscounts}
setDiscounts={setRegistrationDiscounts}
handleCreate={(newData) =>
handleCreate(
`${BE_SCHOOL_DISCOUNTS_URL}`,
newData,
setRegistrationDiscounts
)
}
handleEdit={(id, updatedData) =>
handleEdit(
`${BE_SCHOOL_DISCOUNTS_URL}`,
id,
updatedData,
setRegistrationDiscounts
)
}
handleDelete={(id) =>
handleDelete(
`${BE_SCHOOL_DISCOUNTS_URL}`,
id,
setRegistrationDiscounts
)
}
onDiscountDelete={(id) => handleDiscountDelete(id, 0)}
type={0}
/>
</div>
<div className="mt-12 grid grid-cols-1 md:grid-cols-2 gap-6">
<div className="col-span-1 mt-4">
<PaymentPlanSelector
paymentPlans={registrationPaymentPlans}
setPaymentPlans={setRegistrationPaymentPlans}
handleEdit={(id, updatedData) =>
handleEdit(
`${BE_SCHOOL_PAYMENT_PLANS_URL}`,
id,
updatedData,
setRegistrationPaymentPlans
)
}
type={0}
/>
</div>
<div className="col-span-1 mt-4">
<PaymentModeSelector
paymentModes={registrationPaymentModes}
setPaymentModes={setRegistrationPaymentModes}
handleEdit={(id, updatedData) =>
handleEdit(
`${BE_SCHOOL_PAYMENT_MODES_URL}`,
id,
updatedData,
setRegistrationPaymentModes
)
}
type={0}
/>
</div>
</div>
<div className="bg-white p-2 rounded-lg shadow-md">
<h2 className="text-2xl font-semibold mb-4">Frais de scolarité</h2>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div className="col-span-1 p-6 rounded-lg shadow-inner mt-4">
<FeesSection
fees={tuitionFees}
setFees={setTuitionFees}
discounts={tuitionDiscounts}
handleCreate={(newData) =>
handleCreate(`${BE_SCHOOL_FEES_URL}`, newData, setTuitionFees)
}
handleEdit={(id, updatedData) =>
handleEdit(
`${BE_SCHOOL_FEES_URL}`,
id,
updatedData,
setTuitionFees
)
}
handleDelete={(id) =>
handleDelete(`${BE_SCHOOL_FEES_URL}`, id, setTuitionFees)
}
type={1}
/>
</div>
<div className="col-span-1 p-6 rounded-lg shadow-inner mt-4">
<DiscountsSection
discounts={tuitionDiscounts}
setDiscounts={setTuitionDiscounts}
handleCreate={(newData) =>
handleCreate(
`${BE_SCHOOL_DISCOUNTS_URL}`,
newData,
setTuitionDiscounts
)
}
handleEdit={(id, updatedData) =>
handleEdit(
`${BE_SCHOOL_DISCOUNTS_URL}`,
id,
updatedData,
setTuitionDiscounts
)
}
handleDelete={(id) =>
handleDelete(
`${BE_SCHOOL_DISCOUNTS_URL}`,
id,
setTuitionDiscounts
)
}
onDiscountDelete={(id) => handleDiscountDelete(id, 1)}
type={1}
/>
</div>
<div className="col-span-1 p-6 rounded-lg shadow-inner mt-4">
<PaymentPlanSelector
paymentPlans={tuitionPaymentPlans}
setPaymentPlans={setTuitionPaymentPlans}
handleEdit={(id, updatedData) =>
handleEdit(
`${BE_SCHOOL_PAYMENT_PLANS_URL}`,
id,
updatedData,
setRegistrationPaymentPlans
)
}
type={1}
/>
</div>
<div className="col-span-1 p-6 rounded-lg shadow-inner mt-4">
<PaymentModeSelector
paymentModes={tuitionPaymentModes}
setPaymentModes={setTuitionPaymentModes}
handleEdit={(id, updatedData) =>
handleEdit(
`${BE_SCHOOL_PAYMENT_MODES_URL}`,
id,
updatedData,
setTuitionPaymentModes
)
}
type={1}
/>
</div>
<div className="w-4/5 mx-auto flex items-center mt-16">
<hr className="flex-grow border-t-2 border-gray-300" />
<span className="mx-4 text-gray-600 font-semibold">
Frais de scolarité
</span>
<hr className="flex-grow border-t-2 border-gray-300" />
</div>
<div className="mt-8 w-4/5">
<FeesSection
fees={tuitionFees}
setFees={setTuitionFees}
discounts={tuitionDiscounts}
handleCreate={(newData) =>
handleCreate(`${BE_SCHOOL_FEES_URL}`, newData, setTuitionFees)
}
handleEdit={(id, updatedData) =>
handleEdit(`${BE_SCHOOL_FEES_URL}`, id, updatedData, setTuitionFees)
}
handleDelete={(id) =>
handleDelete(`${BE_SCHOOL_FEES_URL}`, id, setTuitionFees)
}
type={1}
/>
</div>
<div className="mt-12 w-4/5">
<DiscountsSection
discounts={tuitionDiscounts}
setDiscounts={setTuitionDiscounts}
handleCreate={(newData) =>
handleCreate(
`${BE_SCHOOL_DISCOUNTS_URL}`,
newData,
setTuitionDiscounts
)
}
handleEdit={(id, updatedData) =>
handleEdit(
`${BE_SCHOOL_DISCOUNTS_URL}`,
id,
updatedData,
setTuitionDiscounts
)
}
handleDelete={(id) =>
handleDelete(`${BE_SCHOOL_DISCOUNTS_URL}`, id, setTuitionDiscounts)
}
onDiscountDelete={(id) => handleDiscountDelete(id, 1)}
type={1}
/>
</div>
<div className="mt-12 grid grid-cols-1 md:grid-cols-2 gap-6">
<div className="col-span-1 mt-4">
<PaymentPlanSelector
paymentPlans={tuitionPaymentPlans}
setPaymentPlans={setTuitionPaymentPlans}
handleEdit={(id, updatedData) =>
handleEdit(
`${BE_SCHOOL_PAYMENT_PLANS_URL}`,
id,
updatedData,
setRegistrationPaymentPlans
)
}
type={1}
/>
</div>
<div className="col-span-1 mt-4">
<PaymentModeSelector
paymentModes={tuitionPaymentModes}
setPaymentModes={setTuitionPaymentModes}
handleEdit={(id, updatedData) =>
handleEdit(
`${BE_SCHOOL_PAYMENT_MODES_URL}`,
id,
updatedData,
setTuitionPaymentModes
)
}
type={1}
/>
</div>
</div>
</div>

View File

@ -1,20 +1,11 @@
import React, { useState } from 'react';
import {
Plus,
Trash2,
Edit3,
Check,
X,
EyeOff,
Eye,
CreditCard,
BookOpen,
} from 'lucide-react';
import { Trash2, Edit3, Check, X, EyeOff, Eye, CreditCard } from 'lucide-react';
import Table from '@/components/Table';
import Popup from '@/components/Popup';
import CheckBox from '@/components/CheckBox';
import InputText from '@/components/InputText';
import logger from '@/utils/logger';
import SectionHeader from '@/components/SectionHeader';
import { ESTABLISHMENT_ID } from '@/utils/Url';
@ -325,21 +316,13 @@ const FeesSection = ({
return (
<div className="space-y-4">
{!subscriptionMode && (
<div className="flex justify-between items-center">
<div className="flex items-center mb-4">
<CreditCard className="w-6 h-6 text-emerald-500 mr-2" />
<h2 className="text-xl font-semibold">Liste des frais</h2>
</div>
<button
type="button"
onClick={handleAddFee}
className="text-emerald-500 hover:text-emerald-700"
>
<Plus className="w-5 h-5" />
</button>
</div>
)}
<SectionHeader
icon={CreditCard}
title={`${type == 0 ? "Liste des frais d'inscription" : 'Liste des frais de scolarité'}`}
description={`${subscriptionMode ? 'Sélectionnez' : 'Gérez'} ${type == 0 ? " vos frais d'inscription" : ' vos frais de scolarité'}`}
button={!subscriptionMode}
onClick={handleAddFee}
/>
<Table
data={newFee ? [newFee, ...fees] : fees}
columns={columns}

View File

@ -1,6 +1,6 @@
import React from 'react';
import PropTypes from 'prop-types';
import Pagination from '@/components/Pagination'; // Correction du chemin d'importatio,
import Pagination from '@/components/Pagination'; // Correction du chemin d'importation
const Table = ({
data,
@ -39,11 +39,20 @@ const Table = ({
<tr
key={rowIndex}
className={`
${isSelectable ? 'cursor-pointer' : ''}
${selectedRows?.includes(row.id) ? 'bg-emerald-300 text-white' : rowIndex % 2 === 0 ? `${defaultTheme}` : ''}
${isSelectable ? 'hover:bg-emerald-200' : ''}
${isSelectable ? 'cursor-pointer' : ''}
${selectedRows?.includes(row.id) ? 'bg-emerald-300 text-white' : rowIndex % 2 === 0 ? `${defaultTheme}` : ''}
${isSelectable ? 'hover:bg-emerald-200' : ''}
`}
onClick={() => isSelectable && onRowClick && onRowClick(row)}
onClick={() => {
if (isSelectable && onRowClick) {
// Si la ligne est déjà sélectionnée, transmettre une indication explicite de désélection
if (selectedRows?.includes(row.id)) {
onRowClick({ deselected: true, row }); // Désélectionner
} else {
onRowClick(row); // Sélectionner
}
}
}}
>
{columns.map((column, colIndex) => (
<td
@ -83,6 +92,10 @@ Table.propTypes = {
currentPage: PropTypes.number.isRequired,
totalPages: PropTypes.number.isRequired,
onPageChange: PropTypes.func.isRequired,
onRowClick: PropTypes.func,
selectedRows: PropTypes.arrayOf(PropTypes.any),
isSelectable: PropTypes.bool,
defaultTheme: PropTypes.string,
};
export default Table;