mirror of
https://git.v0id.ovh/n3wt-innov/n3wt-school.git
synced 2026-04-03 16:51:26 +00:00
feat: Ajout bouton de refus de dossier avec zone de saisie de motif [N3WTS-2]
This commit is contained in:
@ -2,6 +2,7 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import Table from '@/components/Table';
|
||||
import Tab from '@/components/Tab';
|
||||
import Textarea from '@/components/Textarea';
|
||||
import { useTranslations } from 'next-intl';
|
||||
import StatusLabel from '@/components/StatusLabel';
|
||||
import Popup from '@/components/Popup';
|
||||
@ -96,6 +97,38 @@ export default function Page({ params: { locale } }) {
|
||||
const [isSepaUploadModalOpen, setIsSepaUploadModalOpen] = useState(false);
|
||||
const [selectedRowForUpload, setSelectedRowForUpload] = useState(null);
|
||||
|
||||
// Refus popup state
|
||||
const [isRefusePopupOpen, setIsRefusePopupOpen] = useState(false);
|
||||
const [refuseReason, setRefuseReason] = useState('');
|
||||
const [rowToRefuse, setRowToRefuse] = useState(null);
|
||||
// Ouvre la popup de refus
|
||||
const openRefusePopup = (row) => {
|
||||
setRowToRefuse(row);
|
||||
setRefuseReason('');
|
||||
setIsRefusePopupOpen(true);
|
||||
};
|
||||
|
||||
// Valide le refus
|
||||
const handleRefuse = () => {
|
||||
if (!refuseReason.trim()) {
|
||||
showNotification('Merci de préciser la raison du refus.', 'error', 'Erreur');
|
||||
return;
|
||||
}
|
||||
editRegisterForm(
|
||||
rowToRefuse.student.id,
|
||||
{ status: RegistrationFormStatus.STATUS_ARCHIVED, notes: refuseReason },
|
||||
csrfToken
|
||||
)
|
||||
.then(() => {
|
||||
showNotification('Le dossier a été refusé et archivé.', 'success', 'Succès');
|
||||
setReloadFetch(true);
|
||||
setIsRefusePopupOpen(false);
|
||||
})
|
||||
.catch(() => {
|
||||
showNotification('Erreur lors du refus du dossier.', 'error', 'Erreur');
|
||||
});
|
||||
};
|
||||
|
||||
const csrfToken = useCsrfToken();
|
||||
const router = useRouter();
|
||||
const { selectedEstablishmentId, profileRole } = useEstablishment();
|
||||
@ -491,6 +524,14 @@ export default function Page({ params: { locale } }) {
|
||||
router.push(`${url}`);
|
||||
},
|
||||
},
|
||||
{
|
||||
icon: (
|
||||
<span title="Refuser le dossier">
|
||||
<Archive className="w-5 h-5 text-red-500 hover:text-red-700" />
|
||||
</span>
|
||||
),
|
||||
onClick: () => openRefusePopup(row),
|
||||
},
|
||||
],
|
||||
// Etat "A relancer" - NON TESTE
|
||||
[RegistrationFormStatus.STATUS_TO_FOLLOW_UP]: [
|
||||
@ -852,6 +893,25 @@ export default function Page({ params: { locale } }) {
|
||||
onCancel={() => setConfirmPopupVisible(false)}
|
||||
/>
|
||||
|
||||
{/* Popup de refus de dossier */}
|
||||
<Popup
|
||||
isOpen={isRefusePopupOpen}
|
||||
message={
|
||||
<div>
|
||||
<div className="mb-2 font-semibold">Veuillez indiquer la raison du refus :</div>
|
||||
<Textarea
|
||||
value={refuseReason}
|
||||
onChange={(e) => setRefuseReason(e.target.value)}
|
||||
placeholder="Ex : Réception de dossier trop tardive"
|
||||
rows={3}
|
||||
className="w-full"
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
onConfirm={handleRefuse}
|
||||
onCancel={() => setIsRefusePopupOpen(false)}
|
||||
/>
|
||||
|
||||
{isSepaUploadModalOpen && (
|
||||
<Modal
|
||||
isOpen={isSepaUploadModalOpen}
|
||||
|
||||
@ -12,6 +12,7 @@ import { FE_ADMIN_SUBSCRIPTIONS_URL } from '@/utils/Url';
|
||||
import { useNotification } from '@/context/NotificationContext';
|
||||
|
||||
export default function Page() {
|
||||
const [isLoadingRefuse, setIsLoadingRefuse] = useState(false);
|
||||
const searchParams = useSearchParams();
|
||||
const router = useRouter();
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
@ -84,6 +85,29 @@ export default function Page() {
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
const handleRefuseRF = (reason) => {
|
||||
setIsLoadingRefuse(true);
|
||||
const data = {
|
||||
status: 6, // STATUS_ARCHIVED
|
||||
notes: reason,
|
||||
};
|
||||
const formData = new FormData();
|
||||
formData.append('data', JSON.stringify(data));
|
||||
editRegisterForm(studentId, formData, csrfToken)
|
||||
.then((response) => {
|
||||
logger.debug('RF refusé et archivé:', response);
|
||||
showNotification('Le dossier a été refusé et archivé.', 'success', 'Succès');
|
||||
router.push(FE_ADMIN_SUBSCRIPTIONS_URL);
|
||||
setIsLoadingRefuse(false);
|
||||
})
|
||||
.catch((error) => {
|
||||
showNotification('Erreur lors du refus du dossier.', 'error', 'Erreur');
|
||||
setIsLoadingRefuse(false);
|
||||
logger.error('Erreur lors du refus du RF:', error);
|
||||
});
|
||||
};
|
||||
|
||||
if (isLoading) {
|
||||
return <Loader />;
|
||||
}
|
||||
@ -97,6 +121,8 @@ export default function Page() {
|
||||
student_file={student_file}
|
||||
onAccept={handleAcceptRF}
|
||||
classes={classes}
|
||||
onRefuse={handleRefuseRF}
|
||||
isLoadingRefuse={isLoadingRefuse}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
69
Front-End/src/components/Inscription/RefuseSubscription.js
Normal file
69
Front-End/src/components/Inscription/RefuseSubscription.js
Normal file
@ -0,0 +1,69 @@
|
||||
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;
|
||||
@ -11,6 +11,7 @@ import logger from '@/utils/logger';
|
||||
import { School, CheckCircle, Hourglass, FileText } from 'lucide-react';
|
||||
import SectionHeader from '@/components/SectionHeader';
|
||||
import Button from '@/components/Form/Button';
|
||||
import RefuseSubscription from './RefuseSubscription';
|
||||
|
||||
export default function ValidateSubscription({
|
||||
studentId,
|
||||
@ -20,6 +21,8 @@ export default function ValidateSubscription({
|
||||
student_file,
|
||||
onAccept,
|
||||
classes,
|
||||
onRefuse, // callback pour refus
|
||||
isLoadingRefuse = false, // état de chargement refus
|
||||
}) {
|
||||
const [schoolFileTemplates, setSchoolFileTemplates] = useState([]);
|
||||
const [parentFileTemplates, setParentFileTemplates] = useState([]);
|
||||
@ -162,8 +165,8 @@ export default function ValidateSubscription({
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Colonne droite : Liste des documents, Option de fusion et Affectation */}
|
||||
<div className="w-1/4 flex flex-col gap-4">
|
||||
{/* Colonne droite : Liste des documents, Option de fusion, Affectation, Refus */}
|
||||
<div className="w-1/4 flex flex-col flex-1 gap-4">
|
||||
{/* Liste des documents */}
|
||||
<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">
|
||||
@ -194,52 +197,50 @@ export default function ValidateSubscription({
|
||||
</div>
|
||||
|
||||
{/* Option de fusion */}
|
||||
<div className="bg-gray-50 p-4 rounded-lg shadow-sm border border-gray-200">
|
||||
<h3 className="text-lg font-semibold text-gray-800 mb-4">
|
||||
Option de fusion
|
||||
</h3>
|
||||
<div className="flex items-center justify-between">
|
||||
<ToggleSwitch
|
||||
label="Fusionner les documents"
|
||||
checked={mergeDocuments}
|
||||
onChange={handleToggleMergeDocuments}
|
||||
<div className="bg-white p-4 rounded-lg shadow-sm border border-gray-200 flex items-center justify-center mb-2 min-h-[70px]">
|
||||
<ToggleSwitch
|
||||
label="Fusionner les documents"
|
||||
checked={mergeDocuments}
|
||||
onChange={handleToggleMergeDocuments}
|
||||
className="ml-0"
|
||||
/>
|
||||
</div>
|
||||
{/* Affectation à une classe */}
|
||||
<div className="bg-white p-4 rounded-lg shadow-sm border border-gray-200 flex items-center gap-4">
|
||||
<div className="flex-1 flex flex-col">
|
||||
<SelectChoice
|
||||
name="associated_class"
|
||||
label="Liste des classes"
|
||||
placeHolder="Sélectionner une classe"
|
||||
selected={formData.associated_class || ''}
|
||||
callback={(e) => onChange('associated_class', e.target.value)}
|
||||
choices={classes.map((classe) => ({
|
||||
value: classe.id,
|
||||
label: classe.atmosphere_name,
|
||||
}))}
|
||||
required
|
||||
className="mt-2"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Section Affectation */}
|
||||
<div className="bg-white p-4 rounded-lg shadow-sm border border-gray-200">
|
||||
<h3 className="text-lg font-semibold text-gray-800 mb-4">
|
||||
Affectation à une classe
|
||||
</h3>
|
||||
<div className="flex flex-col gap-4">
|
||||
<SelectChoice
|
||||
name="associated_class"
|
||||
label="Classe"
|
||||
placeHolder="Sélectionner une classe"
|
||||
selected={formData.associated_class || ''} // La valeur actuelle de la classe associée
|
||||
callback={(e) => onChange('associated_class', e.target.value)} // Met à jour formData
|
||||
choices={classes.map((classe) => ({
|
||||
value: classe.id,
|
||||
label: classe.atmosphere_name,
|
||||
}))} // Liste des classes disponibles
|
||||
required
|
||||
/>
|
||||
<Button
|
||||
text="Valider le dossier d'inscription"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
handleAssignClass();
|
||||
}}
|
||||
primary
|
||||
className={`px-4 py-2 rounded-md shadow-sm focus:outline-none ${
|
||||
!isPageValid
|
||||
? 'bg-gray-300 text-gray-700 cursor-not-allowed'
|
||||
: 'bg-emerald-500 text-white hover:bg-emerald-600'
|
||||
}`}
|
||||
disabled={!isPageValid}
|
||||
/>
|
||||
</div>
|
||||
{/* Boutons Valider/Refuser en bas, centrés */}
|
||||
<div className="mt-auto flex justify-center gap-4 py-4">
|
||||
<Button
|
||||
text="Valider le dossier d'inscription"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
handleAssignClass();
|
||||
}}
|
||||
primary
|
||||
className={`min-w-[220px] h-10 rounded-md shadow-sm focus:outline-none ${
|
||||
!isPageValid
|
||||
? 'bg-gray-300 text-gray-700 cursor-not-allowed'
|
||||
: 'bg-emerald-500 text-white hover:bg-emerald-600'
|
||||
}`}
|
||||
disabled={!isPageValid}
|
||||
/>
|
||||
<RefuseSubscription onRefuse={onRefuse} isLoading={isLoadingRefuse} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
23
Front-End/src/components/Textarea.js
Normal file
23
Front-End/src/components/Textarea.js
Normal file
@ -0,0 +1,23 @@
|
||||
import React from 'react';
|
||||
|
||||
/**
|
||||
* Textarea composant réutilisable
|
||||
* @param {string} value - Valeur du textarea
|
||||
* @param {function} onChange - Fonction appelée lors d'un changement
|
||||
* @param {string} placeholder - Texte d'exemple
|
||||
* @param {number} rows - Nombre de lignes
|
||||
* @param {string} className - Classes CSS additionnelles
|
||||
* @param {object} props - Props additionnels
|
||||
*/
|
||||
const Textarea = ({ value, onChange, placeholder = '', rows = 3, className = '', ...props }) => (
|
||||
<textarea
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
placeholder={placeholder}
|
||||
rows={rows}
|
||||
className={`border border-gray-300 rounded-md px-3 py-2 focus:outline-none focus:ring-2 focus:ring-emerald-500 resize-y ${className}`}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
|
||||
export default Textarea;
|
||||
Reference in New Issue
Block a user