feat: Ajout d'un nouvel état dans l'automatique lorsqu'un mandat SEPA

doit être envoyé aux parent
This commit is contained in:
N3WT DE COMPET
2025-04-27 13:40:48 +02:00
parent 3c62cc9ad2
commit 545349c7db
14 changed files with 214 additions and 153 deletions

View File

@ -7,6 +7,7 @@ import { useCsrfToken } from '@/context/CsrfContext';
import { useEstablishment } from '@/context/EstablishmentContext';
import { editRegisterForm } from '@/app/actions/subscriptionAction';
import logger from '@/utils/logger';
import Loader from '@/components/Loader';
export default function Page() {
const router = useRouter();
@ -16,14 +17,18 @@ export default function Page() {
const [formErrors, setFormErrors] = useState({});
const csrfToken = useCsrfToken();
const { selectedEstablishmentId } = useEstablishment();
const [isLoading, setIsLoading] = useState(false);
const handleSubmit = (data) => {
setIsLoading(true);
editRegisterForm(studentId, data, csrfToken)
.then((result) => {
setIsLoading(false);
logger.debug('Success:', result);
router.push(FE_ADMIN_SUBSCRIPTIONS_URL);
})
.catch((error) => {
setIsLoading(false);
logger.error('Error:', error.message);
if (error.details) {
logger.error('Form errors:', error.details);
@ -32,6 +37,10 @@ export default function Page() {
});
};
if (isLoading) {
return <Loader />;
}
return (
<InscriptionFormShared
studentId={studentId}

View File

@ -18,7 +18,7 @@ import {
FileText,
CheckCircle,
Plus,
XCircle,
Upload,
} from 'lucide-react';
import Modal from '@/components/Modal';
import InscriptionForm from '@/components/Inscription/InscriptionForm';
@ -35,6 +35,7 @@ import {
archiveRegisterForm,
fetchStudents,
editRegisterForm,
sendSEPARegisterForm,
} from '@/app/actions/subscriptionAction';
import {
@ -66,6 +67,7 @@ import DjangoCSRFToken from '@/components/DjangoCSRFToken';
import { useCsrfToken } from '@/context/CsrfContext';
import logger from '@/utils/logger';
import { PhoneLabel } from '@/components/PhoneLabel';
import FileUpload from '@/components/FileUpload';
export default function Page({ params: { locale } }) {
const t = useTranslations('subscriptions');
@ -80,11 +82,6 @@ export default function Page({ params: { locale } }) {
const [searchTerm, setSearchTerm] = useState('');
const [alertPage, setAlertPage] = useState(false);
const [isLoading, setIsLoading] = useState(false);
const [popup, setPopup] = useState({
visible: false,
message: '',
onConfirm: null,
});
const [activeTab, setActiveTab] = useState('pending');
const [currentPage, setCurrentPage] = useState(1);
const [totalPages, setTotalPages] = useState(1);
@ -113,10 +110,29 @@ export default function Page({ params: { locale } }) {
const [isFilesModalOpen, setIsFilesModalOpen] = useState(false);
const [selectedRowFiles, setSelectedRowFiles] = useState([]);
const [popupVisible, setPopupVisible] = useState(false);
const [popupMessage, setPopupMessage] = useState('');
const [confirmPopupVisible, setConfirmPopupVisible] = useState(false);
const [confirmPopupMessage, setConfirmPopupMessage] = useState('');
const [confirmPopupOnConfirm, setConfirmPopupOnConfirm] = useState(() => {});
const [isSepaUploadModalOpen, setIsSepaUploadModalOpen] = useState(false);
const [selectedRowForUpload, setSelectedRowForUpload] = useState(null);
const csrfToken = useCsrfToken();
const router = useRouter();
const { selectedEstablishmentId } = useEstablishment();
const openSepaUploadModal = (row) => {
setSelectedRowForUpload(row);
setIsSepaUploadModalOpen(true);
};
const closeSepaUploadModal = () => {
setSelectedRowForUpload(null);
setIsSepaUploadModalOpen(false);
};
const openModal = () => {
setIsOpen(true);
};
@ -221,28 +237,6 @@ export default function Page({ params: { locale } }) {
}
};
useEffect(() => {
if (selectedEstablishmentId) {
const fetchInitialData = () => {
Promise.all([
fetchClasses(selectedEstablishmentId),
fetchStudents(selectedEstablishmentId),
])
.then(([classesData, studentsData]) => {
setClasses(classesData);
setEleves(studentsData);
logger.debug('Success - Classes:', classesData);
logger.debug('Success - Students:', studentsData);
})
.catch((error) => {
logger.error('Error fetching initial data:', error);
});
};
fetchInitialData();
}
}, [selectedEstablishmentId]);
useEffect(() => {
if (selectedEstablishmentId) {
const fetchDataAndSetState = () => {
@ -257,6 +251,19 @@ export default function Page({ params: { locale } }) {
)
.then(registerFormPendingDataHandler)
.catch(requestErrorHandler),
fetchClasses(selectedEstablishmentId)
.then((classesData) => {
setClasses(classesData);
})
.catch(requestErrorHandler),
fetchStudents(selectedEstablishmentId)
.then((studentsData) => {
setEleves(studentsData);
})
.catch(requestErrorHandler),
fetchRegisterForms(selectedEstablishmentId, SUBSCRIBED)
.then(registerFormSubscribedDataHandler)
.catch(requestErrorHandler),
@ -378,6 +385,33 @@ export default function Page({ params: { locale } }) {
setTotalPages(Math.ceil(totalArchives / itemsPerPage));
}
}, [currentPage]);
const handleSepaFileUpload = (file, row) => {
if (!file || !row) {
logger.error("Aucun fichier ou ligne sélectionnée pour l'upload.");
return;
}
const formData = new FormData();
formData.append('status', 7);
formData.append('sepa_file', file);
// Appeler l'API pour uploader le fichier SEPA
sendSEPARegisterForm(row.student.id, formData, csrfToken)
.then((response) => {
logger.debug('Mandat SEPA uploadé avec succès :', response);
setPopupMessage('Le mandat SEPA a été uploadé avec succès.');
setPopupVisible(true);
setReloadFetch(true);
closeSepaUploadModal();
})
.catch((error) => {
logger.error("Erreur lors de l'upload du mandat SEPA :", error);
setPopupMessage("Erreur lors de l'upload du mandat SEPA.");
setPopupVisible(true);
});
};
/**
* Archives a registration form after user confirmation.
*
@ -386,44 +420,56 @@ export default function Page({ params: { locale } }) {
* @param {string} prenom - The first name of the person whose registration form is being archived.
*/
const archiveFicheInscription = (id, nom, prenom) => {
setPopup({
visible: true,
message: `Attentions ! \nVous êtes sur le point d'archiver le dossier d'inscription de ${nom} ${prenom}\nÊtes-vous sûr(e) de vouloir poursuivre l'opération ?`,
onConfirm: () => {
archiveRegisterForm(id)
.then((data) => {
logger.debug('Success:', data);
setRegistrationForms(
registrationForms.filter((fiche) => fiche.id !== id)
);
setReloadFetch(true);
alert("Le dossier d'inscription a été correctement archivé");
})
.catch((error) => {
logger.error('Error archiving data:', error);
alert(
"Erreur lors de l'archivage du dossier d'inscription.\nContactez l'administrateur."
);
});
},
setConfirmPopupMessage(
`Attentions ! \nVous êtes sur le point d'archiver le dossier d'inscription de ${nom} ${prenom}\nÊtes-vous sûr(e) de vouloir poursuivre l'opération ?`
);
setConfirmPopupOnConfirm(() => () => {
archiveRegisterForm(id)
.then((data) => {
logger.debug('Success:', data);
setPopupMessage(
`Le dossier d'inscription a été correctement archivé`
);
setPopupVisible(true);
setRegistrationForms(
registrationForms.filter((fiche) => fiche.id !== id)
);
setReloadFetch(true);
})
.catch((error) => {
logger.error('Error archiving data:', error);
setPopupMessage(
`Erreur lors de l'archivage du dossier d'inscription.\nContactez l'administrateur.`
);
setPopupVisible(true);
});
setConfirmPopupVisible(false);
});
setConfirmPopupVisible(true);
};
const sendConfirmRegisterForm = (id, nom, prenom) => {
setPopup({
visible: true,
message: `Avertissement ! \nVous êtes sur le point d'envoyer un dossier d'inscription à ${nom} ${prenom}\nÊtes-vous sûr(e) de vouloir poursuivre l'opération ?`,
onConfirm: () => {
sendRegisterForm(id)
.then((data) => {
logger.debug('Success:', data);
setReloadFetch(true);
})
.catch((error) => {
logger.error('Error fetching data:', error);
});
},
setConfirmPopupMessage(
`Avertissement ! \nVous êtes sur le point d'envoyer un dossier d'inscription à ${nom} ${prenom}\nÊtes-vous sûr(e) de vouloir poursuivre l'opération ?`
);
setConfirmPopupOnConfirm(() => () => {
sendRegisterForm(id)
.then((data) => {
logger.debug('Success:', data);
setPopupMessage(`Le dossier d'inscription a été envoyé avec succès`);
setPopupVisible(true);
setReloadFetch(true);
})
.catch((error) => {
logger.error('Error archiving data:', error);
setPopupMessage(
`Erreur lors de l'envoi du dossier d'inscription.\nContactez l'administrateur.`
);
setPopupVisible(true);
});
setConfirmPopupVisible(false);
});
setConfirmPopupVisible(true);
};
const affectationClassFormSubmitHandler = (formdata) => {
@ -437,25 +483,6 @@ export default function Page({ params: { locale } }) {
});
};
const refuseRegistrationForm = (id, lastname, firstname, guardianEmail) => {
const data = { status: 2, establishment: selectedEstablishmentId };
setPopup({
visible: true,
message: `Avertissement ! \nVous êtes sur le point de refuser le dossier d'inscription de ${lastname} ${firstname}\nUne notification va être envoyée à l'adresse ${guardianEmail}\nÊtes-vous sûr(e) de vouloir poursuivre l'opération ?`,
onConfirm: () => {
editRegisterForm(id, data, csrfToken)
.then((data) => {
logger.debug('Success:', data);
setReloadFetch(true);
})
.catch((error) => {
logger.error('Error refusing RF:', error);
});
},
});
};
const updateStatusAction = (id, newStatus) => {
logger.debug(
`Mise à jour du statut du dossier d'inscription avec l'ID : ${id} vers le statut : ${newStatus}`
@ -551,6 +578,7 @@ export default function Page({ params: { locale } }) {
establishment: selectedEstablishmentId,
};
setIsLoading(true);
createRegisterForm(data, csrfToken)
.then((data) => {
// Cloner les schoolFileTemplates pour chaque templateMaster du fileGroup
@ -582,6 +610,7 @@ export default function Page({ params: { locale } }) {
logger.debug('Template enregistré avec succès:', response);
})
.catch((error) => {
setIsLoading(false);
logger.error(
"Erreur lors de l'enregistrement du template:",
error
@ -589,6 +618,7 @@ export default function Page({ params: { locale } }) {
});
})
.catch((error) => {
setIsLoading(false);
logger.error('Error during cloning or sending:', error);
});
});
@ -608,6 +638,7 @@ export default function Page({ params: { locale } }) {
logger.debug('Parent template enregistré avec succès:', response);
})
.catch((error) => {
setIsLoading(false);
logger.error(
"Erreur lors de l'enregistrement du parent template:",
error
@ -634,12 +665,15 @@ export default function Page({ params: { locale } }) {
closeModal(); // Appeler closeModal ici après que tout soit terminé
// Forcer le rechargement complet des données
setReloadFetch(true);
setIsLoading(false);
})
.catch((error) => {
setIsLoading(false);
logger.error('Error during cloning or sending:', error);
});
})
.catch((error) => {
setIsLoading(false);
logger.error('Error:', error);
});
};
@ -810,6 +844,24 @@ export default function Page({ params: { locale } }) {
onClick: () => openFilesModal(row),
},
],
8: [
{
icon: (
<span title="Voir les fichiers">
<FileText className="w-5 h-5 text-cyan-500 hover:text-cyan-700" />
</span>
),
onClick: () => openFilesModal(row),
},
{
icon: (
<span title="Uploader un mandat SEPA">
<Upload className="w-5 h-5 text-emerald-500 hover:text-emerald-700" />
</span>
),
onClick: () => openSepaUploadModal(row),
},
],
default: [
{
icon: (
@ -879,37 +931,6 @@ export default function Page({ params: { locale } }) {
</div>
),
},
{
name: t('files'),
transform: (row) => (
<ul>
{row.registration_file && (
<li className="flex justify-center items-center gap-2">
<FileText size={16} />
<a
href={`${BASE_URL}${row.registration_file}`}
target="_blank"
rel="noopener noreferrer"
>
{row.registration_file?.split('/').pop()}
</a>
</li>
)}
{row.sepa_file && (
<li className="flex justify-center items-center gap-2">
<FileText size={16} />
<a
href={`${BASE_URL}${row.sepa_file}`}
target="_blank"
rel="noopener noreferrer"
>
{row.sepa_file?.split('/').pop()}
</a>
</li>
)}
</ul>
),
},
{
name: 'Actions',
transform: (row) => (
@ -1116,13 +1137,16 @@ export default function Page({ params: { locale } }) {
) : null}
</div>
<Popup
visible={popup.visible}
message={popup.message}
onConfirm={() => {
popup.onConfirm();
setPopup({ ...popup, visible: false });
}}
onCancel={() => setPopup({ ...popup, visible: false })}
visible={popupVisible}
message={popupMessage}
onConfirm={() => setPopupVisible(false)}
uniqueConfirmButton={true}
/>
<Popup
visible={confirmPopupVisible}
message={confirmPopupMessage}
onConfirm={confirmPopupOnConfirm}
onCancel={() => setConfirmPopupVisible(false)}
/>
{isOpen && (
@ -1176,6 +1200,21 @@ export default function Page({ params: { locale } }) {
)}
/>
)}
{isSepaUploadModalOpen && (
<Modal
isOpen={isSepaUploadModalOpen}
setIsOpen={setIsSepaUploadModalOpen}
title="Uploader un mandat SEPA"
ContentComponent={() => (
<FileUpload
selectionMessage="Sélectionnez un mandat SEPA à uploader"
onFileSelect={(file) =>
handleSepaFileUpload(file, selectedRowForUpload)
}
/>
)}
/>
)}
{isFilesModalOpen && (
<Modal
isOpen={isFilesModalOpen}

View File

@ -24,15 +24,16 @@ export default function ParentHomePage() {
const [uploadState, setUploadState] = useState('off'); // État "on" ou "off" pour l'affichage du composant
const router = useRouter();
const csrfToken = useCsrfToken();
const [reloadFetch, setReloadFetch] = useState(false);
useEffect(() => {
const userIdFromSession = user.user_id;
setUserId(userIdFromSession);
console.log(selectedEstablishmentId);
fetchChildren(userIdFromSession, selectedEstablishmentId).then((data) => {
setChildren(data);
});
}, [selectedEstablishmentId]);
setReloadFetch(false);
}, [selectedEstablishmentId, reloadFetch]);
function handleView(eleveId) {
logger.debug(`View dossier for student id: ${eleveId}`);
@ -70,7 +71,8 @@ export default function ParentHomePage() {
sendSEPARegisterForm(uploadingStudentId, formData, csrfToken)
.then((response) => {
logger.debug('RF mis à jour avec succès:', response);
// Logique supplémentaire après la mise à jour (par exemple, redirection ou notification)
setReloadFetch(true);
setUploadState('off');
})
.catch((error) => {
logger.error('Erreur lors de la mise à jour du RF:', error);
@ -118,7 +120,7 @@ export default function ParentHomePage() {
</button>
)}
{row.status === 3 && (
{(row.status === 3 || row.status === 8) && (
<button
className="text-purple-500 hover:text-purple-700"
onClick={(e) => {

View File

@ -23,7 +23,7 @@ import StudentInfoForm from '@/components/Inscription/StudentInfoForm';
import ResponsableInputFields from '@/components/Inscription/ResponsableInputFields';
import PaymentMethodSelector from '@/components/Inscription/PaymentMethodSelector';
import ProgressStep from '@/components/ProgressStep';
import { CheckCircle, Loader2 } from 'lucide-react';
import { CheckCircle, Hourglass } from 'lucide-react';
/**
* Composant de formulaire d'inscription partagé
@ -315,16 +315,21 @@ export default function InscriptionFormShared({
// Soumission du formulaire
const handleSubmit = (e) => {
e.preventDefault();
// Vérifier si le mode de paiement sélectionné est un prélèvement SEPA
const isSepaPayment =
formData.registration_payment === '1' || formData.tuition_payment === '1';
const data = {
student: {
...formData,
guardians,
},
establishment: selectedEstablishmentId,
status: 3,
status: isSepaPayment ? 8 : 3,
tuition_payment: formData.tuition_payment,
registration_payment: formData.registration_payment,
};
onSubmit(data);
};
@ -448,7 +453,7 @@ export default function InscriptionFormShared({
{template.file !== null ? (
<CheckCircle className="w-5 h-5 text-green-600" />
) : (
<Loader2 className="w-5 h-5 text-gray-600" />
<Hourglass className="w-5 h-5 text-gray-600" />
)}
</span>
{template.name || 'Document sans nom'}

View File

@ -13,7 +13,6 @@ export default function PaymentMethodSelector({
const isValid = !Object.keys(formData).some(
(field) => getLocalError(field) !== ''
);
console.log(isValid);
setIsPageValid(isValid);
}, [formData, setIsPageValid]);

View File

@ -1,18 +1,9 @@
import React, { useState } from 'react';
import {
Trash2,
Eye,
EyeOff,
ToggleLeft,
ToggleRight,
Info,
XCircle,
} from 'lucide-react';
import { Trash2, ToggleLeft, ToggleRight, Info, XCircle } from 'lucide-react';
import Table from '@/components/Table';
import Popup from '@/components/Popup';
import StatusLabel from '@/components/StatusLabel';
import SpecialityItem from '@/components/Structure/Configuration/SpecialityItem';
import Tooltip from '@/components/Tooltip';
const roleTypeToLabel = (roleType) => {
switch (roleType) {

View File

@ -11,6 +11,7 @@ const StatusLabel = ({ status, onChange, showDropdown = true, parent }) => {
{ value: 2, label: 'Nouveau' },
{ value: 3, label: 'En validation' },
{ value: 7, label: 'SEPA reçu' },
{ value: 8, label: 'En validation' },
]
: [
{ value: 1, label: 'A envoyer' },
@ -20,6 +21,7 @@ const StatusLabel = ({ status, onChange, showDropdown = true, parent }) => {
{ value: 5, label: 'Validé' },
{ value: 6, label: 'Archivé' },
{ value: 7, label: 'En attente SEPA' },
{ value: 8, label: 'SEPA à envoyer' },
];
const currentStatus = statusOptions.find((option) => option.value === status);
@ -29,7 +31,7 @@ const StatusLabel = ({ status, onChange, showDropdown = true, parent }) => {
if (parent) {
return (
(status === 2 && 'bg-orange-50 text-orange-600') ||
(status === 3 && 'bg-purple-50 text-purple-600') ||
((status === 3 || status === 8) && 'bg-purple-50 text-purple-600') ||
(status === 7 && 'bg-yellow-50 text-yellow-600')
);
}
@ -40,7 +42,8 @@ const StatusLabel = ({ status, onChange, showDropdown = true, parent }) => {
(status === 4 && 'bg-red-50 text-red-600') ||
(status === 5 && 'bg-green-50 text-green-600') ||
(status === 6 && 'bg-red-50 text-red-600') ||
(status === 7 && 'bg-yellow-50 text-yellow-600')
(status === 7 && 'bg-yellow-50 text-yellow-600') ||
(status === 8 && 'bg-cyan-50 text-cyan-600')
);
};

View File

@ -163,7 +163,7 @@ export default function ParentFilesSection({
return (
<MultiSelect
name="groups"
label="Sélection de groupes de fichiers"
label="Sélection du(des) dossier(s) d'inscription"
options={groups}
selectedOptions={selectedGroups}
onChange={handleGroupChange}

View File

@ -1,10 +1,10 @@
/**
*
* @param {*} phoneString au format E.164
* @param {*} toFormat L=Country code, X=Number Format="LX XX XX XX XX"
* @param {*} toFormat L=Country code, X=Number Format="L X XX XX XX XX"
* @returns
*/
export function formatPhoneNumber(phoneString, toFormat = 'LX XX XX XX XX') {
export function formatPhoneNumber(phoneString, toFormat = 'L X XX XX XX XX') {
if (!phoneString) return;
// Vérifier si le numéro est au format international
if (!validateE164PhoneNumber(phoneString)) {