feat: Gestion des pièces à fournir par les parents (configuration école)

This commit is contained in:
N3WT DE COMPET
2025-04-15 18:00:58 +02:00
parent 4c2e2f8756
commit a65bd47905
20 changed files with 956 additions and 569 deletions

View File

@ -163,6 +163,9 @@ class RegistrationFileGroup(models.Model):
def __str__(self):
return self.name
def __str__(self):
return f'{self.group.name} - {self.id}'
def registration_file_path(instance, filename):
# Génère le chemin : registration_files/dossier_rf_{student_id}/filename
return f'registration_files/dossier_rf_{instance.student_id}/{filename}'
@ -211,7 +214,7 @@ class RegistrationForm(models.Model):
# Many-to-Many Relationship
discounts = models.ManyToManyField(Discount, blank=True, related_name='register_forms')
fileGroup = models.ForeignKey(RegistrationFileGroup,
on_delete=models.CASCADE,
on_delete=models.SET_NULL,
related_name='register_forms',
null=True,
blank=True)
@ -223,6 +226,20 @@ class RegistrationForm(models.Model):
def __str__(self):
return "RF_" + self.student.last_name + "_" + self.student.first_name
def save(self, *args, **kwargs):
# Vérifier si un fichier existant doit être remplacé
if self.pk: # Si l'objet existe déjà dans la base de données
try:
old_instance = RegistrationForm.objects.get(pk=self.pk)
if old_instance.sepa_file and old_instance.sepa_file != self.sepa_file:
# Supprimer l'ancien fichier
old_instance.sepa_file.delete(save=False)
except RegistrationForm.DoesNotExist:
pass # L'objet n'existe pas encore, rien à supprimer
# 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}"
@ -247,3 +264,9 @@ class RegistrationTemplate(models.Model):
for reg_file in registration_files:
filenames.append(reg_file.file.path)
return filenames
class RegistrationParentFile(models.Model):
groups = models.ManyToManyField(RegistrationFileGroup, related_name='parent_files', blank=True)
name = models.CharField(max_length=255, default="")
description = models.CharField(blank=True, null=True)
file = models.FileField(null=True,blank=True, upload_to=registration_file_upload_to)

View File

@ -1,5 +1,5 @@
from rest_framework import serializers
from .models import RegistrationFileGroup, RegistrationForm, Student, Guardian, Sibling, Language, RegistrationTemplateMaster, RegistrationTemplate
from .models import RegistrationFileGroup, RegistrationForm, Student, Guardian, Sibling, Language, RegistrationTemplateMaster, RegistrationTemplate, RegistrationParentFile
from School.models import SchoolClass, Fee, Discount, FeeType
from School.serializers import FeeSerializer, DiscountSerializer
from Auth.models import ProfileRole, Profile
@ -24,6 +24,12 @@ class RegistrationTemplateSerializer(serializers.ModelSerializer):
model = RegistrationTemplate
fields = '__all__'
class RegistrationParentFileSerializer(serializers.ModelSerializer):
id = serializers.IntegerField(required=False)
class Meta:
model = RegistrationParentFile
fields = '__all__'
class GuardianSimpleSerializer(serializers.ModelSerializer):
associated_profile_email = serializers.SerializerMethodField()
@ -280,7 +286,7 @@ class RegistrationFormByParentSerializer(serializers.ModelSerializer):
class Meta:
model = RegistrationForm
fields = ['student', 'status']
fields = ['student', 'status', 'sepa_file']
def __init__(self, *args, **kwargs):
super(RegistrationFormByParentSerializer, self).__init__(*args, **kwargs)

View File

@ -7,7 +7,7 @@ from .views import RegisterFormView, RegisterFormWithIdView, send, resend, archi
# SubClasses
from .views import StudentView, GuardianView, ChildrenListView, StudentListView, DissociateGuardianView
# Files
from .views import RegistrationTemplateMasterView, RegistrationTemplateMasterSimpleView, RegistrationTemplateView, RegistrationTemplateSimpleView
from .views import RegistrationTemplateMasterView, RegistrationTemplateMasterSimpleView, RegistrationTemplateView, RegistrationTemplateSimpleView, RegistrationParentFileSimpleView, RegistrationParentFileView
from .views import RegistrationFileGroupView, RegistrationFileGroupSimpleView, get_registration_files_by_group
from .views import registration_file_views, get_templates_by_rf
@ -39,6 +39,9 @@ urlpatterns = [
re_path(r'^registrationTemplates/(?P<id>[0-9]+)$', RegistrationTemplateSimpleView.as_view(), name='registrationTemplates'),
re_path(r'^registrationTemplates$', RegistrationTemplateView.as_view(), name="registrationTemplates"),
re_path(r'^registrationParentFiles/(?P<id>[0-9]+)$', RegistrationParentFileSimpleView.as_view(), name='registrationParentFiles'),
re_path(r'^registrationParentFiles$', RegistrationParentFileView.as_view(), name="registrationParentFiles"),
re_path(r'^students/(?P<student_id>[0-9]+)/guardians/(?P<guardian_id>[0-9]+)/dissociate', DissociateGuardianView.as_view(), name='dissociate-guardian'),
]

View File

@ -1,5 +1,5 @@
from .register_form_views import RegisterFormView, RegisterFormWithIdView, send, resend, archive, get_templates_by_rf
from .registration_file_views import RegistrationTemplateMasterView, RegistrationTemplateMasterSimpleView, RegistrationTemplateView, RegistrationTemplateSimpleView
from .registration_file_views import RegistrationTemplateMasterView, RegistrationTemplateMasterSimpleView, RegistrationTemplateView, RegistrationTemplateSimpleView, RegistrationParentFileView, RegistrationParentFileSimpleView
from .registration_file_group_views import RegistrationFileGroupView, RegistrationFileGroupSimpleView, get_registration_files_by_group
from .student_views import StudentView, StudentListView, ChildrenListView
from .guardian_views import GuardianView, DissociateGuardianView
@ -12,6 +12,8 @@ __all__ = [
'archive',
'RegistrationTemplateView',
'RegistrationTemplateSimpleView',
'RegistrationParentFileSimpleView',
'RegistrationParentFileView',
'RegistrationTemplateMasterView',
'RegistrationTemplateMasterSimpleView',
'RegistrationFileGroupView',

View File

@ -0,0 +1,159 @@
from django.http.response import JsonResponse
from drf_yasg.utils import swagger_auto_schema
from drf_yasg import openapi
from rest_framework.parsers import MultiPartParser, FormParser
from rest_framework.response import Response
from rest_framework.views import APIView
from rest_framework import status
from Subscriptions.serializers import RegistrationTemplateMasterSerializer, RegistrationTemplateSerializer
from Subscriptions.models import RegistrationTemplateMaster, RegistrationTemplate
from N3wtSchool import bdd
class RegistrationTemplateMasterView(APIView):
@swagger_auto_schema(
operation_description="Récupère tous les masters de templates d'inscription",
responses={200: RegistrationTemplateMasterSerializer(many=True)}
)
def get(self, request):
masters = RegistrationTemplateMaster.objects.all()
serializer = RegistrationTemplateMasterSerializer(masters, many=True)
return Response(serializer.data)
@swagger_auto_schema(
operation_description="Crée un nouveau master de template d'inscription",
request_body=RegistrationTemplateMasterSerializer,
responses={
201: RegistrationTemplateMasterSerializer,
400: "Données invalides"
}
)
def post(self, request):
serializer = RegistrationTemplateMasterSerializer(data=request.data)
if serializer.is_valid():
serializer.save()
return Response(serializer.data, status=status.HTTP_201_CREATED)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
class RegistrationTemplateMasterSimpleView(APIView):
@swagger_auto_schema(
operation_description="Récupère un master de template d'inscription spécifique",
responses={
200: RegistrationTemplateMasterSerializer,
404: "Master non trouvé"
}
)
def get(self, request, id):
master = bdd.getObject(_objectName=RegistrationTemplateMaster, _columnName='id', _value=id)
if master is None:
return JsonResponse({"errorMessage":'Le master de template n\'a pas été trouvé'}, safe=False, status=status.HTTP_404_NOT_FOUND)
serializer = RegistrationTemplateMasterSerializer(master)
return JsonResponse(serializer.data, safe=False)
@swagger_auto_schema(
operation_description="Met à jour un master de template d'inscription existant",
request_body=RegistrationTemplateMasterSerializer,
responses={
200: RegistrationTemplateMasterSerializer,
400: "Données invalides",
404: "Master non trouvé"
}
)
def put(self, request, id):
master = bdd.getObject(_objectName=RegistrationTemplateMaster, _columnName='id', _value=id)
if master is None:
return JsonResponse({'erreur': 'Le master de template n\'a pas été trouvé'}, safe=False, status=status.HTTP_404_NOT_FOUND)
serializer = RegistrationTemplateMasterSerializer(master, data=request.data)
if serializer.is_valid():
serializer.save()
return Response(serializer.data, status=status.HTTP_200_OK)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
@swagger_auto_schema(
operation_description="Supprime un master de template d'inscription",
responses={
204: "Suppression réussie",
404: "Master non trouvé"
}
)
def delete(self, request, id):
master = bdd.getObject(_objectName=RegistrationTemplateMaster, _columnName='id', _value=id)
if master is not None:
master.delete()
return JsonResponse({'message': 'La suppression du master de template a été effectuée avec succès'}, safe=False, status=status.HTTP_204_NO_CONTENT)
else:
return JsonResponse({'erreur': 'Le master de template n\'a pas été trouvé'}, safe=False, status=status.HTTP_404_NOT_FOUND)
class RegistrationTemplateView(APIView):
@swagger_auto_schema(
operation_description="Récupère tous les templates d'inscription",
responses={200: RegistrationTemplateSerializer(many=True)}
)
def get(self, request):
templates = RegistrationTemplate.objects.all()
serializer = RegistrationTemplateSerializer(templates, many=True)
return Response(serializer.data)
@swagger_auto_schema(
operation_description="Crée un nouveau template d'inscription",
request_body=RegistrationTemplateSerializer,
responses={
201: RegistrationTemplateSerializer,
400: "Données invalides"
}
)
def post(self, request):
serializer = RegistrationTemplateSerializer(data=request.data)
if serializer.is_valid():
serializer.save()
return Response(serializer.data, status=status.HTTP_201_CREATED)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
class RegistrationTemplateSimpleView(APIView):
@swagger_auto_schema(
operation_description="Récupère un template d'inscription spécifique",
responses={
200: RegistrationTemplateSerializer,
404: "Template non trouvé"
}
)
def get(self, request, id):
template = bdd.getObject(_objectName=RegistrationTemplate, _columnName='id', _value=id)
if template is None:
return JsonResponse({"errorMessage":'Le template d\'inscription n\'a pas été trouvé'}, safe=False, status=status.HTTP_404_NOT_FOUND)
serializer = RegistrationTemplateSerializer(template)
return JsonResponse(serializer.data, safe=False)
@swagger_auto_schema(
operation_description="Met à jour un template d'inscription existant",
request_body=RegistrationTemplateSerializer,
responses={
200: RegistrationTemplateSerializer,
400: "Données invalides",
404: "Template non trouvé"
}
)
def put(self, request, id):
template = bdd.getObject(_objectName=RegistrationTemplate, _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 = RegistrationTemplateSerializer(template, data=request.data)
if serializer.is_valid():
serializer.save()
return Response({'message': 'Template mis à jour avec succès', 'data': serializer.data}, status=status.HTTP_200_OK)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
@swagger_auto_schema(
operation_description="Supprime un template d'inscription",
responses={
204: "Suppression réussie",
404: "Template non trouvé"
}
)
def delete(self, request, id):
template = bdd.getObject(_objectName=RegistrationTemplate, _columnName='id', _value=id)
if template is not None:
template.delete()
return JsonResponse({'message': 'La suppression du template a été effectuée avec succès'}, safe=False, status=status.HTTP_204_NO_CONTENT)
else:
return JsonResponse({'erreur': 'Le template n\'a pas été trouvé'}, safe=False, status=status.HTTP_404_NOT_FOUND)

View File

@ -6,8 +6,8 @@ from rest_framework.response import Response
from rest_framework.views import APIView
from rest_framework import status
from Subscriptions.serializers import RegistrationTemplateMasterSerializer, RegistrationTemplateSerializer
from Subscriptions.models import RegistrationTemplateMaster, RegistrationTemplate
from Subscriptions.serializers import RegistrationTemplateMasterSerializer, RegistrationTemplateSerializer, RegistrationParentFileSerializer
from Subscriptions.models import RegistrationTemplateMaster, RegistrationTemplate, RegistrationParentFile
from N3wtSchool import bdd
class RegistrationTemplateMasterView(APIView):
@ -157,3 +157,78 @@ class RegistrationTemplateSimpleView(APIView):
return JsonResponse({'message': 'La suppression du template a été effectuée avec succès'}, safe=False, status=status.HTTP_204_NO_CONTENT)
else:
return JsonResponse({'erreur': 'Le template n\'a pas été trouvé'}, safe=False, status=status.HTTP_404_NOT_FOUND)
class RegistrationParentFileView(APIView):
@swagger_auto_schema(
operation_description="Récupère tous les fichiers parents",
responses={200: RegistrationParentFileSerializer(many=True)}
)
def get(self, request):
templates = RegistrationParentFile.objects.all()
serializer = RegistrationParentFileSerializer(templates, many=True)
return Response(serializer.data)
@swagger_auto_schema(
operation_description="Crée un nouveau fichier parent",
request_body=RegistrationParentFileSerializer,
responses={
201: RegistrationParentFileSerializer,
400: "Données invalides"
}
)
def post(self, request):
serializer = RegistrationParentFileSerializer(data=request.data)
if serializer.is_valid():
serializer.save()
return Response(serializer.data, status=status.HTTP_201_CREATED)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
class RegistrationParentFileSimpleView(APIView):
@swagger_auto_schema(
operation_description="Récupère un fichier parent spécifique",
responses={
200: RegistrationParentFileSerializer,
404: "Fichier parent non trouvé"
}
)
def get(self, request, id):
template = bdd.getObject(_objectName=RegistrationParentFile, _columnName='id', _value=id)
if template is None:
return JsonResponse({"errorMessage":'Le fichier parent n\'a pas été trouvé'}, safe=False, status=status.HTTP_404_NOT_FOUND)
serializer = RegistrationParentFileSerializer(template)
return JsonResponse(serializer.data, safe=False)
@swagger_auto_schema(
operation_description="Met à jour un fichier parent existant",
request_body=RegistrationParentFileSerializer,
responses={
200: RegistrationParentFileSerializer,
400: "Données invalides",
404: "Fichier parent non trouvé"
}
)
def put(self, request, id):
template = bdd.getObject(_objectName=RegistrationParentFile, _columnName='id', _value=id)
if template is None:
return JsonResponse({'erreur': 'Le fichier parent n\'a pas été trouvé'}, safe=False, status=status.HTTP_404_NOT_FOUND)
serializer = RegistrationParentFileSerializer(template, data=request.data)
if serializer.is_valid():
serializer.save()
return Response({'message': 'Fichier parent mis à jour avec succès', 'data': serializer.data}, status=status.HTTP_200_OK)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
@swagger_auto_schema(
operation_description="Supprime un fichier parent",
responses={
204: "Suppression réussie",
404: "Fichier parent non trouvé"
}
)
def delete(self, request, id):
template = bdd.getObject(_objectName=RegistrationParentFile, _columnName='id', _value=id)
if template is not None:
template.delete()
return JsonResponse({'message': 'La suppression du fichier parent a été effectuée avec succès'}, safe=False, status=status.HTTP_204_NO_CONTENT)
else:
return JsonResponse({'erreur': 'Le fichier parent n\'a pas été trouvé'}, safe=False, status=status.HTTP_404_NOT_FOUND)

View File

@ -2,13 +2,13 @@
import React, { useEffect, useState } from 'react';
import { useRouter } from 'next/navigation';
import Table from '@/components/Table';
import { Edit, Users } from 'lucide-react';
import { Edit3, Users, Download, Eye } from 'lucide-react';
import StatusLabel from '@/components/StatusLabel';
import { FE_PARENTS_EDIT_INSCRIPTION_URL } from '@/utils/Url';
import { fetchChildren } from '@/app/actions/subscriptionAction';
import logger from '@/utils/logger';
import { useSession } from 'next-auth/react';
import { FE_USERS_LOGIN_URL } from '@/utils/Url';
import { FE_USERS_LOGIN_URL, BASE_URL } from '@/utils/Url';
import { useEstablishment } from '@/context/EstablishmentContext';
export default function ParentHomePage() {
@ -40,10 +40,12 @@ export default function ParentHomePage() {
if (!selectedEstablishmentId && userEstablishments.length > 0) {
setSelectedEstablishmentId(userEstablishments[0].id);
}
console.log(selectedEstablishmentId)
fetchChildren(userIdFromSession, selectedEstablishmentId).then(data => {
setChildren(data);
});
if (selectedEstablishmentId) {
fetchChildren(userIdFromSession, selectedEstablishmentId).then(data => {
setChildren(data);
});
}
}
}, [status, session, selectedEstablishmentId]);
@ -52,6 +54,12 @@ export default function ParentHomePage() {
setSelectedEstablishmentId(establishmentId);
};
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}`);
@ -77,17 +85,57 @@ export default function ParentHomePage() {
{
name: 'Actions',
transform: (row) => (
<div className="flex justify-center">
<button
className="p-2 hover:bg-gray-100 rounded-full transition-colors"
onClick={(e) => {
e.stopPropagation();
handleEdit(row.student.id);
}}
aria-label="Modifier"
>
<Edit className="h-5 w-5" />
</button>
<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
}}
aria-label="Remplir le dossier"
>
<Edit3 className="h-5 w-5" />
</button>
)}
{row.status === 3 && (
<button
className="text-purple-500 hover:text-purple-700"
onClick={(e) => {
e.stopPropagation();
handleView(row.student.id); // Visualiser le dossier
}}
aria-label="Visualiser le dossier"
>
<Eye className="h-5 w-5" />
</button>
)}
{row.status === 7 && (
<>
<button
className="text-purple-500 hover:text-purple-700"
onClick={(e) => {
e.stopPropagation();
handleView(row.student.id); // Visualiser le dossier
}}
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
target="_blank"
rel="noopener noreferrer"
className="text-green-500 hover:text-green-700"
aria-label="Télécharger le mandat SEPA"
>
<Download className="h-5 w-5" />
</a>
</>
)}
</div>
)
}

View File

@ -1,6 +1,7 @@
import { BE_SUBSCRIPTION_REGISTRATIONFILE_GROUPS_URL,
BE_SUBSCRIPTION_REGISTRATION_TEMPLATES_URL,
BE_SUBSCRIPTION_REGISTRATION_TEMPLATE_MASTER_URL,
BE_SUBSCRIPTION_REGISTRATION_PARENT_FILES_URL,
FE_API_DOCUSEAL_CLONE_URL,
FE_API_DOCUSEAL_DOWNLOAD_URL,
FE_API_DOCUSEAL_GENERATE_TOKEN
@ -91,6 +92,60 @@ export const fetchRegistrationFileFromGroup = async (groupId) => {
return response.json();
}
export const fetchRegistrationParentFiles = (id = null) => {
let url = `${BE_SUBSCRIPTION_REGISTRATION_PARENT_FILES_URL}`
if (id) {
url = `${BE_SUBSCRIPTION_REGISTRATION_PARENT_FILES_URL}/${id}`;
}
const request = new Request(
`${url}`,
{
method:'GET',
headers: {
'Content-Type':'application/json'
},
}
);
return fetch(request).then(requestResponseHandler)
};
export const createRegistrationParentFiles = (data,csrfToken) => {
return fetch(`${BE_SUBSCRIPTION_REGISTRATION_PARENT_FILES_URL}`, {
method: 'POST',
body: JSON.stringify(data),
headers: {
'X-CSRFToken': csrfToken,
'Content-Type': 'application/json',
},
credentials: 'include',
})
.then(requestResponseHandler)
}
export const editRegistrationParentFiles = (id, data, csrfToken) => {
return fetch(`${BE_SUBSCRIPTION_REGISTRATION_PARENT_FILES_URL}/${id}`, {
method: 'PUT',
body: JSON.stringify(data),
headers: {
'X-CSRFToken': csrfToken,
'Content-Type': 'application/json',
},
credentials: 'include',
})
.then(requestResponseHandler)
}
export const deleteRegistrationParentFiles = (id, csrfToken) => {
return fetch(`${BE_SUBSCRIPTION_REGISTRATION_PARENT_FILES_URL}/${id}`, {
method: 'DELETE',
headers: {
'X-CSRFToken': csrfToken,
},
credentials: 'include',
})
}
export const fetchRegistrationTemplates = (id = null) => {
let url = `${BE_SUBSCRIPTION_REGISTRATION_TEMPLATES_URL}`
if (id) {

View File

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

View File

@ -3,6 +3,11 @@ import { PhoneInput } from 'react-international-phone';
import 'react-international-phone/style.css';
export default function InputPhone({ name, label, value, onChange, errorMsg, className, required }) {
const handlePhoneChange = (phone) => {
// Appeler onChange avec un objet personnalisé
onChange({ target: { name, value: phone } });
};
return (
<div className={`${className}`}>
<label htmlFor={name} className="block text-sm font-medium text-gray-700">
@ -13,7 +18,7 @@ export default function InputPhone({ name, label, value, onChange, errorMsg, cla
<PhoneInput
defaultCountry="fr"
value={value}
onChange={(phone) => onChange(phone)}
onChange={handlePhoneChange}
inputProps={{
name: name,
required: required,

View File

@ -0,0 +1,59 @@
import React, { useState } 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 handleFileChange = (e) => {
const file = e.target.files[0];
if (file) {
setLocalFileName(file.name);
logger.debug('Fichier sélectionné:', file.name);
onFileSelect(file); // Appelle la fonction passée en prop
}
};
const handleFileDrop = (e) => {
e.preventDefault();
const file = e.dataTransfer.files[0];
if (file) {
setLocalFileName(file.name);
logger.debug('Fichier déposé:', file.name);
onFileSelect(file); // Appelle la fonction passée en prop
}
};
return (
<div className="border p-4 rounded-md shadow-md">
<h3 className="text-lg font-semibold mb-4">{`${selectionMessage}`}</h3>
<div
className="border-2 border-dashed border-gray-500 p-6 rounded-lg flex flex-col items-center justify-center cursor-pointer hover:border-emerald-500"
onClick={() => document.getElementById('fileInput').click()} // Ouvre l'explorateur de fichiers au clic
onDragOver={(e) => e.preventDefault()}
onDrop={handleFileDrop}
>
<CloudUpload className="w-12 h-12 text-emerald-500 mb-4" /> {/* Icône de cloud */}
<input
type="file"
accept=".pdf"
onChange={handleFileChange}
className="hidden"
id="fileInput"
/>
<label htmlFor="fileInput" className="text-center text-gray-500">
<p className="text-lg font-semibold text-gray-800">Déposez votre fichier ici</p>
<p className="text-sm text-gray-500 mt-2">ou cliquez pour sélectionner un fichier PDF</p>
</label>
</div>
{localFileName && (
<div className="mt-4 flex items-center space-x-4 bg-gray-100 p-3 rounded-md shadow-sm">
<CloudUpload className="w-6 h-6 text-emerald-500" />
<p className="text-sm font-medium text-gray-800">
<span className="font-semibold">{localFileName}</span>
</p>
</div>
)}
</div>
);
}

View File

@ -120,7 +120,7 @@ const InscriptionForm = ( { students, registrationDiscounts, tuitionDiscounts, r
};
const handleChange = (e) => {
const { name, value, type } = e.target;
const { name, value } = e.target;
setFormData((prevState) => ({
...prevState,
[name]: value,
@ -365,7 +365,7 @@ const InscriptionForm = ( { students, registrationDiscounts, tuitionDiscounts, r
/>
<InputPhone
name="guardianPhone"
label={t('Numéro de téléphone (optionnel)')}
label="Numéro de téléphone (optionnel)"
value={formData.guardianPhone}
onChange={handleChange}
className="w-full mt-4"

View File

@ -21,6 +21,7 @@ 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é
@ -385,6 +386,14 @@ export default function InscriptionFormShared({
fileTemplates={fileTemplates.filter(template => !template.is_required)}
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}
/>
</>
)}

View File

@ -6,6 +6,7 @@ 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';
export default function ValidateSubscription({ studentId, firstName, lastName, paymentMode, file, onAccept }) {
const [token, setToken] = useState(null);
@ -100,47 +101,14 @@ export default function ValidateSubscription({ studentId, firstName, lastName, p
)}
{currentPage === 2 && isSepa && (
<div className="border p-4 rounded-md shadow-md">
<h3 className="text-lg font-semibold mb-4">Sélection du mandat de pélèvement SEPA</h3>
<div
className="border-2 border-dashed border-gray-500 p-6 rounded-lg flex flex-col items-center justify-center cursor-pointer hover:border-emerald-500"
onClick={() => document.getElementById('fileInput').click()} // Ouvre l'explorateur de fichiers au clic
onDragOver={(e) => e.preventDefault()}
onDrop={(e) => {
e.preventDefault();
const file = e.dataTransfer.files[0];
if (file) {
setUploadedFileName(file.name); // Stocke uniquement le nom du fichier
logger.debug('Fichier déposé:', file.name);
}
}}
>
<CloudUpload className="w-12 h-12 text-emerald-500 mb-4" /> {/* Icône de cloud */}
<input
type="file"
accept=".pdf"
onChange={(e) => {
const file = e.target.files[0];
if (file) {
setUploadedFileName(file.name); // Stocke uniquement le nom du fichier
logger.debug('Fichier sélectionné:', file.name);
}
}}
className="hidden"
id="fileInput"
/>
<label htmlFor="fileInput" className="text-center text-gray-500">
<p className="text-lg font-semibold text-gray-800">Déposez votre fichier ici</p>
<p className="text-sm text-gray-500 mt-2">ou cliquez pour sélectionner un fichier PDF</p>
</label>
</div>
{uploadedFileName && (
<div className="mt-4 flex items-center space-x-4 bg-gray-100 p-3 rounded-md shadow-sm">
<CloudUpload className="w-6 h-6 text-emerald-500" />
<p className="text-sm font-medium text-gray-800"><span className="font-semibold">{uploadedFileName}</span></p>
</div>
)}
</div>
<FileUpload
selectionMessage='Sélectionnez un mandat de prélèvement SEPA'
onFileSelect={(file) => {
setUploadedFileName(file.name); // Stocke uniquement le nom du fichier
logger.debug('Fichier sélectionné:', file.name);
}}
uploadedFileName={uploadedFileName}
/>
)}
{/* Boutons de navigation */}

View File

@ -6,6 +6,7 @@ import InputTextWithColorIcon from '@/components/InputTextWithColorIcon';
import { DndProvider } from 'react-dnd';
import { HTML5Backend } from 'react-dnd-html5-backend';
import SpecialityItem from '@/components/Structure/Configuration/SpecialityItem';
import { useEstablishment } from '@/context/EstablishmentContext';
import logger from '@/utils/logger';
const SpecialitiesSection = ({ specialities, setSpecialities, handleCreate, handleEdit, handleDelete }) => {
@ -21,6 +22,8 @@ const SpecialitiesSection = ({ specialities, setSpecialities, handleCreate, hand
const [removePopupMessage, setRemovePopupMessage] = useState("");
const [removePopupOnConfirm, setRemovePopupOnConfirm] = useState(() => {});
const { selectedEstablishmentId } = useEstablishment();
// Récupération des messages d'erreur
const getError = (field) => {
return localErrors?.[field]?.[0];
@ -41,9 +44,14 @@ const SpecialitiesSection = ({ specialities, setSpecialities, handleCreate, hand
};
const handleSaveNewSpeciality = () => {
if (
newSpeciality.name) {
handleCreate(newSpeciality)
if (newSpeciality.name) {
// Ajouter l'ID de l'établissement à la nouvelle spécialité
const specialityData = {
...newSpeciality,
establishment: selectedEstablishmentId, // Inclure l'ID de l'établissement
};
handleCreate(specialityData)
.then((createdSpeciality) => {
setSpecialities([createdSpeciality, ...specialities]);
setNewSpeciality(null);
@ -52,8 +60,8 @@ const SpecialitiesSection = ({ specialities, setSpecialities, handleCreate, hand
.catch((error) => {
logger.error('Error:', error.message);
if (error.details) {
logger.error('Form errors:', error.details);
setLocalErrors(error.details);
logger.error('Form errors:', error.details);
setLocalErrors(error.details);
}
});
} else {

View File

@ -9,7 +9,7 @@ import MultiSelect from '@/components/MultiSelect'; // Import du composant Multi
import { useCsrfToken } from '@/context/CsrfContext';
import { useEstablishment } from '@/context/EstablishmentContext';
export default function FileUpload({ handleCreateTemplateMaster, handleEditTemplateMaster, fileToEdit = null, onSuccess }) {
export default function FileUploadDocuSeal({ handleCreateTemplateMaster, handleEditTemplateMaster, fileToEdit = null, onSuccess }) {
const [isRequired, setIsRequired] = useState(false); // État pour le toggle isRequired
const [order, setOrder] = useState(0);
const [groups, setGroups] = useState([]);

View File

@ -1,8 +1,8 @@
import React, { useState, useEffect } from 'react';
import { Plus, Download, Edit3, Trash2, FolderPlus, Signature } from 'lucide-react';
import { Plus, Download, Edit3, Trash2, FolderPlus, Signature, FileText, Check, X } from 'lucide-react';
import Modal from '@/components/Modal';
import Table from '@/components/Table';
import FileUpload from '@/components/Structure/Files/FileUpload';
import FileUploadDocuSeal from '@/components/Structure/Files/FileUploadDocuSeal';
import { BASE_URL } from '@/utils/Url';
import {
fetchRegistrationFileGroups,
@ -13,10 +13,15 @@ import {
createRegistrationTemplateMaster,
editRegistrationTemplateMaster,
deleteRegistrationTemplateMaster,
fetchRegistrationTemplates
fetchRegistrationTemplates,
fetchRegistrationParentFiles,
createRegistrationParentFiles,
editRegistrationParentFiles,
deleteRegistrationParentFiles
} from '@/app/actions/registerFileGroupAction';
import RegistrationFileGroupForm from '@/components/Structure/Files/RegistrationFileGroupForm';
import logger from '@/utils/logger';
import ParentFilesSection from '@/components/Structure/Files/ParentFilesSection';
export default function FilesGroupsManagement({ csrfToken, selectedEstablishmentId }) {
const [templateMasters, setTemplateMasters] = useState([]);
@ -29,6 +34,10 @@ export default function FilesGroupsManagement({ csrfToken, selectedEstablishment
const [isGroupModalOpen, setIsGroupModalOpen] = useState(false);
const [groupToEdit, setGroupToEdit] = useState(null);
const [reloadTemplates, setReloadTemplates] = useState(false);
const [editingDocumentId, setEditingDocumentId] = useState(null);
const [formData, setFormData] = useState({});
const [parentFiles, setParentFiles] = useState([]);
const handleReloadTemplates = () => {
setReloadTemplates(true);
@ -47,10 +56,12 @@ export default function FilesGroupsManagement({ csrfToken, selectedEstablishment
Promise.all([
fetchRegistrationTemplateMaster(),
fetchRegistrationFileGroups(selectedEstablishmentId),
fetchRegistrationTemplates()
]).then(([filesTemplateMasters, groupsData, filesTemplates]) => {
fetchRegistrationTemplates(),
fetchRegistrationParentFiles()
]).then(([filesTemplateMasters, groupsData, filesTemplates, filesParentFiles]) => {
setGroups(groupsData);
setTemplates(filesTemplates);
setParentFiles(filesParentFiles);
// Transformer chaque fichier pour inclure les informations complètes du groupe
const transformedFiles = filesTemplateMasters.map(file => transformFileData(file, groupsData));
setTemplateMasters(transformedFiles);
@ -189,7 +200,13 @@ export default function FilesGroupsManagement({ csrfToken, selectedEstablishment
alert('Erreur lors de l\'opération sur le groupe');
});
} else {
createRegistrationFileGroup(groupData, csrfToken)
// Ajouter l'établissement sélectionné lors de la création d'un nouveau groupe
const newGroupData = {
...groupData,
establishment: selectedEstablishmentId
};
createRegistrationFileGroup(newGroupData, csrfToken)
.then(newGroup => {
setGroups([...groups, newGroup]);
setIsGroupModalOpen(false);
@ -233,14 +250,135 @@ 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 createRegistrationParentFiles(newParentFile, csrfToken)
.then((createdFile) => {
// Ajouter le nouveau fichier parent à la liste existante
setParentFiles((prevFiles) => [...prevFiles, createdFile]);
logger.debug('Document parent créé avec succès:', createdFile);
})
.catch((error) => {
logger.error('Erreur lors de la création du document parent:', error);
alert('Une erreur est survenue lors de la création du document parent.');
});
};
const handleEdit = (id, updatedFile) => {
return editRegistrationParentFiles(id, updatedFile, csrfToken)
.then((response) => {
const modifiedFile = response.data; // Extraire les données mises à jour
// Mettre à jour la liste des fichiers parents
setParentFiles((prevFiles) =>
prevFiles.map((file) => (file.id === id ? modifiedFile : file))
);
logger.debug('Document parent mis à jour avec succès:', modifiedFile);
return modifiedFile; // Retourner le fichier mis à jour
})
.catch((error) => {
logger.error('Erreur lors de la modification du document parent:', error);
alert('Une erreur est survenue lors de la modification du document parent.');
throw error;
});
};
const handleDelete = (id) => {
return deleteRegistrationParentFiles(id, csrfToken)
.then(() => {
// Mettre à jour la liste des fichiers parents en supprimant l'élément correspondant
setParentFiles((prevFiles) => prevFiles.filter((file) => file.id !== id));
logger.debug('Document parent supprimé avec succès:', id);
})
.catch((error) => {
logger.error('Erreur lors de la suppression du fichier parent:', error);
});
};
const filteredFiles = templateMasters.filter(file => {
if (!selectedGroup) return true;
return file.groups && file.groups.some(group => group.id === parseInt(selectedGroup));
});
const columnsFiles = [
{ name: 'Nom du fichier', transform: (row) => row.name },
{ name: 'Groupes', transform: (row) => row.groups && row.groups.length > 0 ? row.groups.map(group => group.name).join(', ') : 'Aucun' },
{ name: 'Nom du formulaire', transform: (row) => row.name },
{ name: 'Dossiers d\'inscription', transform: (row) => row.groups && row.groups.length > 0 ? row.groups.map(group => group.name).join(', ') : 'Aucun' },
{ name: 'Actions', transform: (row) => (
<div className="flex items-center justify-center gap-2">
{row.file && (
@ -259,7 +397,7 @@ export default function FilesGroupsManagement({ csrfToken, selectedEstablishment
];
const columnsGroups = [
{ name: 'Nom du groupe', transform: (row) => row.name },
{ name: 'Nom du dossier', transform: (row) => row.name },
{ name: 'Description', transform: (row) => row.description },
{ name: 'Actions', transform: (row) => (
<div className="flex items-center justify-center gap-2">
@ -273,8 +411,15 @@ 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>
<div className="space-y-12">
{/* Modal pour les fichiers */}
<Modal
isOpen={isModalOpen}
setIsOpen={(isOpen) => {
@ -285,19 +430,21 @@ export default function FilesGroupsManagement({ csrfToken, selectedEstablishment
}}
title={isEditing ? 'Modification du document' : 'Ajouter un document'}
ContentComponent={() => (
<FileUpload
<FileUploadDocuSeal
handleCreateTemplateMaster={handleCreateTemplateMaster}
handleEditTemplateMaster={handleEditTemplateMaster}
fileToEdit={fileToEdit}
onSuccess={handleReloadTemplates}
/>
)}
modalClassName='w-4/5 h-4/5'
modalClassName="w-4/5 h-4/5"
/>
{/* Modal pour les groupes */}
<Modal
isOpen={isGroupModalOpen}
setIsOpen={setIsGroupModalOpen}
title={groupToEdit ? "Modifier le groupe" : "Ajouter un groupe de templateMasters"}
title={groupToEdit ? 'Modifier le groupe' : 'Ajouter un groupe de templateMasters'}
ContentComponent={() => (
<RegistrationFileGroupForm
onSubmit={handleGroupSubmit}
@ -305,56 +452,73 @@ export default function FilesGroupsManagement({ csrfToken, selectedEstablishment
/>
)}
/>
<div className="mt-8 mb-4">
<div className="flex justify-between items-center mb-4">
<h2 className="text-xl font-bold">Groupes de fichiers</h2>
{/* 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-blue-600 text-white p-2 rounded-full shadow hover:bg-blue-900 transition duration-200"
className="flex items-center bg-emerald-600 text-white p-2 rounded-full shadow hover:bg-emerald-700 transition duration-200"
>
<FolderPlus className="w-5 h-5" />
<Plus className="w-6 h-6" />
</button>
</div>
<Table
data={groups}
columns={columnsGroups}
itemsPerPage={5}
currentPage={1}
totalPages={Math.ceil(groups.length / 5)}
/>
</div>
{groups.length > 0 && (
<div className="mt-8">
<div className="flex justify-between items-center mb-4">
<h2 className="text-xl font-bold">Fichiers</h2>
<div className="flex items-center gap-4">
<select
className="border rounded p-2"
value={selectedGroup || ''}
onChange={(e) => setSelectedGroup(e.target.value)}
>
<option value="">Tous les groupes</option>
{groups.map(group => (
<option key={group.id} value={group.id}>{group.name}</option>
))}
</select>
<button
onClick={() => { setIsModalOpen(true); setIsEditing(false); }}
className="flex items-center bg-emerald-600 text-white p-2 rounded-full shadow hover:bg-emerald-900 transition duration-200"
>
<Plus className="w-5 h-5" />
</button>
{/* 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>
<Table
data={filteredFiles}
columns={columnsFiles}
itemsPerPage={10}
currentPage={1}
totalPages={Math.ceil(filteredFiles.length / 10)}
/>
<button
onClick={() => {
setIsModalOpen(true);
setIsEditing(false);
}}
className="flex items-center bg-emerald-600 text-white p-2 rounded-full shadow hover:bg-emerald-700 transition duration-200"
>
<Plus className="w-6 h-6" />
</button>
</div>
)}
<Table
data={filteredFiles}
columns={columnsFiles}
/>
</div>
{/* Section Pièces à fournir */}
<ParentFilesSection
parentFiles={parentFiles}
setParentFiles={setParentFiles}
groups={groups}
handleCreate={handleCreate}
handleEdit={handleEdit}
handleDelete={handleDelete}
/>
</div>
);
}

View File

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

View File

@ -0,0 +1,246 @@
import React, { useState } from 'react';
import { Plus, Edit3, Trash2, Check, X, FileText } from 'lucide-react';
import Table from '@/components/Table';
import InputText from '@/components/InputText';
import MultiSelect from '@/components/MultiSelect';
import Popup from '@/components/Popup';
import logger from '@/utils/logger';
export default function ParentFilesSection({ parentFiles, groups, handleCreate, handleEdit, handleDelete }) {
const [editingDocumentId, setEditingDocumentId] = useState(null);
const [formData, setFormData] = useState(null);
const [selectedGroups, setSelectedGroups] = useState([]); // Gestion des groupes sélectionnés
const [popupVisible, setPopupVisible] = useState(false);
const [popupMessage, setPopupMessage] = useState("");
const [removePopupVisible, setRemovePopupVisible] = useState(false);
const [removePopupMessage, setRemovePopupMessage] = useState("");
const [removePopupOnConfirm, setRemovePopupOnConfirm] = useState(() => {});
const handleAddEmptyRequiredDocument = () => {
setEditingDocumentId('new');
setFormData({ name: '', description: '', groups: [] });
setSelectedGroups([]); // Réinitialiser les groupes sélectionnés
};
const handleEditDocument = (document) => {
setEditingDocumentId(document.id);
setFormData(document);
const initialSelectedGroups = document.groups.map((groupId) =>
groups.find((group) => group.id === groupId)
);
setSelectedGroups(initialSelectedGroups);
};
const handleSaveDocument = () => {
if (!formData.name) {
alert('Le nom de la pièce est requis.');
return;
}
const updatedFormData = {
...formData,
groups: selectedGroups.map((group) => group.id),
};
if (editingDocumentId === 'new') {
handleCreate(updatedFormData).then(() => {
setEditingDocumentId(null);
setFormData(null);
setSelectedGroups([]);
});
} else {
handleEdit(editingDocumentId, updatedFormData).then(() => {
setEditingDocumentId(null);
setFormData(null);
setSelectedGroups([]);
});
}
};
const handleRemoveDocument = (id) => {
return handleDelete(id)
.then(() => {
setEditingDocumentId(null);
setFormData(null);
setSelectedGroups([]);
})
.catch((error) => {
logger.error(error);
});
};
const handleCancelEdit = () => {
setEditingDocumentId(null);
setFormData(null);
setSelectedGroups([]);
};
const handleGroupChange = (selected) => {
setSelectedGroups(selected);
};
const renderRequiredDocumentCell = (document, column) => {
const isEditing = editingDocumentId === document.id || (editingDocumentId === 'new' && !document.id);
if (isEditing) {
switch (column) {
case 'Nom de la pièce':
return (
<InputText
name="name"
value={formData.name}
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
placeholder="Nom de la pièce"
className="w-full"
/>
);
case 'Description':
return (
<InputText
name="description"
value={formData.description}
onChange={(e) => setFormData({ ...formData, description: e.target.value })}
placeholder="Description"
className="w-full"
/>
);
case 'Dossiers d\'inscription':
return (
<MultiSelect
name="groups"
label="Sélection de groupes de fichiers"
options={groups.map((group) => ({ id: group.id, name: group.name }))}
selectedOptions={selectedGroups}
onChange={handleGroupChange}
errorMsg={null}
/>
);
case 'Actions':
return (
<div className="flex justify-center space-x-2">
<button
type="button"
onClick={handleSaveDocument}
className="text-green-500 hover:text-green-700"
>
<Check className="w-5 h-5" />
</button>
<button
type="button"
onClick={handleCancelEdit}
className="text-red-500 hover:text-red-700"
>
<X className="w-5 h-5" />
</button>
</div>
);
default:
return null;
}
} else {
switch (column) {
case 'Nom de la pièce':
return <span>{document.name}</span>;
case 'Description':
return <span>{document.description}</span>;
case 'Dossiers d\'inscription':
return (
<span>
{document.groups
.map((groupId) => groups.find((group) => group.id === groupId)?.name || 'Dossiers d\'inscription inconnu')
.join(', ')}
</span>
);
case 'Actions':
return (
<div className="flex justify-center space-x-2">
<button
type="button"
onClick={() => handleEditDocument(document)}
className="text-blue-500 hover:text-blue-700"
>
<Edit3 className="w-5 h-5" />
</button>
<button
type="button"
onClick={() => {
setRemovePopupVisible(true);
setRemovePopupMessage(
`Attentions ! \nVous êtes sur le point de supprimer le document "${document.name}".\nÊtes-vous sûr(e) de vouloir poursuivre l'opération ?`
);
setRemovePopupOnConfirm(() => () => {
handleRemoveDocument(document.id)
.then(() => {
setPopupMessage(`Le document "${document.name}" a été correctement supprimé.`);
setPopupVisible(true);
setRemovePopupVisible(false);
})
.catch((error) => {
logger.error('Erreur lors de la suppression du document:', error);
setPopupMessage(`Erreur lors de la suppression du document "${document.name}".`);
setPopupVisible(true);
setRemovePopupVisible(false);
});
});
}}
className="text-red-500 hover:text-red-700"
>
<Trash2 className="w-5 h-5" />
</button>
</div>
);
default:
return null;
}
}
};
const columnsRequiredDocuments = [
{ name: 'Nom de la pièce', transform: (row) => renderRequiredDocumentCell(row, 'Nom de la pièce') },
{ name: 'Description', transform: (row) => renderRequiredDocumentCell(row, 'Description') },
{ name: 'Dossiers d\'inscription', transform: (row) => renderRequiredDocumentCell(row, 'Dossiers d\'inscription') },
{ name: 'Actions', transform: (row) => renderRequiredDocumentCell(row, 'Actions') },
];
return (
<div className="mt-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>
<Table
data={editingDocumentId === 'new' ? [formData, ...parentFiles] : parentFiles}
columns={columnsRequiredDocuments}
/>
<Popup
visible={popupVisible}
message={popupMessage}
onConfirm={() => setPopupVisible(false)}
onCancel={() => setPopupVisible(false)}
uniqueConfirmButton={true}
/>
<Popup
visible={removePopupVisible}
message={removePopupMessage}
onConfirm={removePopupOnConfirm}
onCancel={() => setRemovePopupVisible(false)}
/>
</div>
);
}

View File

@ -29,6 +29,7 @@ export const BE_SUBSCRIPTION_REGISTERFORMS_URL = `${BASE_URL}/Subscriptions/regi
export const BE_SUBSCRIPTION_REGISTRATION_TEMPLATE_MASTER_URL = `${BASE_URL}/Subscriptions/registrationTemplateMasters`
export const BE_SUBSCRIPTION_REGISTRATION_TEMPLATES_URL = `${BASE_URL}/Subscriptions/registrationTemplates`
export const BE_SUBSCRIPTION_REGISTRATIONFILE_GROUPS_URL = `${BASE_URL}/Subscriptions/registrationFileGroups`
export const BE_SUBSCRIPTION_REGISTRATION_PARENT_FILES_URL = `${BASE_URL}/Subscriptions/registrationParentFiles`
export const BE_SUBSCRIPTION_LAST_GUARDIAN_ID_URL = `${BASE_URL}/Subscriptions/lastGuardianId`