5 Commits

9 changed files with 135 additions and 34 deletions

View File

@ -9,6 +9,7 @@ from .models import Planning, Events, RecursionType
from .serializers import PlanningSerializer, EventsSerializer from .serializers import PlanningSerializer, EventsSerializer
from N3wtSchool import bdd from N3wtSchool import bdd
from Subscriptions.util import getCurrentSchoolYear
class PlanningView(APIView): class PlanningView(APIView):
@ -17,6 +18,7 @@ class PlanningView(APIView):
def get(self, request): def get(self, request):
establishment_id = request.GET.get('establishment_id', None) establishment_id = request.GET.get('establishment_id', None)
planning_mode = request.GET.get('planning_mode', None) planning_mode = request.GET.get('planning_mode', None)
current_school_year = getCurrentSchoolYear()
plannings = bdd.getAllObjects(Planning) plannings = bdd.getAllObjects(Planning)
@ -25,7 +27,10 @@ class PlanningView(APIView):
# Filtrer en fonction du planning_mode # Filtrer en fonction du planning_mode
if planning_mode == "classSchedule": if planning_mode == "classSchedule":
plannings = plannings.filter(school_class__isnull=False) plannings = plannings.filter(
school_class__isnull=False,
school_class__school_year=current_school_year,
)
elif planning_mode == "planning": elif planning_mode == "planning":
plannings = plannings.filter(school_class__isnull=True) plannings = plannings.filter(school_class__isnull=True)
@ -79,6 +84,7 @@ class EventsView(APIView):
def get(self, request): def get(self, request):
establishment_id = request.GET.get('establishment_id', None) establishment_id = request.GET.get('establishment_id', None)
planning_mode = request.GET.get('planning_mode', None) planning_mode = request.GET.get('planning_mode', None)
current_school_year = getCurrentSchoolYear()
filterParams = {} filterParams = {}
plannings=[] plannings=[]
events = Events.objects.all() events = Events.objects.all()
@ -86,6 +92,8 @@ class EventsView(APIView):
filterParams['establishment'] = establishment_id filterParams['establishment'] = establishment_id
if planning_mode is not None: if planning_mode is not None:
filterParams['school_class__isnull'] = (planning_mode!="classSchedule") filterParams['school_class__isnull'] = (planning_mode!="classSchedule")
if planning_mode == "classSchedule":
filterParams['school_class__school_year'] = current_school_year
if filterParams: if filterParams:
plannings = Planning.objects.filter(**filterParams) plannings = Planning.objects.filter(**filterParams)
events = Events.objects.filter(planning__in=plannings) events = Events.objects.filter(planning__in=plannings)

View File

@ -21,7 +21,6 @@ from N3wtSchool import settings
from django.utils import timezone from django.utils import timezone
import pytz import pytz
import Subscriptions.util as util import Subscriptions.util as util
from N3wtSchool.mailManager import sendRegisterForm
class AbsenceManagementSerializer(serializers.ModelSerializer): class AbsenceManagementSerializer(serializers.ModelSerializer):
student_name = serializers.SerializerMethodField() student_name = serializers.SerializerMethodField()
@ -228,14 +227,6 @@ class StudentSerializer(serializers.ModelSerializer):
profile_role_serializer = ProfileRoleSerializer(data=profile_role_data) profile_role_serializer = ProfileRoleSerializer(data=profile_role_data)
profile_role_serializer.is_valid(raise_exception=True) profile_role_serializer.is_valid(raise_exception=True)
profile_role = profile_role_serializer.save() profile_role = profile_role_serializer.save()
# Envoi du mail d'inscription si un nouveau profil vient d'être créé
email = None
if profile_data and 'email' in profile_data:
email = profile_data['email']
elif profile_role and profile_role.profile:
email = profile_role.profile.email
if email:
sendRegisterForm(email, establishment_id)
elif profile_role: elif profile_role:
# Récupérer un ProfileRole existant par son ID # Récupérer un ProfileRole existant par son ID
profile_role = ProfileRole.objects.get(id=profile_role.id) profile_role = ProfileRole.objects.get(id=profile_role.id)

View File

@ -1,5 +1,6 @@
'use client'; 'use client';
import React, { useState, useEffect } from 'react'; import React, { useState, useEffect } from 'react';
import { getCurrentSchoolYear } from '@/utils/Date';
import StructureManagement from '@/components/Structure/Configuration/StructureManagement'; import StructureManagement from '@/components/Structure/Configuration/StructureManagement';
import ScheduleManagement from '@/components/Structure/Planning/ScheduleManagement'; import ScheduleManagement from '@/components/Structure/Planning/ScheduleManagement';
@ -54,6 +55,17 @@ export default function Page() {
const csrfToken = useCsrfToken(); const csrfToken = useCsrfToken();
const { selectedEstablishmentId, profileRole } = useEstablishment(); const { selectedEstablishmentId, profileRole } = useEstablishment();
const currentSchoolYear = getCurrentSchoolYear();
const scheduleClasses = classes.filter(
(classe) => classe?.school_year === currentSchoolYear
);
const scheduleSpecialities = specialities.filter(
(speciality) => speciality?.school_year === currentSchoolYear
);
const scheduleTeachers = teachers.filter(
(teacher) => teacher?.school_year === currentSchoolYear
);
useEffect(() => { useEffect(() => {
if (selectedEstablishmentId) { if (selectedEstablishmentId) {
@ -299,9 +311,9 @@ export default function Page() {
<ClassesProvider> <ClassesProvider>
<ScheduleManagement <ScheduleManagement
handleUpdatePlanning={handleUpdatePlanning} handleUpdatePlanning={handleUpdatePlanning}
classes={classes} classes={scheduleClasses}
specialities={specialities} specialities={scheduleSpecialities}
teachers={teachers} teachers={scheduleTeachers}
/> />
</ClassesProvider> </ClassesProvider>
</div> </div>

View File

@ -55,6 +55,11 @@ export default function DynamicFormsList({
return cleaned; return cleaned;
}; };
const isDynamicForm = (template) =>
template.formTemplateData &&
Array.isArray(template.formTemplateData.fields) &&
template.formTemplateData.fields.length > 0;
const hasLocalCompletion = (templateId) => { const hasLocalCompletion = (templateId) => {
if (formsValidation[templateId] === true) return true; if (formsValidation[templateId] === true) return true;
@ -65,11 +70,22 @@ export default function DynamicFormsList({
} }
const savedResponses = existingResponses[templateId]; const savedResponses = existingResponses[templateId];
return !!( if (
savedResponses && savedResponses &&
typeof savedResponses === 'object' && typeof savedResponses === 'object' &&
Object.keys(savedResponses).length > 0 Object.keys(savedResponses).length > 0
); ) {
return true;
}
// Pour les formulaires non dynamiques (upload de fichier),
// vérifier si un fichier a déjà été uploadé sur le template
const template = schoolFileTemplates.find((tpl) => tpl.id === templateId);
if (template && template.file && !isDynamicForm(template)) {
return true;
}
return false;
}; };
// Initialiser les données avec les réponses existantes // Initialiser les données avec les réponses existantes
@ -233,11 +249,6 @@ export default function DynamicFormsList({
} }
}; };
const isDynamicForm = (template) =>
template.formTemplateData &&
Array.isArray(template.formTemplateData.fields) &&
template.formTemplateData.fields.length > 0;
if (!schoolFileTemplates || schoolFileTemplates.length === 0) { if (!schoolFileTemplates || schoolFileTemplates.length === 0) {
return ( return (
<div className="text-center py-8"> <div className="text-center py-8">
@ -497,10 +508,10 @@ export default function DynamicFormsList({
{/* Cas non validé : bouton télécharger + upload */} {/* Cas non validé : bouton télécharger + upload */}
{currentTemplate.isValidated !== true && ( {currentTemplate.isValidated !== true && (
<div className="flex flex-col items-center gap-4 w-full"> <div className="flex flex-col items-center gap-4 w-full">
{/* Bouton télécharger le document source */} {/* Bouton télécharger le document source (fichier maître) */}
{currentTemplate.file && ( {(currentTemplate.master_file_url || currentTemplate.file) && (
<a <a
href={getSecureFileUrl(currentTemplate.file)} href={getSecureFileUrl(currentTemplate.master_file_url || currentTemplate.file)}
className="flex items-center gap-2 px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600 transition" className="flex items-center gap-2 px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600 transition"
download download
> >
@ -517,6 +528,7 @@ export default function DynamicFormsList({
onFileSelect={(file) => onFileSelect={(file) =>
handleUpload(file, currentTemplate) handleUpload(file, currentTemplate)
} }
existingFile={currentTemplate.file_url || currentTemplate.file}
required required
enable={true} enable={true}
/> />

View File

@ -67,11 +67,17 @@ const FilesModal = ({
: null, : null,
schoolFiles: fetchedSchoolFiles.map((file) => ({ schoolFiles: fetchedSchoolFiles.map((file) => ({
name: file.name || 'Document scolaire', name: file.name || 'Document scolaire',
url: file.file ? getSecureFileUrl(file.file) : null, url:
file.file_url || file.file
? getSecureFileUrl(file.file_url || file.file)
: null,
})), })),
parentFiles: parentFiles.map((file) => ({ parentFiles: parentFiles.map((file) => ({
name: file.master_name || 'Document parent', name: file.master_name || 'Document parent',
url: file.file ? getSecureFileUrl(file.file) : null, url:
file.file_url || file.file
? getSecureFileUrl(file.file_url || file.file)
: null,
})), })),
sepaFile: selectedRegisterForm.sepa_file sepaFile: selectedRegisterForm.sepa_file
? { ? {

View File

@ -11,6 +11,7 @@ import {
} from '@/app/actions/subscriptionAction'; } from '@/app/actions/subscriptionAction';
import { import {
editRegistrationSchoolFileTemplates, editRegistrationSchoolFileTemplates,
editRegistrationParentFileTemplates,
} from '@/app/actions/registerFileGroupAction'; } from '@/app/actions/registerFileGroupAction';
import { import {
fetchRegistrationPaymentModes, fetchRegistrationPaymentModes,
@ -415,7 +416,8 @@ export default function InscriptionFormShared({
const templateData = await fetchFormResponses(template.id); const templateData = await fetchFormResponses(template.id);
if (templateData && templateData.formTemplateData) { if (templateData && templateData.formTemplateData) {
if (templateData.formTemplateData.responses) { if (templateData.formTemplateData.responses) {
responsesMap[template.id] = templateData.formTemplateData.responses; responsesMap[template.id] =
templateData.formTemplateData.responses;
} else { } else {
// Extraire les réponses depuis les champs // Extraire les réponses depuis les champs
const responses = {}; const responses = {};
@ -554,7 +556,7 @@ export default function InscriptionFormShared({
const updateData = new FormData(); const updateData = new FormData();
updateData.append('file', file, finalFileName); updateData.append('file', file, finalFileName);
return editRegistrationSchoolFileTemplates( return editRegistrationParentFileTemplates(
selectedFile.id, selectedFile.id,
updateData, updateData,
csrfToken csrfToken
@ -596,6 +598,46 @@ export default function InscriptionFormShared({
}); });
}; };
const handleSchoolFileUpload = (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 editRegistrationSchoolFileTemplates(
selectedFile.id,
updateData,
csrfToken
)
.then((response) => {
logger.debug('School file template mis à jour avec succès :', response);
// Mettre à jour uniquement schoolFileTemplates
setSchoolFileTemplates((prevTemplates) =>
prevTemplates.map((template) =>
template.id === selectedFile.id
? {
...template,
file: response.data.file,
file_url: response.data.file_url,
}
: template
)
);
return response;
})
.catch((error) => {
logger.error('Erreur lors de la mise à jour du school file :', error);
throw error;
});
};
const handleDeleteFile = (templateId) => { const handleDeleteFile = (templateId) => {
const fileToDelete = uploadedFiles.find( const fileToDelete = uploadedFiles.find(
(file) => parseInt(file.id) === templateId && file.fileName (file) => parseInt(file.id) === templateId && file.fileName
@ -827,7 +869,7 @@ export default function InscriptionFormShared({
onFormSubmit={handleDynamicFormSubmit} onFormSubmit={handleDynamicFormSubmit}
onValidationChange={handleDynamicFormsValidationChange} onValidationChange={handleDynamicFormsValidationChange}
enable={enable} enable={enable}
onFileUpload={handleFileUpload} onFileUpload={handleSchoolFileUpload}
/> />
)} )}

View File

@ -6,7 +6,7 @@ import React, {
useRef, useRef,
useCallback, useCallback,
} from 'react'; } from 'react';
import { useSession } from 'next-auth/react'; import { useSession, getSession } from 'next-auth/react';
import logger from '@/utils/logger'; import logger from '@/utils/logger';
import { WS_CHAT_URL } from '@/utils/Url'; import { WS_CHAT_URL } from '@/utils/Url';
@ -54,14 +54,15 @@ export const ChatConnectionProvider = ({ children }) => {
}, []); }, []);
// Configuration WebSocket // Configuration WebSocket
const getWebSocketUrl = (userId) => { const getWebSocketUrl = async (userId) => {
if (!userId) { if (!userId) {
logger.warn('ChatConnection: No user ID provided for WebSocket URL'); logger.warn('ChatConnection: No user ID provided for WebSocket URL');
return null; return null;
} }
// Récupérer le token d'authentification depuis NextAuth session // Forcer un refresh de session pour obtenir un token JWT valide
const token = session?.user?.token; const freshSession = await getSession();
const token = freshSession?.user?.token;
if (!token) { if (!token) {
logger.warn( logger.warn(
@ -78,7 +79,7 @@ export const ChatConnectionProvider = ({ children }) => {
}; };
// Connexion WebSocket // Connexion WebSocket
const connectToChat = (userId = null) => { const connectToChat = async (userId = null) => {
const userIdToUse = userId || currentUserId; const userIdToUse = userId || currentUserId;
// Vérifier que la session est chargée // Vérifier que la session est chargée
@ -107,7 +108,7 @@ export const ChatConnectionProvider = ({ children }) => {
setConnectionStatus('connecting'); setConnectionStatus('connecting');
try { try {
const wsUrl = getWebSocketUrl(userIdToUse); const wsUrl = await getWebSocketUrl(userIdToUse);
if (!wsUrl) { if (!wsUrl) {
throw new Error( throw new Error(

View File

@ -27,5 +27,13 @@ export const getSecureFileUrl = (filePath) => {
} }
} }
// Décoder le chemin au cas où il est déjà URL-encodé (ex: %20 pour les espaces)
// puis ré-encoder proprement pour éviter le double encodage (%2520).
try {
filePath = decodeURIComponent(filePath);
} catch {
// Si le décodage échoue, le chemin n'était pas encodé : on le garde tel quel.
}
return `/api/download?path=${encodeURIComponent(filePath)}`; return `/api/download?path=${encodeURIComponent(filePath)}`;
}; };

View File

@ -0,0 +1,21 @@
{
"establishments": [
{
"name": "Ecole Demo",
"address": "1 rue de la Paix, Paris",
"total_capacity": 300,
"establishment_type": [
1,
2
],
"evaluation_frequency": 1,
"licence_code": "LIC-DEMO-001",
"directeur": {
"email": "luc.sorignet@gmail.com",
"password": "a",
"last_name": "Dupont",
"first_name": "Jean"
}
}
]
}