mirror of
https://git.v0id.ovh/n3wt-innov/n3wt-school.git
synced 2026-04-03 16:51:26 +00:00
feat: Validation document par document [N3WTS-2]
This commit is contained in:
@ -498,6 +498,7 @@ class RegistrationSchoolFileTemplate(models.Model):
|
|||||||
registration_form = models.ForeignKey(RegistrationForm, on_delete=models.CASCADE, related_name='school_file_templates', blank=True)
|
registration_form = models.ForeignKey(RegistrationForm, on_delete=models.CASCADE, related_name='school_file_templates', blank=True)
|
||||||
file = models.FileField(null=True,blank=True, upload_to=registration_school_file_upload_to)
|
file = models.FileField(null=True,blank=True, upload_to=registration_school_file_upload_to)
|
||||||
formTemplateData = models.JSONField(default=list, blank=True, null=True)
|
formTemplateData = models.JSONField(default=list, blank=True, null=True)
|
||||||
|
isValidated = models.BooleanField(default=False)
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return self.name
|
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)
|
master = models.ForeignKey(RegistrationParentFileMaster, on_delete=models.CASCADE, related_name='parent_file_templates', blank=True)
|
||||||
registration_form = models.ForeignKey(RegistrationForm, on_delete=models.CASCADE, related_name='parent_file_templates', blank=True)
|
registration_form = models.ForeignKey(RegistrationForm, on_delete=models.CASCADE, related_name='parent_file_templates', blank=True)
|
||||||
file = models.FileField(null=True,blank=True, upload_to=registration_parent_file_upload_to)
|
file = models.FileField(null=True,blank=True, upload_to=registration_parent_file_upload_to)
|
||||||
|
isValidated = models.BooleanField(default=False)
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return self.name
|
return self.name
|
||||||
|
|||||||
@ -520,7 +520,7 @@ export default function Page({ params: { locale } }) {
|
|||||||
</span>
|
</span>
|
||||||
),
|
),
|
||||||
onClick: () => {
|
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}`);
|
router.push(`${url}`);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@ -10,6 +10,7 @@ import Loader from '@/components/Loader';
|
|||||||
import { useEstablishment } from '@/context/EstablishmentContext';
|
import { useEstablishment } from '@/context/EstablishmentContext';
|
||||||
import { FE_ADMIN_SUBSCRIPTIONS_URL } from '@/utils/Url';
|
import { FE_ADMIN_SUBSCRIPTIONS_URL } from '@/utils/Url';
|
||||||
import { useNotification } from '@/context/NotificationContext';
|
import { useNotification } from '@/context/NotificationContext';
|
||||||
|
import { editRegistrationSchoolFileTemplates, editRegistrationParentFileTemplates } from '@/app/actions/registerFileGroupAction';
|
||||||
|
|
||||||
export default function Page() {
|
export default function Page() {
|
||||||
const [isLoadingRefuse, setIsLoadingRefuse] = useState(false);
|
const [isLoadingRefuse, setIsLoadingRefuse] = useState(false);
|
||||||
@ -21,6 +22,7 @@ export default function Page() {
|
|||||||
const studentId = searchParams.get('studentId');
|
const studentId = searchParams.get('studentId');
|
||||||
const firstName = searchParams.get('firstName');
|
const firstName = searchParams.get('firstName');
|
||||||
const lastName = searchParams.get('lastName');
|
const lastName = searchParams.get('lastName');
|
||||||
|
const email = searchParams.get('email');
|
||||||
const level = searchParams.get('level');
|
const level = searchParams.get('level');
|
||||||
const sepa_file =
|
const sepa_file =
|
||||||
searchParams.get('sepa_file') === 'null'
|
searchParams.get('sepa_file') === 'null'
|
||||||
@ -86,12 +88,7 @@ export default function Page() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
const handleRefuseRF = (reason) => {
|
const handleRefuseRF = (data) => {
|
||||||
setIsLoadingRefuse(true);
|
|
||||||
const data = {
|
|
||||||
status: 6, // STATUS_ARCHIVED
|
|
||||||
notes: reason,
|
|
||||||
};
|
|
||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
formData.append('data', JSON.stringify(data));
|
formData.append('data', JSON.stringify(data));
|
||||||
editRegisterForm(studentId, formData, csrfToken)
|
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) {
|
if (isLoading) {
|
||||||
return <Loader />;
|
return <Loader />;
|
||||||
}
|
}
|
||||||
@ -117,12 +145,15 @@ export default function Page() {
|
|||||||
studentId={studentId}
|
studentId={studentId}
|
||||||
firstName={firstName}
|
firstName={firstName}
|
||||||
lastName={lastName}
|
lastName={lastName}
|
||||||
|
email={email}
|
||||||
sepa_file={sepa_file}
|
sepa_file={sepa_file}
|
||||||
student_file={student_file}
|
student_file={student_file}
|
||||||
onAccept={handleAcceptRF}
|
onAccept={handleAcceptRF}
|
||||||
classes={classes}
|
classes={classes}
|
||||||
onRefuse={handleRefuseRF}
|
onRefuse={handleRefuseRF}
|
||||||
isLoadingRefuse={isLoadingRefuse}
|
isLoadingRefuse={isLoadingRefuse}
|
||||||
|
handleValidateOrRefuseDoc={handleValidateOrRefuseDoc}
|
||||||
|
csrfToken={csrfToken}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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 (
|
|
||||||
<div className="flex flex-col items-stretch justify-center h-full">
|
|
||||||
{!showTextarea ? (
|
|
||||||
<Button
|
|
||||||
text="Refuser le dossier d'inscription"
|
|
||||||
onClick={handleRefuseClick}
|
|
||||||
className="bg-red-500 hover:bg-red-700 text-white min-w-[220px] h-10"
|
|
||||||
type="button"
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<form onSubmit={handleSubmit} className="flex flex-col gap-2 mt-2">
|
|
||||||
<div className="flex items-start gap-2">
|
|
||||||
<Textarea
|
|
||||||
value={reason}
|
|
||||||
onChange={(e) => setReason(e.target.value)}
|
|
||||||
placeholder="Ex : Réception de dossier trop tardive"
|
|
||||||
rows={3}
|
|
||||||
className="w-full"
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={handleRefuseClick}
|
|
||||||
className="text-sm text-gray-500 hover:text-red-600 underline mt-1"
|
|
||||||
tabIndex={-1}
|
|
||||||
>
|
|
||||||
Annuler
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<Button
|
|
||||||
text={isLoading ? 'Refus en cours...' : 'Confirmer le refus'}
|
|
||||||
type="submit"
|
|
||||||
className="bg-red-600 hover:bg-red-800 text-white w-full"
|
|
||||||
style={{ height: '40px' }}
|
|
||||||
disabled={isLoading || !reason.trim()}
|
|
||||||
/>
|
|
||||||
</form>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default RefuseSubscription;
|
|
||||||
@ -1,5 +1,6 @@
|
|||||||
'use client';
|
'use client';
|
||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
|
import Popup from '@/components/Popup';
|
||||||
import ToggleSwitch from '@/components/Form/ToggleSwitch';
|
import ToggleSwitch from '@/components/Form/ToggleSwitch';
|
||||||
import SelectChoice from '@/components/Form/SelectChoice';
|
import SelectChoice from '@/components/Form/SelectChoice';
|
||||||
import { BASE_URL } from '@/utils/Url';
|
import { BASE_URL } from '@/utils/Url';
|
||||||
@ -8,27 +9,34 @@ import {
|
|||||||
fetchParentFileTemplatesFromRegistrationFiles,
|
fetchParentFileTemplatesFromRegistrationFiles,
|
||||||
} from '@/app/actions/subscriptionAction';
|
} from '@/app/actions/subscriptionAction';
|
||||||
import logger from '@/utils/logger';
|
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 SectionHeader from '@/components/SectionHeader';
|
||||||
import Button from '@/components/Form/Button';
|
import Button from '@/components/Form/Button';
|
||||||
import RefuseSubscription from './RefuseSubscription';
|
|
||||||
|
|
||||||
export default function ValidateSubscription({
|
export default function ValidateSubscription({
|
||||||
studentId,
|
studentId,
|
||||||
firstName,
|
firstName,
|
||||||
|
email,
|
||||||
lastName,
|
lastName,
|
||||||
sepa_file,
|
sepa_file,
|
||||||
student_file,
|
student_file,
|
||||||
onAccept,
|
onAccept,
|
||||||
|
onRefuse,
|
||||||
classes,
|
classes,
|
||||||
onRefuse, // callback pour refus
|
handleValidateOrRefuseDoc,
|
||||||
isLoadingRefuse = false, // état de chargement refus
|
csrfToken,
|
||||||
}) {
|
}) {
|
||||||
const [schoolFileTemplates, setSchoolFileTemplates] = useState([]);
|
const [schoolFileTemplates, setSchoolFileTemplates] = useState([]);
|
||||||
const [parentFileTemplates, setParentFileTemplates] = useState([]);
|
const [parentFileTemplates, setParentFileTemplates] = useState([]);
|
||||||
const [currentTemplateIndex, setCurrentTemplateIndex] = useState(0);
|
const [currentTemplateIndex, setCurrentTemplateIndex] = useState(0);
|
||||||
const [mergeDocuments, setMergeDocuments] = useState(false);
|
const [mergeDocuments, setMergeDocuments] = useState(false);
|
||||||
const [isPageValid, setIsPageValid] = 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({
|
const [formData, setFormData] = useState({
|
||||||
associated_class: null,
|
associated_class: null,
|
||||||
@ -91,14 +99,27 @@ export default function ValidateSubscription({
|
|||||||
},
|
},
|
||||||
status: 5,
|
status: 5,
|
||||||
fusionParam: mergeDocuments,
|
fusionParam: mergeDocuments,
|
||||||
|
notes: 'Dossier validé',
|
||||||
};
|
};
|
||||||
|
|
||||||
onAccept(data);
|
onAccept(data);
|
||||||
} else {
|
} else {
|
||||||
logger.warn('Aucune classe sélectionnée.');
|
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) => {
|
const onChange = (field, value) => {
|
||||||
setFormData((prev) => ({
|
setFormData((prev) => ({
|
||||||
...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);
|
logger.debug(allTemplates);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -166,7 +198,7 @@ export default function ValidateSubscription({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Colonne droite : Liste des documents, Option de fusion, Affectation, Refus */}
|
{/* Colonne droite : Liste des documents, Option de fusion, Affectation, Refus */}
|
||||||
<div className="w-1/4 flex flex-col flex-1 gap-4">
|
<div className="w-1/4 flex flex-col flex-1 gap-4 h-full">
|
||||||
{/* Liste des documents */}
|
{/* Liste des documents */}
|
||||||
<div className="flex-1 bg-gray-50 p-4 rounded-lg shadow-sm border border-gray-200 overflow-y-auto">
|
<div className="flex-1 bg-gray-50 p-4 rounded-lg shadow-sm border border-gray-200 overflow-y-auto">
|
||||||
<h3 className="text-lg font-semibold text-gray-800 mb-4">
|
<h3 className="text-lg font-semibold text-gray-800 mb-4">
|
||||||
@ -190,58 +222,185 @@ export default function ValidateSubscription({
|
|||||||
<FileText className="w-5 h-5 text-green-600" />
|
<FileText className="w-5 h-5 text-green-600" />
|
||||||
)}
|
)}
|
||||||
</span>
|
</span>
|
||||||
{template.name}
|
<span className="flex-1">{template.name}</span>
|
||||||
|
{/* 3 boutons côte à côte : À traiter / Validé / Refusé (pour tous les documents, y compris fiche élève) */}
|
||||||
|
<span className="ml-2 flex gap-1">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className={`px-2 py-1 rounded-full text-xs font-medium border transition-colors focus:outline-none focus:ring-2 focus:ring-blue-400
|
||||||
|
${docStatuses[index] === undefined ? 'bg-gray-300 text-gray-700 border-gray-400' : 'bg-white text-gray-500 border-gray-300'}`}
|
||||||
|
aria-pressed={docStatuses[index] === undefined}
|
||||||
|
onClick={e => {
|
||||||
|
e.stopPropagation();
|
||||||
|
setDocStatuses(s => ({ ...s, [index]: undefined }));
|
||||||
|
}}
|
||||||
|
>À traiter</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className={`px-2 py-1 rounded-full text-xs font-medium border transition-colors focus:outline-none focus:ring-2 focus:ring-blue-400 flex items-center gap-1
|
||||||
|
${docStatuses[index] === 'accepted' ? 'bg-emerald-500 text-white border-emerald-500' : 'bg-white text-emerald-600 border-emerald-300'}`}
|
||||||
|
aria-pressed={docStatuses[index] === 'accepted'}
|
||||||
|
onClick={e => {
|
||||||
|
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,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span className="text-lg">✓</span> Validé
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className={`px-2 py-1 rounded-full text-xs font-medium border transition-colors focus:outline-none focus:ring-2 focus:ring-blue-400 flex items-center gap-1
|
||||||
|
${docStatuses[index] === 'refused' ? 'bg-red-500 text-white border-red-500' : 'bg-white text-red-600 border-red-300'}`}
|
||||||
|
aria-pressed={docStatuses[index] === 'refused'}
|
||||||
|
onClick={e => {
|
||||||
|
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,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span className="text-lg">✗</span> Refusé
|
||||||
|
</button>
|
||||||
|
</span>
|
||||||
</li>
|
</li>
|
||||||
))}
|
))}
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Option de fusion */}
|
{/* Nouvelle section Options de validation : carte unique, sélecteur de classe (ligne 1), toggle fusion (ligne 2 aligné à droite) */}
|
||||||
<div className="bg-white p-4 rounded-lg shadow-sm border border-gray-200 flex items-center justify-center mb-2 min-h-[70px]">
|
{allChecked && allValidated && (
|
||||||
<ToggleSwitch
|
<div className="bg-white p-4 rounded-lg shadow-sm border border-gray-200 flex flex-col gap-4">
|
||||||
label="Fusionner les documents"
|
<div>
|
||||||
checked={mergeDocuments}
|
<SelectChoice
|
||||||
onChange={handleToggleMergeDocuments}
|
name="associated_class"
|
||||||
className="ml-0"
|
label="Liste des classes"
|
||||||
/>
|
placeHolder="Sélectionner une classe"
|
||||||
</div>
|
selected={formData.associated_class || ''}
|
||||||
{/* Affectation à une classe */}
|
callback={(e) => onChange('associated_class', e.target.value)}
|
||||||
<div className="bg-white p-4 rounded-lg shadow-sm border border-gray-200 flex items-center gap-4">
|
choices={classes.map((classe) => ({
|
||||||
<div className="flex-1 flex flex-col">
|
value: classe.id,
|
||||||
<SelectChoice
|
label: classe.atmosphere_name,
|
||||||
name="associated_class"
|
}))}
|
||||||
label="Liste des classes"
|
required
|
||||||
placeHolder="Sélectionner une classe"
|
className="w-full"
|
||||||
selected={formData.associated_class || ''}
|
/>
|
||||||
callback={(e) => onChange('associated_class', e.target.value)}
|
</div>
|
||||||
choices={classes.map((classe) => ({
|
<div className="flex justify-end items-center mt-2">
|
||||||
value: classe.id,
|
<ToggleSwitch
|
||||||
label: classe.atmosphere_name,
|
label="Fusionner les documents"
|
||||||
}))}
|
checked={mergeDocuments}
|
||||||
required
|
onChange={handleToggleMergeDocuments}
|
||||||
className="mt-2"
|
className="ml-0"
|
||||||
/>
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
)}
|
||||||
|
|
||||||
{/* Boutons Valider/Refuser en bas, centrés */}
|
{/* Boutons Valider/Refuser en bas, centrés */}
|
||||||
<div className="mt-auto flex justify-center gap-4 py-4">
|
<div className="mt-auto py-4">
|
||||||
<Button
|
<Button
|
||||||
text="Valider le dossier d'inscription"
|
text="Soumettre"
|
||||||
onClick={(e) => {
|
onClick={e => {
|
||||||
e.preventDefault();
|
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
|
primary
|
||||||
className={`min-w-[220px] h-10 rounded-md shadow-sm focus:outline-none ${
|
className={`w-full h-12 rounded-md shadow-sm focus:outline-none ${
|
||||||
!isPageValid
|
!allChecked || (allChecked && allValidated && !formData.associated_class)
|
||||||
? 'bg-gray-300 text-gray-700 cursor-not-allowed'
|
? 'bg-gray-300 text-gray-700 cursor-not-allowed'
|
||||||
: 'bg-emerald-500 text-white hover:bg-emerald-600'
|
: 'bg-emerald-500 text-white hover:bg-emerald-600'
|
||||||
}`}
|
}`}
|
||||||
disabled={!isPageValid}
|
disabled={
|
||||||
|
!allChecked || (allChecked && allValidated && !formData.associated_class)
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
<RefuseSubscription onRefuse={onRefuse} isLoading={isLoadingRefuse} />
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Popup de confirmation si refus */}
|
||||||
|
<Popup
|
||||||
|
isOpen={showRefusedPopup}
|
||||||
|
onCancel={() => setShowRefusedPopup(false)}
|
||||||
|
onConfirm={() => {
|
||||||
|
setShowRefusedPopup(false);
|
||||||
|
handleRefuseDossier();
|
||||||
|
}}
|
||||||
|
message={
|
||||||
|
<span>
|
||||||
|
{`Le dossier d'inscription de ${firstName} ${lastName} va être refusé. Un email sera envoyé au responsable à l'adresse : `}
|
||||||
|
<span className="font-semibold text-blue-700">{email}</span>
|
||||||
|
{` avec la liste des documents non validés :`}
|
||||||
|
<ul className="list-disc ml-6 mt-2">
|
||||||
|
{refusedDocs.map(doc => (
|
||||||
|
<li key={doc.idx}>{doc.name}</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</span>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Popup de confirmation finale si tous validés et classe sélectionnée */}
|
||||||
|
<Popup
|
||||||
|
isOpen={showFinalValidationPopup}
|
||||||
|
onCancel={() => setShowFinalValidationPopup(false)}
|
||||||
|
onConfirm={() => {
|
||||||
|
setShowFinalValidationPopup(false);
|
||||||
|
handleAssignClass();
|
||||||
|
}}
|
||||||
|
message={
|
||||||
|
<span>
|
||||||
|
{`Le dossier d'inscription de ${lastName} ${firstName} va être validé et l'élève affecté à la classe sélectionnée.`}
|
||||||
|
</span>
|
||||||
|
}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user