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 from xhtml2pdf import pisa
class PDFResult:
def __init__(self, content):
self.content = content
def render_to_pdf(template_src, context_dict={}): 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) template = get_template(template_src)
html = template.render(context_dict) html = template.render(context_dict)
result = BytesIO() result = BytesIO()
pdf = pisa.pisaDocument(BytesIO(html.encode("UTF-8")), result) pdf = pisa.pisaDocument(BytesIO(html.encode("UTF-8")), result)
if pdf.err: if pdf.err:
return HttpResponse("Invalid PDF", status_code=400, content_type='text/plain') # Lever une exception ou retourner None en cas d'erreur
return HttpResponse(result.getvalue(), content_type='application/pdf') 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 from datetime import datetime
import os
class Language(models.Model): class Language(models.Model):
""" """
Représente une langue parlée par lélève. Représente une langue parlée par lélève.
@ -231,8 +233,11 @@ class RegistrationForm(models.Model):
# Appeler la méthode save originale # Appeler la méthode save originale
super().save(*args, **kwargs) super().save(*args, **kwargs)
def registration_file_upload_to(instance, filename): def registration_school_file_upload_to(instance, filename):
return f"registration_files/dossier_rf_{instance.registration_form.pk}/{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 ######################## ####################### MASTER FILES ########################
@ -265,7 +270,7 @@ class RegistrationSchoolFileTemplate(models.Model):
slug = models.CharField(max_length=255, default="") slug = models.CharField(max_length=255, default="")
name = 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) 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): def __str__(self):
return self.name return self.name
@ -285,17 +290,31 @@ class RegistrationSchoolFileTemplate(models.Model):
class RegistrationParentFileTemplate(models.Model): class RegistrationParentFileTemplate(models.Model):
master = models.ForeignKey(RegistrationParentFileMaster, on_delete=models.CASCADE, related_name='parent_file_templates', blank=True) 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) 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): def __str__(self):
return self.name 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 @staticmethod
def get_files_from_rf(register_form_id): def get_files_from_rf(register_form_id):
""" """
Récupère tous les fichiers liés à un dossier dinscription donné. 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 = [] filenames = []
for reg_file in registration_files: for reg_file in registration_files:
filenames.append(reg_file.file.path) filenames.append(reg_file.file.path)

View File

@ -38,14 +38,14 @@ class RegistrationSchoolFileTemplateSerializer(serializers.ModelSerializer):
class RegistrationParentFileTemplateSerializer(serializers.ModelSerializer): class RegistrationParentFileTemplateSerializer(serializers.ModelSerializer):
id = serializers.IntegerField(required=False) id = serializers.IntegerField(required=False)
file = serializers.SerializerMethodField() file_url = serializers.SerializerMethodField()
master_name = serializers.CharField(source='master.name', read_only=True) master_name = serializers.CharField(source='master.name', read_only=True)
master_description = serializers.CharField(source='master.description', read_only=True) master_description = serializers.CharField(source='master.description', read_only=True)
class Meta: class Meta:
model = RegistrationParentFileTemplate model = RegistrationParentFileTemplate
fields = '__all__' fields = '__all__'
def get_file(self, obj): def get_file_url(self, obj):
# Retourne l'URL complète du fichier si disponible # Retourne l'URL complète du fichier si disponible
return obj.file.url if obj.file else None 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 from PyPDF2 import PdfMerger
import shutil import shutil
import logging
logger = logging.getLogger(__name__)
def recupereListeFichesInscription(): 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. Génère le PDF d'un dossier d'inscription et l'associe au RegistrationForm.
""" """
filename = filename.replace(" ", "_") filename = filename.replace(" ", "_")
data = { data = {
'pdf_title': f"Dossier d'inscription de {registerForm.student.first_name}", 'pdf_title': f"Dossier d'inscription de {registerForm.student.first_name}",
'signatureDate': convertToStr(_now(), '%d-%m-%Y'), 'signatureDate': convertToStr(_now(), '%d-%m-%Y'),
@ -131,20 +133,37 @@ def rfToPDF(registerForm, filename):
# Générer le PDF # Générer le PDF
pdf = renderers.render_to_pdf('pdfs/dossier_inscription.html', data) 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 # 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): if registerForm.registration_file and registerForm.registration_file.name:
os.remove(registerForm.registration_file.path) # Vérifiez si le chemin est déjà absolu ou relatif
registerForm.registration_file.delete(save=False) 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 # Enregistrer directement le fichier dans le champ registration_file
registerForm.registration_file.save( try:
os.path.basename(filename), registerForm.registration_file.save(
File(BytesIO(pdf.content)), # Utilisation de BytesIO pour éviter l'écriture sur le disque os.path.basename(filename), # Utiliser uniquement le nom de fichier
save=True 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): def delete_registration_files(registerForm):
""" """

View File

@ -254,30 +254,37 @@ class RegisterFormWithIdView(APIView):
if _status == RegistrationForm.RegistrationFormStatus.RF_UNDER_REVIEW: if _status == RegistrationForm.RegistrationFormStatus.RF_UNDER_REVIEW:
try: try:
# Génération de la fiche d'inscription au format PDF # 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) os.makedirs(base_dir, exist_ok=True)
# Fichier PDF initial # 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.registration_file = util.rfToPDF(registerForm, initial_pdf)
registerForm.save() registerForm.save()
# Récupération des fichiers d'inscription # Récupération des fichiers d'inscription
fileNames = RegistrationSchoolFileTemplate.get_files_from_rf(registerForm.pk) # fileNames = RegistrationSchoolFileTemplate.get_files_from_rf(registerForm.pk)
if registerForm.registration_file: # if registerForm.registration_file:
fileNames.insert(0, registerForm.registration_file.path) # fileNames.insert(0, registerForm.registration_file.path)
# Création du fichier PDF Fusionné # # Création du fichier PDF Fusionné
merged_pdf_content = util.merge_files_pdf(fileNames) # 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 # Mise à jour de l'automate
updateStateMachine(registerForm, 'EVENT_SIGNATURE') # 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: except Exception as e:
return JsonResponse({'error': str(e)}, status=status.HTTP_500_INTERNAL_SERVER_ERROR) 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) template = bdd.getObject(_objectName=RegistrationParentFileTemplate, _columnName='id', _value=id)
if template is None: if template is None:
return JsonResponse({'erreur': 'Le template d\'inscription n\'a pas été trouvé'}, safe=False, status=status.HTTP_404_NOT_FOUND) 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(): if serializer.is_valid():
serializer.save() serializer.save()
return Response({'message': 'Template mis à jour avec succès', 'data': serializer.data}, status=status.HTTP_200_OK) 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) students = bdd.getObjects(_objectName=RegistrationForm, _columnName='student__guardians__profile_role__profile__id', _value=id)
if students: 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) students_serializer = RegistrationFormByParentSerializer(students, many=True)
return JsonResponse(students_serializer.data, safe=False) return JsonResponse(students_serializer.data, safe=False)

View File

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

View File

@ -9,7 +9,7 @@ import Popup from '@/components/Popup';
import Loader from '@/components/Loader'; import Loader from '@/components/Loader';
import AlertWithModal from '@/components/AlertWithModal'; import AlertWithModal from '@/components/AlertWithModal';
import DropdownMenu from "@/components/DropdownMenu"; 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 Modal from '@/components/Modal';
import InscriptionForm from '@/components/Inscription/InscriptionForm' import InscriptionForm from '@/components/Inscription/InscriptionForm'
import AffectationClasseForm from '@/components/AffectationClasseForm' import AffectationClasseForm from '@/components/AffectationClasseForm'
@ -606,35 +606,62 @@ useEffect(()=>{
const actions = { const actions = {
1: [ 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`, 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), onClick: () => sendConfirmRegisterForm(row.student.id, row.student.last_name, row.student.first_name),
}, },
], ],
2: [ 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`, onClick: () => window.location.href = `${FE_ADMIN_SUBSCRIPTIONS_EDIT_URL}?studentId=${row.student.id}&id=1`,
}, },
], ],
3: [ 3: [
{ {
icon: <CircleCheck className="w-5 h-5 text-green-500 hover:text-green-700" />, icon: (
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}`, <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: [ 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), onClick: () => openModalAssociationEleve(row.student),
}, },
], ],
default: [ 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), onClick: () => archiveFicheInscription(row.student.id, row.student.last_name, row.student.first_name),
}, },
], ],
@ -674,15 +701,35 @@ const columns = [
</div> </div>
) )
}, },
{ name: t('files'), transform: (row) => { name: t('files'), transform: (row) => (
(row.registration_file != null) &&( <ul>
<ul> {row.registration_file && (
<li className="flex justify-center items-center gap-2"> <li className="flex justify-center items-center gap-2">
<FileText size={16} /> <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> </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> </ul>
) }, )
},
{ name: 'Actions', { name: 'Actions',
transform: (row) => ( transform: (row) => (
<div className="flex justify-center space-x-2"> <div className="flex justify-center space-x-2">
@ -728,7 +775,7 @@ const columnsSubscribed = [
items={[ items={[
{ label: ( { label: (
<> <>
<CircleCheck size={16} className="mr-2" /> Rattacher <CheckCircle size={16} className="mr-2" /> Rattacher
</> </>
), ),
onClick: () => openModalAssociationEleve(row.student) onClick: () => openModalAssociationEleve(row.student)

View File

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

View File

@ -2,14 +2,16 @@
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
import Table from '@/components/Table'; 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 StatusLabel from '@/components/StatusLabel';
import FileUpload from '@/components/FileUpload';
import { FE_PARENTS_EDIT_INSCRIPTION_URL } from '@/utils/Url'; 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 logger from '@/utils/logger';
import { useSession } from 'next-auth/react'; import { useSession } from 'next-auth/react';
import { FE_USERS_LOGIN_URL, BASE_URL } from '@/utils/Url'; import { FE_USERS_LOGIN_URL, BASE_URL } from '@/utils/Url';
import { useEstablishment } from '@/context/EstablishmentContext'; import { useEstablishment } from '@/context/EstablishmentContext';
import { useCsrfToken } from '@/context/CsrfContext';
export default function ParentHomePage() { export default function ParentHomePage() {
const [children, setChildren] = useState([]); const [children, setChildren] = useState([]);
@ -18,8 +20,11 @@ export default function ParentHomePage() {
const [currentPage, setCurrentPage] = useState(1); const [currentPage, setCurrentPage] = useState(1);
const [establishments, setEstablishments] = useState([]); const [establishments, setEstablishments] = useState([]);
const { selectedEstablishmentId, setSelectedEstablishmentId } = useEstablishment(); 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 router = useRouter();
const csrfToken = useCsrfToken();
useEffect(() => { useEffect(() => {
if (status === 'loading') return; if (status === 'loading') return;
@ -55,22 +60,57 @@ export default function ParentHomePage() {
}; };
function handleView(eleveId) { function handleView(eleveId) {
// Logique pour éditer le dossier de l'élève
logger.debug(`View dossier for student id: ${eleveId}`); logger.debug(`View dossier for student id: ${eleveId}`);
router.push(`${FE_PARENTS_EDIT_INSCRIPTION_URL}?id=${userId}&studentId=${eleveId}&view=true`); router.push(`${FE_PARENTS_EDIT_INSCRIPTION_URL}?id=${userId}&studentId=${eleveId}&view=true`);
} }
function handleEdit(eleveId) { function handleEdit(eleveId) {
// Logique pour éditer le dossier de l'élève
logger.debug(`Edit dossier for student id: ${eleveId}`); logger.debug(`Edit dossier for student id: ${eleveId}`);
router.push(`${FE_PARENTS_EDIT_INSCRIPTION_URL}?id=${userId}&studentId=${eleveId}`); router.push(`${FE_PARENTS_EDIT_INSCRIPTION_URL}?id=${userId}&studentId=${eleveId}`);
} }
const actionColumns = [ const handleFileUpload = (file) => {
{ name: 'Action', transform: (row) => row.action }, 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 = [ const childrenColumns = [
{ name: 'Nom', transform: (row) => `${row.student.last_name}` }, { name: 'Nom', transform: (row) => `${row.student.last_name}` },
{ name: 'Prénom', transform: (row) => `${row.student.first_name}` }, { name: 'Prénom', transform: (row) => `${row.student.first_name}` },
@ -86,13 +126,12 @@ export default function ParentHomePage() {
name: 'Actions', name: 'Actions',
transform: (row) => ( transform: (row) => (
<div className="flex justify-center items-center gap-2"> <div className="flex justify-center items-center gap-2">
{/* Actions en fonction du statut */}
{row.status === 2 && ( {row.status === 2 && (
<button <button
className="text-blue-500 hover:text-blue-700" className="text-blue-500 hover:text-blue-700"
onClick={(e) => { onClick={(e) => {
e.stopPropagation(); e.stopPropagation();
handleEdit(row.student.id); // Remplir le dossier handleEdit(row.student.id);
}} }}
aria-label="Remplir le dossier" aria-label="Remplir le dossier"
> >
@ -105,7 +144,7 @@ export default function ParentHomePage() {
className="text-purple-500 hover:text-purple-700" className="text-purple-500 hover:text-purple-700"
onClick={(e) => { onClick={(e) => {
e.stopPropagation(); e.stopPropagation();
handleView(row.student.id); // Visualiser le dossier handleView(row.student.id);
}} }}
aria-label="Visualiser le dossier" aria-label="Visualiser le dossier"
> >
@ -116,24 +155,38 @@ export default function ParentHomePage() {
{row.status === 7 && ( {row.status === 7 && (
<> <>
<button <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) => { onClick={(e) => {
e.stopPropagation(); e.stopPropagation();
handleView(row.student.id); // Visualiser le dossier handleView(row.student.id);
}} }}
aria-label="Visualiser le dossier" aria-label="Visualiser le dossier"
> >
<Eye className="h-5 w-5" /> <Eye className="h-5 w-5" />
</button> </button>
<a <a
href={`${BASE_URL}${row.sepa_file}`} // Télécharger le mandat SEPA href={`${BASE_URL}${row.sepa_file}`}
target="_blank" target="_blank"
rel="noopener noreferrer" 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" aria-label="Télécharger le mandat SEPA"
> >
<Download className="h-5 w-5" /> <Download className="h-5 w-5" />
</a> </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> </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 ( return (
<div className="px-2 py-4 md:px-4 max-w-full"> <div className="px-2 py-4 md:px-4 max-w-full">
<div> <div>
@ -175,13 +221,27 @@ export default function ParentHomePage() {
<Table <Table
data={children} data={children}
columns={childrenColumns} columns={childrenColumns}
itemsPerPage={itemsPerPage}
currentPage={currentPage}
totalPages={totalPages}
onPageChange={handlePageChange}
defaultTheme="bg-gray-50" defaultTheme="bg-gray-50"
/> />
</div> </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>
</div> </div>
); );

View File

@ -229,6 +229,18 @@ export const editRegistrationSchoolFileTemplates = (fileId, data, csrfToken) =>
.then(requestResponseHandler) .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 // DELETE requests
export async function deleteRegistrationFileGroup(groupId, csrfToken) { export async function deleteRegistrationFileGroup(groupId, csrfToken) {
@ -243,7 +255,7 @@ export async function deleteRegistrationFileGroup(groupId, csrfToken) {
return response; return response;
}; };
export const deleteRegistrationSchoolFileMaster = (fileId,csrfToken) => { export const deleteRegistrationSchoolFileMaster = (fileId, csrfToken) => {
return fetch(`${BE_SUBSCRIPTION_REGISTRATION_SCHOOL_FILE_MASTERS_URL}/${fileId}`, { return fetch(`${BE_SUBSCRIPTION_REGISTRATION_SCHOOL_FILE_MASTERS_URL}/${fileId}`, {
method: 'DELETE', method: 'DELETE',
headers: { headers: {
@ -263,7 +275,7 @@ export const deleteRegistrationParentFileMaster = (id, csrfToken) => {
}) })
}; };
export const deleteRegistrationSchoolFileTemplates = (fileId,csrfToken) => { export const deleteRegistrationSchoolFileTemplates = (fileId, csrfToken) => {
return fetch(`${BE_SUBSCRIPTION_REGISTRATION_SCHOOL_FILE_TEMPLATES_URL}/${fileId}`, { return fetch(`${BE_SUBSCRIPTION_REGISTRATION_SCHOOL_FILE_TEMPLATES_URL}/${fileId}`, {
method: 'DELETE', method: 'DELETE',
headers: { headers: {
@ -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 // API requests
export const cloneTemplate = (templateId, email, is_required) => { 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 { CloudUpload } from 'lucide-react';
import logger from '@/utils/logger'; import logger from '@/utils/logger';
export default function FileUpload({ selectionMessage, onFileSelect, uploadedFileName }) { export default function FileUpload({ selectionMessage, onFileSelect, uploadedFileName }) {
const [localFileName, setLocalFileName] = useState(uploadedFileName || ''); const [localFileName, setLocalFileName] = useState(uploadedFileName || '');
const fileInputRef = useRef(null); // Utilisation de useRef pour cibler l'input
const handleFileChange = (e) => { const handleFileChange = (e) => {
const file = e.target.files[0]; 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> <h3 className="text-lg font-semibold mb-4">{`${selectionMessage}`}</h3>
<div <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" 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()} onDragOver={(e) => e.preventDefault()}
onDrop={handleFileDrop} onDrop={handleFileDrop}
> >
@ -39,7 +40,7 @@ export default function FileUpload({ selectionMessage, onFileSelect, uploadedFil
accept=".pdf" accept=".pdf"
onChange={handleFileChange} onChange={handleFileChange}
className="hidden" className="hidden"
id="fileInput" ref={fileInputRef} // Attachement de la référence
/> />
<label htmlFor="fileInput" className="text-center text-gray-500"> <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-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 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({ parentFileTemplates, columns }) {
return ( return (
<div className="bg-white p-6 rounded-lg shadow-sm border border-gray-200"> <div className="mt-8 mb-4 w-3/5">
<h2 className="text-xl font-bold mb-4 text-gray-800">Fichiers à uploader</h2> <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 <Table
data={parentFileTemplates} data={parentFileTemplates}
columns={columns} columns={columns}
itemsPerPage={5} />
currentPage={1} {selectedFile && (
totalPages={1} <div className="mt-4">
onPageChange={() => {}} {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> </div>
); );

View File

@ -5,23 +5,17 @@ import Button from '@/components/Button';
import DjangoCSRFToken from '@/components/DjangoCSRFToken'; import DjangoCSRFToken from '@/components/DjangoCSRFToken';
import { fetchRegisterForm, fetchSchoolFileTemplatesFromRegistrationFiles, fetchParentFileTemplatesFromRegistrationFiles } from '@/app/actions/subscriptionAction'; import { fetchRegisterForm, fetchSchoolFileTemplatesFromRegistrationFiles, fetchParentFileTemplatesFromRegistrationFiles } from '@/app/actions/subscriptionAction';
import { downloadTemplate, import { downloadTemplate,
createRegistrationSchoolFileTemplate,
editRegistrationSchoolFileTemplates, editRegistrationSchoolFileTemplates,
deleteRegistrationSchoolFileTemplates editRegistrationParentFileTemplates
} from '@/app/actions/registerFileGroupAction'; } from '@/app/actions/registerFileGroupAction';
import { import {
fetchRegistrationPaymentModes, fetchRegistrationPaymentModes,
fetchTuitionPaymentModes } from '@/app/actions/schoolAction'; fetchTuitionPaymentModes } from '@/app/actions/schoolAction';
import { Download, Upload, Trash2, Eye } from 'lucide-react';
import { BASE_URL } from '@/utils/Url'; 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 logger from '@/utils/logger';
import StudentInfoForm, { validateStudentInfo } from '@/components/Inscription/StudentInfoForm'; import StudentInfoForm, { validateStudentInfo } from '@/components/Inscription/StudentInfoForm';
import FilesToUpload from '@/components/Inscription/FilesToUpload'; import FilesToUpload from '@/components/Inscription/FilesToUpload';
import { DocusealForm } from '@docuseal/react'; import { DocusealForm } from '@docuseal/react';
import FileUpload from '@/components/Inscription/FileUpload';
/** /**
* Composant de formulaire d'inscription partagé * Composant de formulaire d'inscription partagé
@ -36,7 +30,6 @@ export default function InscriptionFormShared({
csrfToken, csrfToken,
selectedEstablishmentId, selectedEstablishmentId,
onSubmit, onSubmit,
cancelUrl,
errors = {} // Nouvelle prop pour les erreurs errors = {} // Nouvelle prop pour les erreurs
}) { }) {
// États pour gérer les données du formulaire // États pour gérer les données du formulaire
@ -65,12 +58,6 @@ export default function InscriptionFormShared({
const [uploadedFiles, setUploadedFiles] = useState([]); const [uploadedFiles, setUploadedFiles] = useState([]);
const [schoolFileTemplates, setSchoolFileTemplates] = useState([]); const [schoolFileTemplates, setSchoolFileTemplates] = useState([]);
const [parentFileTemplates, setParentFileTemplates] = 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 [currentPage, setCurrentPage] = useState(1);
const isCurrentPageValid = () => { const isCurrentPageValid = () => {
@ -105,7 +92,6 @@ export default function InscriptionFormShared({
totalTuitionFees: data?.totalTuitionFees, totalTuitionFees: data?.totalTuitionFees,
}); });
setGuardians(data?.student?.guardians || []); setGuardians(data?.student?.guardians || []);
setUploadedFiles(data.registration_files || []);
}); });
setIsLoading(false); setIsLoading(false);
@ -119,6 +105,13 @@ export default function InscriptionFormShared({
fetchParentFileTemplatesFromRegistrationFiles(studentId).then((data) => { fetchParentFileTemplatesFromRegistrationFiles(studentId).then((data) => {
setParentFileTemplates(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) { if (selectedEstablishmentId) {
@ -153,66 +146,74 @@ export default function InscriptionFormShared({
setFormData(prev => ({...prev, [field]: value})); setFormData(prev => ({...prev, [field]: value}));
}; };
// Gestion du téléversement de fichiers const handleFileUpload = (file, selectedFile) => {
const handleFileUpload = async (file, fileName) => { if (!file || !selectedFile) {
if (!file || !currentTemplateId || !formData.id) { logger.error('Données manquantes pour le téléversement.');
logger.error('Missing required data for upload'); 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; return;
} }
const data = new FormData(); // Créer un FormData avec un champ vide pour "file"
data.append('file', file); const updateData = new FormData();
data.append('name', fileName); updateData.append('file', ''); // Envoyer chaine vide pour indiquer qu'aucun fichier n'est uploadé
data.append('template', currentTemplateId);
data.append('register_form', formData.id);
try { return editRegistrationParentFileTemplates(templateId, updateData, csrfToken)
const response = await createRegistrationSchoolFileTemplate(data, csrfToken); .then((response) => {
if (response) { logger.debug('Fichier supprimé avec succès dans la base :', response);
setUploadedFiles(prev => {
const newFiles = prev.filter(f => parseInt(f.template) !== currentTemplateId);
return [...newFiles, {
name: fileName,
template: currentTemplateId,
file: response.file
}];
});
// Rafraîchir les données du formulaire pour avoir les fichiers à jour // Mettre à jour l'état local pour refléter la suppression
if (studentId) { setUploadedFiles((prev) =>
fetchRegisterForm(studentId).then((data) => { prev.map((uploadedFile) =>
setUploadedFiles(data.registration_files || []); uploadedFile.id === templateId
}); ? { ...uploadedFile, fileName: null, fileUrl: null } // Réinitialiser les champs liés au fichier
} : uploadedFile
} )
} catch (error) { );
logger.error('Error uploading file:', error); return response;
} })
}; .catch((error) => {
logger.error('Erreur lors de la suppression du fichier dans la base :', error);
// Vérification si un fichier est déjà uploadé throw error;
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 deleteRegistrationSchoolFileTemplates(fileToDelete.id, csrfToken);
setUploadedFiles(prev => prev.filter(f => parseInt(f.template) !== templateId));
} catch (error) {
logger.error('Error deleting file:', error);
}
}; };
// Soumission du formulaire // Soumission du formulaire
@ -252,67 +253,12 @@ export default function InscriptionFormShared({
setCurrentPage(currentPage - 1); 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 // Affichage du loader pendant le chargement
if (isLoading) return <Loader />; if (isLoading) return <Loader />;
// Rendu du composant // Rendu du composant
return ( return (
<div className="max-w-4xl mx-auto p-6"> <div className="mx-auto p-6">
<form onSubmit={handleSubmit} className="space-y-8"> <form onSubmit={handleSubmit} className="space-y-8">
<DjangoCSRFToken csrfToken={csrfToken}/> <DjangoCSRFToken csrfToken={csrfToken}/>
{/* Page 1 : Informations de l'élève et Responsables */} {/* Page 1 : Informations de l'élève et Responsables */}
@ -330,76 +276,70 @@ export default function InscriptionFormShared({
{/* Pages suivantes : Section Fichiers d'inscription */} {/* Pages suivantes : Section Fichiers d'inscription */}
{currentPage > 1 && currentPage <= schoolFileTemplates.length + 1 && ( {currentPage > 1 && currentPage <= schoolFileTemplates.length + 1 && (
<div className="bg-white p-6 rounded-lg shadow-sm border border-gray-200"> <div className="mt-8 mb-4 w-3/5">
{/* Titre du document */} <div className="bg-white p-6 rounded-lg shadow-sm border border-gray-200">
<div className="mb-4"> {/* Titre du document */}
<h2 className="text-lg font-semibold text-gray-800"> <div className="mb-4">
{schoolFileTemplates[currentPage - 2].name || "Document sans nom"} <h2 className="text-lg font-semibold text-gray-800">
</h2> {schoolFileTemplates[currentPage - 2].name || "Document sans nom"}
<p className="text-sm text-gray-500"> </h2>
{schoolFileTemplates[currentPage - 2].description || "Aucune description disponible pour ce document."} <p className="text-sm text-gray-500">
</p> {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> </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> </div>
)} )}
{/* Dernière page : Section Fichiers parents */} {/* Dernière page : Section Fichiers parents */}
{currentPage === schoolFileTemplates.length + 2 && ( {currentPage === schoolFileTemplates.length + 2 && (
<> <FilesToUpload
<FilesToUpload parentFileTemplates={parentFileTemplates}
parentFileTemplates={parentFileTemplates} uploadedFiles={uploadedFiles}
columns={columns} 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 */} {/* Boutons de contrôle */}
<div className="flex justify-end space-x-4"> <div className="flex justify-center space-x-4">
<Button <Button
text="Sauvegarder" text="Sauvegarder"
onClick={handleSave} onClick={handleSave}
@ -429,52 +369,6 @@ export default function InscriptionFormShared({
)} )}
</div> </div>
</form> </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> </div>
); );
} }

View File

@ -6,14 +6,16 @@ import { BASE_URL } from '@/utils/Url';
import { generateToken } from '@/app/actions/registerFileGroupAction'; import { generateToken } from '@/app/actions/registerFileGroupAction';
import logger from '@/utils/logger'; import logger from '@/utils/logger';
import { GraduationCap, CloudUpload } from 'lucide-react'; 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 [token, setToken] = useState(null);
const [uploadedFileName, setUploadedFileName] = useState(''); const [uploadedFileName, setUploadedFileName] = useState('');
const [selectedFile, setSelectedFile] = useState(null); // Nouvel état pour le fichier sélectionné
const [pdfUrl, setPdfUrl] = useState(`${BASE_URL}/${file}`); const [pdfUrl, setPdfUrl] = useState(`${BASE_URL}/${file}`);
const [isSepa, setIsSepa] = useState(paymentMode === '1'); // Vérifie si le mode de paiement est SEPA const [isSepa, setIsSepa] = useState(paymentSepa); // Vérifie si le mode de paiement est SEPA
const [currentPage, setCurrentPage] = useState(1); // Gestion des pages const [currentPage, setCurrentPage] = useState(1); // Gestion des étapes
useEffect(() => { useEffect(() => {
if (isSepa) { if (isSepa) {
@ -25,34 +27,21 @@ export default function ValidateSubscription({ studentId, firstName, lastName, p
} }
}, [isSepa]); }, [isSepa]);
const handleUpload = (detail) => {
logger.debug('Uploaded file detail:', detail);
setUploadedFileName(detail.name);
};
const handleAccept = () => { const handleAccept = () => {
const fileInput = document.getElementById('fileInput'); // Récupère l'élément input if (!selectedFile && isSepa) {
const file = fileInput?.files[0]; // Récupère le fichier sélectionné
if (!file) {
logger.error('Aucun fichier sélectionné pour le champ SEPA.'); logger.error('Aucun fichier sélectionné pour le champ SEPA.');
return; return;
} }
const data = { const data = {
status: 7, 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 // Appeler la fonction passée par le parent pour mettre à jour le RF
onAccept(data); onAccept(data);
}; };
const handleRefuse = () => {
logger.debug('Dossier refusé pour l\'étudiant:', studentId);
// Logique pour refuser l'inscription
};
const isValidateButtonDisabled = isSepa && !uploadedFileName; const isValidateButtonDisabled = isSepa && !uploadedFileName;
const goToNextPage = () => { const goToNextPage = () => {
@ -68,25 +57,16 @@ export default function ValidateSubscription({ studentId, firstName, lastName, p
}; };
return ( return (
<div className="p-8 space-y-6 bg-gray-50 rounded-lg shadow-lg"> <div className="space-y-6 p-6">
{/* Titre */} <SectionHeader
<div className="flex items-center space-x-4"> icon={GraduationCap}
<div className="bg-emerald-100 p-3 rounded-full shadow-md"> title={`Dossier scolaire de ${firstName} ${lastName}`}
<GraduationCap className="w-8 h-8 text-emerald-600" /> description={`Année scolaire ${new Date().getFullYear()}-${new Date().getFullYear() + 1}`}
</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>
{/* Contenu principal */} {/* Contenu principal */}
{currentPage === 1 && ( {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 <iframe
src={pdfUrl} src={pdfUrl}
title="Aperçu du PDF" title="Aperçu du PDF"
@ -102,9 +82,10 @@ export default function ValidateSubscription({ studentId, firstName, lastName, p
{currentPage === 2 && isSepa && ( {currentPage === 2 && isSepa && (
<FileUpload <FileUpload
selectionMessage='Sélectionnez un mandat de prélèvement SEPA' selectionMessage="Sélectionnez un mandat de prélèvement SEPA"
onFileSelect={(file) => { onFileSelect={(file) => {
setUploadedFileName(file.name); // Stocke uniquement le nom du fichier setUploadedFileName(file.name); // Stocke uniquement le nom du fichier
setSelectedFile(file); // Stocke le fichier dans l'état
logger.debug('Fichier sélectionné:', file.name); logger.debug('Fichier sélectionné:', file.name);
}} }}
uploadedFileName={uploadedFileName} 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" className="bg-gray-300 text-gray-700 hover:bg-gray-400 px-6 py-2"
/> />
)} )}
{currentPage < (isSepa ? 2 : 1) && ( {isSepa && currentPage === 1 && (
<Button <Button
text="Suivant" text="Suivant"
onClick={goToNextPage} 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 ( return (
<div className="w-full"> <div className="w-full">
<div className="flex border-b-2 border-gray-200"> <div className="flex border-b-2 border-gray-200">
{tabs.map(tab => ( {tabs.map((tab) => (
<button <button
key={tab.id} 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)} onClick={() => setActiveTab(tab.id)}
> >
{tab.label} {tab.label}
@ -17,7 +21,7 @@ const SidebarTabs = ({ tabs }) => {
))} ))}
</div> </div>
<div className="p-4"> <div className="p-4">
{tabs.map(tab => ( {tabs.map((tab) => (
<div key={tab.id} className={`${activeTab === tab.id ? 'block' : 'hidden'}`}> <div key={tab.id} className={`${activeTab === tab.id ? 'block' : 'hidden'}`}>
{tab.content} {tab.content}
</div> </div>

View File

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

View File

@ -8,6 +8,7 @@ import { HTML5Backend } from 'react-dnd-html5-backend';
import SpecialityItem from '@/components/Structure/Configuration/SpecialityItem'; import SpecialityItem from '@/components/Structure/Configuration/SpecialityItem';
import { useEstablishment } from '@/context/EstablishmentContext'; import { useEstablishment } from '@/context/EstablishmentContext';
import logger from '@/utils/logger'; import logger from '@/utils/logger';
import SectionHeader from '@/components/SectionHeader';
const SpecialitiesSection = ({ specialities, setSpecialities, handleCreate, handleEdit, handleDelete }) => { const SpecialitiesSection = ({ specialities, setSpecialities, handleCreate, handleEdit, handleDelete }) => {
@ -214,15 +215,13 @@ const SpecialitiesSection = ({ specialities, setSpecialities, handleCreate, hand
return ( return (
<DndProvider backend={HTML5Backend}> <DndProvider backend={HTML5Backend}>
<div className="space-y-4"> <div className="space-y-4">
<div className="flex justify-between items-center"> <SectionHeader
<div className="flex items-center mb-4"> icon={BookOpen}
<BookOpen className="w-6 h-6 text-emerald-500 mr-2" /> title="Liste des spécialités"
<h2 className="text-xl font-semibold">Spécialités</h2> description="Gérez les spécialités de votre école"
</div> button={true}
<button type="button" onClick={handleAddSpeciality} className="text-emerald-500 hover:text-emerald-700"> onClick={handleAddSpeciality}
<Plus className="w-5 h-5" /> />
</button>
</div>
<Table <Table
data={newSpeciality ? [newSpeciality, ...specialities] : specialities} data={newSpeciality ? [newSpeciality, ...specialities] : specialities}
columns={columns} 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 }) => { const StructureManagement = ({ specialities, setSpecialities, teachers, setTeachers, classes, setClasses, profiles, handleCreate, handleEdit, handleDelete }) => {
return ( return (
<div className="max-w-8xl mx-auto p-4 mt-6 space-y-8"> <div className="w-full mx-auto mt-6">
<ClassesProvider> <ClassesProvider>
<div className="w-2/5 p-4 bg-white rounded-lg shadow-md"> <div className="mt-8 w-2/5">
<SpecialitiesSection <SpecialitiesSection
specialities={specialities} specialities={specialities}
setSpecialities={setSpecialities} setSpecialities={setSpecialities}
@ -18,7 +18,7 @@ const StructureManagement = ({ specialities, setSpecialities, teachers, setTeach
handleDelete={(id) => handleDelete(`${BE_SCHOOL_SPECIALITIES_URL}`, id, setSpecialities)} handleDelete={(id) => handleDelete(`${BE_SCHOOL_SPECIALITIES_URL}`, id, setSpecialities)}
/> />
</div> </div>
<div className="w-4/5 p-4 bg-white rounded-lg shadow-md"> <div className="w-4/5 mt-12">
<TeachersSection <TeachersSection
teachers={teachers} teachers={teachers}
setTeachers={setTeachers} setTeachers={setTeachers}
@ -29,7 +29,7 @@ const StructureManagement = ({ specialities, setSpecialities, teachers, setTeach
handleDelete={(id) => handleDelete(`${BE_SCHOOL_TEACHERS_URL}`, id, setTeachers)} handleDelete={(id) => handleDelete(`${BE_SCHOOL_TEACHERS_URL}`, id, setTeachers)}
/> />
</div> </div>
<div className="w-full p-4 bg-white rounded-lg shadow-md"> <div className="w-full mt-12">
<ClassesSection <ClassesSection
classes={classes} classes={classes}
setClasses={setClasses} 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 Table from '@/components/Table';
import Popup from '@/components/Popup'; import Popup from '@/components/Popup';
import ToggleSwitch from '@/components/ToggleSwitch'; import ToggleSwitch from '@/components/ToggleSwitch';
import { createProfile, updateProfile } from '@/app/actions/authAction';
import { useCsrfToken } from '@/context/CsrfContext'; import { useCsrfToken } from '@/context/CsrfContext';
import { DndProvider, useDrag, useDrop } from 'react-dnd'; import { DndProvider, useDrag, useDrop } from 'react-dnd';
import { HTML5Backend } from 'react-dnd-html5-backend'; import { HTML5Backend } from 'react-dnd-html5-backend';
@ -11,8 +10,8 @@ import InputText from '@/components/InputText';
import SpecialityItem from '@/components/Structure/Configuration/SpecialityItem'; import SpecialityItem from '@/components/Structure/Configuration/SpecialityItem';
import TeacherItem from './TeacherItem'; import TeacherItem from './TeacherItem';
import logger from '@/utils/logger'; import logger from '@/utils/logger';
import { fetchProfiles } from '@/app/actions/authAction';
import { useEstablishment } from '@/context/EstablishmentContext'; import { useEstablishment } from '@/context/EstablishmentContext';
import SectionHeader from '@/components/SectionHeader';
const ItemTypes = { const ItemTypes = {
SPECIALITY: 'speciality', SPECIALITY: 'speciality',
@ -468,15 +467,13 @@ const TeachersSection = ({ teachers, setTeachers, specialities, profiles, handle
return ( return (
<DndProvider backend={HTML5Backend}> <DndProvider backend={HTML5Backend}>
<div className="space-y-4"> <div className="space-y-4">
<div className="flex justify-between items-center"> <SectionHeader
<div className="flex items-center mb-4"> icon={GraduationCap}
<GraduationCap className="w-6 h-6 text-emerald-500 mr-2" /> title="Liste des enseignants.es"
<h2 className="text-xl font-semibold">Enseignants</h2> description="Gérez les enseignants.es de votre école"
</div> button={true}
<button type="button" onClick={handleAddTeacher} className="text-emerald-500 hover:text-emerald-700"> onClick={handleAddTeacher}
<Plus className="w-5 h-5" /> />
</button>
</div>
<Table <Table
data={newTeacher ? [newTeacher, ...teachers] : teachers} data={newTeacher ? [newTeacher, ...teachers] : teachers}
columns={columns} columns={columns}

View File

@ -3,6 +3,7 @@ import { Plus, Download, Edit3, Trash2, FolderPlus, Signature, FileText, Check,
import Modal from '@/components/Modal'; import Modal from '@/components/Modal';
import Table from '@/components/Table'; import Table from '@/components/Table';
import FileUploadDocuSeal from '@/components/Structure/Files/FileUploadDocuSeal'; import FileUploadDocuSeal from '@/components/Structure/Files/FileUploadDocuSeal';
import FileUpload from '@/components/FileUpload'
import { BASE_URL } from '@/utils/Url'; import { BASE_URL } from '@/utils/Url';
import { import {
// GET // GET
@ -26,6 +27,7 @@ import {
import RegistrationFileGroupForm from '@/components/Structure/Files/RegistrationFileGroupForm'; import RegistrationFileGroupForm from '@/components/Structure/Files/RegistrationFileGroupForm';
import logger from '@/utils/logger'; import logger from '@/utils/logger';
import ParentFilesSection from '@/components/Structure/Files/ParentFilesSection'; import ParentFilesSection from '@/components/Structure/Files/ParentFilesSection';
import SectionHeader from '@/components/SectionHeader';
export default function FilesGroupsManagement({ csrfToken, selectedEstablishmentId }) { export default function FilesGroupsManagement({ csrfToken, selectedEstablishmentId }) {
const [schoolFileMasters, setSchoolFileMasters] = useState([]); const [schoolFileMasters, setSchoolFileMasters] = useState([]);
@ -42,6 +44,8 @@ export default function FilesGroupsManagement({ csrfToken, selectedEstablishment
const [editingDocumentId, setEditingDocumentId] = useState(null); const [editingDocumentId, setEditingDocumentId] = useState(null);
const [formData, setFormData] = useState({}); const [formData, setFormData] = useState({});
const [uploadedFileName, setUploadedFileName] = useState('');
const handleReloadTemplates = () => { const handleReloadTemplates = () => {
setReloadTemplates(true); 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) => { const handleCreate = (newParentFile) => {
return createRegistrationParentFileMaster(newParentFile, csrfToken) return createRegistrationParentFileMaster(newParentFile, csrfToken)
.then((response) => { .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 ( return (
<div className="space-y-12"> <div className="w-full mx-auto mt-6">
{/* Modal pour les fichiers */} {/* Modal pour les fichiers */}
<Modal <Modal
isOpen={isModalOpen} isOpen={isModalOpen}
@ -460,26 +380,15 @@ export default function FilesGroupsManagement({ csrfToken, selectedEstablishment
/> />
{/* Section Groupes de fichiers */} {/* Section Groupes de fichiers */}
<div className="mt-8 mb-4 w-3/5"> <div className="mt-8 w-3/5">
<div className="flex items-center justify-between mb-6"> <SectionHeader
<div className="flex items-center space-x-4"> icon={FolderPlus}
<div className="bg-emerald-100 p-3 rounded-full shadow-md"> title="Dossiers d'inscriptions"
<FolderPlus className="w-8 h-8 text-emerald-600" /> description="Gérez les dossiers d'inscription pour organiser vos documents."
</div> button={true}
<div> buttonOpeningModal={true}
<h2 className="text-2xl font-bold text-gray-800">Dossiers d'inscriptions</h2> onClick={() => setIsGroupModalOpen(true)}
<p className="text-sm text-gray-500 italic"> />
Gérez les dossiers d'inscription pour organiser vos documents.
</p>
</div>
</div>
<button
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 <Table
data={groups} data={groups}
columns={columnsGroups} columns={columnsGroups}
@ -487,29 +396,18 @@ export default function FilesGroupsManagement({ csrfToken, selectedEstablishment
</div> </div>
{/* Section Fichiers */} {/* Section Fichiers */}
<div className="mt-8 mb-4 w-3/5"> <div className="mt-12 mb-4 w-3/5">
<div className="flex items-center justify-between mb-6"> <SectionHeader
<div className="flex items-center space-x-4"> icon={Signature}
<div className="bg-emerald-100 p-3 rounded-full shadow-md"> title="Formulaires à remplir"
<Signature className="w-8 h-8 text-emerald-600" /> description="Gérez les formulaires nécessitant une signature électronique."
</div> button={true}
<div> buttonOpeningModal={true}
<h2 className="text-2xl font-bold text-gray-800">Formulaires à remplir</h2> onClick={() => {
<p className="text-sm text-gray-500 italic"> setIsModalOpen(true);
Gérez les formulaires nécessitant une signature électronique. setIsEditing(false);
</p> }}
</div> />
</div>
<button
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 <Table
data={filteredFiles} data={filteredFiles}
columns={columnsFiles} columns={columnsFiles}

View File

@ -7,6 +7,7 @@ import Popup from '@/components/Popup';
import logger from '@/utils/logger'; import logger from '@/utils/logger';
import { createRegistrationParentFileTemplate } from '@/app/actions/registerFileGroupAction'; import { createRegistrationParentFileTemplate } from '@/app/actions/registerFileGroupAction';
import { useCsrfToken } from '@/context/CsrfContext'; import { useCsrfToken } from '@/context/CsrfContext';
import SectionHeader from '@/components/SectionHeader';
export default function ParentFilesSection({ parentFiles, groups, handleCreate, handleEdit, handleDelete }) { export default function ParentFilesSection({ parentFiles, groups, handleCreate, handleEdit, handleDelete }) {
const [editingDocumentId, setEditingDocumentId] = useState(null); const [editingDocumentId, setEditingDocumentId] = useState(null);
@ -65,7 +66,6 @@ export default function ParentFilesSection({ parentFiles, groups, handleCreate,
createRegistrationParentFileTemplate(data, csrfToken) createRegistrationParentFileTemplate(data, csrfToken)
.then(response => { .then(response => {
logger.debug('Template enregistré avec succès:', response); logger.debug('Template enregistré avec succès:', response);
onSuccess();
}) })
.catch(error => { .catch(error => {
logger.error('Erreur lors de l\'enregistrement du template:', error); logger.error('Erreur lors de l\'enregistrement du template:', error);
@ -243,26 +243,14 @@ export default function ParentFilesSection({ parentFiles, groups, handleCreate,
]; ];
return ( return (
<div className="mt-8 mb-4 w-4/5"> <div className="mt-12 w-4/5">
<div className="flex items-center justify-between mb-6"> <SectionHeader
<div className="flex items-center space-x-4"> icon={FileText}
<div className="bg-emerald-100 p-3 rounded-full shadow-md"> title="Pièces à fournir"
<FileText className="w-8 h-8 text-emerald-600" /> description="Configurez la liste des documents que les parents doivent fournir."
</div> button={true}
<div> onClick={handleAddEmptyRequiredDocument}
<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
onClick={handleAddEmptyRequiredDocument}
className="text-emerald-500 hover:text-emerald-700"
>
<Plus className="w-6 h-6" />
</button>
</div>
<Table <Table
data={editingDocumentId === 'new' ? [formData, ...parentFiles] : parentFiles} data={editingDocumentId === 'new' ? [formData, ...parentFiles] : parentFiles}
columns={columnsRequiredDocuments} columns={columnsRequiredDocuments}

View File

@ -6,8 +6,9 @@ import CheckBox from '@/components/CheckBox';
import InputText from '@/components/InputText'; import InputText from '@/components/InputText';
import logger from '@/utils/logger'; import logger from '@/utils/logger';
import { ESTABLISHMENT_ID } from '@/utils/Url'; 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 [editingDiscount, setEditingDiscount] = useState(null);
const [newDiscount, setNewDiscount] = useState(null); const [newDiscount, setNewDiscount] = useState(null);
const [formData, setFormData] = useState({}); const [formData, setFormData] = useState({});
@ -253,14 +254,7 @@ const DiscountsSection = ({ discounts, setDiscounts, handleCreate, handleEdit, h
} }
}; };
const columns = subscriptionMode const columns = [
? [
{ name: 'LIBELLE', label: 'Libellé' },
{ name: 'DESCRIPTION', label: 'Description' },
{ name: 'REMISE', label: 'Remise' },
{ name: '', label: 'Sélection' }
]
: [
{ name: 'LIBELLE', label: 'Libellé' }, { name: 'LIBELLE', label: 'Libellé' },
{ name: 'REMISE', label: 'Remise' }, { name: 'REMISE', label: 'Remise' },
{ name: 'DESCRIPTION', label: 'Description' }, { name: 'DESCRIPTION', label: 'Description' },
@ -269,18 +263,15 @@ const DiscountsSection = ({ discounts, setDiscounts, handleCreate, handleEdit, h
]; ];
return ( return (
<div className="space-y-4"> <div className="space-y-4 mt-8">
{!subscriptionMode && ( <SectionHeader
<div className="flex justify-between items-center"> icon={Tag}
<div className="flex items-center mb-4"> discountStyle = {true}
<Tag className="w-6 h-6 text-emerald-500 mr-2" /> title={`${type == 0 ? "Liste des réductions sur les frais d'inscription" : "Liste des réductions sur les frais de scolarité"}`}
<h2 className="text-xl font-semibold">Liste des réductions</h2> description={`${type == 0 ? "Gérez vos réductions sur les frais d'inscription" : "Gérez vos réductions sur les frais de scolarité"}`}
</div> button={true}
<button type="button" onClick={handleAddDiscount} className="text-emerald-500 hover:text-emerald-700"> onClick={handleAddDiscount}
<Plus className="w-5 h-5" /> />
</button>
</div>
)}
<Table <Table
data={newDiscount ? [newDiscount, ...discounts] : discounts} data={newDiscount ? [newDiscount, ...discounts] : discounts}
columns={columns} columns={columns}

View File

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

View File

@ -5,10 +5,11 @@ import Popup from '@/components/Popup';
import CheckBox from '@/components/CheckBox'; import CheckBox from '@/components/CheckBox';
import InputText from '@/components/InputText'; import InputText from '@/components/InputText';
import logger from '@/utils/logger'; import logger from '@/utils/logger';
import SectionHeader from '@/components/SectionHeader';
import { ESTABLISHMENT_ID } from '@/utils/Url'; 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 [editingFee, setEditingFee] = useState(null);
const [newFee, setNewFee] = useState(null); const [newFee, setNewFee] = useState(null);
const [formData, setFormData] = useState({}); const [formData, setFormData] = useState({});
@ -243,14 +244,7 @@ const FeesSection = ({ fees, setFees, discounts, handleCreate, handleEdit, handl
} }
}; };
const columns = subscriptionMode const columns = [
? [
{ name: 'NOM', label: 'Nom' },
{ name: 'DESCRIPTION', label: 'Description' },
{ name: 'MONTANT', label: 'Montant de base' },
{ name: '', label: 'Sélection' }
]
: [
{ name: 'NOM', label: 'Nom' }, { name: 'NOM', label: 'Nom' },
{ name: 'MONTANT', label: 'Montant de base' }, { name: 'MONTANT', label: 'Montant de base' },
{ name: 'DESCRIPTION', label: 'Description' }, { name: 'DESCRIPTION', label: 'Description' },
@ -260,17 +254,13 @@ const FeesSection = ({ fees, setFees, discounts, handleCreate, handleEdit, handl
return ( return (
<div className="space-y-4"> <div className="space-y-4">
{!subscriptionMode && ( <SectionHeader
<div className="flex justify-between items-center"> icon={CreditCard}
<div className="flex items-center mb-4"> title={`${type == 0 ? "Liste des frais d'inscription" : "Liste des frais de scolarité"}`}
<CreditCard className="w-6 h-6 text-emerald-500 mr-2" /> description={`${type == 0 ? "Gérez vos frais d'inscription" : "Gérez vos frais de scolarité"}`}
<h2 className="text-xl font-semibold">Liste des frais</h2> button={true}
</div> onClick={handleAddFee}
<button type="button" onClick={handleAddFee} className="text-emerald-500 hover:text-emerald-700"> />
<Plus className="w-5 h-5" />
</button>
</div>
)}
<Table <Table
data={newFee ? [newFee, ...fees] : fees} data={newFee ? [newFee, ...fees] : fees}
columns={columns} columns={columns}

View File

@ -1,8 +1,8 @@
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types'; 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 Table = ({ data, columns, renderCell, itemsPerPage = 0, currentPage, totalPages, onPageChange, onRowClick, selectedRows, isSelectable = false, defaultTheme = 'bg-emerald-50' }) => {
const handlePageChange = (newPage) => { const handlePageChange = (newPage) => {
onPageChange(newPage); onPageChange(newPage);
}; };
@ -28,10 +28,19 @@ const Table = ({ data, columns, renderCell, itemsPerPage = 0, currentPage, total
${selectedRows?.includes(row.id) ? 'bg-emerald-300 text-white' : rowIndex % 2 === 0 ? `${defaultTheme}` : ''} ${selectedRows?.includes(row.id) ? 'bg-emerald-300 text-white' : rowIndex % 2 === 0 ? `${defaultTheme}` : ''}
${isSelectable ? 'hover:bg-emerald-200' : ''} ${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) => ( {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'}`} > <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'}`}>
{renderCell ? renderCell(row, column.name) : column.transform(row)} {renderCell ? renderCell(row, column.name) : column.transform(row)}
</td> </td>
))} ))}
@ -61,6 +70,10 @@ Table.propTypes = {
currentPage: PropTypes.number.isRequired, currentPage: PropTypes.number.isRequired,
totalPages: PropTypes.number.isRequired, totalPages: PropTypes.number.isRequired,
onPageChange: PropTypes.func.isRequired, onPageChange: PropTypes.func.isRequired,
onRowClick: PropTypes.func,
selectedRows: PropTypes.arrayOf(PropTypes.any),
isSelectable: PropTypes.bool,
defaultTheme: PropTypes.string,
}; };
export default Table; export default Table;