feat: Upload du SEPA par les parents / Création d'un composant header

pour les titres de tableau
This commit is contained in:
N3WT DE COMPET
2025-04-20 19:19:27 +02:00
parent 59aee80c2e
commit 8417d3eb14
28 changed files with 893 additions and 695 deletions

View File

@ -4,11 +4,22 @@ from django.template.loader import get_template
from xhtml2pdf import pisa
class PDFResult:
def __init__(self, content):
self.content = content
def render_to_pdf(template_src, context_dict={}):
"""
Génère un PDF à partir d'un template HTML et retourne le contenu en mémoire.
"""
template = get_template(template_src)
html = template.render(context_dict)
result = BytesIO()
pdf = pisa.pisaDocument(BytesIO(html.encode("UTF-8")), result)
if pdf.err:
return HttpResponse("Invalid PDF", status_code=400, content_type='text/plain')
return HttpResponse(result.getvalue(), content_type='application/pdf')
# Lever une exception ou retourner None en cas d'erreur
raise ValueError("Erreur lors de la génération du PDF.")
# Retourner le contenu du PDF en mémoire
return PDFResult(result.getvalue())

View File

@ -9,6 +9,8 @@ from Establishment.models import Establishment
from datetime import datetime
import os
class Language(models.Model):
"""
Représente une langue parlée par lélève.
@ -231,8 +233,11 @@ class RegistrationForm(models.Model):
# Appeler la méthode save originale
super().save(*args, **kwargs)
def registration_file_upload_to(instance, filename):
return f"registration_files/dossier_rf_{instance.registration_form.pk}/{filename}"
def registration_school_file_upload_to(instance, filename):
return f"registration_files/dossier_rf_{instance.registration_form.pk}/school/{filename}"
def registration_parent_file_upload_to(instance, filename):
return f"registration_files/dossier_rf_{instance.registration_form.pk}/parent/{filename}"
#############################################################
####################### MASTER FILES ########################
@ -265,7 +270,7 @@ class RegistrationSchoolFileTemplate(models.Model):
slug = models.CharField(max_length=255, default="")
name = models.CharField(max_length=255, default="")
registration_form = models.ForeignKey(RegistrationForm, on_delete=models.CASCADE, related_name='school_file_templates', blank=True)
file = models.FileField(null=True,blank=True, upload_to=registration_file_upload_to)
file = models.FileField(null=True,blank=True, upload_to=registration_school_file_upload_to)
def __str__(self):
return self.name
@ -285,17 +290,31 @@ class RegistrationSchoolFileTemplate(models.Model):
class RegistrationParentFileTemplate(models.Model):
master = models.ForeignKey(RegistrationParentFileMaster, on_delete=models.CASCADE, related_name='parent_file_templates', blank=True)
registration_form = models.ForeignKey(RegistrationForm, on_delete=models.CASCADE, related_name='parent_file_templates', blank=True)
file = models.FileField(null=True,blank=True, upload_to=registration_file_upload_to)
file = models.FileField(null=True,blank=True, upload_to=registration_parent_file_upload_to)
def __str__(self):
return self.name
def save(self, *args, **kwargs):
if self.pk: # Si l'objet existe déjà dans la base de données
try:
old_instance = RegistrationParentFileTemplate.objects.get(pk=self.pk)
if old_instance.file and (not self.file or self.file.name == ''):
if os.path.exists(old_instance.file.path):
old_instance.file.delete(save=False)
self.file = None
else:
print(f"Le fichier {old_instance.file.path} n'existe pas.")
except RegistrationParentFileTemplate.DoesNotExist:
print("Ancienne instance introuvable.")
super().save(*args, **kwargs)
@staticmethod
def get_files_from_rf(register_form_id):
"""
Récupère tous les fichiers liés à un dossier dinscription donné.
"""
registration_files = RegistrationSchoolFileTemplate.objects.filter(registration_form=register_form_id)
registration_files = RegistrationParentFileTemplate.objects.filter(registration_form=register_form_id)
filenames = []
for reg_file in registration_files:
filenames.append(reg_file.file.path)

View File

@ -38,14 +38,14 @@ class RegistrationSchoolFileTemplateSerializer(serializers.ModelSerializer):
class RegistrationParentFileTemplateSerializer(serializers.ModelSerializer):
id = serializers.IntegerField(required=False)
file = serializers.SerializerMethodField()
file_url = serializers.SerializerMethodField()
master_name = serializers.CharField(source='master.name', read_only=True)
master_description = serializers.CharField(source='master.description', read_only=True)
class Meta:
model = RegistrationParentFileTemplate
fields = '__all__'
def get_file(self, obj):
def get_file_url(self, obj):
# Retourne l'URL complète du fichier si disponible
return obj.file.url if obj.file else None

View File

@ -19,6 +19,9 @@ from rest_framework.parsers import JSONParser
from PyPDF2 import PdfMerger
import shutil
import logging
logger = logging.getLogger(__name__)
def recupereListeFichesInscription():
"""
@ -121,7 +124,6 @@ def rfToPDF(registerForm, filename):
Génère le PDF d'un dossier d'inscription et l'associe au RegistrationForm.
"""
filename = filename.replace(" ", "_")
data = {
'pdf_title': f"Dossier d'inscription de {registerForm.student.first_name}",
'signatureDate': convertToStr(_now(), '%d-%m-%Y'),
@ -131,20 +133,37 @@ def rfToPDF(registerForm, filename):
# Générer le PDF
pdf = renderers.render_to_pdf('pdfs/dossier_inscription.html', data)
if not pdf:
raise ValueError("Erreur lors de la génération du PDF.")
# Vérifier si un fichier avec le même nom existe déjà et le supprimer
if registerForm.registration_file and os.path.exists(registerForm.registration_file.path):
os.remove(registerForm.registration_file.path)
if registerForm.registration_file and registerForm.registration_file.name:
# Vérifiez si le chemin est déjà absolu ou relatif
if os.path.isabs(registerForm.registration_file.name):
existing_file_path = registerForm.registration_file.name
else:
existing_file_path = os.path.join(settings.MEDIA_ROOT, registerForm.registration_file.name.lstrip('/'))
# Vérifier si le fichier existe et le supprimer
if os.path.exists(existing_file_path):
print(f'exist ! REMOVE')
os.remove(existing_file_path)
registerForm.registration_file.delete(save=False)
else:
print(f'File does not exist: {existing_file_path}')
# Enregistrer directement le fichier dans le champ registration_file
try:
registerForm.registration_file.save(
os.path.basename(filename),
File(BytesIO(pdf.content)), # Utilisation de BytesIO pour éviter l'écriture sur le disque
os.path.basename(filename), # Utiliser uniquement le nom de fichier
File(BytesIO(pdf.content)),
save=True
)
except Exception as e:
logger.error(f"Erreur lors de la sauvegarde du fichier PDF : {e}")
raise
return registerForm.registration_file.path
return registerForm.registration_file
def delete_registration_files(registerForm):
"""

View File

@ -254,29 +254,36 @@ class RegisterFormWithIdView(APIView):
if _status == RegistrationForm.RegistrationFormStatus.RF_UNDER_REVIEW:
try:
# Génération de la fiche d'inscription au format PDF
base_dir = f"data/registration_files/dossier_rf_{registerForm.pk}"
base_dir = os.path.join(settings.MEDIA_ROOT, f"registration_files/dossier_rf_{registerForm.pk}")
os.makedirs(base_dir, exist_ok=True)
# Fichier PDF initial
initial_pdf = f"{base_dir}/rf_{registerForm.student.last_name}_{registerForm.student.first_name}.pdf"
initial_pdf = f"{base_dir}/Inscription_{registerForm.student.last_name}_{registerForm.student.first_name}.pdf"
registerForm.registration_file = util.rfToPDF(registerForm, initial_pdf)
registerForm.save()
# Récupération des fichiers d'inscription
fileNames = RegistrationSchoolFileTemplate.get_files_from_rf(registerForm.pk)
if registerForm.registration_file:
fileNames.insert(0, registerForm.registration_file.path)
# fileNames = RegistrationSchoolFileTemplate.get_files_from_rf(registerForm.pk)
# if registerForm.registration_file:
# fileNames.insert(0, registerForm.registration_file.path)
# Création du fichier PDF Fusionné
merged_pdf_content = util.merge_files_pdf(fileNames)
# # Création du fichier PDF Fusionné
# merged_pdf_content = util.merge_files_pdf(fileNames)
# # Mise à jour du champ registration_file avec le fichier fusionné
# registerForm.registration_file.save(
# f"dossier_complet.pdf",
# File(merged_pdf_content),
# save=True
# )
# Mise à jour du champ registration_file avec le fichier fusionné
registerForm.registration_file.save(
f"dossier_complet_{registerForm.pk}.pdf",
File(merged_pdf_content),
save=True
)
# Mise à jour de l'automate
# Vérification de la présence du fichier SEPA
if registerForm.sepa_file:
# Mise à jour de l'automate pour SEPA
updateStateMachine(registerForm, 'EVENT_SIGNATURE_SEPA')
else:
# Mise à jour de l'automate pour une signature classique
updateStateMachine(registerForm, 'EVENT_SIGNATURE')
except Exception as e:
return JsonResponse({'error': str(e)}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)

View File

@ -285,7 +285,8 @@ class RegistrationParentFileTemplateSimpleView(APIView):
template = bdd.getObject(_objectName=RegistrationParentFileTemplate, _columnName='id', _value=id)
if template is None:
return JsonResponse({'erreur': 'Le template d\'inscription n\'a pas été trouvé'}, safe=False, status=status.HTTP_404_NOT_FOUND)
serializer = RegistrationParentFileTemplateSerializer(template, data=request.data)
serializer = RegistrationParentFileTemplateSerializer(template, data=request.data, partial=True)
if serializer.is_valid():
serializer.save()
return Response({'message': 'Template mis à jour avec succès', 'data': serializer.data}, status=status.HTTP_200_OK)

View File

@ -96,6 +96,13 @@ class ChildrenListView(APIView):
students = bdd.getObjects(_objectName=RegistrationForm, _columnName='student__guardians__profile_role__profile__id', _value=id)
if students:
students = students.filter(establishment=establishment_id).distinct()
students = students.filter(
establishment=establishment_id,
status__in=[
RegistrationForm.RegistrationFormStatus.RF_SENT,
RegistrationForm.RegistrationFormStatus.RF_UNDER_REVIEW,
RegistrationForm.RegistrationFormStatus.RF_SEPA_SENT
]
).distinct()
students_serializer = RegistrationFormByParentSerializer(students, many=True)
return JsonResponse(students_serializer.data, safe=False)

View File

@ -22,7 +22,7 @@ import {
fetchTuitionPaymentPlans,
fetchRegistrationPaymentModes,
fetchTuitionPaymentModes } from '@/app/actions/schoolAction';
import { fetchProfileRoles, fetchProfiles } from '@/app/actions/authAction';
import { fetchProfiles } from '@/app/actions/authAction';
import SidebarTabs from '@/components/SidebarTabs';
import FilesGroupsManagement from '@/components/Structure/Files/FilesGroupsManagement';
import { fetchRegistrationSchoolFileMasters } from "@/app/actions/registerFileGroupAction";
@ -258,7 +258,7 @@ export default function Page() {
const tabs = [
{
id: 'Configuration',
label: "Configuration de l'école",
label: "Classes",
content: (
<StructureManagement
specialities={specialities}
@ -276,7 +276,7 @@ export default function Page() {
},
{
id: 'Schedule',
label: "Gestion de l'emploi du temps",
label: "Emploi du temps",
content: (
<ClassesProvider>
<ScheduleManagement
@ -288,7 +288,7 @@ export default function Page() {
},
{
id: 'Fees',
label: 'Tarifications',
label: 'Tarifs',
content: (
<FeesManagement
registrationDiscounts={registrationDiscounts}
@ -315,13 +315,13 @@ export default function Page() {
},
{
id: 'Files',
label: 'Documents d\'inscription',
label: 'Documents',
content: <FilesGroupsManagement csrfToken={csrfToken} selectedEstablishmentId={selectedEstablishmentId} />
}
];
return (
<div className='p-8'>
<div className='p-4'>
<DjangoCSRFToken csrfToken={csrfToken} />
<div className="w-full p-4">

View File

@ -9,7 +9,7 @@ import Popup from '@/components/Popup';
import Loader from '@/components/Loader';
import AlertWithModal from '@/components/AlertWithModal';
import DropdownMenu from "@/components/DropdownMenu";
import { MoreVertical, Send, Edit, Archive, FileText, CircleCheck, Plus, XCircle } from 'lucide-react';
import { MoreVertical, Send, Edit, Archive, FileText, CheckCircle, Plus, XCircle } from 'lucide-react';
import Modal from '@/components/Modal';
import InscriptionForm from '@/components/Inscription/InscriptionForm'
import AffectationClasseForm from '@/components/AffectationClasseForm'
@ -606,35 +606,62 @@ useEffect(()=>{
const actions = {
1: [
{
icon: <Edit className="w-5 h-5 text-blue-500 hover:text-blue-700" />,
icon: (
<span title="Editer le dossier">
<Edit className="w-5 h-5 text-blue-500 hover:text-blue-700" />
</span>
),
onClick: () => window.location.href = `${FE_ADMIN_SUBSCRIPTIONS_EDIT_URL}?studentId=${row.student.id}&id=1`,
},
{
icon: <Send className="w-5 h-5 text-green-500 hover:text-green-700" />,
icon: (
<span title="Envoyer le dossier">
<Send className="w-5 h-5 text-green-500 hover:text-green-700" />
</span>
),
onClick: () => sendConfirmRegisterForm(row.student.id, row.student.last_name, row.student.first_name),
},
],
2: [
{
icon: <Edit className="w-5 h-5 text-blue-500 hover:text-blue-700" />,
icon: (
<span title="Editer le dossier">
<Edit className="w-5 h-5 text-blue-500 hover:text-blue-700" />
</span>
),
onClick: () => window.location.href = `${FE_ADMIN_SUBSCRIPTIONS_EDIT_URL}?studentId=${row.student.id}&id=1`,
},
],
3: [
{
icon: <CircleCheck className="w-5 h-5 text-green-500 hover:text-green-700" />,
onClick: () => window.location.href = `${FE_ADMIN_SUBSCRIPTIONS_VALIDATE_URL}?studentId=${row.student.id}&firstName=${row.student.first_name}&lastName=${row.student.last_name}&paymentMode=${row.registration_payment}&file=${row.registration_file}`,
icon: (
<span title="Valider le dossier">
<CheckCircle className="w-5 h-5 text-green-500 hover:text-green-700" />
</span>
),
onClick: () => {
const paymentSepa = row.registration_payment === 1 || row.tuition_payment === 1 ? 1 : 0;
window.location.href = `${FE_ADMIN_SUBSCRIPTIONS_VALIDATE_URL}?studentId=${row.student.id}&firstName=${row.student.first_name}&lastName=${row.student.last_name}&paymentSepa=${paymentSepa}&file=${row.registration_file}`
}
},
],
5: [
{
icon: <CircleCheck className="w-5 h-5 text-green-500 hover:text-green-700" />,
icon: (
<span title="Valider le dossier">
<CheckCircle className="w-5 h-5 text-green-500 hover:text-green-700" />
</span>
),
onClick: () => openModalAssociationEleve(row.student),
},
],
default: [
{
icon: <Archive className="w-5 h-5 text-gray-500 hover:text-gray-700" />,
icon: (
<span title="Archiver le dossier">
<Archive className="w-5 h-5 text-gray-500 hover:text-gray-700" />
</span>
),
onClick: () => archiveFicheInscription(row.student.id, row.student.last_name, row.student.first_name),
},
],
@ -674,15 +701,35 @@ const columns = [
</div>
)
},
{ name: t('files'), transform: (row) =>
(row.registration_file != null) &&(
{ name: t('files'), transform: (row) => (
<ul>
{row.registration_file && (
<li className="flex justify-center items-center gap-2">
<FileText size={16} />
<a href={ `${BASE_URL}${row.registration_file}`} target='_blank'>{row.registration_file?.split('/').pop()}</a>
<a
href={`${BASE_URL}${row.registration_file}`}
target="_blank"
rel="noopener noreferrer"
>
{row.registration_file?.split('/').pop()}
</a>
</li>
)}
{row.sepa_file && (
<li className="flex justify-center items-center gap-2">
<FileText size={16} />
<a
href={`${BASE_URL}${row.sepa_file}`}
target="_blank"
rel="noopener noreferrer"
>
{row.sepa_file?.split('/').pop()}
</a>
</li>
)}
</ul>
) },
)
},
{ name: 'Actions',
transform: (row) => (
<div className="flex justify-center space-x-2">
@ -728,7 +775,7 @@ const columnsSubscribed = [
items={[
{ label: (
<>
<CircleCheck size={16} className="mr-2" /> Rattacher
<CheckCircle size={16} className="mr-2" /> Rattacher
</>
),
onClick: () => openModalAssociationEleve(row.student)

View File

@ -15,7 +15,7 @@ export default function Page() {
const studentId = searchParams.get('studentId');
const firstName = searchParams.get('firstName');
const lastName = searchParams.get('lastName');
const paymentMode = searchParams.get('paymentMode');
const paymentSepa = searchParams.get('paymentSepa') === '1';
const file = searchParams.get('file');
const csrfToken = useCsrfToken();
@ -45,7 +45,7 @@ export default function Page() {
studentId={studentId}
firstName={firstName}
lastName={lastName}
paymentMode={paymentMode}
paymentSepa={paymentSepa}
file={file}
onAccept={handleAcceptRF}
/>

View File

@ -2,14 +2,16 @@
import React, { useEffect, useState } from 'react';
import { useRouter } from 'next/navigation';
import Table from '@/components/Table';
import { Edit3, Users, Download, Eye } from 'lucide-react';
import { Edit3, Users, Download, Eye, Upload, CheckCircle } from 'lucide-react';
import StatusLabel from '@/components/StatusLabel';
import FileUpload from '@/components/FileUpload';
import { FE_PARENTS_EDIT_INSCRIPTION_URL } from '@/utils/Url';
import { fetchChildren } from '@/app/actions/subscriptionAction';
import { fetchChildren, sendSEPARegisterForm } from '@/app/actions/subscriptionAction';
import logger from '@/utils/logger';
import { useSession } from 'next-auth/react';
import { FE_USERS_LOGIN_URL, BASE_URL } from '@/utils/Url';
import { useEstablishment } from '@/context/EstablishmentContext';
import { useCsrfToken } from '@/context/CsrfContext';
export default function ParentHomePage() {
const [children, setChildren] = useState([]);
@ -18,8 +20,11 @@ export default function ParentHomePage() {
const [currentPage, setCurrentPage] = useState(1);
const [establishments, setEstablishments] = useState([]);
const { selectedEstablishmentId, setSelectedEstablishmentId } = useEstablishment();
const [uploadingStudentId, setUploadingStudentId] = useState(null); // ID de l'étudiant pour l'upload
const [uploadedFile, setUploadedFile] = useState(null); // Fichier uploadé
const [uploadState, setUploadState] = useState("off"); // État "on" ou "off" pour l'affichage du composant
const router = useRouter();
const csrfToken = useCsrfToken();
useEffect(() => {
if (status === 'loading') return;
@ -55,22 +60,57 @@ export default function ParentHomePage() {
};
function handleView(eleveId) {
// Logique pour éditer le dossier de l'élève
logger.debug(`View dossier for student id: ${eleveId}`);
router.push(`${FE_PARENTS_EDIT_INSCRIPTION_URL}?id=${userId}&studentId=${eleveId}&view=true`);
}
function handleEdit(eleveId) {
// Logique pour éditer le dossier de l'élève
logger.debug(`Edit dossier for student id: ${eleveId}`);
router.push(`${FE_PARENTS_EDIT_INSCRIPTION_URL}?id=${userId}&studentId=${eleveId}`);
}
const actionColumns = [
{ name: 'Action', transform: (row) => row.action },
];
const handleFileUpload = (file) => {
if (!file) {
logger.error('Aucun fichier sélectionné pour l\'upload.');
return;
}
setUploadedFile(file); // Conserve le fichier en mémoire
logger.debug('Fichier sélectionné :', file.name);
};
const handleSubmit = () => {
if (!uploadedFile || !uploadingStudentId) {
logger.error('Aucun fichier ou étudiant sélectionné.');
return;
}
const formData = new FormData();
formData.append('sepa_file', uploadedFile); // Ajoute le fichier SEPA
formData.append('status', 3); // Statut à envoyer
sendSEPARegisterForm(uploadingStudentId, formData, csrfToken)
.then((response) => {
logger.debug('RF mis à jour avec succès:', response);
// Logique supplémentaire après la mise à jour (par exemple, redirection ou notification)
})
.catch((error) => {
logger.error('Erreur lors de la mise à jour du RF:', error);
});
};
const toggleUpload = (studentId) => {
if (uploadingStudentId === studentId && uploadState === "on") {
// Si le composant est déjà affiché pour cet étudiant, on le masque
setUploadState("off");
setUploadingStudentId(null);
setUploadedFile(null); // Réinitialise le fichier
} else {
// Sinon, on l'affiche pour cet étudiant
setUploadState("on");
setUploadingStudentId(studentId);
}
};
// Définir les colonnes du tableau
const childrenColumns = [
{ name: 'Nom', transform: (row) => `${row.student.last_name}` },
{ name: 'Prénom', transform: (row) => `${row.student.first_name}` },
@ -86,13 +126,12 @@ export default function ParentHomePage() {
name: 'Actions',
transform: (row) => (
<div className="flex justify-center items-center gap-2">
{/* Actions en fonction du statut */}
{row.status === 2 && (
<button
className="text-blue-500 hover:text-blue-700"
onClick={(e) => {
e.stopPropagation();
handleEdit(row.student.id); // Remplir le dossier
handleEdit(row.student.id);
}}
aria-label="Remplir le dossier"
>
@ -105,7 +144,7 @@ export default function ParentHomePage() {
className="text-purple-500 hover:text-purple-700"
onClick={(e) => {
e.stopPropagation();
handleView(row.student.id); // Visualiser le dossier
handleView(row.student.id);
}}
aria-label="Visualiser le dossier"
>
@ -116,24 +155,38 @@ export default function ParentHomePage() {
{row.status === 7 && (
<>
<button
className="text-purple-500 hover:text-purple-700"
className="flex items-center justify-center w-8 h-8 rounded-full text-purple-500 hover:text-purple-700"
onClick={(e) => {
e.stopPropagation();
handleView(row.student.id); // Visualiser le dossier
handleView(row.student.id);
}}
aria-label="Visualiser le dossier"
>
<Eye className="h-5 w-5" />
</button>
<a
href={`${BASE_URL}${row.sepa_file}`} // Télécharger le mandat SEPA
href={`${BASE_URL}${row.sepa_file}`}
target="_blank"
rel="noopener noreferrer"
className="text-green-500 hover:text-green-700"
className="flex items-center justify-center w-8 h-8 rounded-full text-green-500 hover:text-green-700"
aria-label="Télécharger le mandat SEPA"
>
<Download className="h-5 w-5" />
</a>
{/* Nouvelle action Upload */}
<button
className={`flex items-center justify-center w-8 h-8 rounded-full ${
uploadingStudentId === row.student.id && uploadState === "on" ? 'bg-blue-100 text-blue-600 ring-3 ring-blue-500'
: 'text-blue-500 hover:text-blue-700'
}`}
onClick={(e) => {
e.stopPropagation();
toggleUpload(row.student.id); // Activer ou désactiver l'upload pour cet étudiant
}}
aria-label="Uploader un fichier"
>
<Upload className="h-5 w-5" />
</button>
</>
)}
</div>
@ -141,13 +194,6 @@ export default function ParentHomePage() {
}
];
const itemsPerPage = 5;
const totalPages = Math.ceil(children.length / itemsPerPage) || 1;
const handlePageChange = (newPage) => {
setCurrentPage(newPage);
};
return (
<div className="px-2 py-4 md:px-4 max-w-full">
<div>
@ -175,13 +221,27 @@ export default function ParentHomePage() {
<Table
data={children}
columns={childrenColumns}
itemsPerPage={itemsPerPage}
currentPage={currentPage}
totalPages={totalPages}
onPageChange={handlePageChange}
defaultTheme="bg-gray-50"
/>
</div>
{/* Composant FileUpload et bouton Valider en dessous du tableau */}
{uploadState === "on" && uploadingStudentId && (
<div className="mt-4">
<FileUpload
selectionMessage="Sélectionnez un fichier à uploader"
onFileSelect={handleFileUpload}
/>
<button
className={`mt-4 px-6 py-2 rounded-md ${
uploadedFile ? 'bg-emerald-500 text-white hover:bg-emerald-600' : 'bg-gray-300 text-gray-700 cursor-not-allowed'
}`}
onClick={handleSubmit}
disabled={!uploadedFile}
>
Valider
</button>
</div>
)}
</div>
</div>
);

View File

@ -229,6 +229,18 @@ export const editRegistrationSchoolFileTemplates = (fileId, data, csrfToken) =>
.then(requestResponseHandler)
};
export const editRegistrationParentFileTemplates = (fileId, data, csrfToken) => {
return fetch(`${BE_SUBSCRIPTION_REGISTRATION_PARENT_FILE_TEMPLATES_URL}/${fileId}`, {
method: 'PUT',
body: data,
headers: {
'X-CSRFToken': csrfToken,
},
credentials: 'include',
})
.then(requestResponseHandler)
};
// DELETE requests
export async function deleteRegistrationFileGroup(groupId, csrfToken) {
@ -273,6 +285,16 @@ export const deleteRegistrationSchoolFileTemplates = (fileId,csrfToken) => {
})
};
export const deleteRegistrationParentFileTemplate = (id, csrfToken) => {
return fetch(`${BE_SUBSCRIPTION_REGISTRATION_PARENT_FILE_TEMPLATES_URL}/${id}`, {
method: 'DELETE',
headers: {
'X-CSRFToken': csrfToken,
},
credentials: 'include',
})
};
// API requests
export const cloneTemplate = (templateId, email, is_required) => {

View File

@ -1,9 +1,10 @@
import React, { useState } from 'react';
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];
@ -29,7 +30,7 @@ export default function FileUpload({ selectionMessage, onFileSelect, uploadedFil
<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={() => document.getElementById('fileInput').click()} // Ouvre l'explorateur de fichiers au clic
onClick={() => fileInputRef.current.click()} // Utilisation de la référence pour ouvrir l'explorateur
onDragOver={(e) => e.preventDefault()}
onDrop={handleFileDrop}
>
@ -39,7 +40,7 @@ export default function FileUpload({ selectionMessage, onFileSelect, uploadedFil
accept=".pdf"
onChange={handleFileChange}
className="hidden"
id="fileInput"
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>

View File

@ -1,17 +1,214 @@
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, columns }) {
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 (
<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>
<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>
);
}},
];
return (
<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}
itemsPerPage={5}
currentPage={1}
totalPages={1}
onPageChange={() => {}}
/>
{selectedFile && (
<div className="mt-4">
{actionType === 'view' && selectedFile.fileName ? (
<iframe
src={`${BASE_URL}/${selectedFile.fileName}`}
title="Document Viewer"
className="w-full"
style={{
height: '75vh',
border: 'none',
}}
/>
) : actionType === 'upload' ? (
<FileUpload
selectionMessage={`Téléversez le fichier ${selectedFile.master_name}`}
onFileSelect={(file) => handleUpload(file, selectedFile)}
uploadedFileName={selectedFile.fileName || ''}
/>
) : null}
</div>
)}
<Popup
visible={popupVisible}
message={popupMessage}
onConfirm={() => setPopupVisible(false)}
onCancel={() => setPopupVisible(false)}
uniqueConfirmButton={true}
/>
<Popup
visible={removePopupVisible}
message={removePopupMessage}
onConfirm={removePopupOnConfirm}
onCancel={() => setRemovePopupVisible(false)}
/>
</div>
);

View File

@ -5,23 +5,17 @@ import Button from '@/components/Button';
import DjangoCSRFToken from '@/components/DjangoCSRFToken';
import { fetchRegisterForm, fetchSchoolFileTemplatesFromRegistrationFiles, fetchParentFileTemplatesFromRegistrationFiles } from '@/app/actions/subscriptionAction';
import { downloadTemplate,
createRegistrationSchoolFileTemplate,
editRegistrationSchoolFileTemplates,
deleteRegistrationSchoolFileTemplates
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 } from '@/components/Inscription/StudentInfoForm';
import FilesToUpload from '@/components/Inscription/FilesToUpload';
import { DocusealForm } from '@docuseal/react';
import FileUpload from '@/components/Inscription/FileUpload';
/**
* Composant de formulaire d'inscription partagé
@ -36,7 +30,6 @@ export default function InscriptionFormShared({
csrfToken,
selectedEstablishmentId,
onSubmit,
cancelUrl,
errors = {} // Nouvelle prop pour les erreurs
}) {
// États pour gérer les données du formulaire
@ -65,12 +58,6 @@ export default function InscriptionFormShared({
const [uploadedFiles, setUploadedFiles] = useState([]);
const [schoolFileTemplates, setSchoolFileTemplates] = useState([]);
const [parentFileTemplates, setParentFileTemplates] = 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 [currentPage, setCurrentPage] = useState(1);
const isCurrentPageValid = () => {
@ -105,7 +92,6 @@ export default function InscriptionFormShared({
totalTuitionFees: data?.totalTuitionFees,
});
setGuardians(data?.student?.guardians || []);
setUploadedFiles(data.registration_files || []);
});
setIsLoading(false);
@ -119,6 +105,13 @@ export default function InscriptionFormShared({
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);
})
if (selectedEstablishmentId) {
@ -153,66 +146,74 @@ 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 createRegistrationSchoolFileTemplate(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
// 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
)
);
};
// 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 deleteRegistrationSchoolFileTemplates(fileToDelete.id, csrfToken);
setUploadedFiles(prev => prev.filter(f => parseInt(f.template) !== templateId));
} catch (error) {
logger.error('Error deleting file:', error);
}
return response;
})
.catch((error) => {
logger.error('Erreur lors de la suppression du fichier dans la base :', error);
throw error;
});
};
// Soumission du formulaire
@ -252,67 +253,12 @@ export default function InscriptionFormShared({
setCurrentPage(currentPage - 1);
};
// Configuration des colonnes pour le tableau des fichiers
const columns = [
{ name: 'Nom du fichier', transform: (row) => row.master_name },
{ name: 'Description du fichier', transform: (row) => row.master_description },
{ 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 */}
@ -330,6 +276,7 @@ export default function InscriptionFormShared({
{/* Pages suivantes : Section Fichiers d'inscription */}
{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">
@ -378,28 +325,21 @@ export default function InscriptionFormShared({
/>
)}
</div>
</div>
)}
{/* Dernière page : Section Fichiers parents */}
{currentPage === schoolFileTemplates.length + 2 && (
<>
<FilesToUpload
parentFileTemplates={parentFileTemplates}
columns={columns}
uploadedFiles={uploadedFiles}
onFileUpload={handleFileUpload}
onFileDelete={handleDeleteFile}
/>
<FileUpload
selectionMessage='Sélectionnez un fichier'
onFileSelect={(file) => {
setUploadedFiles(file.name); // Stocke uniquement le nom du fichier
logger.debug('Fichier sélectionné:', file.name);
}}
uploadedFileName={uploadedFiles}
/>
</>
)}
{/* 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}
@ -429,52 +369,6 @@ export default function InscriptionFormShared({
)}
</div>
</form>
{schoolFileTemplates.length > 0 && (
<Modal
isOpen={showUploadModal}
setIsOpen={setShowUploadModal}
title="Téléverser un fichier"
ContentComponent={() => (
<>
<DraggableFileUpload
className="w-full"
fileName={fileName}
onFileSelect={(selectedFile) => {
if (selectedFile) {
setFile(selectedFile);
setFileName(selectedFile.name);
}
}}
/>
<div className="mt-4 flex justify-center space-x-4">
<Button
text="Annuler"
onClick={() => {
setShowUploadModal(false);
setCurrentTemplateId(null);
setFile(null);
setFileName("");
}}
/>
<Button
text="Valider"
onClick={() => {
if (file && fileName) {
handleFileUpload(file, fileName);
setShowUploadModal(false);
setCurrentTemplateId(null);
setFile(null);
setFileName("");
}
}}
primary={true}
disabled={!file || !fileName}
/>
</div>
</>
)}
/>
)}
</div>
);
}

View File

@ -6,14 +6,16 @@ import { BASE_URL } from '@/utils/Url';
import { generateToken } from '@/app/actions/registerFileGroupAction';
import logger from '@/utils/logger';
import { GraduationCap, CloudUpload } from 'lucide-react';
import FileUpload from '@/components/Inscription/FileUpload';
import FileUpload from '@/components/FileUpload';
import SectionHeader from '@/components/SectionHeader';
export default function ValidateSubscription({ studentId, firstName, lastName, paymentMode, file, onAccept }) {
export default function ValidateSubscription({ studentId, firstName, lastName, 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
useEffect(() => {
if (isSepa) {
@ -25,34 +27,21 @@ export default function ValidateSubscription({ studentId, firstName, lastName, p
}
}, [isSepa]);
const handleUpload = (detail) => {
logger.debug('Uploaded file detail:', detail);
setUploadedFileName(detail.name);
};
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;
}
const data = {
status: 7,
sepa_file: file,
sepa_file: selectedFile, // Utilise le fichier sélectionné depuis l'état
};
// 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 isValidateButtonDisabled = isSepa && !uploadedFileName;
const goToNextPage = () => {
@ -68,25 +57,16 @@ export default function ValidateSubscription({ studentId, firstName, lastName, p
};
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">
<div className="p-6 items-center">
<iframe
src={pdfUrl}
title="Aperçu du PDF"
@ -102,9 +82,10 @@ export default function ValidateSubscription({ studentId, firstName, lastName, p
{currentPage === 2 && isSepa && (
<FileUpload
selectionMessage='Sélectionnez un mandat de prélèvement SEPA'
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}
@ -120,7 +101,7 @@ export default function ValidateSubscription({ studentId, firstName, lastName, p
className="bg-gray-300 text-gray-700 hover:bg-gray-400 px-6 py-2"
/>
)}
{currentPage < (isSepa ? 2 : 1) && (
{isSepa && currentPage === 1 && (
<Button
text="Suivant"
onClick={goToNextPage}

View File

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

View File

@ -6,10 +6,14 @@ const SidebarTabs = ({ tabs }) => {
return (
<div className="w-full">
<div className="flex border-b-2 border-gray-200">
{tabs.map(tab => (
{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}
@ -17,7 +21,7 @@ const SidebarTabs = ({ tabs }) => {
))}
</div>
<div className="p-4">
{tabs.map(tab => (
{tabs.map((tab) => (
<div key={tab.id} className={`${activeTab === tab.id ? 'block' : 'hidden'}`}>
{tab.content}
</div>

View File

@ -12,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',
@ -444,15 +445,13 @@ const ClassesSection = ({ classes, setClasses, teachers, handleCreate, handleEdi
return (
<DndProvider backend={HTML5Backend}>
<div className="space-y-4">
<div className="flex justify-between items-center">
<div className="flex items-center mb-4">
<Users className="w-6 h-6 text-emerald-500 mr-2" />
<h2 className="text-xl font-semibold">Classes</h2>
</div>
<button type="button" onClick={handleAddClass} className="text-emerald-500 hover:text-emerald-700">
<Plus className="w-5 h-5" />
</button>
</div>
<SectionHeader
icon={Users}
title="Liste des classes"
description="Gérez les classes de votre école"
button={true}
onClick={handleAddClass}
/>
<Table
data={newClass ? [newClass, ...classes] : classes}
columns={columns}

View File

@ -8,6 +8,7 @@ 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, setSpecialities, handleCreate, handleEdit, handleDelete }) => {
@ -214,15 +215,13 @@ const SpecialitiesSection = ({ specialities, setSpecialities, handleCreate, hand
return (
<DndProvider backend={HTML5Backend}>
<div className="space-y-4">
<div className="flex justify-between items-center">
<div className="flex items-center mb-4">
<BookOpen className="w-6 h-6 text-emerald-500 mr-2" />
<h2 className="text-xl font-semibold">Spécialités</h2>
</div>
<button type="button" onClick={handleAddSpeciality} className="text-emerald-500 hover:text-emerald-700">
<Plus className="w-5 h-5" />
</button>
</div>
<SectionHeader
icon={BookOpen}
title="Liste des spécialités"
description="Gérez les spécialités de votre école"
button={true}
onClick={handleAddSpeciality}
/>
<Table
data={newSpeciality ? [newSpeciality, ...specialities] : specialities}
columns={columns}

View File

@ -7,9 +7,9 @@ import { BE_SCHOOL_SPECIALITIES_URL, BE_SCHOOL_TEACHERS_URL, BE_SCHOOL_SCHOOLCLA
const StructureManagement = ({ specialities, setSpecialities, teachers, setTeachers, classes, setClasses, profiles, handleCreate, handleEdit, 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}
@ -18,7 +18,7 @@ const StructureManagement = ({ specialities, setSpecialities, teachers, setTeach
handleDelete={(id) => handleDelete(`${BE_SCHOOL_SPECIALITIES_URL}`, id, setSpecialities)}
/>
</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}
@ -29,7 +29,7 @@ const StructureManagement = ({ specialities, setSpecialities, teachers, setTeach
handleDelete={(id) => handleDelete(`${BE_SCHOOL_TEACHERS_URL}`, id, setTeachers)}
/>
</div>
<div className="w-full p-4 bg-white rounded-lg shadow-md">
<div className="w-full mt-12">
<ClassesSection
classes={classes}
setClasses={setClasses}

View File

@ -3,7 +3,6 @@ import { Plus, Edit3, Trash2, GraduationCap, Check, X, Hand, Search } from 'luci
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';
@ -11,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',
@ -468,15 +467,13 @@ const TeachersSection = ({ teachers, setTeachers, specialities, profiles, handle
return (
<DndProvider backend={HTML5Backend}>
<div className="space-y-4">
<div className="flex justify-between items-center">
<div className="flex items-center mb-4">
<GraduationCap className="w-6 h-6 text-emerald-500 mr-2" />
<h2 className="text-xl font-semibold">Enseignants</h2>
</div>
<button type="button" onClick={handleAddTeacher} className="text-emerald-500 hover:text-emerald-700">
<Plus className="w-5 h-5" />
</button>
</div>
<SectionHeader
icon={GraduationCap}
title="Liste des enseignants.es"
description="Gérez les enseignants.es de votre école"
button={true}
onClick={handleAddTeacher}
/>
<Table
data={newTeacher ? [newTeacher, ...teachers] : teachers}
columns={columns}

View File

@ -3,6 +3,7 @@ import { Plus, Download, Edit3, Trash2, FolderPlus, Signature, FileText, Check,
import Modal from '@/components/Modal';
import Table from '@/components/Table';
import FileUploadDocuSeal from '@/components/Structure/Files/FileUploadDocuSeal';
import FileUpload from '@/components/FileUpload'
import { BASE_URL } from '@/utils/Url';
import {
// GET
@ -26,6 +27,7 @@ import {
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 [schoolFileMasters, setSchoolFileMasters] = useState([]);
@ -42,6 +44,8 @@ export default function FilesGroupsManagement({ csrfToken, selectedEstablishment
const [editingDocumentId, setEditingDocumentId] = useState(null);
const [formData, setFormData] = useState({});
const [uploadedFileName, setUploadedFileName] = useState('');
const handleReloadTemplates = () => {
setReloadTemplates(true);
}
@ -253,84 +257,6 @@ export default function FilesGroupsManagement({ csrfToken, selectedEstablishment
}
};
const renderRequiredDocumentCell = (document, column) => {
const isEditing = editingDocumentId === document.id;
if (isEditing) {
switch (column) {
case 'Nom de la pièce':
return (
<InputText
name="name"
value={formData.name}
onChange={(e) => handleChange(e, 'name')}
placeholder="Nom de la pièce"
className="w-full"
/>
);
case 'Description':
return (
<InputText
name="description"
value={formData.description}
onChange={(e) => handleChange(e, 'description')}
placeholder="Description"
className="w-full"
/>
);
case 'Actions':
return (
<div className="flex justify-center space-x-2">
<button
type="button"
onClick={() => handleSaveDocument(document.id)}
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 '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={() => handleDeleteRequiredDocument(document.id)}
className="text-red-500 hover:text-red-700"
>
<Trash2 className="w-5 h-5" />
</button>
</div>
);
default:
return null;
}
}
};
const handleCreate = (newParentFile) => {
return createRegistrationParentFileMaster(newParentFile, csrfToken)
.then((response) => {
@ -417,14 +343,8 @@ export default function FilesGroupsManagement({ csrfToken, selectedEstablishment
)}
];
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: 'Actions', transform: (row) => renderRequiredDocumentCell(row, 'Actions') },
];
return (
<div className="space-y-12">
<div className="w-full mx-auto mt-6">
{/* Modal pour les fichiers */}
<Modal
isOpen={isModalOpen}
@ -460,26 +380,15 @@ export default function FilesGroupsManagement({ csrfToken, selectedEstablishment
/>
{/* Section Groupes de fichiers */}
<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">
<FolderPlus className="w-8 h-8 text-emerald-600" />
</div>
<div>
<h2 className="text-2xl font-bold text-gray-800">Dossiers d'inscriptions</h2>
<p className="text-sm text-gray-500 italic">
Gérez les dossiers d'inscription pour organiser vos documents.
</p>
</div>
</div>
<button
<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)}
className="flex items-center bg-emerald-600 text-white p-2 rounded-full shadow hover:bg-emerald-700 transition duration-200"
>
<Plus className="w-6 h-6" />
</button>
</div>
/>
<Table
data={groups}
columns={columnsGroups}
@ -487,29 +396,18 @@ export default function FilesGroupsManagement({ csrfToken, selectedEstablishment
</div>
{/* Section Fichiers */}
<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">
<Signature className="w-8 h-8 text-emerald-600" />
</div>
<div>
<h2 className="text-2xl font-bold text-gray-800">Formulaires à remplir</h2>
<p className="text-sm text-gray-500 italic">
Gérez les formulaires nécessitant une signature électronique.
</p>
</div>
</div>
<button
<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);
}}
className="flex items-center bg-emerald-600 text-white p-2 rounded-full shadow hover:bg-emerald-700 transition duration-200"
>
<Plus className="w-6 h-6" />
</button>
</div>
/>
<Table
data={filteredFiles}
columns={columnsFiles}

View File

@ -7,6 +7,7 @@ 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);
@ -65,7 +66,6 @@ export default function ParentFilesSection({ parentFiles, groups, handleCreate,
createRegistrationParentFileTemplate(data, csrfToken)
.then(response => {
logger.debug('Template enregistré avec succès:', response);
onSuccess();
})
.catch(error => {
logger.error('Erreur lors de l\'enregistrement du template:', error);
@ -243,26 +243,14 @@ export default function ParentFilesSection({ parentFiles, groups, handleCreate,
];
return (
<div className="mt-8 mb-4 w-4/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">
Configurez la liste des documents que les parents doivent fournir.
</p>
</div>
</div>
<button
<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}
className="text-emerald-500 hover:text-emerald-700"
>
<Plus className="w-6 h-6" />
</button>
</div>
/>
<Table
data={editingDocumentId === 'new' ? [formData, ...parentFiles] : parentFiles}
columns={columnsRequiredDocuments}

View File

@ -6,8 +6,9 @@ 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, setDiscounts, handleCreate, handleEdit, handleDelete, type, subscriptionMode = false, selectedDiscounts, handleDiscountSelection }) => {
const DiscountsSection = ({ discounts, setDiscounts, handleCreate, handleEdit, handleDelete, type, selectedDiscounts, handleDiscountSelection }) => {
const [editingDiscount, setEditingDiscount] = useState(null);
const [newDiscount, setNewDiscount] = useState(null);
const [formData, setFormData] = useState({});
@ -253,14 +254,7 @@ const DiscountsSection = ({ discounts, setDiscounts, handleCreate, handleEdit, h
}
};
const columns = subscriptionMode
? [
{ name: 'LIBELLE', label: 'Libellé' },
{ name: 'DESCRIPTION', label: 'Description' },
{ name: 'REMISE', label: 'Remise' },
{ name: '', label: 'Sélection' }
]
: [
const columns = [
{ name: 'LIBELLE', label: 'Libellé' },
{ name: 'REMISE', label: 'Remise' },
{ name: 'DESCRIPTION', label: 'Description' },
@ -269,18 +263,15 @@ const DiscountsSection = ({ discounts, setDiscounts, handleCreate, handleEdit, h
];
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={`${type == 0 ? "Gérez vos réductions sur les frais d'inscription" : "Gérez vos réductions sur les frais de scolarité"}`}
button={true}
onClick={handleAddDiscount}
/>
<Table
data={newDiscount ? [newDiscount, ...discounts] : discounts}
columns={columns}

View File

@ -44,11 +44,15 @@ const FeesManagement = ({ registrationDiscounts,
};
return (
<div className="w-full mx-auto p-2 mt-6 space-y-6">
<div className="bg-white p-2 rounded-lg shadow-md">
<h2 className="text-2xl font-semibold mb-4">Frais d&apos;inscription</h2>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div className="col-span-1 p-6 rounded-lg shadow-inner mt-4">
<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}
@ -59,7 +63,7 @@ const FeesManagement = ({ registrationDiscounts,
type={0}
/>
</div>
<div className="col-span-1 p-6 rounded-lg shadow-inner mt-4">
<div className="mt-12 w-4/5">
<DiscountsSection
discounts={registrationDiscounts}
setDiscounts={setRegistrationDiscounts}
@ -70,7 +74,8 @@ const FeesManagement = ({ registrationDiscounts,
type={0}
/>
</div>
<div className="col-span-1 p-6 rounded-lg shadow-inner mt-4">
<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}
@ -78,7 +83,7 @@ const FeesManagement = ({ registrationDiscounts,
type={0}
/>
</div>
<div className="col-span-1 p-6 rounded-lg shadow-inner mt-4">
<div className="col-span-1 mt-4">
<PaymentModeSelector
paymentModes={registrationPaymentModes}
setPaymentModes={setRegistrationPaymentModes}
@ -87,11 +92,15 @@ const FeesManagement = ({ registrationDiscounts,
/>
</div>
</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="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">
<div className="mt-8 w-4/5">
<FeesSection
fees={tuitionFees}
setFees={setTuitionFees}
@ -102,7 +111,7 @@ const FeesManagement = ({ registrationDiscounts,
type={1}
/>
</div>
<div className="col-span-1 p-6 rounded-lg shadow-inner mt-4">
<div className="mt-12 w-4/5">
<DiscountsSection
discounts={tuitionDiscounts}
setDiscounts={setTuitionDiscounts}
@ -113,7 +122,8 @@ const FeesManagement = ({ registrationDiscounts,
type={1}
/>
</div>
<div className="col-span-1 p-6 rounded-lg shadow-inner mt-4">
<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}
@ -121,7 +131,7 @@ const FeesManagement = ({ registrationDiscounts,
type={1}
/>
</div>
<div className="col-span-1 p-6 rounded-lg shadow-inner mt-4">
<div className="col-span-1 mt-4">
<PaymentModeSelector
paymentModes={tuitionPaymentModes}
setPaymentModes={setTuitionPaymentModes}
@ -131,7 +141,6 @@ const FeesManagement = ({ registrationDiscounts,
</div>
</div>
</div>
</div>
);
};

View File

@ -5,10 +5,11 @@ 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';
const FeesSection = ({ fees, setFees, discounts, handleCreate, handleEdit, handleDelete, type, subscriptionMode = false, selectedFees, handleFeeSelection }) => {
const FeesSection = ({ fees, setFees, discounts, handleCreate, handleEdit, handleDelete, type, selectedFees, handleFeeSelection }) => {
const [editingFee, setEditingFee] = useState(null);
const [newFee, setNewFee] = useState(null);
const [formData, setFormData] = useState({});
@ -243,14 +244,7 @@ const FeesSection = ({ fees, setFees, discounts, handleCreate, handleEdit, handl
}
};
const columns = subscriptionMode
? [
{ name: 'NOM', label: 'Nom' },
{ name: 'DESCRIPTION', label: 'Description' },
{ name: 'MONTANT', label: 'Montant de base' },
{ name: '', label: 'Sélection' }
]
: [
const columns = [
{ name: 'NOM', label: 'Nom' },
{ name: 'MONTANT', label: 'Montant de base' },
{ name: 'DESCRIPTION', label: 'Description' },
@ -260,17 +254,13 @@ const FeesSection = ({ fees, setFees, discounts, handleCreate, handleEdit, handl
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={`${type == 0 ? "Gérez vos frais d'inscription" : "Gérez vos frais de scolarité"}`}
button={true}
onClick={handleAddFee}
/>
<Table
data={newFee ? [newFee, ...fees] : fees}
columns={columns}

View File

@ -1,6 +1,6 @@
import React from 'react';
import PropTypes from 'prop-types';
import Pagination from '@/components/Pagination'; // Correction du chemin d'importatio,
import Pagination from '@/components/Pagination'; // Correction du chemin d'importation
const Table = ({ data, columns, renderCell, itemsPerPage = 0, currentPage, totalPages, onPageChange, onRowClick, selectedRows, isSelectable = false, defaultTheme = 'bg-emerald-50' }) => {
const handlePageChange = (newPage) => {
@ -28,7 +28,16 @@ const Table = ({ data, columns, renderCell, itemsPerPage = 0, currentPage, total
${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 key={colIndex} className={`py-2 px-4 border-b border-gray-200 text-center text-sm ${selectedRows?.includes(row.id) ? 'text-white' : 'text-gray-700'}`}>
@ -61,6 +70,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;