diff --git a/Back-End/Subscriptions/models.py b/Back-End/Subscriptions/models.py
index 3e3caba..1cf7897 100644
--- a/Back-End/Subscriptions/models.py
+++ b/Back-End/Subscriptions/models.py
@@ -498,6 +498,7 @@ class RegistrationSchoolFileTemplate(models.Model):
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_school_file_upload_to)
formTemplateData = models.JSONField(default=list, blank=True, null=True)
+ isValidated = models.BooleanField(default=False)
def __str__(self):
return self.name
@@ -540,6 +541,7 @@ 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_parent_file_upload_to)
+ isValidated = models.BooleanField(default=False)
def __str__(self):
return self.name
diff --git a/Front-End/src/app/[locale]/admin/subscriptions/page.js b/Front-End/src/app/[locale]/admin/subscriptions/page.js
index f73089e..4254a21 100644
--- a/Front-End/src/app/[locale]/admin/subscriptions/page.js
+++ b/Front-End/src/app/[locale]/admin/subscriptions/page.js
@@ -520,7 +520,7 @@ export default function Page({ params: { locale } }) {
),
onClick: () => {
- const url = `${FE_ADMIN_SUBSCRIPTIONS_VALIDATE_URL}?studentId=${row.student.id}&firstName=${row.student.first_name}&lastName=${row.student.last_name}&level=${row.student.level}&sepa_file=${row.sepa_file}&student_file=${row.registration_file}`;
+ const url = `${FE_ADMIN_SUBSCRIPTIONS_VALIDATE_URL}?studentId=${row.student.id}&firstName=${row.student.first_name}&lastName=${row.student.last_name}&email=${row.student.guardians[0].associated_profile_email}&level=${row.student.level}&sepa_file=${row.sepa_file}&student_file=${row.registration_file}`;
router.push(`${url}`);
},
},
diff --git a/Front-End/src/app/[locale]/admin/subscriptions/validateSubscription/page.js b/Front-End/src/app/[locale]/admin/subscriptions/validateSubscription/page.js
index 4d16716..886b6a6 100644
--- a/Front-End/src/app/[locale]/admin/subscriptions/validateSubscription/page.js
+++ b/Front-End/src/app/[locale]/admin/subscriptions/validateSubscription/page.js
@@ -10,6 +10,7 @@ import Loader from '@/components/Loader';
import { useEstablishment } from '@/context/EstablishmentContext';
import { FE_ADMIN_SUBSCRIPTIONS_URL } from '@/utils/Url';
import { useNotification } from '@/context/NotificationContext';
+import { editRegistrationSchoolFileTemplates, editRegistrationParentFileTemplates } from '@/app/actions/registerFileGroupAction';
export default function Page() {
const [isLoadingRefuse, setIsLoadingRefuse] = useState(false);
@@ -21,6 +22,7 @@ export default function Page() {
const studentId = searchParams.get('studentId');
const firstName = searchParams.get('firstName');
const lastName = searchParams.get('lastName');
+ const email = searchParams.get('email');
const level = searchParams.get('level');
const sepa_file =
searchParams.get('sepa_file') === 'null'
@@ -86,12 +88,7 @@ export default function Page() {
};
- const handleRefuseRF = (reason) => {
- setIsLoadingRefuse(true);
- const data = {
- status: 6, // STATUS_ARCHIVED
- notes: reason,
- };
+ const handleRefuseRF = (data) => {
const formData = new FormData();
formData.append('data', JSON.stringify(data));
editRegisterForm(studentId, formData, csrfToken)
@@ -108,6 +105,37 @@ export default function Page() {
});
};
+ // Validation/refus d'un document individuel (hors fiche élève)
+ const handleValidateOrRefuseDoc = ({ templateId, type, validated, csrfToken }) => {
+ if (!templateId) return;
+ let editFn = null;
+ if (type === 'school') {
+ editFn = editRegistrationSchoolFileTemplates;
+ } else if (type === 'parent') {
+ editFn = editRegistrationParentFileTemplates;
+ }
+ if (!editFn) return;
+ const updateData = new FormData();
+ updateData.append('data', JSON.stringify({ isValidated: validated }));
+ editFn(templateId, updateData, csrfToken)
+ .then((response) => {
+ logger.debug(`Document ${validated ? 'validé' : 'refusé'} (type: ${type}, id: ${templateId})`, response);
+ showNotification(
+ `Le document a bien été ${validated ? 'validé' : 'refusé'}.`,
+ 'success',
+ 'Succès'
+ );
+ })
+ .catch((error) => {
+ logger.error('Erreur lors de la validation/refus du document:', error);
+ showNotification(
+ `Erreur lors de la ${validated ? 'validation' : 'refus'} du document.`,
+ 'error',
+ 'Erreur'
+ );
+ });
+ };
+
if (isLoading) {
return ;
}
@@ -117,12 +145,15 @@ export default function Page() {
studentId={studentId}
firstName={firstName}
lastName={lastName}
+ email={email}
sepa_file={sepa_file}
student_file={student_file}
onAccept={handleAcceptRF}
classes={classes}
onRefuse={handleRefuseRF}
isLoadingRefuse={isLoadingRefuse}
+ handleValidateOrRefuseDoc={handleValidateOrRefuseDoc}
+ csrfToken={csrfToken}
/>
);
}
diff --git a/Front-End/src/components/Inscription/RefuseSubscription.js b/Front-End/src/components/Inscription/RefuseSubscription.js
deleted file mode 100644
index eb2fdd9..0000000
--- a/Front-End/src/components/Inscription/RefuseSubscription.js
+++ /dev/null
@@ -1,69 +0,0 @@
-import React, { useState } from 'react';
-import Textarea from '@/components/Textarea';
-import Button from '@/components/Form/Button';
-
-/**
- * Composant pour afficher le textarea de refus et le bouton d'action
- * @param {function} onRefuse - callback appelée avec la raison du refus
- * @param {boolean} isLoading - état de chargement
- */
-const RefuseSubscription = ({ onRefuse, isLoading }) => {
- const [reason, setReason] = useState('');
- const [showTextarea, setShowTextarea] = useState(false);
-
- const handleRefuseClick = () => {
- setShowTextarea((prev) => !prev);
- };
-
- const handleSubmit = (e) => {
- e.preventDefault();
- if (reason.trim()) {
- onRefuse(reason);
- setShowTextarea(false);
- setReason('');
- }
- };
-
- return (
-
- {!showTextarea ? (
-
- ) : (
-
- )}
-
- );
-};
-
-export default RefuseSubscription;
diff --git a/Front-End/src/components/Inscription/ValidateSubscription.js b/Front-End/src/components/Inscription/ValidateSubscription.js
index c6adca9..d14d2a0 100644
--- a/Front-End/src/components/Inscription/ValidateSubscription.js
+++ b/Front-End/src/components/Inscription/ValidateSubscription.js
@@ -1,5 +1,6 @@
'use client';
import React, { useState, useEffect } from 'react';
+import Popup from '@/components/Popup';
import ToggleSwitch from '@/components/Form/ToggleSwitch';
import SelectChoice from '@/components/Form/SelectChoice';
import { BASE_URL } from '@/utils/Url';
@@ -8,27 +9,34 @@ import {
fetchParentFileTemplatesFromRegistrationFiles,
} from '@/app/actions/subscriptionAction';
import logger from '@/utils/logger';
-import { School, CheckCircle, Hourglass, FileText } from 'lucide-react';
+import { School, FileText } from 'lucide-react';
import SectionHeader from '@/components/SectionHeader';
import Button from '@/components/Form/Button';
-import RefuseSubscription from './RefuseSubscription';
export default function ValidateSubscription({
studentId,
firstName,
+ email,
lastName,
sepa_file,
student_file,
onAccept,
+ onRefuse,
classes,
- onRefuse, // callback pour refus
- isLoadingRefuse = false, // état de chargement refus
+ handleValidateOrRefuseDoc,
+ csrfToken,
}) {
const [schoolFileTemplates, setSchoolFileTemplates] = useState([]);
const [parentFileTemplates, setParentFileTemplates] = useState([]);
const [currentTemplateIndex, setCurrentTemplateIndex] = useState(0);
const [mergeDocuments, setMergeDocuments] = useState(false);
const [isPageValid, setIsPageValid] = useState(false);
+ // Pour la validation/refus des documents
+ const [docStatuses, setDocStatuses] = useState({}); // {index: 'accepted'|'refused'}
+ const [showRefusedPopup, setShowRefusedPopup] = useState(false);
+
+ // Affiche la popup de confirmation finale (tous docs validés et classe sélectionnée)
+ const [showFinalValidationPopup, setShowFinalValidationPopup] = useState(false);
const [formData, setFormData] = useState({
associated_class: null,
@@ -91,14 +99,27 @@ export default function ValidateSubscription({
},
status: 5,
fusionParam: mergeDocuments,
+ notes: 'Dossier validé',
};
-
onAccept(data);
} else {
logger.warn('Aucune classe sélectionnée.');
}
};
+ const handleRefuseDossier = () => {
+ // Message clair avec la liste des documents refusés
+ let notes = 'Dossier non validé pour les raisons suivantes :\n';
+ notes += refusedDocs.map(doc => `- ${doc.name}`).join('\n');
+ const data = {
+ status: 2,
+ notes,
+ };
+ if (onRefuse) {
+ onRefuse(data);
+ }
+ };
+
const onChange = (field, value) => {
setFormData((prev) => ({
...prev,
@@ -128,6 +149,17 @@ export default function ValidateSubscription({
]
: []),
];
+
+ // Récupère la liste des documents refusés (inclut la fiche élève si refusée)
+ const refusedDocs = allTemplates
+ .map((doc, idx) => ({ ...doc, idx }))
+ .filter((doc, idx) => docStatuses[idx] === 'refused');
+
+ // Récupère la liste des documents à cocher (inclut la fiche élève)
+ const docIndexes = allTemplates.map((_, idx) => idx);
+ const allChecked = docIndexes.length > 0 && docIndexes.every(idx => docStatuses[idx] === 'accepted' || docStatuses[idx] === 'refused');
+ const allValidated = docIndexes.length > 0 && docIndexes.every(idx => docStatuses[idx] === 'accepted');
+ const hasRefused = docIndexes.some(idx => docStatuses[idx] === 'refused');
logger.debug(allTemplates);
return (
@@ -166,7 +198,7 @@ export default function ValidateSubscription({
{/* Colonne droite : Liste des documents, Option de fusion, Affectation, Refus */}
-
+
{/* Liste des documents */}
@@ -190,58 +222,185 @@ export default function ValidateSubscription({
)}
- {template.name}
+ {template.name}
+ {/* 3 boutons côte à côte : À traiter / Validé / Refusé (pour tous les documents, y compris fiche élève) */}
+
+ {
+ e.stopPropagation();
+ setDocStatuses(s => ({ ...s, [index]: undefined }));
+ }}
+ >À traiter
+ {
+ e.stopPropagation();
+ setDocStatuses(s => ({ ...s, [index]: 'accepted' }));
+ // Appel API pour valider le document (hors fiche élève)
+ if (index > 0 && handleValidateOrRefuseDoc) {
+ // index 0 = fiche élève, ensuite school puis parent puis SEPA
+ let template = null;
+ let type = null;
+ if (index > 0 && index <= schoolFileTemplates.length) {
+ template = schoolFileTemplates[index - 1];
+ type = 'school';
+ } else if (index > schoolFileTemplates.length && index <= schoolFileTemplates.length + parentFileTemplates.length) {
+ template = parentFileTemplates[index - 1 - schoolFileTemplates.length];
+ type = 'parent';
+ }
+ if (template && template.id) {
+ handleValidateOrRefuseDoc({
+ templateId: template.id,
+ type,
+ validated: true,
+ csrfToken,
+ });
+ }
+ }
+ }}
+ >
+ ✓ Validé
+
+ {
+ e.stopPropagation();
+ setDocStatuses(s => ({ ...s, [index]: 'refused' }));
+ // Appel API pour refuser le document (hors fiche élève)
+ if (index > 0 && handleValidateOrRefuseDoc) {
+ let template = null;
+ let type = null;
+ if (index > 0 && index <= schoolFileTemplates.length) {
+ template = schoolFileTemplates[index - 1];
+ type = 'school';
+ } else if (index > schoolFileTemplates.length && index <= schoolFileTemplates.length + parentFileTemplates.length) {
+ template = parentFileTemplates[index - 1 - schoolFileTemplates.length];
+ type = 'parent';
+ }
+ if (template && template.id) {
+ handleValidateOrRefuseDoc({
+ templateId: template.id,
+ type,
+ validated: false,
+ csrfToken,
+ });
+ }
+ }
+ }}
+ >
+ ✗ Refusé
+
+
))}
- {/* Option de fusion */}
-
-
-
- {/* Affectation à une classe */}
-
-
-
onChange('associated_class', e.target.value)}
- choices={classes.map((classe) => ({
- value: classe.id,
- label: classe.atmosphere_name,
- }))}
- required
- className="mt-2"
- />
+ {/* Nouvelle section Options de validation : carte unique, sélecteur de classe (ligne 1), toggle fusion (ligne 2 aligné à droite) */}
+ {allChecked && allValidated && (
+
+
+ onChange('associated_class', e.target.value)}
+ choices={classes.map((classe) => ({
+ value: classe.id,
+ label: classe.atmosphere_name,
+ }))}
+ required
+ className="w-full"
+ />
+
+
+
+
-
+ )}
{/* Boutons Valider/Refuser en bas, centrés */}
-
+
{
+ text="Soumettre"
+ onClick={e => {
e.preventDefault();
- handleAssignClass();
+ // 1. Si tous les documents ne sont pas cochés, rien ne se passe (bouton désactivé)
+ // 2. Si tous cochés et au moins un refusé : popup refus
+ if (allChecked && hasRefused) {
+ setShowRefusedPopup(true);
+ return;
+ }
+ // 3. Si tous cochés et tous validés mais pas de classe sélectionnée : bouton désactivé
+ // 4. Si tous cochés, tous validés et classe sélectionnée : popup de validation finale
+ if (allChecked && allValidated && formData.associated_class) {
+ setShowFinalValidationPopup(true);
+ }
}}
primary
- className={`min-w-[220px] h-10 rounded-md shadow-sm focus:outline-none ${
- !isPageValid
+ className={`w-full h-12 rounded-md shadow-sm focus:outline-none ${
+ !allChecked || (allChecked && allValidated && !formData.associated_class)
? 'bg-gray-300 text-gray-700 cursor-not-allowed'
: 'bg-emerald-500 text-white hover:bg-emerald-600'
}`}
- disabled={!isPageValid}
+ disabled={
+ !allChecked || (allChecked && allValidated && !formData.associated_class)
+ }
/>
-
+
+ {/* Popup de confirmation si refus */}
+
setShowRefusedPopup(false)}
+ onConfirm={() => {
+ setShowRefusedPopup(false);
+ handleRefuseDossier();
+ }}
+ message={
+
+ {`Le dossier d'inscription de ${firstName} ${lastName} va être refusé. Un email sera envoyé au responsable à l'adresse : `}
+ {email}
+ {` avec la liste des documents non validés :`}
+
+ {refusedDocs.map(doc => (
+ {doc.name}
+ ))}
+
+
+ }
+ />
+
+ {/* Popup de confirmation finale si tous validés et classe sélectionnée */}
+ setShowFinalValidationPopup(false)}
+ onConfirm={() => {
+ setShowFinalValidationPopup(false);
+ handleAssignClass();
+ }}
+ message={
+
+ {`Le dossier d'inscription de ${lastName} ${firstName} va être validé et l'élève affecté à la classe sélectionnée.`}
+
+ }
+ />