mirror of
https://git.v0id.ovh/n3wt-innov/n3wt-school.git
synced 2026-01-28 23:43:22 +00:00
feat: Upload du SEPA par les parents / Création d'un composant header
pour les titres de tableau
This commit is contained in:
@ -4,11 +4,22 @@ from django.template.loader import get_template
|
||||
|
||||
from xhtml2pdf import pisa
|
||||
|
||||
class PDFResult:
|
||||
def __init__(self, content):
|
||||
self.content = content
|
||||
|
||||
def render_to_pdf(template_src, context_dict={}):
|
||||
"""
|
||||
Génère un PDF à partir d'un template HTML et retourne le contenu en mémoire.
|
||||
"""
|
||||
template = get_template(template_src)
|
||||
html = template.render(context_dict)
|
||||
html = template.render(context_dict)
|
||||
result = BytesIO()
|
||||
pdf = pisa.pisaDocument(BytesIO(html.encode("UTF-8")), result)
|
||||
|
||||
if pdf.err:
|
||||
return HttpResponse("Invalid PDF", status_code=400, content_type='text/plain')
|
||||
return HttpResponse(result.getvalue(), content_type='application/pdf')
|
||||
# Lever une exception ou retourner None en cas d'erreur
|
||||
raise ValueError("Erreur lors de la génération du PDF.")
|
||||
|
||||
# Retourner le contenu du PDF en mémoire
|
||||
return PDFResult(result.getvalue())
|
||||
@ -9,6 +9,8 @@ from Establishment.models import Establishment
|
||||
|
||||
from datetime import datetime
|
||||
|
||||
import os
|
||||
|
||||
class Language(models.Model):
|
||||
"""
|
||||
Représente une langue parlée par l’élève.
|
||||
@ -231,8 +233,11 @@ class RegistrationForm(models.Model):
|
||||
# Appeler la méthode save originale
|
||||
super().save(*args, **kwargs)
|
||||
|
||||
def registration_file_upload_to(instance, filename):
|
||||
return f"registration_files/dossier_rf_{instance.registration_form.pk}/{filename}"
|
||||
def registration_school_file_upload_to(instance, filename):
|
||||
return f"registration_files/dossier_rf_{instance.registration_form.pk}/school/{filename}"
|
||||
|
||||
def registration_parent_file_upload_to(instance, filename):
|
||||
return f"registration_files/dossier_rf_{instance.registration_form.pk}/parent/{filename}"
|
||||
|
||||
#############################################################
|
||||
####################### MASTER FILES ########################
|
||||
@ -265,7 +270,7 @@ class RegistrationSchoolFileTemplate(models.Model):
|
||||
slug = models.CharField(max_length=255, default="")
|
||||
name = models.CharField(max_length=255, default="")
|
||||
registration_form = models.ForeignKey(RegistrationForm, on_delete=models.CASCADE, related_name='school_file_templates', blank=True)
|
||||
file = models.FileField(null=True,blank=True, upload_to=registration_file_upload_to)
|
||||
file = models.FileField(null=True,blank=True, upload_to=registration_school_file_upload_to)
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
@ -285,17 +290,31 @@ class RegistrationSchoolFileTemplate(models.Model):
|
||||
class RegistrationParentFileTemplate(models.Model):
|
||||
master = models.ForeignKey(RegistrationParentFileMaster, on_delete=models.CASCADE, related_name='parent_file_templates', blank=True)
|
||||
registration_form = models.ForeignKey(RegistrationForm, on_delete=models.CASCADE, related_name='parent_file_templates', blank=True)
|
||||
file = models.FileField(null=True,blank=True, upload_to=registration_file_upload_to)
|
||||
file = models.FileField(null=True,blank=True, upload_to=registration_parent_file_upload_to)
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
if self.pk: # Si l'objet existe déjà dans la base de données
|
||||
try:
|
||||
old_instance = RegistrationParentFileTemplate.objects.get(pk=self.pk)
|
||||
if old_instance.file and (not self.file or self.file.name == ''):
|
||||
if os.path.exists(old_instance.file.path):
|
||||
old_instance.file.delete(save=False)
|
||||
self.file = None
|
||||
else:
|
||||
print(f"Le fichier {old_instance.file.path} n'existe pas.")
|
||||
except RegistrationParentFileTemplate.DoesNotExist:
|
||||
print("Ancienne instance introuvable.")
|
||||
super().save(*args, **kwargs)
|
||||
|
||||
@staticmethod
|
||||
def get_files_from_rf(register_form_id):
|
||||
"""
|
||||
Récupère tous les fichiers liés à un dossier d’inscription donné.
|
||||
"""
|
||||
registration_files = RegistrationSchoolFileTemplate.objects.filter(registration_form=register_form_id)
|
||||
registration_files = RegistrationParentFileTemplate.objects.filter(registration_form=register_form_id)
|
||||
filenames = []
|
||||
for reg_file in registration_files:
|
||||
filenames.append(reg_file.file.path)
|
||||
|
||||
@ -38,14 +38,14 @@ class RegistrationSchoolFileTemplateSerializer(serializers.ModelSerializer):
|
||||
|
||||
class RegistrationParentFileTemplateSerializer(serializers.ModelSerializer):
|
||||
id = serializers.IntegerField(required=False)
|
||||
file = serializers.SerializerMethodField()
|
||||
file_url = serializers.SerializerMethodField()
|
||||
master_name = serializers.CharField(source='master.name', read_only=True)
|
||||
master_description = serializers.CharField(source='master.description', read_only=True)
|
||||
class Meta:
|
||||
model = RegistrationParentFileTemplate
|
||||
fields = '__all__'
|
||||
|
||||
def get_file(self, obj):
|
||||
def get_file_url(self, obj):
|
||||
# Retourne l'URL complète du fichier si disponible
|
||||
return obj.file.url if obj.file else None
|
||||
|
||||
|
||||
@ -19,6 +19,9 @@ from rest_framework.parsers import JSONParser
|
||||
from PyPDF2 import PdfMerger
|
||||
|
||||
import shutil
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
def recupereListeFichesInscription():
|
||||
"""
|
||||
@ -121,7 +124,6 @@ def rfToPDF(registerForm, filename):
|
||||
Génère le PDF d'un dossier d'inscription et l'associe au RegistrationForm.
|
||||
"""
|
||||
filename = filename.replace(" ", "_")
|
||||
|
||||
data = {
|
||||
'pdf_title': f"Dossier d'inscription de {registerForm.student.first_name}",
|
||||
'signatureDate': convertToStr(_now(), '%d-%m-%Y'),
|
||||
@ -131,20 +133,37 @@ def rfToPDF(registerForm, filename):
|
||||
|
||||
# Générer le PDF
|
||||
pdf = renderers.render_to_pdf('pdfs/dossier_inscription.html', data)
|
||||
if not pdf:
|
||||
raise ValueError("Erreur lors de la génération du PDF.")
|
||||
|
||||
# Vérifier si un fichier avec le même nom existe déjà et le supprimer
|
||||
if registerForm.registration_file and os.path.exists(registerForm.registration_file.path):
|
||||
os.remove(registerForm.registration_file.path)
|
||||
registerForm.registration_file.delete(save=False)
|
||||
if registerForm.registration_file and registerForm.registration_file.name:
|
||||
# Vérifiez si le chemin est déjà absolu ou relatif
|
||||
if os.path.isabs(registerForm.registration_file.name):
|
||||
existing_file_path = registerForm.registration_file.name
|
||||
else:
|
||||
existing_file_path = os.path.join(settings.MEDIA_ROOT, registerForm.registration_file.name.lstrip('/'))
|
||||
|
||||
# Vérifier si le fichier existe et le supprimer
|
||||
if os.path.exists(existing_file_path):
|
||||
print(f'exist ! REMOVE')
|
||||
os.remove(existing_file_path)
|
||||
registerForm.registration_file.delete(save=False)
|
||||
else:
|
||||
print(f'File does not exist: {existing_file_path}')
|
||||
|
||||
# Enregistrer directement le fichier dans le champ registration_file
|
||||
registerForm.registration_file.save(
|
||||
os.path.basename(filename),
|
||||
File(BytesIO(pdf.content)), # Utilisation de BytesIO pour éviter l'écriture sur le disque
|
||||
save=True
|
||||
)
|
||||
try:
|
||||
registerForm.registration_file.save(
|
||||
os.path.basename(filename), # Utiliser uniquement le nom de fichier
|
||||
File(BytesIO(pdf.content)),
|
||||
save=True
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"Erreur lors de la sauvegarde du fichier PDF : {e}")
|
||||
raise
|
||||
|
||||
return registerForm.registration_file.path
|
||||
return registerForm.registration_file
|
||||
|
||||
def delete_registration_files(registerForm):
|
||||
"""
|
||||
|
||||
@ -254,30 +254,37 @@ class RegisterFormWithIdView(APIView):
|
||||
if _status == RegistrationForm.RegistrationFormStatus.RF_UNDER_REVIEW:
|
||||
try:
|
||||
# Génération de la fiche d'inscription au format PDF
|
||||
base_dir = f"data/registration_files/dossier_rf_{registerForm.pk}"
|
||||
base_dir = os.path.join(settings.MEDIA_ROOT, f"registration_files/dossier_rf_{registerForm.pk}")
|
||||
os.makedirs(base_dir, exist_ok=True)
|
||||
|
||||
# Fichier PDF initial
|
||||
initial_pdf = f"{base_dir}/rf_{registerForm.student.last_name}_{registerForm.student.first_name}.pdf"
|
||||
initial_pdf = f"{base_dir}/Inscription_{registerForm.student.last_name}_{registerForm.student.first_name}.pdf"
|
||||
registerForm.registration_file = util.rfToPDF(registerForm, initial_pdf)
|
||||
registerForm.save()
|
||||
|
||||
# Récupération des fichiers d'inscription
|
||||
fileNames = RegistrationSchoolFileTemplate.get_files_from_rf(registerForm.pk)
|
||||
if registerForm.registration_file:
|
||||
fileNames.insert(0, registerForm.registration_file.path)
|
||||
# fileNames = RegistrationSchoolFileTemplate.get_files_from_rf(registerForm.pk)
|
||||
# if registerForm.registration_file:
|
||||
# fileNames.insert(0, registerForm.registration_file.path)
|
||||
|
||||
# Création du fichier PDF Fusionné
|
||||
merged_pdf_content = util.merge_files_pdf(fileNames)
|
||||
# # Création du fichier PDF Fusionné
|
||||
# merged_pdf_content = util.merge_files_pdf(fileNames)
|
||||
|
||||
# # Mise à jour du champ registration_file avec le fichier fusionné
|
||||
# registerForm.registration_file.save(
|
||||
# f"dossier_complet.pdf",
|
||||
# File(merged_pdf_content),
|
||||
# save=True
|
||||
# )
|
||||
|
||||
# Mise à jour du champ registration_file avec le fichier fusionné
|
||||
registerForm.registration_file.save(
|
||||
f"dossier_complet_{registerForm.pk}.pdf",
|
||||
File(merged_pdf_content),
|
||||
save=True
|
||||
)
|
||||
# Mise à jour de l'automate
|
||||
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:
|
||||
return JsonResponse({'error': str(e)}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
|
||||
|
||||
|
||||
@ -285,7 +285,8 @@ class RegistrationParentFileTemplateSimpleView(APIView):
|
||||
template = bdd.getObject(_objectName=RegistrationParentFileTemplate, _columnName='id', _value=id)
|
||||
if template is None:
|
||||
return JsonResponse({'erreur': 'Le template d\'inscription n\'a pas été trouvé'}, safe=False, status=status.HTTP_404_NOT_FOUND)
|
||||
serializer = RegistrationParentFileTemplateSerializer(template, data=request.data)
|
||||
|
||||
serializer = RegistrationParentFileTemplateSerializer(template, data=request.data, partial=True)
|
||||
if serializer.is_valid():
|
||||
serializer.save()
|
||||
return Response({'message': 'Template mis à jour avec succès', 'data': serializer.data}, status=status.HTTP_200_OK)
|
||||
|
||||
@ -96,6 +96,13 @@ class ChildrenListView(APIView):
|
||||
|
||||
students = bdd.getObjects(_objectName=RegistrationForm, _columnName='student__guardians__profile_role__profile__id', _value=id)
|
||||
if students:
|
||||
students = students.filter(establishment=establishment_id).distinct()
|
||||
students = students.filter(
|
||||
establishment=establishment_id,
|
||||
status__in=[
|
||||
RegistrationForm.RegistrationFormStatus.RF_SENT,
|
||||
RegistrationForm.RegistrationFormStatus.RF_UNDER_REVIEW,
|
||||
RegistrationForm.RegistrationFormStatus.RF_SEPA_SENT
|
||||
]
|
||||
).distinct()
|
||||
students_serializer = RegistrationFormByParentSerializer(students, many=True)
|
||||
return JsonResponse(students_serializer.data, safe=False)
|
||||
|
||||
@ -22,7 +22,7 @@ import {
|
||||
fetchTuitionPaymentPlans,
|
||||
fetchRegistrationPaymentModes,
|
||||
fetchTuitionPaymentModes } from '@/app/actions/schoolAction';
|
||||
import { fetchProfileRoles, fetchProfiles } from '@/app/actions/authAction';
|
||||
import { fetchProfiles } from '@/app/actions/authAction';
|
||||
import SidebarTabs from '@/components/SidebarTabs';
|
||||
import FilesGroupsManagement from '@/components/Structure/Files/FilesGroupsManagement';
|
||||
import { fetchRegistrationSchoolFileMasters } from "@/app/actions/registerFileGroupAction";
|
||||
@ -258,7 +258,7 @@ export default function Page() {
|
||||
const tabs = [
|
||||
{
|
||||
id: 'Configuration',
|
||||
label: "Configuration de l'école",
|
||||
label: "Classes",
|
||||
content: (
|
||||
<StructureManagement
|
||||
specialities={specialities}
|
||||
@ -276,7 +276,7 @@ export default function Page() {
|
||||
},
|
||||
{
|
||||
id: 'Schedule',
|
||||
label: "Gestion de l'emploi du temps",
|
||||
label: "Emploi du temps",
|
||||
content: (
|
||||
<ClassesProvider>
|
||||
<ScheduleManagement
|
||||
@ -288,7 +288,7 @@ export default function Page() {
|
||||
},
|
||||
{
|
||||
id: 'Fees',
|
||||
label: 'Tarifications',
|
||||
label: 'Tarifs',
|
||||
content: (
|
||||
<FeesManagement
|
||||
registrationDiscounts={registrationDiscounts}
|
||||
@ -315,13 +315,13 @@ export default function Page() {
|
||||
},
|
||||
{
|
||||
id: 'Files',
|
||||
label: 'Documents d\'inscription',
|
||||
label: 'Documents',
|
||||
content: <FilesGroupsManagement csrfToken={csrfToken} selectedEstablishmentId={selectedEstablishmentId} />
|
||||
}
|
||||
];
|
||||
|
||||
return (
|
||||
<div className='p-8'>
|
||||
<div className='p-4'>
|
||||
<DjangoCSRFToken csrfToken={csrfToken} />
|
||||
|
||||
<div className="w-full p-4">
|
||||
|
||||
@ -9,7 +9,7 @@ import Popup from '@/components/Popup';
|
||||
import Loader from '@/components/Loader';
|
||||
import AlertWithModal from '@/components/AlertWithModal';
|
||||
import DropdownMenu from "@/components/DropdownMenu";
|
||||
import { MoreVertical, Send, Edit, Archive, FileText, CircleCheck, Plus, XCircle } from 'lucide-react';
|
||||
import { MoreVertical, Send, Edit, Archive, FileText, CheckCircle, Plus, XCircle } from 'lucide-react';
|
||||
import Modal from '@/components/Modal';
|
||||
import InscriptionForm from '@/components/Inscription/InscriptionForm'
|
||||
import AffectationClasseForm from '@/components/AffectationClasseForm'
|
||||
@ -606,35 +606,62 @@ useEffect(()=>{
|
||||
const actions = {
|
||||
1: [
|
||||
{
|
||||
icon: <Edit className="w-5 h-5 text-blue-500 hover:text-blue-700" />,
|
||||
icon: (
|
||||
<span title="Editer le dossier">
|
||||
<Edit className="w-5 h-5 text-blue-500 hover:text-blue-700" />
|
||||
</span>
|
||||
),
|
||||
onClick: () => window.location.href = `${FE_ADMIN_SUBSCRIPTIONS_EDIT_URL}?studentId=${row.student.id}&id=1`,
|
||||
},
|
||||
{
|
||||
icon: <Send className="w-5 h-5 text-green-500 hover:text-green-700" />,
|
||||
icon: (
|
||||
<span title="Envoyer le dossier">
|
||||
<Send className="w-5 h-5 text-green-500 hover:text-green-700" />
|
||||
</span>
|
||||
),
|
||||
onClick: () => sendConfirmRegisterForm(row.student.id, row.student.last_name, row.student.first_name),
|
||||
},
|
||||
],
|
||||
2: [
|
||||
{
|
||||
icon: <Edit className="w-5 h-5 text-blue-500 hover:text-blue-700" />,
|
||||
icon: (
|
||||
<span title="Editer le dossier">
|
||||
<Edit className="w-5 h-5 text-blue-500 hover:text-blue-700" />
|
||||
</span>
|
||||
),
|
||||
onClick: () => window.location.href = `${FE_ADMIN_SUBSCRIPTIONS_EDIT_URL}?studentId=${row.student.id}&id=1`,
|
||||
},
|
||||
],
|
||||
3: [
|
||||
{
|
||||
icon: <CircleCheck className="w-5 h-5 text-green-500 hover:text-green-700" />,
|
||||
onClick: () => window.location.href = `${FE_ADMIN_SUBSCRIPTIONS_VALIDATE_URL}?studentId=${row.student.id}&firstName=${row.student.first_name}&lastName=${row.student.last_name}&paymentMode=${row.registration_payment}&file=${row.registration_file}`,
|
||||
icon: (
|
||||
<span title="Valider le dossier">
|
||||
<CheckCircle className="w-5 h-5 text-green-500 hover:text-green-700" />
|
||||
</span>
|
||||
),
|
||||
onClick: () => {
|
||||
const paymentSepa = row.registration_payment === 1 || row.tuition_payment === 1 ? 1 : 0;
|
||||
window.location.href = `${FE_ADMIN_SUBSCRIPTIONS_VALIDATE_URL}?studentId=${row.student.id}&firstName=${row.student.first_name}&lastName=${row.student.last_name}&paymentSepa=${paymentSepa}&file=${row.registration_file}`
|
||||
}
|
||||
},
|
||||
],
|
||||
5: [
|
||||
{
|
||||
icon: <CircleCheck className="w-5 h-5 text-green-500 hover:text-green-700" />,
|
||||
icon: (
|
||||
<span title="Valider le dossier">
|
||||
<CheckCircle className="w-5 h-5 text-green-500 hover:text-green-700" />
|
||||
</span>
|
||||
),
|
||||
onClick: () => openModalAssociationEleve(row.student),
|
||||
},
|
||||
],
|
||||
default: [
|
||||
{
|
||||
icon: <Archive className="w-5 h-5 text-gray-500 hover:text-gray-700" />,
|
||||
icon: (
|
||||
<span title="Archiver le dossier">
|
||||
<Archive className="w-5 h-5 text-gray-500 hover:text-gray-700" />
|
||||
</span>
|
||||
),
|
||||
onClick: () => archiveFicheInscription(row.student.id, row.student.last_name, row.student.first_name),
|
||||
},
|
||||
],
|
||||
@ -674,15 +701,35 @@ const columns = [
|
||||
</div>
|
||||
)
|
||||
},
|
||||
{ name: t('files'), transform: (row) =>
|
||||
(row.registration_file != null) &&(
|
||||
<ul>
|
||||
<li className="flex justify-center items-center gap-2">
|
||||
{ name: t('files'), transform: (row) => (
|
||||
<ul>
|
||||
{row.registration_file && (
|
||||
<li className="flex justify-center items-center gap-2">
|
||||
<FileText size={16} />
|
||||
<a href={ `${BASE_URL}${row.registration_file}`} target='_blank'>{row.registration_file?.split('/').pop()}</a>
|
||||
<a
|
||||
href={`${BASE_URL}${row.registration_file}`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
{row.registration_file?.split('/').pop()}
|
||||
</a>
|
||||
</li>
|
||||
)}
|
||||
{row.sepa_file && (
|
||||
<li className="flex justify-center items-center gap-2">
|
||||
<FileText size={16} />
|
||||
<a
|
||||
href={`${BASE_URL}${row.sepa_file}`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
{row.sepa_file?.split('/').pop()}
|
||||
</a>
|
||||
</li>
|
||||
)}
|
||||
</ul>
|
||||
) },
|
||||
)
|
||||
},
|
||||
{ name: 'Actions',
|
||||
transform: (row) => (
|
||||
<div className="flex justify-center space-x-2">
|
||||
@ -728,7 +775,7 @@ const columnsSubscribed = [
|
||||
items={[
|
||||
{ label: (
|
||||
<>
|
||||
<CircleCheck size={16} className="mr-2" /> Rattacher
|
||||
<CheckCircle size={16} className="mr-2" /> Rattacher
|
||||
</>
|
||||
),
|
||||
onClick: () => openModalAssociationEleve(row.student)
|
||||
|
||||
@ -15,7 +15,7 @@ export default function Page() {
|
||||
const studentId = searchParams.get('studentId');
|
||||
const firstName = searchParams.get('firstName');
|
||||
const lastName = searchParams.get('lastName');
|
||||
const paymentMode = searchParams.get('paymentMode');
|
||||
const paymentSepa = searchParams.get('paymentSepa') === '1';
|
||||
const file = searchParams.get('file');
|
||||
|
||||
const csrfToken = useCsrfToken();
|
||||
@ -45,7 +45,7 @@ export default function Page() {
|
||||
studentId={studentId}
|
||||
firstName={firstName}
|
||||
lastName={lastName}
|
||||
paymentMode={paymentMode}
|
||||
paymentSepa={paymentSepa}
|
||||
file={file}
|
||||
onAccept={handleAcceptRF}
|
||||
/>
|
||||
|
||||
@ -2,14 +2,16 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import Table from '@/components/Table';
|
||||
import { Edit3, Users, Download, Eye } from 'lucide-react';
|
||||
import { Edit3, Users, Download, Eye, Upload, CheckCircle } from 'lucide-react';
|
||||
import StatusLabel from '@/components/StatusLabel';
|
||||
import FileUpload from '@/components/FileUpload';
|
||||
import { FE_PARENTS_EDIT_INSCRIPTION_URL } from '@/utils/Url';
|
||||
import { fetchChildren } from '@/app/actions/subscriptionAction';
|
||||
import { fetchChildren, sendSEPARegisterForm } from '@/app/actions/subscriptionAction';
|
||||
import logger from '@/utils/logger';
|
||||
import { useSession } from 'next-auth/react';
|
||||
import { FE_USERS_LOGIN_URL, BASE_URL } from '@/utils/Url';
|
||||
import { useEstablishment } from '@/context/EstablishmentContext';
|
||||
import { useCsrfToken } from '@/context/CsrfContext';
|
||||
|
||||
export default function ParentHomePage() {
|
||||
const [children, setChildren] = useState([]);
|
||||
@ -18,8 +20,11 @@ export default function ParentHomePage() {
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
const [establishments, setEstablishments] = useState([]);
|
||||
const { selectedEstablishmentId, setSelectedEstablishmentId } = useEstablishment();
|
||||
|
||||
const [uploadingStudentId, setUploadingStudentId] = useState(null); // ID de l'étudiant pour l'upload
|
||||
const [uploadedFile, setUploadedFile] = useState(null); // Fichier uploadé
|
||||
const [uploadState, setUploadState] = useState("off"); // État "on" ou "off" pour l'affichage du composant
|
||||
const router = useRouter();
|
||||
const csrfToken = useCsrfToken();
|
||||
|
||||
useEffect(() => {
|
||||
if (status === 'loading') return;
|
||||
@ -55,22 +60,57 @@ export default function ParentHomePage() {
|
||||
};
|
||||
|
||||
function handleView(eleveId) {
|
||||
// Logique pour éditer le dossier de l'élève
|
||||
logger.debug(`View dossier for student id: ${eleveId}`);
|
||||
router.push(`${FE_PARENTS_EDIT_INSCRIPTION_URL}?id=${userId}&studentId=${eleveId}&view=true`);
|
||||
}
|
||||
|
||||
function handleEdit(eleveId) {
|
||||
// Logique pour éditer le dossier de l'élève
|
||||
logger.debug(`Edit dossier for student id: ${eleveId}`);
|
||||
router.push(`${FE_PARENTS_EDIT_INSCRIPTION_URL}?id=${userId}&studentId=${eleveId}`);
|
||||
}
|
||||
|
||||
const actionColumns = [
|
||||
{ name: 'Action', transform: (row) => row.action },
|
||||
];
|
||||
const handleFileUpload = (file) => {
|
||||
if (!file) {
|
||||
logger.error('Aucun fichier sélectionné pour l\'upload.');
|
||||
return;
|
||||
}
|
||||
setUploadedFile(file); // Conserve le fichier en mémoire
|
||||
logger.debug('Fichier sélectionné :', file.name);
|
||||
};
|
||||
|
||||
const handleSubmit = () => {
|
||||
if (!uploadedFile || !uploadingStudentId) {
|
||||
logger.error('Aucun fichier ou étudiant sélectionné.');
|
||||
return;
|
||||
}
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append('sepa_file', uploadedFile); // Ajoute le fichier SEPA
|
||||
formData.append('status', 3); // Statut à envoyer
|
||||
|
||||
sendSEPARegisterForm(uploadingStudentId, formData, csrfToken)
|
||||
.then((response) => {
|
||||
logger.debug('RF mis à jour avec succès:', response);
|
||||
// Logique supplémentaire après la mise à jour (par exemple, redirection ou notification)
|
||||
})
|
||||
.catch((error) => {
|
||||
logger.error('Erreur lors de la mise à jour du RF:', error);
|
||||
});
|
||||
};
|
||||
|
||||
const toggleUpload = (studentId) => {
|
||||
if (uploadingStudentId === studentId && uploadState === "on") {
|
||||
// Si le composant est déjà affiché pour cet étudiant, on le masque
|
||||
setUploadState("off");
|
||||
setUploadingStudentId(null);
|
||||
setUploadedFile(null); // Réinitialise le fichier
|
||||
} else {
|
||||
// Sinon, on l'affiche pour cet étudiant
|
||||
setUploadState("on");
|
||||
setUploadingStudentId(studentId);
|
||||
}
|
||||
};
|
||||
|
||||
// Définir les colonnes du tableau
|
||||
const childrenColumns = [
|
||||
{ name: 'Nom', transform: (row) => `${row.student.last_name}` },
|
||||
{ name: 'Prénom', transform: (row) => `${row.student.first_name}` },
|
||||
@ -86,13 +126,12 @@ export default function ParentHomePage() {
|
||||
name: 'Actions',
|
||||
transform: (row) => (
|
||||
<div className="flex justify-center items-center gap-2">
|
||||
{/* Actions en fonction du statut */}
|
||||
{row.status === 2 && (
|
||||
<button
|
||||
className="text-blue-500 hover:text-blue-700"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handleEdit(row.student.id); // Remplir le dossier
|
||||
handleEdit(row.student.id);
|
||||
}}
|
||||
aria-label="Remplir le dossier"
|
||||
>
|
||||
@ -105,7 +144,7 @@ export default function ParentHomePage() {
|
||||
className="text-purple-500 hover:text-purple-700"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handleView(row.student.id); // Visualiser le dossier
|
||||
handleView(row.student.id);
|
||||
}}
|
||||
aria-label="Visualiser le dossier"
|
||||
>
|
||||
@ -116,24 +155,38 @@ export default function ParentHomePage() {
|
||||
{row.status === 7 && (
|
||||
<>
|
||||
<button
|
||||
className="text-purple-500 hover:text-purple-700"
|
||||
className="flex items-center justify-center w-8 h-8 rounded-full text-purple-500 hover:text-purple-700"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handleView(row.student.id); // Visualiser le dossier
|
||||
handleView(row.student.id);
|
||||
}}
|
||||
aria-label="Visualiser le dossier"
|
||||
>
|
||||
<Eye className="h-5 w-5" />
|
||||
</button>
|
||||
<a
|
||||
href={`${BASE_URL}${row.sepa_file}`} // Télécharger le mandat SEPA
|
||||
href={`${BASE_URL}${row.sepa_file}`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-green-500 hover:text-green-700"
|
||||
className="flex items-center justify-center w-8 h-8 rounded-full text-green-500 hover:text-green-700"
|
||||
aria-label="Télécharger le mandat SEPA"
|
||||
>
|
||||
<Download className="h-5 w-5" />
|
||||
</a>
|
||||
{/* Nouvelle action Upload */}
|
||||
<button
|
||||
className={`flex items-center justify-center w-8 h-8 rounded-full ${
|
||||
uploadingStudentId === row.student.id && uploadState === "on" ? 'bg-blue-100 text-blue-600 ring-3 ring-blue-500'
|
||||
: 'text-blue-500 hover:text-blue-700'
|
||||
}`}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
toggleUpload(row.student.id); // Activer ou désactiver l'upload pour cet étudiant
|
||||
}}
|
||||
aria-label="Uploader un fichier"
|
||||
>
|
||||
<Upload className="h-5 w-5" />
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
@ -141,13 +194,6 @@ export default function ParentHomePage() {
|
||||
}
|
||||
];
|
||||
|
||||
const itemsPerPage = 5;
|
||||
const totalPages = Math.ceil(children.length / itemsPerPage) || 1;
|
||||
|
||||
const handlePageChange = (newPage) => {
|
||||
setCurrentPage(newPage);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="px-2 py-4 md:px-4 max-w-full">
|
||||
<div>
|
||||
@ -175,13 +221,27 @@ export default function ParentHomePage() {
|
||||
<Table
|
||||
data={children}
|
||||
columns={childrenColumns}
|
||||
itemsPerPage={itemsPerPage}
|
||||
currentPage={currentPage}
|
||||
totalPages={totalPages}
|
||||
onPageChange={handlePageChange}
|
||||
defaultTheme="bg-gray-50"
|
||||
/>
|
||||
</div>
|
||||
{/* Composant FileUpload et bouton Valider en dessous du tableau */}
|
||||
{uploadState === "on" && uploadingStudentId && (
|
||||
<div className="mt-4">
|
||||
<FileUpload
|
||||
selectionMessage="Sélectionnez un fichier à uploader"
|
||||
onFileSelect={handleFileUpload}
|
||||
/>
|
||||
<button
|
||||
className={`mt-4 px-6 py-2 rounded-md ${
|
||||
uploadedFile ? 'bg-emerald-500 text-white hover:bg-emerald-600' : 'bg-gray-300 text-gray-700 cursor-not-allowed'
|
||||
}`}
|
||||
onClick={handleSubmit}
|
||||
disabled={!uploadedFile}
|
||||
>
|
||||
Valider
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@ -229,6 +229,18 @@ export const editRegistrationSchoolFileTemplates = (fileId, data, csrfToken) =>
|
||||
.then(requestResponseHandler)
|
||||
};
|
||||
|
||||
export const editRegistrationParentFileTemplates = (fileId, data, csrfToken) => {
|
||||
return fetch(`${BE_SUBSCRIPTION_REGISTRATION_PARENT_FILE_TEMPLATES_URL}/${fileId}`, {
|
||||
method: 'PUT',
|
||||
body: data,
|
||||
headers: {
|
||||
'X-CSRFToken': csrfToken,
|
||||
},
|
||||
credentials: 'include',
|
||||
})
|
||||
.then(requestResponseHandler)
|
||||
};
|
||||
|
||||
// DELETE requests
|
||||
|
||||
export async function deleteRegistrationFileGroup(groupId, csrfToken) {
|
||||
@ -243,7 +255,7 @@ export async function deleteRegistrationFileGroup(groupId, csrfToken) {
|
||||
return response;
|
||||
};
|
||||
|
||||
export const deleteRegistrationSchoolFileMaster = (fileId,csrfToken) => {
|
||||
export const deleteRegistrationSchoolFileMaster = (fileId, csrfToken) => {
|
||||
return fetch(`${BE_SUBSCRIPTION_REGISTRATION_SCHOOL_FILE_MASTERS_URL}/${fileId}`, {
|
||||
method: 'DELETE',
|
||||
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}`, {
|
||||
method: 'DELETE',
|
||||
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
|
||||
|
||||
export const cloneTemplate = (templateId, email, is_required) => {
|
||||
|
||||
@ -1,9 +1,10 @@
|
||||
import React, { useState } from 'react';
|
||||
import React, { useState, useRef } from 'react';
|
||||
import { CloudUpload } from 'lucide-react';
|
||||
import logger from '@/utils/logger';
|
||||
|
||||
export default function FileUpload({ selectionMessage, onFileSelect, uploadedFileName }) {
|
||||
const [localFileName, setLocalFileName] = useState(uploadedFileName || '');
|
||||
const fileInputRef = useRef(null); // Utilisation de useRef pour cibler l'input
|
||||
|
||||
const handleFileChange = (e) => {
|
||||
const file = e.target.files[0];
|
||||
@ -29,7 +30,7 @@ export default function FileUpload({ selectionMessage, onFileSelect, uploadedFil
|
||||
<h3 className="text-lg font-semibold mb-4">{`${selectionMessage}`}</h3>
|
||||
<div
|
||||
className="border-2 border-dashed border-gray-500 p-6 rounded-lg flex flex-col items-center justify-center cursor-pointer hover:border-emerald-500"
|
||||
onClick={() => document.getElementById('fileInput').click()} // Ouvre l'explorateur de fichiers au clic
|
||||
onClick={() => fileInputRef.current.click()} // Utilisation de la référence pour ouvrir l'explorateur
|
||||
onDragOver={(e) => e.preventDefault()}
|
||||
onDrop={handleFileDrop}
|
||||
>
|
||||
@ -39,7 +40,7 @@ export default function FileUpload({ selectionMessage, onFileSelect, uploadedFil
|
||||
accept=".pdf"
|
||||
onChange={handleFileChange}
|
||||
className="hidden"
|
||||
id="fileInput"
|
||||
ref={fileInputRef} // Attachement de la référence
|
||||
/>
|
||||
<label htmlFor="fileInput" className="text-center text-gray-500">
|
||||
<p className="text-lg font-semibold text-gray-800">Déposez votre fichier ici</p>
|
||||
@ -1,17 +1,214 @@
|
||||
import React from 'react';
|
||||
import React, { useState } from 'react';
|
||||
import Table from '@/components/Table';
|
||||
import FileUpload from '@/components/FileUpload';
|
||||
import { Upload, Eye, Trash2, FileText } from 'lucide-react';
|
||||
import { BASE_URL } from '@/utils/Url';
|
||||
import Popup from '@/components/Popup';
|
||||
import logger from '@/utils/logger';
|
||||
|
||||
export default function FilesToUpload({ parentFileTemplates, 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 (
|
||||
<div className="bg-white p-6 rounded-lg shadow-sm border border-gray-200">
|
||||
<h2 className="text-xl font-bold mb-4 text-gray-800">Fichiers à uploader</h2>
|
||||
<div className="mt-8 mb-4 w-3/5">
|
||||
<div className="flex items-center justify-between mb-6">
|
||||
<div className="flex items-center space-x-4">
|
||||
<div className="bg-emerald-100 p-3 rounded-full shadow-md">
|
||||
<FileText className="w-8 h-8 text-emerald-600" />
|
||||
</div>
|
||||
<div>
|
||||
<h2 className="text-2xl font-bold text-gray-800">Pièces à fournir</h2>
|
||||
<p className="text-sm text-gray-500 italic">
|
||||
Ajoutez les documents pour compléter votre inscription
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<Table
|
||||
data={parentFileTemplates}
|
||||
columns={columns}
|
||||
itemsPerPage={5}
|
||||
currentPage={1}
|
||||
totalPages={1}
|
||||
onPageChange={() => {}}
|
||||
/>
|
||||
{selectedFile && (
|
||||
<div className="mt-4">
|
||||
{actionType === 'view' && selectedFile.fileName ? (
|
||||
<iframe
|
||||
src={`${BASE_URL}/${selectedFile.fileName}`}
|
||||
title="Document Viewer"
|
||||
className="w-full"
|
||||
style={{
|
||||
height: '75vh',
|
||||
border: 'none',
|
||||
}}
|
||||
/>
|
||||
) : actionType === 'upload' ? (
|
||||
<FileUpload
|
||||
selectionMessage={`Téléversez le fichier ${selectedFile.master_name}`}
|
||||
onFileSelect={(file) => handleUpload(file, selectedFile)}
|
||||
uploadedFileName={selectedFile.fileName || ''}
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
)}
|
||||
<Popup
|
||||
visible={popupVisible}
|
||||
message={popupMessage}
|
||||
onConfirm={() => setPopupVisible(false)}
|
||||
onCancel={() => setPopupVisible(false)}
|
||||
uniqueConfirmButton={true}
|
||||
/>
|
||||
<Popup
|
||||
visible={removePopupVisible}
|
||||
message={removePopupMessage}
|
||||
onConfirm={removePopupOnConfirm}
|
||||
onCancel={() => setRemovePopupVisible(false)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
@ -5,23 +5,17 @@ import Button from '@/components/Button';
|
||||
import DjangoCSRFToken from '@/components/DjangoCSRFToken';
|
||||
import { fetchRegisterForm, fetchSchoolFileTemplatesFromRegistrationFiles, fetchParentFileTemplatesFromRegistrationFiles } from '@/app/actions/subscriptionAction';
|
||||
import { downloadTemplate,
|
||||
createRegistrationSchoolFileTemplate,
|
||||
editRegistrationSchoolFileTemplates,
|
||||
deleteRegistrationSchoolFileTemplates
|
||||
editRegistrationParentFileTemplates
|
||||
} from '@/app/actions/registerFileGroupAction';
|
||||
import {
|
||||
fetchRegistrationPaymentModes,
|
||||
fetchTuitionPaymentModes } from '@/app/actions/schoolAction';
|
||||
import { Download, Upload, Trash2, Eye } from 'lucide-react';
|
||||
import { BASE_URL } from '@/utils/Url';
|
||||
import DraggableFileUpload from '@/components/DraggableFileUpload';
|
||||
import Modal from '@/components/Modal';
|
||||
import FileStatusLabel from '@/components/FileStatusLabel';
|
||||
import logger from '@/utils/logger';
|
||||
import StudentInfoForm, { validateStudentInfo } from '@/components/Inscription/StudentInfoForm';
|
||||
import FilesToUpload from '@/components/Inscription/FilesToUpload';
|
||||
import { DocusealForm } from '@docuseal/react';
|
||||
import FileUpload from '@/components/Inscription/FileUpload';
|
||||
|
||||
/**
|
||||
* Composant de formulaire d'inscription partagé
|
||||
@ -36,7 +30,6 @@ export default function InscriptionFormShared({
|
||||
csrfToken,
|
||||
selectedEstablishmentId,
|
||||
onSubmit,
|
||||
cancelUrl,
|
||||
errors = {} // Nouvelle prop pour les erreurs
|
||||
}) {
|
||||
// États pour gérer les données du formulaire
|
||||
@ -65,12 +58,6 @@ export default function InscriptionFormShared({
|
||||
const [uploadedFiles, setUploadedFiles] = useState([]);
|
||||
const [schoolFileTemplates, setSchoolFileTemplates] = useState([]);
|
||||
const [parentFileTemplates, setParentFileTemplates] = useState([]);
|
||||
const [fileGroup, setFileGroup] = useState(null);
|
||||
const [fileName, setFileName] = useState("");
|
||||
const [file, setFile] = useState("");
|
||||
const [showUploadModal, setShowUploadModal] = useState(false);
|
||||
const [currentTemplateId, setCurrentTemplateId] = useState(null);
|
||||
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
|
||||
const isCurrentPageValid = () => {
|
||||
@ -105,7 +92,6 @@ export default function InscriptionFormShared({
|
||||
totalTuitionFees: data?.totalTuitionFees,
|
||||
});
|
||||
setGuardians(data?.student?.guardians || []);
|
||||
setUploadedFiles(data.registration_files || []);
|
||||
});
|
||||
|
||||
setIsLoading(false);
|
||||
@ -119,6 +105,13 @@ export default function InscriptionFormShared({
|
||||
|
||||
fetchParentFileTemplatesFromRegistrationFiles(studentId).then((data) => {
|
||||
setParentFileTemplates(data);
|
||||
|
||||
// Initialiser uploadedFiles avec uniquement les fichiers dont `file` n'est pas null
|
||||
const filteredFiles = data.filter(item => item.file !== null).map(item => ({
|
||||
id: item.id,
|
||||
fileName: item.file
|
||||
}));
|
||||
setUploadedFiles(filteredFiles);
|
||||
})
|
||||
|
||||
if (selectedEstablishmentId) {
|
||||
@ -153,66 +146,74 @@ export default function InscriptionFormShared({
|
||||
setFormData(prev => ({...prev, [field]: value}));
|
||||
};
|
||||
|
||||
// Gestion du téléversement de fichiers
|
||||
const handleFileUpload = async (file, fileName) => {
|
||||
if (!file || !currentTemplateId || !formData.id) {
|
||||
logger.error('Missing required data for upload');
|
||||
const handleFileUpload = (file, selectedFile) => {
|
||||
if (!file || !selectedFile) {
|
||||
logger.error('Données manquantes pour le téléversement.');
|
||||
return Promise.reject(new Error('Données manquantes pour le téléversement.'));
|
||||
}
|
||||
|
||||
const updateData = new FormData();
|
||||
updateData.append('file', file);
|
||||
|
||||
return editRegistrationParentFileTemplates(selectedFile.id, updateData, csrfToken)
|
||||
.then((response) => {
|
||||
logger.debug('Template mis à jour avec succès :', response);
|
||||
|
||||
setUploadedFiles((prev) => {
|
||||
const updatedFiles = prev.map((uploadedFile) =>
|
||||
uploadedFile.id === selectedFile.id
|
||||
? { ...uploadedFile, fileName: response.data.file } // Met à jour le fichier téléversé
|
||||
: uploadedFile
|
||||
);
|
||||
|
||||
// Si le fichier n'existe pas encore, l'ajouter
|
||||
if (!updatedFiles.find(file => file.id === selectedFile.id)) {
|
||||
updatedFiles.push({
|
||||
id: selectedFile.id,
|
||||
fileName: response.data.file
|
||||
});
|
||||
}
|
||||
|
||||
return updatedFiles;
|
||||
});
|
||||
|
||||
return response; // Retourner la réponse pour signaler le succès
|
||||
})
|
||||
.catch((error) => {
|
||||
logger.error('Erreur lors de la mise à jour du fichier :', error);
|
||||
throw error; // Relancer l'erreur pour que l'appelant puisse la capturer
|
||||
});
|
||||
};
|
||||
|
||||
const handleDeleteFile = (templateId) => {
|
||||
const fileToDelete = uploadedFiles.find(file => parseInt(file.id) === templateId && file.fileName);
|
||||
if (!fileToDelete) {
|
||||
logger.error('Aucun fichier trouvé pour suppression.');
|
||||
return;
|
||||
}
|
||||
|
||||
const data = new FormData();
|
||||
data.append('file', file);
|
||||
data.append('name', fileName);
|
||||
data.append('template', currentTemplateId);
|
||||
data.append('register_form', formData.id);
|
||||
// Créer un FormData avec un champ vide pour "file"
|
||||
const updateData = new FormData();
|
||||
updateData.append('file', ''); // Envoyer chaine vide pour indiquer qu'aucun fichier n'est uploadé
|
||||
|
||||
try {
|
||||
const response = await createRegistrationSchoolFileTemplate(data, csrfToken);
|
||||
if (response) {
|
||||
setUploadedFiles(prev => {
|
||||
const newFiles = prev.filter(f => parseInt(f.template) !== currentTemplateId);
|
||||
return [...newFiles, {
|
||||
name: fileName,
|
||||
template: currentTemplateId,
|
||||
file: response.file
|
||||
}];
|
||||
});
|
||||
return editRegistrationParentFileTemplates(templateId, updateData, csrfToken)
|
||||
.then((response) => {
|
||||
logger.debug('Fichier supprimé avec succès dans la base :', response);
|
||||
|
||||
// Rafraîchir les données du formulaire pour avoir les fichiers à jour
|
||||
if (studentId) {
|
||||
fetchRegisterForm(studentId).then((data) => {
|
||||
setUploadedFiles(data.registration_files || []);
|
||||
});
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error('Error uploading file:', error);
|
||||
}
|
||||
};
|
||||
|
||||
// Vérification si un fichier est déjà uploadé
|
||||
const isFileUploaded = (templateId) => {
|
||||
return uploadedFiles.find(template =>
|
||||
template.template === templateId
|
||||
);
|
||||
};
|
||||
|
||||
// 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);
|
||||
}
|
||||
// Mettre à jour l'état local pour refléter la suppression
|
||||
setUploadedFiles((prev) =>
|
||||
prev.map((uploadedFile) =>
|
||||
uploadedFile.id === templateId
|
||||
? { ...uploadedFile, fileName: null, fileUrl: null } // Réinitialiser les champs liés au fichier
|
||||
: uploadedFile
|
||||
)
|
||||
);
|
||||
return response;
|
||||
})
|
||||
.catch((error) => {
|
||||
logger.error('Erreur lors de la suppression du fichier dans la base :', error);
|
||||
throw error;
|
||||
});
|
||||
};
|
||||
|
||||
// Soumission du formulaire
|
||||
@ -252,67 +253,12 @@ export default function InscriptionFormShared({
|
||||
setCurrentPage(currentPage - 1);
|
||||
};
|
||||
|
||||
// Configuration des colonnes pour le tableau des fichiers
|
||||
const columns = [
|
||||
{ name: 'Nom du fichier', transform: (row) => row.master_name },
|
||||
{ name: 'Description du fichier', transform: (row) => row.master_description },
|
||||
{ name: 'Fichier de référence', transform: (row) => row.file && <div className="flex items-center justify-center gap-2"> <a href={`${BASE_URL}${row.file}`} target='_blank' className="text-blue-500 hover:text-blue-700">
|
||||
<Download size={16} />
|
||||
</a> </div>},
|
||||
{ name: 'Statut', transform: (row) =>
|
||||
row.is_required && (
|
||||
<FileStatusLabel
|
||||
status={isFileUploaded(row.id) ? 'sent' : 'pending'}
|
||||
/>
|
||||
)
|
||||
},
|
||||
{ name: 'Actions', transform: (row) => {
|
||||
if (!row.is_required) return null;
|
||||
|
||||
const uploadedFile = getUploadedFile(row.id);
|
||||
|
||||
if (uploadedFile) {
|
||||
return (
|
||||
<div className="flex items-center justify-center gap-2">
|
||||
<a
|
||||
href={`${BASE_URL}${uploadedFile.file}`}
|
||||
target="_blank"
|
||||
className="text-blue-500 hover:text-blue-700"
|
||||
>
|
||||
<Eye size={16} />
|
||||
</a>
|
||||
<button
|
||||
className="text-red-500 hover:text-red-700"
|
||||
onClick={() => handleDeleteFile(row.id)}
|
||||
type="button"
|
||||
>
|
||||
<Trash2 size={16} />
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<button
|
||||
className="text-emerald-500 hover:text-emerald-700"
|
||||
type="button"
|
||||
onClick={() => {
|
||||
setCurrentTemplateId(row.id);
|
||||
setShowUploadModal(true);
|
||||
}}
|
||||
>
|
||||
<Upload size={16} />
|
||||
</button>
|
||||
);
|
||||
}},
|
||||
];
|
||||
|
||||
// Affichage du loader pendant le chargement
|
||||
if (isLoading) return <Loader />;
|
||||
|
||||
// Rendu du composant
|
||||
return (
|
||||
<div className="max-w-4xl mx-auto p-6">
|
||||
<div className="mx-auto p-6">
|
||||
<form onSubmit={handleSubmit} className="space-y-8">
|
||||
<DjangoCSRFToken csrfToken={csrfToken}/>
|
||||
{/* Page 1 : Informations de l'élève et Responsables */}
|
||||
@ -330,76 +276,70 @@ export default function InscriptionFormShared({
|
||||
|
||||
{/* Pages suivantes : Section Fichiers d'inscription */}
|
||||
{currentPage > 1 && currentPage <= schoolFileTemplates.length + 1 && (
|
||||
<div className="bg-white p-6 rounded-lg shadow-sm border border-gray-200">
|
||||
{/* Titre du document */}
|
||||
<div className="mb-4">
|
||||
<h2 className="text-lg font-semibold text-gray-800">
|
||||
{schoolFileTemplates[currentPage - 2].name || "Document sans nom"}
|
||||
</h2>
|
||||
<p className="text-sm text-gray-500">
|
||||
{schoolFileTemplates[currentPage - 2].description || "Aucune description disponible pour ce document."}
|
||||
</p>
|
||||
<div className="mt-8 mb-4 w-3/5">
|
||||
<div className="bg-white p-6 rounded-lg shadow-sm border border-gray-200">
|
||||
{/* Titre du document */}
|
||||
<div className="mb-4">
|
||||
<h2 className="text-lg font-semibold text-gray-800">
|
||||
{schoolFileTemplates[currentPage - 2].name || "Document sans nom"}
|
||||
</h2>
|
||||
<p className="text-sm text-gray-500">
|
||||
{schoolFileTemplates[currentPage - 2].description || "Aucune description disponible pour ce document."}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Affichage du formulaire ou du document */}
|
||||
{schoolFileTemplates[currentPage - 2].file === null ? (
|
||||
<DocusealForm
|
||||
id="docusealForm"
|
||||
src={"https://docuseal.com/s/" + schoolFileTemplates[currentPage - 2].slug}
|
||||
withDownloadButton={false}
|
||||
onComplete={() => {
|
||||
downloadTemplate(schoolFileTemplates[currentPage - 2].slug)
|
||||
.then((data) => fetch(data))
|
||||
.then((response) => response.blob())
|
||||
.then((blob) => {
|
||||
const file = new File([blob], `${schoolFileTemplates[currentPage - 2].name}.pdf`, { type: blob.type });
|
||||
const updateData = new FormData();
|
||||
updateData.append('file', file);
|
||||
|
||||
return editRegistrationSchoolFileTemplates(schoolFileTemplates[currentPage - 2].id, updateData, csrfToken);
|
||||
})
|
||||
.then((data) => {
|
||||
logger.debug("EDIT TEMPLATE : ", data);
|
||||
})
|
||||
.catch((error) => {
|
||||
logger.error("error editing template : ", error);
|
||||
});
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<iframe
|
||||
src={`${BASE_URL}/${schoolFileTemplates[currentPage - 2].file}`}
|
||||
title="Document Viewer"
|
||||
className="w-full"
|
||||
style={{
|
||||
height: '75vh', // Ajuster la hauteur à 75% de la fenêtre
|
||||
border: 'none',
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Affichage du formulaire ou du document */}
|
||||
{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>
|
||||
)}
|
||||
|
||||
{/* Dernière page : Section Fichiers parents */}
|
||||
{currentPage === schoolFileTemplates.length + 2 && (
|
||||
<>
|
||||
<FilesToUpload
|
||||
parentFileTemplates={parentFileTemplates}
|
||||
columns={columns}
|
||||
/>
|
||||
<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}
|
||||
/>
|
||||
</>
|
||||
<FilesToUpload
|
||||
parentFileTemplates={parentFileTemplates}
|
||||
uploadedFiles={uploadedFiles}
|
||||
onFileUpload={handleFileUpload}
|
||||
onFileDelete={handleDeleteFile}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Boutons de contrôle */}
|
||||
<div className="flex justify-end space-x-4">
|
||||
<div className="flex justify-center space-x-4">
|
||||
<Button
|
||||
text="Sauvegarder"
|
||||
onClick={handleSave}
|
||||
@ -429,52 +369,6 @@ export default function InscriptionFormShared({
|
||||
)}
|
||||
</div>
|
||||
</form>
|
||||
{schoolFileTemplates.length > 0 && (
|
||||
<Modal
|
||||
isOpen={showUploadModal}
|
||||
setIsOpen={setShowUploadModal}
|
||||
title="Téléverser un fichier"
|
||||
ContentComponent={() => (
|
||||
<>
|
||||
<DraggableFileUpload
|
||||
className="w-full"
|
||||
fileName={fileName}
|
||||
onFileSelect={(selectedFile) => {
|
||||
if (selectedFile) {
|
||||
setFile(selectedFile);
|
||||
setFileName(selectedFile.name);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<div className="mt-4 flex justify-center space-x-4">
|
||||
<Button
|
||||
text="Annuler"
|
||||
onClick={() => {
|
||||
setShowUploadModal(false);
|
||||
setCurrentTemplateId(null);
|
||||
setFile(null);
|
||||
setFileName("");
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
text="Valider"
|
||||
onClick={() => {
|
||||
if (file && fileName) {
|
||||
handleFileUpload(file, fileName);
|
||||
setShowUploadModal(false);
|
||||
setCurrentTemplateId(null);
|
||||
setFile(null);
|
||||
setFileName("");
|
||||
}
|
||||
}}
|
||||
primary={true}
|
||||
disabled={!file || !fileName}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -6,14 +6,16 @@ import { BASE_URL } from '@/utils/Url';
|
||||
import { generateToken } from '@/app/actions/registerFileGroupAction';
|
||||
import logger from '@/utils/logger';
|
||||
import { GraduationCap, CloudUpload } from 'lucide-react';
|
||||
import FileUpload from '@/components/Inscription/FileUpload';
|
||||
import FileUpload from '@/components/FileUpload';
|
||||
import SectionHeader from '@/components/SectionHeader';
|
||||
|
||||
export default function ValidateSubscription({ studentId, firstName, lastName, paymentMode, file, onAccept }) {
|
||||
export default function ValidateSubscription({ studentId, firstName, lastName, paymentSepa, file, onAccept }) {
|
||||
const [token, setToken] = useState(null);
|
||||
const [uploadedFileName, setUploadedFileName] = useState('');
|
||||
const [selectedFile, setSelectedFile] = useState(null); // Nouvel état pour le fichier sélectionné
|
||||
const [pdfUrl, setPdfUrl] = useState(`${BASE_URL}/${file}`);
|
||||
const [isSepa, setIsSepa] = useState(paymentMode === '1'); // Vérifie si le mode de paiement est SEPA
|
||||
const [currentPage, setCurrentPage] = useState(1); // Gestion des pages
|
||||
const [isSepa, setIsSepa] = useState(paymentSepa); // Vérifie si le mode de paiement est SEPA
|
||||
const [currentPage, setCurrentPage] = useState(1); // Gestion des étapes
|
||||
|
||||
useEffect(() => {
|
||||
if (isSepa) {
|
||||
@ -25,34 +27,21 @@ export default function ValidateSubscription({ studentId, firstName, lastName, p
|
||||
}
|
||||
}, [isSepa]);
|
||||
|
||||
const handleUpload = (detail) => {
|
||||
logger.debug('Uploaded file detail:', detail);
|
||||
setUploadedFileName(detail.name);
|
||||
};
|
||||
|
||||
const handleAccept = () => {
|
||||
const fileInput = document.getElementById('fileInput'); // Récupère l'élément input
|
||||
const file = fileInput?.files[0]; // Récupère le fichier sélectionné
|
||||
|
||||
if (!file) {
|
||||
if (!selectedFile && isSepa) {
|
||||
logger.error('Aucun fichier sélectionné pour le champ SEPA.');
|
||||
return;
|
||||
}
|
||||
|
||||
const data = {
|
||||
status: 7,
|
||||
sepa_file: file,
|
||||
sepa_file: selectedFile, // Utilise le fichier sélectionné depuis l'état
|
||||
};
|
||||
|
||||
// Appeler la fonction passée par le parent pour mettre à jour le RF
|
||||
onAccept(data);
|
||||
};
|
||||
|
||||
const handleRefuse = () => {
|
||||
logger.debug('Dossier refusé pour l\'étudiant:', studentId);
|
||||
// Logique pour refuser l'inscription
|
||||
};
|
||||
|
||||
const isValidateButtonDisabled = isSepa && !uploadedFileName;
|
||||
|
||||
const goToNextPage = () => {
|
||||
@ -68,25 +57,16 @@ export default function ValidateSubscription({ studentId, firstName, lastName, p
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="p-8 space-y-6 bg-gray-50 rounded-lg shadow-lg">
|
||||
{/* Titre */}
|
||||
<div className="flex items-center space-x-4">
|
||||
<div className="bg-emerald-100 p-3 rounded-full shadow-md">
|
||||
<GraduationCap className="w-8 h-8 text-emerald-600" />
|
||||
</div>
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold text-gray-800">
|
||||
Dossier scolaire de <span className="text-emerald-600">{firstName} {lastName}</span>
|
||||
</h1>
|
||||
<p className="text-sm text-gray-500 italic">
|
||||
Année scolaire {new Date().getFullYear()}-{new Date().getFullYear() + 1}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-6 p-6">
|
||||
<SectionHeader
|
||||
icon={GraduationCap}
|
||||
title={`Dossier scolaire de ${firstName} ${lastName}`}
|
||||
description={`Année scolaire ${new Date().getFullYear()}-${new Date().getFullYear() + 1}`}
|
||||
/>
|
||||
|
||||
{/* Contenu principal */}
|
||||
{currentPage === 1 && (
|
||||
<div className="border p-6 rounded-lg shadow-md bg-white flex justify-center items-center">
|
||||
<div className="p-6 items-center">
|
||||
<iframe
|
||||
src={pdfUrl}
|
||||
title="Aperçu du PDF"
|
||||
@ -102,9 +82,10 @@ export default function ValidateSubscription({ studentId, firstName, lastName, p
|
||||
|
||||
{currentPage === 2 && isSepa && (
|
||||
<FileUpload
|
||||
selectionMessage='Sélectionnez un mandat de prélèvement SEPA'
|
||||
selectionMessage="Sélectionnez un mandat de prélèvement SEPA"
|
||||
onFileSelect={(file) => {
|
||||
setUploadedFileName(file.name); // Stocke uniquement le nom du fichier
|
||||
setSelectedFile(file); // Stocke le fichier dans l'état
|
||||
logger.debug('Fichier sélectionné:', file.name);
|
||||
}}
|
||||
uploadedFileName={uploadedFileName}
|
||||
@ -120,7 +101,7 @@ export default function ValidateSubscription({ studentId, firstName, lastName, p
|
||||
className="bg-gray-300 text-gray-700 hover:bg-gray-400 px-6 py-2"
|
||||
/>
|
||||
)}
|
||||
{currentPage < (isSepa ? 2 : 1) && (
|
||||
{isSepa && currentPage === 1 && (
|
||||
<Button
|
||||
text="Suivant"
|
||||
onClick={goToNextPage}
|
||||
|
||||
44
Front-End/src/components/SectionHeader.js
Normal file
44
Front-End/src/components/SectionHeader.js
Normal file
@ -0,0 +1,44 @@
|
||||
import React from 'react';
|
||||
import { Plus } from 'lucide-react';
|
||||
|
||||
const SectionHeader = ({
|
||||
icon: Icon,
|
||||
discountStyle = false,
|
||||
title,
|
||||
description,
|
||||
button = false,
|
||||
buttonOpeningModal = false,
|
||||
onClick = null
|
||||
}) => {
|
||||
return (
|
||||
<div className="flex items-center justify-between mb-6">
|
||||
<div className="flex items-center space-x-4">
|
||||
<div className={`${discountStyle ? "bg-yellow-100" : "bg-emerald-100"} p-3 rounded-full shadow-md`}>
|
||||
<Icon
|
||||
className={discountStyle ?
|
||||
"w-8 h-8 text-yellow-600" :
|
||||
"w-8 h-8 text-emerald-600"
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<h2 className="text-2xl font-bold text-gray-800">{title}</h2>
|
||||
<p className="text-sm text-gray-500 italic">{description}</p>
|
||||
</div>
|
||||
</div>
|
||||
{button && onClick && (
|
||||
<button
|
||||
onClick={onClick}
|
||||
className={buttonOpeningModal ?
|
||||
"flex items-center bg-emerald-200 text-emerald-700 p-2 rounded-full shadow-sm hover:bg-emerald-300" :
|
||||
"text-emerald-500 hover:bg-emerald-200 rounded-full p-2"
|
||||
}
|
||||
>
|
||||
<Plus className="w-6 h-6" />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default SectionHeader;
|
||||
@ -6,10 +6,14 @@ const SidebarTabs = ({ tabs }) => {
|
||||
return (
|
||||
<div className="w-full">
|
||||
<div className="flex border-b-2 border-gray-200">
|
||||
{tabs.map(tab => (
|
||||
{tabs.map((tab) => (
|
||||
<button
|
||||
key={tab.id}
|
||||
className={`flex-1 p-4 ${activeTab === tab.id ? 'border-b-2 border-emerald-500 text-emerald-500' : 'text-gray-500 hover:text-emerald-500'}`}
|
||||
className={`flex-1 p-4 ${
|
||||
activeTab === tab.id
|
||||
? 'border-b-2 border-emerald-500 text-emerald-500'
|
||||
: 'text-gray-500 hover:text-emerald-500'
|
||||
}`}
|
||||
onClick={() => setActiveTab(tab.id)}
|
||||
>
|
||||
{tab.label}
|
||||
@ -17,7 +21,7 @@ const SidebarTabs = ({ tabs }) => {
|
||||
))}
|
||||
</div>
|
||||
<div className="p-4">
|
||||
{tabs.map(tab => (
|
||||
{tabs.map((tab) => (
|
||||
<div key={tab.id} className={`${activeTab === tab.id ? 'block' : 'hidden'}`}>
|
||||
{tab.content}
|
||||
</div>
|
||||
|
||||
@ -12,6 +12,7 @@ import { HTML5Backend } from 'react-dnd-html5-backend';
|
||||
import { ESTABLISHMENT_ID } from '@/utils/Url';
|
||||
import logger from '@/utils/logger';
|
||||
import ClasseDetails from '@/components/ClasseDetails';
|
||||
import SectionHeader from '@/components/SectionHeader';
|
||||
|
||||
const ItemTypes = {
|
||||
TEACHER: 'teacher',
|
||||
@ -444,15 +445,13 @@ const ClassesSection = ({ classes, setClasses, teachers, handleCreate, handleEdi
|
||||
return (
|
||||
<DndProvider backend={HTML5Backend}>
|
||||
<div className="space-y-4">
|
||||
<div className="flex justify-between items-center">
|
||||
<div className="flex items-center mb-4">
|
||||
<Users className="w-6 h-6 text-emerald-500 mr-2" />
|
||||
<h2 className="text-xl font-semibold">Classes</h2>
|
||||
</div>
|
||||
<button type="button" onClick={handleAddClass} className="text-emerald-500 hover:text-emerald-700">
|
||||
<Plus className="w-5 h-5" />
|
||||
</button>
|
||||
</div>
|
||||
<SectionHeader
|
||||
icon={Users}
|
||||
title="Liste des classes"
|
||||
description="Gérez les classes de votre école"
|
||||
button={true}
|
||||
onClick={handleAddClass}
|
||||
/>
|
||||
<Table
|
||||
data={newClass ? [newClass, ...classes] : classes}
|
||||
columns={columns}
|
||||
|
||||
@ -8,6 +8,7 @@ import { HTML5Backend } from 'react-dnd-html5-backend';
|
||||
import SpecialityItem from '@/components/Structure/Configuration/SpecialityItem';
|
||||
import { useEstablishment } from '@/context/EstablishmentContext';
|
||||
import logger from '@/utils/logger';
|
||||
import SectionHeader from '@/components/SectionHeader';
|
||||
|
||||
const SpecialitiesSection = ({ specialities, setSpecialities, handleCreate, handleEdit, handleDelete }) => {
|
||||
|
||||
@ -214,15 +215,13 @@ const SpecialitiesSection = ({ specialities, setSpecialities, handleCreate, hand
|
||||
return (
|
||||
<DndProvider backend={HTML5Backend}>
|
||||
<div className="space-y-4">
|
||||
<div className="flex justify-between items-center">
|
||||
<div className="flex items-center mb-4">
|
||||
<BookOpen className="w-6 h-6 text-emerald-500 mr-2" />
|
||||
<h2 className="text-xl font-semibold">Spécialités</h2>
|
||||
</div>
|
||||
<button type="button" onClick={handleAddSpeciality} className="text-emerald-500 hover:text-emerald-700">
|
||||
<Plus className="w-5 h-5" />
|
||||
</button>
|
||||
</div>
|
||||
<SectionHeader
|
||||
icon={BookOpen}
|
||||
title="Liste des spécialités"
|
||||
description="Gérez les spécialités de votre école"
|
||||
button={true}
|
||||
onClick={handleAddSpeciality}
|
||||
/>
|
||||
<Table
|
||||
data={newSpeciality ? [newSpeciality, ...specialities] : specialities}
|
||||
columns={columns}
|
||||
|
||||
@ -7,9 +7,9 @@ import { BE_SCHOOL_SPECIALITIES_URL, BE_SCHOOL_TEACHERS_URL, BE_SCHOOL_SCHOOLCLA
|
||||
|
||||
const StructureManagement = ({ specialities, setSpecialities, teachers, setTeachers, classes, setClasses, profiles, handleCreate, handleEdit, handleDelete }) => {
|
||||
return (
|
||||
<div className="max-w-8xl mx-auto p-4 mt-6 space-y-8">
|
||||
<div className="w-full mx-auto mt-6">
|
||||
<ClassesProvider>
|
||||
<div className="w-2/5 p-4 bg-white rounded-lg shadow-md">
|
||||
<div className="mt-8 w-2/5">
|
||||
<SpecialitiesSection
|
||||
specialities={specialities}
|
||||
setSpecialities={setSpecialities}
|
||||
@ -18,7 +18,7 @@ const StructureManagement = ({ specialities, setSpecialities, teachers, setTeach
|
||||
handleDelete={(id) => handleDelete(`${BE_SCHOOL_SPECIALITIES_URL}`, id, setSpecialities)}
|
||||
/>
|
||||
</div>
|
||||
<div className="w-4/5 p-4 bg-white rounded-lg shadow-md">
|
||||
<div className="w-4/5 mt-12">
|
||||
<TeachersSection
|
||||
teachers={teachers}
|
||||
setTeachers={setTeachers}
|
||||
@ -29,7 +29,7 @@ const StructureManagement = ({ specialities, setSpecialities, teachers, setTeach
|
||||
handleDelete={(id) => handleDelete(`${BE_SCHOOL_TEACHERS_URL}`, id, setTeachers)}
|
||||
/>
|
||||
</div>
|
||||
<div className="w-full p-4 bg-white rounded-lg shadow-md">
|
||||
<div className="w-full mt-12">
|
||||
<ClassesSection
|
||||
classes={classes}
|
||||
setClasses={setClasses}
|
||||
|
||||
@ -3,7 +3,6 @@ import { Plus, Edit3, Trash2, GraduationCap, Check, X, Hand, Search } from 'luci
|
||||
import Table from '@/components/Table';
|
||||
import Popup from '@/components/Popup';
|
||||
import ToggleSwitch from '@/components/ToggleSwitch';
|
||||
import { createProfile, updateProfile } from '@/app/actions/authAction';
|
||||
import { useCsrfToken } from '@/context/CsrfContext';
|
||||
import { DndProvider, useDrag, useDrop } from 'react-dnd';
|
||||
import { HTML5Backend } from 'react-dnd-html5-backend';
|
||||
@ -11,8 +10,8 @@ import InputText from '@/components/InputText';
|
||||
import SpecialityItem from '@/components/Structure/Configuration/SpecialityItem';
|
||||
import TeacherItem from './TeacherItem';
|
||||
import logger from '@/utils/logger';
|
||||
import { fetchProfiles } from '@/app/actions/authAction';
|
||||
import { useEstablishment } from '@/context/EstablishmentContext';
|
||||
import SectionHeader from '@/components/SectionHeader';
|
||||
|
||||
const ItemTypes = {
|
||||
SPECIALITY: 'speciality',
|
||||
@ -468,15 +467,13 @@ const TeachersSection = ({ teachers, setTeachers, specialities, profiles, handle
|
||||
return (
|
||||
<DndProvider backend={HTML5Backend}>
|
||||
<div className="space-y-4">
|
||||
<div className="flex justify-between items-center">
|
||||
<div className="flex items-center mb-4">
|
||||
<GraduationCap className="w-6 h-6 text-emerald-500 mr-2" />
|
||||
<h2 className="text-xl font-semibold">Enseignants</h2>
|
||||
</div>
|
||||
<button type="button" onClick={handleAddTeacher} className="text-emerald-500 hover:text-emerald-700">
|
||||
<Plus className="w-5 h-5" />
|
||||
</button>
|
||||
</div>
|
||||
<SectionHeader
|
||||
icon={GraduationCap}
|
||||
title="Liste des enseignants.es"
|
||||
description="Gérez les enseignants.es de votre école"
|
||||
button={true}
|
||||
onClick={handleAddTeacher}
|
||||
/>
|
||||
<Table
|
||||
data={newTeacher ? [newTeacher, ...teachers] : teachers}
|
||||
columns={columns}
|
||||
|
||||
@ -3,6 +3,7 @@ import { Plus, Download, Edit3, Trash2, FolderPlus, Signature, FileText, Check,
|
||||
import Modal from '@/components/Modal';
|
||||
import Table from '@/components/Table';
|
||||
import FileUploadDocuSeal from '@/components/Structure/Files/FileUploadDocuSeal';
|
||||
import FileUpload from '@/components/FileUpload'
|
||||
import { BASE_URL } from '@/utils/Url';
|
||||
import {
|
||||
// GET
|
||||
@ -26,6 +27,7 @@ import {
|
||||
import RegistrationFileGroupForm from '@/components/Structure/Files/RegistrationFileGroupForm';
|
||||
import logger from '@/utils/logger';
|
||||
import ParentFilesSection from '@/components/Structure/Files/ParentFilesSection';
|
||||
import SectionHeader from '@/components/SectionHeader';
|
||||
|
||||
export default function FilesGroupsManagement({ csrfToken, selectedEstablishmentId }) {
|
||||
const [schoolFileMasters, setSchoolFileMasters] = useState([]);
|
||||
@ -42,6 +44,8 @@ export default function FilesGroupsManagement({ csrfToken, selectedEstablishment
|
||||
const [editingDocumentId, setEditingDocumentId] = useState(null);
|
||||
const [formData, setFormData] = useState({});
|
||||
|
||||
const [uploadedFileName, setUploadedFileName] = useState('');
|
||||
|
||||
const handleReloadTemplates = () => {
|
||||
setReloadTemplates(true);
|
||||
}
|
||||
@ -253,84 +257,6 @@ export default function FilesGroupsManagement({ csrfToken, selectedEstablishment
|
||||
}
|
||||
};
|
||||
|
||||
const renderRequiredDocumentCell = (document, column) => {
|
||||
const isEditing = editingDocumentId === document.id;
|
||||
|
||||
if (isEditing) {
|
||||
switch (column) {
|
||||
case 'Nom de la pièce':
|
||||
return (
|
||||
<InputText
|
||||
name="name"
|
||||
value={formData.name}
|
||||
onChange={(e) => handleChange(e, 'name')}
|
||||
placeholder="Nom de la pièce"
|
||||
className="w-full"
|
||||
/>
|
||||
);
|
||||
case 'Description':
|
||||
return (
|
||||
<InputText
|
||||
name="description"
|
||||
value={formData.description}
|
||||
onChange={(e) => handleChange(e, 'description')}
|
||||
placeholder="Description"
|
||||
className="w-full"
|
||||
/>
|
||||
);
|
||||
case 'Actions':
|
||||
return (
|
||||
<div className="flex justify-center space-x-2">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => handleSaveDocument(document.id)}
|
||||
className="text-green-500 hover:text-green-700"
|
||||
>
|
||||
<Check className="w-5 h-5" />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleCancelEdit}
|
||||
className="text-red-500 hover:text-red-700"
|
||||
>
|
||||
<X className="w-5 h-5" />
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
} else {
|
||||
switch (column) {
|
||||
case 'Nom de la pièce':
|
||||
return <span>{document.name}</span>;
|
||||
case 'Description':
|
||||
return <span>{document.description}</span>;
|
||||
case 'Actions':
|
||||
return (
|
||||
<div className="flex justify-center space-x-2">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => handleEditDocument(document)}
|
||||
className="text-blue-500 hover:text-blue-700"
|
||||
>
|
||||
<Edit3 className="w-5 h-5" />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => handleDeleteRequiredDocument(document.id)}
|
||||
className="text-red-500 hover:text-red-700"
|
||||
>
|
||||
<Trash2 className="w-5 h-5" />
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleCreate = (newParentFile) => {
|
||||
return createRegistrationParentFileMaster(newParentFile, csrfToken)
|
||||
.then((response) => {
|
||||
@ -417,14 +343,8 @@ export default function FilesGroupsManagement({ csrfToken, selectedEstablishment
|
||||
)}
|
||||
];
|
||||
|
||||
const columnsRequiredDocuments = [
|
||||
{ name: 'Nom de la pièce', transform: (row) => renderRequiredDocumentCell(row, 'Nom de la pièce') },
|
||||
{ name: 'Description', transform: (row) => renderRequiredDocumentCell(row, 'Description') },
|
||||
{ name: 'Actions', transform: (row) => renderRequiredDocumentCell(row, 'Actions') },
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="space-y-12">
|
||||
<div className="w-full mx-auto mt-6">
|
||||
{/* Modal pour les fichiers */}
|
||||
<Modal
|
||||
isOpen={isModalOpen}
|
||||
@ -460,26 +380,15 @@ export default function FilesGroupsManagement({ csrfToken, selectedEstablishment
|
||||
/>
|
||||
|
||||
{/* Section Groupes de fichiers */}
|
||||
<div className="mt-8 mb-4 w-3/5">
|
||||
<div className="flex items-center justify-between mb-6">
|
||||
<div className="flex items-center space-x-4">
|
||||
<div className="bg-emerald-100 p-3 rounded-full shadow-md">
|
||||
<FolderPlus className="w-8 h-8 text-emerald-600" />
|
||||
</div>
|
||||
<div>
|
||||
<h2 className="text-2xl font-bold text-gray-800">Dossiers d'inscriptions</h2>
|
||||
<p className="text-sm text-gray-500 italic">
|
||||
Gérez les dossiers d'inscription pour organiser vos documents.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
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>
|
||||
<div className="mt-8 w-3/5">
|
||||
<SectionHeader
|
||||
icon={FolderPlus}
|
||||
title="Dossiers d'inscriptions"
|
||||
description="Gérez les dossiers d'inscription pour organiser vos documents."
|
||||
button={true}
|
||||
buttonOpeningModal={true}
|
||||
onClick={() => setIsGroupModalOpen(true)}
|
||||
/>
|
||||
<Table
|
||||
data={groups}
|
||||
columns={columnsGroups}
|
||||
@ -487,29 +396,18 @@ export default function FilesGroupsManagement({ csrfToken, selectedEstablishment
|
||||
</div>
|
||||
|
||||
{/* Section Fichiers */}
|
||||
<div className="mt-8 mb-4 w-3/5">
|
||||
<div className="flex items-center justify-between mb-6">
|
||||
<div className="flex items-center space-x-4">
|
||||
<div className="bg-emerald-100 p-3 rounded-full shadow-md">
|
||||
<Signature className="w-8 h-8 text-emerald-600" />
|
||||
</div>
|
||||
<div>
|
||||
<h2 className="text-2xl font-bold text-gray-800">Formulaires à remplir</h2>
|
||||
<p className="text-sm text-gray-500 italic">
|
||||
Gérez les formulaires nécessitant une signature électronique.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
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>
|
||||
<div className="mt-12 mb-4 w-3/5">
|
||||
<SectionHeader
|
||||
icon={Signature}
|
||||
title="Formulaires à remplir"
|
||||
description="Gérez les formulaires nécessitant une signature électronique."
|
||||
button={true}
|
||||
buttonOpeningModal={true}
|
||||
onClick={() => {
|
||||
setIsModalOpen(true);
|
||||
setIsEditing(false);
|
||||
}}
|
||||
/>
|
||||
<Table
|
||||
data={filteredFiles}
|
||||
columns={columnsFiles}
|
||||
|
||||
@ -7,6 +7,7 @@ import Popup from '@/components/Popup';
|
||||
import logger from '@/utils/logger';
|
||||
import { createRegistrationParentFileTemplate } from '@/app/actions/registerFileGroupAction';
|
||||
import { useCsrfToken } from '@/context/CsrfContext';
|
||||
import SectionHeader from '@/components/SectionHeader';
|
||||
|
||||
export default function ParentFilesSection({ parentFiles, groups, handleCreate, handleEdit, handleDelete }) {
|
||||
const [editingDocumentId, setEditingDocumentId] = useState(null);
|
||||
@ -65,7 +66,6 @@ export default function ParentFilesSection({ parentFiles, groups, handleCreate,
|
||||
createRegistrationParentFileTemplate(data, csrfToken)
|
||||
.then(response => {
|
||||
logger.debug('Template enregistré avec succès:', response);
|
||||
onSuccess();
|
||||
})
|
||||
.catch(error => {
|
||||
logger.error('Erreur lors de l\'enregistrement du template:', error);
|
||||
@ -243,26 +243,14 @@ export default function ParentFilesSection({ parentFiles, groups, handleCreate,
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="mt-8 mb-4 w-4/5">
|
||||
<div className="flex items-center justify-between mb-6">
|
||||
<div className="flex items-center space-x-4">
|
||||
<div className="bg-emerald-100 p-3 rounded-full shadow-md">
|
||||
<FileText className="w-8 h-8 text-emerald-600" />
|
||||
</div>
|
||||
<div>
|
||||
<h2 className="text-2xl font-bold text-gray-800">Pièces à fournir</h2>
|
||||
<p className="text-sm text-gray-500 italic">
|
||||
Configurez la liste des documents que les parents doivent fournir.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
onClick={handleAddEmptyRequiredDocument}
|
||||
className="text-emerald-500 hover:text-emerald-700"
|
||||
>
|
||||
<Plus className="w-6 h-6" />
|
||||
</button>
|
||||
</div>
|
||||
<div className="mt-12 w-4/5">
|
||||
<SectionHeader
|
||||
icon={FileText}
|
||||
title="Pièces à fournir"
|
||||
description="Configurez la liste des documents que les parents doivent fournir."
|
||||
button={true}
|
||||
onClick={handleAddEmptyRequiredDocument}
|
||||
/>
|
||||
<Table
|
||||
data={editingDocumentId === 'new' ? [formData, ...parentFiles] : parentFiles}
|
||||
columns={columnsRequiredDocuments}
|
||||
|
||||
@ -6,8 +6,9 @@ import CheckBox from '@/components/CheckBox';
|
||||
import InputText from '@/components/InputText';
|
||||
import logger from '@/utils/logger';
|
||||
import { ESTABLISHMENT_ID } from '@/utils/Url';
|
||||
import SectionHeader from '@/components/SectionHeader';
|
||||
|
||||
const DiscountsSection = ({ discounts, setDiscounts, handleCreate, handleEdit, handleDelete, type, subscriptionMode = false, selectedDiscounts, handleDiscountSelection }) => {
|
||||
const DiscountsSection = ({ discounts, setDiscounts, handleCreate, handleEdit, handleDelete, type, selectedDiscounts, handleDiscountSelection }) => {
|
||||
const [editingDiscount, setEditingDiscount] = useState(null);
|
||||
const [newDiscount, setNewDiscount] = useState(null);
|
||||
const [formData, setFormData] = useState({});
|
||||
@ -253,14 +254,7 @@ const DiscountsSection = ({ discounts, setDiscounts, handleCreate, handleEdit, h
|
||||
}
|
||||
};
|
||||
|
||||
const columns = subscriptionMode
|
||||
? [
|
||||
{ name: 'LIBELLE', label: 'Libellé' },
|
||||
{ name: 'DESCRIPTION', label: 'Description' },
|
||||
{ name: 'REMISE', label: 'Remise' },
|
||||
{ name: '', label: 'Sélection' }
|
||||
]
|
||||
: [
|
||||
const columns = [
|
||||
{ name: 'LIBELLE', label: 'Libellé' },
|
||||
{ name: 'REMISE', label: 'Remise' },
|
||||
{ name: 'DESCRIPTION', label: 'Description' },
|
||||
@ -269,18 +263,15 @@ const DiscountsSection = ({ discounts, setDiscounts, handleCreate, handleEdit, h
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
{!subscriptionMode && (
|
||||
<div className="flex justify-between items-center">
|
||||
<div className="flex items-center mb-4">
|
||||
<Tag className="w-6 h-6 text-emerald-500 mr-2" />
|
||||
<h2 className="text-xl font-semibold">Liste des réductions</h2>
|
||||
</div>
|
||||
<button type="button" onClick={handleAddDiscount} className="text-emerald-500 hover:text-emerald-700">
|
||||
<Plus className="w-5 h-5" />
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
<div className="space-y-4 mt-8">
|
||||
<SectionHeader
|
||||
icon={Tag}
|
||||
discountStyle = {true}
|
||||
title={`${type == 0 ? "Liste des réductions sur les frais d'inscription" : "Liste des réductions sur les frais de scolarité"}`}
|
||||
description={`${type == 0 ? "Gérez vos réductions sur les frais d'inscription" : "Gérez vos réductions sur les frais de scolarité"}`}
|
||||
button={true}
|
||||
onClick={handleAddDiscount}
|
||||
/>
|
||||
<Table
|
||||
data={newDiscount ? [newDiscount, ...discounts] : discounts}
|
||||
columns={columns}
|
||||
|
||||
@ -44,91 +44,100 @@ const FeesManagement = ({ registrationDiscounts,
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="w-full mx-auto p-2 mt-6 space-y-6">
|
||||
<div className="bg-white p-2 rounded-lg shadow-md">
|
||||
<h2 className="text-2xl font-semibold mb-4">Frais d'inscription</h2>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<div className="col-span-1 p-6 rounded-lg shadow-inner mt-4">
|
||||
<FeesSection
|
||||
fees={registrationFees}
|
||||
setFees={setRegistrationFees}
|
||||
discounts={registrationDiscounts}
|
||||
handleCreate={(newData) => handleCreate(`${BE_SCHOOL_FEES_URL}`, newData, setRegistrationFees)}
|
||||
handleEdit={(id, updatedData) => handleEdit(`${BE_SCHOOL_FEES_URL}`, id, updatedData, setRegistrationFees)}
|
||||
handleDelete={(id) => handleDelete(`${BE_SCHOOL_FEES_URL}`, id, setRegistrationFees)}
|
||||
type={0}
|
||||
/>
|
||||
</div>
|
||||
<div className="col-span-1 p-6 rounded-lg shadow-inner mt-4">
|
||||
<DiscountsSection
|
||||
discounts={registrationDiscounts}
|
||||
setDiscounts={setRegistrationDiscounts}
|
||||
handleCreate={(newData) => handleCreate(`${BE_SCHOOL_DISCOUNTS_URL}`, newData, setRegistrationDiscounts)}
|
||||
handleEdit={(id, updatedData) => handleEdit(`${BE_SCHOOL_DISCOUNTS_URL}`, id, updatedData, setRegistrationDiscounts)}
|
||||
handleDelete={(id) => handleDelete(`${BE_SCHOOL_DISCOUNTS_URL}`, id, setRegistrationDiscounts)}
|
||||
onDiscountDelete={(id) => handleDiscountDelete(id, 0)}
|
||||
type={0}
|
||||
/>
|
||||
</div>
|
||||
<div className="col-span-1 p-6 rounded-lg shadow-inner mt-4">
|
||||
<PaymentPlanSelector
|
||||
paymentPlans={registrationPaymentPlans}
|
||||
setPaymentPlans={setRegistrationPaymentPlans}
|
||||
handleEdit={(id, updatedData) => handleEdit(`${BE_SCHOOL_PAYMENT_PLANS_URL}`, id, updatedData, setRegistrationPaymentPlans)}
|
||||
type={0}
|
||||
/>
|
||||
</div>
|
||||
<div className="col-span-1 p-6 rounded-lg shadow-inner mt-4">
|
||||
<PaymentModeSelector
|
||||
paymentModes={registrationPaymentModes}
|
||||
setPaymentModes={setRegistrationPaymentModes}
|
||||
handleEdit={(id, updatedData) => handleEdit(`${BE_SCHOOL_PAYMENT_MODES_URL}`, id, updatedData, setRegistrationPaymentModes)}
|
||||
type={0}
|
||||
/>
|
||||
</div>
|
||||
<div className="w-full mx-auto mt-6">
|
||||
|
||||
<div className="w-4/5 mx-auto flex items-center mt-8">
|
||||
<hr className="flex-grow border-t-2 border-gray-300" />
|
||||
<span className="mx-4 text-gray-600 font-semibold">Frais d'inscription</span>
|
||||
<hr className="flex-grow border-t-2 border-gray-300" />
|
||||
</div>
|
||||
|
||||
<div className="mt-8 w-4/5">
|
||||
<FeesSection
|
||||
fees={registrationFees}
|
||||
setFees={setRegistrationFees}
|
||||
discounts={registrationDiscounts}
|
||||
handleCreate={(newData) => handleCreate(`${BE_SCHOOL_FEES_URL}`, newData, setRegistrationFees)}
|
||||
handleEdit={(id, updatedData) => handleEdit(`${BE_SCHOOL_FEES_URL}`, id, updatedData, setRegistrationFees)}
|
||||
handleDelete={(id) => handleDelete(`${BE_SCHOOL_FEES_URL}`, id, setRegistrationFees)}
|
||||
type={0}
|
||||
/>
|
||||
</div>
|
||||
<div className="mt-12 w-4/5">
|
||||
<DiscountsSection
|
||||
discounts={registrationDiscounts}
|
||||
setDiscounts={setRegistrationDiscounts}
|
||||
handleCreate={(newData) => handleCreate(`${BE_SCHOOL_DISCOUNTS_URL}`, newData, setRegistrationDiscounts)}
|
||||
handleEdit={(id, updatedData) => handleEdit(`${BE_SCHOOL_DISCOUNTS_URL}`, id, updatedData, setRegistrationDiscounts)}
|
||||
handleDelete={(id) => handleDelete(`${BE_SCHOOL_DISCOUNTS_URL}`, id, setRegistrationDiscounts)}
|
||||
onDiscountDelete={(id) => handleDiscountDelete(id, 0)}
|
||||
type={0}
|
||||
/>
|
||||
</div>
|
||||
<div className="mt-12 grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<div className="col-span-1 mt-4">
|
||||
<PaymentPlanSelector
|
||||
paymentPlans={registrationPaymentPlans}
|
||||
setPaymentPlans={setRegistrationPaymentPlans}
|
||||
handleEdit={(id, updatedData) => handleEdit(`${BE_SCHOOL_PAYMENT_PLANS_URL}`, id, updatedData, setRegistrationPaymentPlans)}
|
||||
type={0}
|
||||
/>
|
||||
</div>
|
||||
<div className="col-span-1 mt-4">
|
||||
<PaymentModeSelector
|
||||
paymentModes={registrationPaymentModes}
|
||||
setPaymentModes={setRegistrationPaymentModes}
|
||||
handleEdit={(id, updatedData) => handleEdit(`${BE_SCHOOL_PAYMENT_MODES_URL}`, id, updatedData, setRegistrationPaymentModes)}
|
||||
type={0}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="bg-white p-2 rounded-lg shadow-md">
|
||||
<h2 className="text-2xl font-semibold mb-4">Frais de scolarité</h2>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<div className="col-span-1 p-6 rounded-lg shadow-inner mt-4">
|
||||
<FeesSection
|
||||
fees={tuitionFees}
|
||||
setFees={setTuitionFees}
|
||||
discounts={tuitionDiscounts}
|
||||
handleCreate={(newData) => handleCreate(`${BE_SCHOOL_FEES_URL}`, newData, setTuitionFees)}
|
||||
handleEdit={(id, updatedData) => handleEdit(`${BE_SCHOOL_FEES_URL}`, id, updatedData, setTuitionFees)}
|
||||
handleDelete={(id) => handleDelete(`${BE_SCHOOL_FEES_URL}`, id, setTuitionFees)}
|
||||
type={1}
|
||||
/>
|
||||
</div>
|
||||
<div className="col-span-1 p-6 rounded-lg shadow-inner mt-4">
|
||||
<DiscountsSection
|
||||
discounts={tuitionDiscounts}
|
||||
setDiscounts={setTuitionDiscounts}
|
||||
handleCreate={(newData) => handleCreate(`${BE_SCHOOL_DISCOUNTS_URL}`, newData, setTuitionDiscounts)}
|
||||
handleEdit={(id, updatedData) => handleEdit(`${BE_SCHOOL_DISCOUNTS_URL}`, id, updatedData, setTuitionDiscounts)}
|
||||
handleDelete={(id) => handleDelete(`${BE_SCHOOL_DISCOUNTS_URL}`, id, setTuitionDiscounts)}
|
||||
onDiscountDelete={(id) => handleDiscountDelete(id, 1)}
|
||||
type={1}
|
||||
/>
|
||||
</div>
|
||||
<div className="col-span-1 p-6 rounded-lg shadow-inner mt-4">
|
||||
<PaymentPlanSelector
|
||||
paymentPlans={tuitionPaymentPlans}
|
||||
setPaymentPlans={setTuitionPaymentPlans}
|
||||
handleEdit={(id, updatedData) => handleEdit(`${BE_SCHOOL_PAYMENT_PLANS_URL}`, id, updatedData, setRegistrationPaymentPlans)}
|
||||
type={1}
|
||||
/>
|
||||
</div>
|
||||
<div className="col-span-1 p-6 rounded-lg shadow-inner mt-4">
|
||||
<PaymentModeSelector
|
||||
paymentModes={tuitionPaymentModes}
|
||||
setPaymentModes={setTuitionPaymentModes}
|
||||
handleEdit={(id, updatedData) => handleEdit(`${BE_SCHOOL_PAYMENT_MODES_URL}`, id, updatedData, setTuitionPaymentModes)}
|
||||
type={1}
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
||||
<div className="w-4/5 mx-auto flex items-center mt-16">
|
||||
<hr className="flex-grow border-t-2 border-gray-300" />
|
||||
<span className="mx-4 text-gray-600 font-semibold">Frais de scolarité</span>
|
||||
<hr className="flex-grow border-t-2 border-gray-300" />
|
||||
</div>
|
||||
|
||||
<div className="mt-8 w-4/5">
|
||||
<FeesSection
|
||||
fees={tuitionFees}
|
||||
setFees={setTuitionFees}
|
||||
discounts={tuitionDiscounts}
|
||||
handleCreate={(newData) => handleCreate(`${BE_SCHOOL_FEES_URL}`, newData, setTuitionFees)}
|
||||
handleEdit={(id, updatedData) => handleEdit(`${BE_SCHOOL_FEES_URL}`, id, updatedData, setTuitionFees)}
|
||||
handleDelete={(id) => handleDelete(`${BE_SCHOOL_FEES_URL}`, id, setTuitionFees)}
|
||||
type={1}
|
||||
/>
|
||||
</div>
|
||||
<div className="mt-12 w-4/5">
|
||||
<DiscountsSection
|
||||
discounts={tuitionDiscounts}
|
||||
setDiscounts={setTuitionDiscounts}
|
||||
handleCreate={(newData) => handleCreate(`${BE_SCHOOL_DISCOUNTS_URL}`, newData, setTuitionDiscounts)}
|
||||
handleEdit={(id, updatedData) => handleEdit(`${BE_SCHOOL_DISCOUNTS_URL}`, id, updatedData, setTuitionDiscounts)}
|
||||
handleDelete={(id) => handleDelete(`${BE_SCHOOL_DISCOUNTS_URL}`, id, setTuitionDiscounts)}
|
||||
onDiscountDelete={(id) => handleDiscountDelete(id, 1)}
|
||||
type={1}
|
||||
/>
|
||||
</div>
|
||||
<div className="mt-12 grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<div className="col-span-1 mt-4">
|
||||
<PaymentPlanSelector
|
||||
paymentPlans={tuitionPaymentPlans}
|
||||
setPaymentPlans={setTuitionPaymentPlans}
|
||||
handleEdit={(id, updatedData) => handleEdit(`${BE_SCHOOL_PAYMENT_PLANS_URL}`, id, updatedData, setRegistrationPaymentPlans)}
|
||||
type={1}
|
||||
/>
|
||||
</div>
|
||||
<div className="col-span-1 mt-4">
|
||||
<PaymentModeSelector
|
||||
paymentModes={tuitionPaymentModes}
|
||||
setPaymentModes={setTuitionPaymentModes}
|
||||
handleEdit={(id, updatedData) => handleEdit(`${BE_SCHOOL_PAYMENT_MODES_URL}`, id, updatedData, setTuitionPaymentModes)}
|
||||
type={1}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -5,10 +5,11 @@ import Popup from '@/components/Popup';
|
||||
import CheckBox from '@/components/CheckBox';
|
||||
import InputText from '@/components/InputText';
|
||||
import logger from '@/utils/logger';
|
||||
import SectionHeader from '@/components/SectionHeader';
|
||||
|
||||
import { ESTABLISHMENT_ID } from '@/utils/Url';
|
||||
|
||||
const FeesSection = ({ fees, setFees, discounts, handleCreate, handleEdit, handleDelete, type, subscriptionMode = false, selectedFees, handleFeeSelection }) => {
|
||||
const FeesSection = ({ fees, setFees, discounts, handleCreate, handleEdit, handleDelete, type, selectedFees, handleFeeSelection }) => {
|
||||
const [editingFee, setEditingFee] = useState(null);
|
||||
const [newFee, setNewFee] = useState(null);
|
||||
const [formData, setFormData] = useState({});
|
||||
@ -243,14 +244,7 @@ const FeesSection = ({ fees, setFees, discounts, handleCreate, handleEdit, handl
|
||||
}
|
||||
};
|
||||
|
||||
const columns = subscriptionMode
|
||||
? [
|
||||
{ name: 'NOM', label: 'Nom' },
|
||||
{ name: 'DESCRIPTION', label: 'Description' },
|
||||
{ name: 'MONTANT', label: 'Montant de base' },
|
||||
{ name: '', label: 'Sélection' }
|
||||
]
|
||||
: [
|
||||
const columns = [
|
||||
{ name: 'NOM', label: 'Nom' },
|
||||
{ name: 'MONTANT', label: 'Montant de base' },
|
||||
{ name: 'DESCRIPTION', label: 'Description' },
|
||||
@ -260,17 +254,13 @@ const FeesSection = ({ fees, setFees, discounts, handleCreate, handleEdit, handl
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
{!subscriptionMode && (
|
||||
<div className="flex justify-between items-center">
|
||||
<div className="flex items-center mb-4">
|
||||
<CreditCard className="w-6 h-6 text-emerald-500 mr-2" />
|
||||
<h2 className="text-xl font-semibold">Liste des frais</h2>
|
||||
</div>
|
||||
<button type="button" onClick={handleAddFee} className="text-emerald-500 hover:text-emerald-700">
|
||||
<Plus className="w-5 h-5" />
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
<SectionHeader
|
||||
icon={CreditCard}
|
||||
title={`${type == 0 ? "Liste des frais d'inscription" : "Liste des frais de scolarité"}`}
|
||||
description={`${type == 0 ? "Gérez vos frais d'inscription" : "Gérez vos frais de scolarité"}`}
|
||||
button={true}
|
||||
onClick={handleAddFee}
|
||||
/>
|
||||
<Table
|
||||
data={newFee ? [newFee, ...fees] : fees}
|
||||
columns={columns}
|
||||
|
||||
@ -1,8 +1,8 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import Pagination from '@/components/Pagination'; // Correction du chemin d'importatio,
|
||||
import Pagination from '@/components/Pagination'; // Correction du chemin d'importation
|
||||
|
||||
const Table = ({ data, columns, renderCell, itemsPerPage = 0, currentPage, totalPages, onPageChange, onRowClick, selectedRows, isSelectable = false, defaultTheme='bg-emerald-50' }) => {
|
||||
const Table = ({ data, columns, renderCell, itemsPerPage = 0, currentPage, totalPages, onPageChange, onRowClick, selectedRows, isSelectable = false, defaultTheme = 'bg-emerald-50' }) => {
|
||||
const handlePageChange = (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}` : ''}
|
||||
${isSelectable ? 'hover:bg-emerald-200' : ''}
|
||||
`}
|
||||
onClick={() => isSelectable && onRowClick && onRowClick(row)}
|
||||
onClick={() => {
|
||||
if (isSelectable && onRowClick) {
|
||||
// Si la ligne est déjà sélectionnée, transmettre une indication explicite de désélection
|
||||
if (selectedRows?.includes(row.id)) {
|
||||
onRowClick({ deselected: true, row }); // Désélectionner
|
||||
} else {
|
||||
onRowClick(row); // Sélectionner
|
||||
}
|
||||
}
|
||||
}}
|
||||
>
|
||||
{columns.map((column, colIndex) => (
|
||||
<td key={colIndex} className={`py-2 px-4 border-b border-gray-200 text-center text-sm ${selectedRows?.includes(row.id) ? 'text-white' : 'text-gray-700'}`} >
|
||||
<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)}
|
||||
</td>
|
||||
))}
|
||||
@ -61,6 +70,10 @@ Table.propTypes = {
|
||||
currentPage: PropTypes.number.isRequired,
|
||||
totalPages: PropTypes.number.isRequired,
|
||||
onPageChange: PropTypes.func.isRequired,
|
||||
onRowClick: PropTypes.func,
|
||||
selectedRows: PropTypes.arrayOf(PropTypes.any),
|
||||
isSelectable: PropTypes.bool,
|
||||
defaultTheme: PropTypes.string,
|
||||
};
|
||||
|
||||
export default Table;
|
||||
|
||||
Reference in New Issue
Block a user