mirror of
https://git.v0id.ovh/n3wt-innov/n3wt-school.git
synced 2026-01-29 07:53:23 +00:00
Merge remote-tracking branch 'origin/WIP_Inscriptions' into develop
This commit is contained in:
@ -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>
|
||||
);
|
||||
}
|
||||
60
Front-End/src/components/FileUpload.js
Normal file
60
Front-End/src/components/FileUpload.js
Normal 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>
|
||||
);
|
||||
}
|
||||
@ -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,
|
||||
|
||||
@ -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>
|
||||
);
|
||||
|
||||
@ -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"
|
||||
|
||||
@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@ -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}
|
||||
|
||||
44
Front-End/src/components/SectionHeader.js
Normal file
44
Front-End/src/components/SectionHeader.js
Normal 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;
|
||||
@ -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}
|
||||
|
||||
@ -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}
|
||||
|
||||
@ -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}
|
||||
|
||||
@ -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}
|
||||
|
||||
@ -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}
|
||||
|
||||
@ -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();
|
||||
@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@ -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>
|
||||
);
|
||||
}
|
||||
273
Front-End/src/components/Structure/Files/ParentFilesSection.js
Normal file
273
Front-End/src/components/Structure/Files/ParentFilesSection.js
Normal 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>
|
||||
);
|
||||
}
|
||||
@ -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}
|
||||
|
||||
@ -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'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>
|
||||
|
||||
@ -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}
|
||||
|
||||
@ -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;
|
||||
|
||||
Reference in New Issue
Block a user