feat: Ajout de la photo pour le dossier de l'élève + correction

sauvegarde des datas des responsables
This commit is contained in:
N3WT DE COMPET
2025-05-01 14:59:19 +02:00
parent d37e6c384d
commit 5851341235
12 changed files with 187 additions and 86 deletions

View File

@ -48,6 +48,9 @@ class Sibling(models.Model):
def __str__(self): def __str__(self):
return "SIBLING" return "SIBLING"
def registration_photo_upload_to(instance, filename):
return f"registration_files/dossier_rf_{instance.pk}/parent/{filename}"
class Student(models.Model): class Student(models.Model):
""" """
Représente lélève inscrit ou en cours dinscription. Représente lélève inscrit ou en cours dinscription.
@ -64,6 +67,11 @@ class Student(models.Model):
MS = 3, _('MS - Moyenne Section') MS = 3, _('MS - Moyenne Section')
GS = 4, _('GS - Grande Section') GS = 4, _('GS - Grande Section')
photo = models.FileField(
upload_to=registration_photo_upload_to,
null=True,
blank=True
)
last_name = models.CharField(max_length=200, default="") last_name = models.CharField(max_length=200, default="")
first_name = models.CharField(max_length=200, default="") first_name = models.CharField(max_length=200, default="")
gender = models.IntegerField(choices=StudentGender, default=StudentGender.NONE, blank=True) gender = models.IntegerField(choices=StudentGender, default=StudentGender.NONE, blank=True)
@ -239,12 +247,6 @@ class RegistrationForm(models.Model):
# Appeler la méthode save originale # Appeler la méthode save originale
super().save(*args, **kwargs) super().save(*args, **kwargs)
def registration_school_file_upload_to(instance, filename):
return f"registration_files/dossier_rf_{instance.registration_form.pk}/school/{filename}"
def registration_parent_file_upload_to(instance, filename):
return f"registration_files/dossier_rf_{instance.registration_form.pk}/parent/{filename}"
############################################################# #############################################################
####################### MASTER FILES ######################## ####################### MASTER FILES ########################
############################################################# #############################################################
@ -269,6 +271,12 @@ class RegistrationParentFileMaster(models.Model):
####################### CLONE FILES ######################## ####################### CLONE FILES ########################
############################################################ ############################################################
def registration_school_file_upload_to(instance, filename):
return f"registration_files/dossier_rf_{instance.registration_form.pk}/school/{filename}"
def registration_parent_file_upload_to(instance, filename):
return f"registration_files/dossier_rf_{instance.registration_form.pk}/parent/{filename}"
####### DocuSeal templates (par dossier d'inscription) ####### ####### DocuSeal templates (par dossier d'inscription) #######
class RegistrationSchoolFileTemplate(models.Model): class RegistrationSchoolFileTemplate(models.Model):
master = models.ForeignKey(RegistrationSchoolFileMaster, on_delete=models.CASCADE, related_name='school_file_templates', blank=True) master = models.ForeignKey(RegistrationSchoolFileMaster, on_delete=models.CASCADE, related_name='school_file_templates', blank=True)

View File

@ -91,7 +91,25 @@
{% load myTemplateTag %} {% load myTemplateTag %}
<div class="container"> <div class="container">
<div class="header"> <div class="header">
<div style="position: relative;">
<h1 class="title">{{ pdf_title }}</h1> <h1 class="title">{{ pdf_title }}</h1>
{% if student.photo %}
<img
src="{{ student.photo }}"
alt="Photo de l'élève"
style="
position: absolute;
top: 0;
right: 0;
width: 100px;
height: 100px;
object-fit: cover;
border: 2px solid #333;
border-radius: 10px;
"
/>
{% endif %}
</div>
</div> </div>
<div class="section"> <div class="section">

View File

@ -229,15 +229,22 @@ class RegisterFormWithIdView(APIView):
""" """
Modifie un dossier d'inscription donné. Modifie un dossier d'inscription donné.
""" """
# Récupérer les données de la requête
studentForm_data = request.data.copy()
logger.info(f"Mise à jour du dossier d'inscription {studentForm_data}") studentForm_data = request.data.get('data', '{}')
try:
data = json.loads(studentForm_data)
except json.JSONDecodeError:
return JsonResponse({"error": "Invalid JSON format in 'data'"}, status=status.HTTP_400_BAD_REQUEST)
_status = studentForm_data.pop('status', 0) # Extraire le fichier photo
if isinstance(_status, list): # Cas Multipart/data, les données sont envoyées sous forme de liste, c'est nul photo_file = request.FILES.get('photo')
_status = int(_status[0])
else: # Ajouter la photo aux données de l'étudiant
if photo_file:
data['student']['photo'] = photo_file
# Gérer le champ `_status`
_status = data.pop('status', 0)
_status = int(_status) _status = int(_status)
# Récupérer le dossier d'inscription # Récupérer le dossier d'inscription
@ -245,9 +252,14 @@ class RegisterFormWithIdView(APIView):
if not registerForm: if not registerForm:
return JsonResponse({"error": "Dossier d'inscription introuvable"}, status=status.HTTP_404_NOT_FOUND) return JsonResponse({"error": "Dossier d'inscription introuvable"}, status=status.HTTP_404_NOT_FOUND)
studentForm_serializer = RegistrationFormSerializer(registerForm, data=studentForm_data, partial=True) studentForm_serializer = RegistrationFormSerializer(registerForm, data=data, partial=True)
if studentForm_serializer.is_valid(): if studentForm_serializer.is_valid():
studentForm_serializer.save() studentForm_serializer.save()
# Sauvegarder la photo si elle est présente dans la requête
if photo_file:
student = registerForm.student
student.photo.save(photo_file.name, photo_file, save=True)
else: else:
return JsonResponse(studentForm_serializer.errors, safe=False, status=status.HTTP_400_BAD_REQUEST) return JsonResponse(studentForm_serializer.errors, safe=False, status=status.HTTP_400_BAD_REQUEST)

View File

@ -5,6 +5,7 @@
"pending": "Pending Registrations", "pending": "Pending Registrations",
"subscribed": "Subscribed", "subscribed": "Subscribed",
"archived": "Archived", "archived": "Archived",
"photo": "Photo",
"name": "Name", "name": "Name",
"class": "Class", "class": "Class",
"status": "Status", "status": "Status",

View File

@ -5,6 +5,7 @@
"pending": "Inscriptions en attente", "pending": "Inscriptions en attente",
"subscribed": "Inscrits", "subscribed": "Inscrits",
"archived": "Archivés", "archived": "Archivés",
"photo": "Photo",
"name": "Nom", "name": "Nom",
"class": "Classe", "class": "Classe",
"status": "Statut", "status": "Statut",

View File

@ -5,7 +5,7 @@ import InscriptionFormShared from '@/components/Inscription/InscriptionFormShare
import { FE_ADMIN_SUBSCRIPTIONS_URL } from '@/utils/Url'; import { FE_ADMIN_SUBSCRIPTIONS_URL } from '@/utils/Url';
import { useCsrfToken } from '@/context/CsrfContext'; import { useCsrfToken } from '@/context/CsrfContext';
import { useEstablishment } from '@/context/EstablishmentContext'; import { useEstablishment } from '@/context/EstablishmentContext';
import { editRegisterForm } from '@/app/actions/subscriptionAction'; import { editRegisterFormWithBinaryFile } from '@/app/actions/subscriptionAction';
import logger from '@/utils/logger'; import logger from '@/utils/logger';
import Loader from '@/components/Loader'; import Loader from '@/components/Loader';
@ -21,7 +21,7 @@ export default function Page() {
const handleSubmit = (data) => { const handleSubmit = (data) => {
setIsLoading(true); setIsLoading(true);
editRegisterForm(studentId, data, csrfToken) editRegisterFormWithBinaryFile(studentId, data, csrfToken)
.then((result) => { .then((result) => {
setIsLoading(false); setIsLoading(false);
logger.debug('Success:', result); logger.debug('Success:', result);

View File

@ -60,6 +60,7 @@ import { fetchProfiles } from '@/app/actions/authAction';
import { import {
FE_ADMIN_SUBSCRIPTIONS_EDIT_URL, FE_ADMIN_SUBSCRIPTIONS_EDIT_URL,
FE_ADMIN_SUBSCRIPTIONS_VALIDATE_URL, FE_ADMIN_SUBSCRIPTIONS_VALIDATE_URL,
BASE_URL,
} from '@/utils/Url'; } from '@/utils/Url';
import DjangoCSRFToken from '@/components/DjangoCSRFToken'; import DjangoCSRFToken from '@/components/DjangoCSRFToken';
@ -743,17 +744,6 @@ export default function Page({ params: { locale } }) {
const getActionsByStatus = (row) => { const getActionsByStatus = (row) => {
const actions = { const actions = {
1: [ 1: [
{
icon: (
<span title="Editer le dossier">
<Edit className="w-5 h-5 text-blue-500 hover:text-blue-700" />
</span>
),
onClick: () =>
router.push(
`${FE_ADMIN_SUBSCRIPTIONS_EDIT_URL}?studentId=${row.student.id}`
),
},
{ {
icon: ( icon: (
<span title="Envoyer le dossier"> <span title="Envoyer le dossier">
@ -865,6 +855,33 @@ export default function Page({ params: { locale } }) {
}; };
const columns = [ const columns = [
{
name: t('photo'),
transform: (row) => (
<div className="flex justify-center items-center">
{row.student.photo ? (
<a
href={`${BASE_URL}${row.student.photo}`} // Lien vers la photo
target="_blank"
rel="noopener noreferrer"
>
<img
src={`${BASE_URL}${row.student.photo}`}
alt={`${row.student.first_name} ${row.student.last_name}`}
className="w-10 h-10 object-cover transition-transform duration-200 hover:scale-125 cursor-pointer"
/>
</a>
) : (
<div className="w-10 h-10 flex items-center justify-center bg-gray-200 rounded-full">
<span className="text-gray-500 text-sm font-semibold">
{row.student.first_name[0]}
{row.student.last_name[0]}
</span>
</div>
)}
</div>
),
},
{ name: t('studentName'), transform: (row) => row.student.last_name }, { name: t('studentName'), transform: (row) => row.student.last_name },
{ name: t('studentFistName'), transform: (row) => row.student.first_name }, { name: t('studentFistName'), transform: (row) => row.student.first_name },
{ {

View File

@ -42,7 +42,7 @@ export default function FileUpload({
{/* Icône de cloud */} {/* Icône de cloud */}
<input <input
type="file" type="file"
accept=".pdf" accept=".pdf, .png, .jpg, .jpeg, .gif, .bmp"
onChange={handleFileChange} onChange={handleFileChange}
className="hidden" className="hidden"
ref={fileInputRef} // Attachement de la référence ref={fileInputRef} // Attachement de la référence

View File

@ -43,6 +43,7 @@ export default function InscriptionFormShared({
// États pour gérer les données du formulaire // États pour gérer les données du formulaire
const [formData, setFormData] = useState({ const [formData, setFormData] = useState({
id: '', id: '',
photo: null,
last_name: '', last_name: '',
first_name: '', first_name: '',
address: '', address: '',
@ -55,7 +56,6 @@ export default function InscriptionFormShared({
registration_payment: '', registration_payment: '',
tuition_payment: '', tuition_payment: '',
}); });
const [guardians, setGuardians] = useState([]); const [guardians, setGuardians] = useState([]);
const [registrationPaymentModes, setRegistrationPaymentModes] = useState([]); const [registrationPaymentModes, setRegistrationPaymentModes] = useState([]);
@ -333,10 +333,12 @@ export default function InscriptionFormShared({
// Vérifier si le mode de paiement sélectionné est un prélèvement SEPA // Vérifier si le mode de paiement sélectionné est un prélèvement SEPA
const isSepaPayment = const isSepaPayment =
formData.registration_payment === '1' || formData.tuition_payment === '1'; formData.registration_payment === '1' || formData.tuition_payment === '1';
const data = {
// Préparer les données JSON
const jsonData = {
student: { student: {
...formData, ...formData,
guardians, guardians: guardians,
}, },
establishment: selectedEstablishmentId, establishment: selectedEstablishmentId,
status: isSepaPayment ? 8 : 3, status: isSepaPayment ? 8 : 3,
@ -344,7 +346,19 @@ export default function InscriptionFormShared({
registration_payment: formData.registration_payment, registration_payment: formData.registration_payment,
}; };
onSubmit(data); // Créer un objet FormData
const formDataToSend = new FormData();
// Ajouter les données JSON sous forme de chaîne
formDataToSend.append('data', JSON.stringify(jsonData));
// Ajouter la photo si elle est présente
if (formData.photo) {
formDataToSend.append('photo', formData.photo);
}
// Appeler la fonction onSubmit avec les données FormData
onSubmit(formDataToSend);
}; };
const handleNextPage = () => { const handleNextPage = () => {

View File

@ -4,6 +4,7 @@ import React, { useEffect } from 'react';
import { useTranslations } from 'next-intl'; import { useTranslations } from 'next-intl';
import { Trash2, Plus, Users } from 'lucide-react'; import { Trash2, Plus, Users } from 'lucide-react';
import SectionHeader from '@/components/SectionHeader'; import SectionHeader from '@/components/SectionHeader';
import { useEstablishment } from '@/context/EstablishmentContext';
export default function ResponsableInputFields({ export default function ResponsableInputFields({
guardians, guardians,
@ -12,6 +13,7 @@ export default function ResponsableInputFields({
setIsPageValid, setIsPageValid,
}) { }) {
const t = useTranslations('ResponsableInputFields'); const t = useTranslations('ResponsableInputFields');
const { selectedEstablishmentId } = useEstablishment();
useEffect(() => { useEffect(() => {
const isValid = const isValid =
@ -38,7 +40,7 @@ export default function ResponsableInputFields({
(field === 'first_name' && (field === 'first_name' &&
(!guardians[index].first_name || (!guardians[index].first_name ||
guardians[index].first_name.trim() === '')) || guardians[index].first_name.trim() === '')) ||
(field === 'email' && (field === 'associated_profile_email' &&
(!guardians[index].associated_profile_email || (!guardians[index].associated_profile_email ||
guardians[index].associated_profile_email.trim() === '')) || guardians[index].associated_profile_email.trim() === '')) ||
(field === 'birth_date' && (field === 'birth_date' &&
@ -56,16 +58,45 @@ export default function ResponsableInputFields({
}; };
const onGuardiansChange = (id, field, value) => { const onGuardiansChange = (id, field, value) => {
const updatedGuardians = guardians.map((guardian) => const updatedGuardians = guardians.map((guardian) => {
guardian.id === id ? { ...guardian, [field]: value } : guardian if (guardian.id === id) {
); const updatedGuardian = { ...guardian, [field]: value };
// Synchroniser profile_data.email et profile_data.username avec associated_profile_email
if (field === 'associated_profile_email') {
updatedGuardian.profile_role_data.profile_data.email = value;
updatedGuardian.profile_role_data.profile_data.username = value;
}
return updatedGuardian;
}
return guardian;
});
setGuardians(updatedGuardians); setGuardians(updatedGuardians);
}; };
const addGuardian = () => { const addGuardian = () => {
setGuardians([ setGuardians([
...guardians, ...guardians,
{ id: Date.now(), name: '', email: '', phone: '' }, {
profile_role_data: {
establishment: selectedEstablishmentId,
role_type: 2,
is_active: false,
profile_data: {
email: '',
password: 'Provisoire01!',
username: '',
},
},
last_name: '',
first_name: '',
birth_date: '',
address: '',
phone: '',
profession: '',
},
]); ]);
}; };
@ -143,7 +174,8 @@ export default function ResponsableInputFields({
}} }}
required required
errorMsg={ errorMsg={
getError(index, 'email') || getLocalError(index, 'email') getError(index, 'associated_profile_email') ||
getLocalError(index, 'associated_profile_email')
} }
/> />
<InputPhone <InputPhone

View File

@ -6,6 +6,7 @@ import { fetchRegisterForm } from '@/app/actions/subscriptionAction';
import logger from '@/utils/logger'; import logger from '@/utils/logger';
import SectionHeader from '@/components/SectionHeader'; import SectionHeader from '@/components/SectionHeader';
import { User } from 'lucide-react'; import { User } from 'lucide-react';
import FileUpload from '@/components/FileUpload';
const levels = [ const levels = [
{ value: '1', label: 'TPS - Très Petite Section' }, { value: '1', label: 'TPS - Très Petite Section' },
@ -38,6 +39,7 @@ export default function StudentInfoForm({
setFormData({ setFormData({
id: data?.student?.id || '', id: data?.student?.id || '',
photo: data?.student?.photo || null,
last_name: data?.student?.last_name || '', last_name: data?.student?.last_name || '',
first_name: data?.student?.first_name || '', first_name: data?.student?.first_name || '',
address: data?.student?.address || '', address: data?.student?.address || '',
@ -110,6 +112,16 @@ export default function StudentInfoForm({
} }
}; };
const handlePhotoUpload = (file) => {
if (file) {
setFormData((prev) => ({
...prev,
photo: file,
}));
logger.debug('Photo sélectionnée :', file.name);
}
};
// Affichage du loader pendant le chargement // Affichage du loader pendant le chargement
if (isLoading) return <Loader />; if (isLoading) return <Loader />;
@ -208,54 +220,18 @@ export default function StudentInfoForm({
</div> </div>
</div> </div>
{/* <div className="bg-white p-6 rounded-lg shadow-sm border border-gray-200"> {/* Section pour l'upload des fichiers */}
<h2 className="text-xl font-bold mb-4 text-gray-800">Responsables</h2> <div className="bg-white p-6 rounded-lg shadow-sm border border-gray-200 mt-6">
<ResponsableInputFields <SectionHeader
guardians={guardians} icon={User}
onGuardiansChange={(id, field, value) => { title={`Photo de l'élève`}
const updatedGuardians = guardians.map((resp) => description={`Ajoutez une photo de votre enfant`}
resp.id === id ? { ...resp, [field]: value } : resp />
); <FileUpload
setGuardians(updatedGuardians); selectionMessage="Sélectionnez une photo à uploader"
}} onFileSelect={(file) => handlePhotoUpload(file)}
addGuardian={(e) => {
e.preventDefault();
setGuardians([...guardians, { id: Date.now() }]);
}}
deleteGuardian={(index) => {
const newArray = [...guardians];
newArray.splice(index, 1);
setGuardians(newArray);
}}
errors={errors?.student?.guardians || []}
/> />
</div> </div>
<PaymentMethodSelector
formData={formData}
title="Frais d'inscription"
name="registration_payment"
onChange={onChange}
selected={formData.registration_payment}
paymentModes={registrationPaymentModes}
paymentModesOptions={paymentModesOptions}
amount={formData.totalRegistrationFees}
getError={getError}
getLocalError={getLocalError}
/>
<PaymentMethodSelector
formData={formData}
title="Frais de scolarité"
name="tuition_payment"
onChange={onChange}
selected={formData.tuition_payment}
paymentModes={tuitionPaymentModes}
paymentModesOptions={paymentModesOptions}
amount={formData.totalTuitionFees}
getError={getError}
getLocalError={getLocalError}
/> */}
</> </>
); );
} }

View File

@ -10,6 +10,7 @@ import logger from '@/utils/logger';
import MultiSelect from '@/components/MultiSelect'; // Import du composant MultiSelect import MultiSelect from '@/components/MultiSelect'; // Import du composant MultiSelect
import { useCsrfToken } from '@/context/CsrfContext'; import { useCsrfToken } from '@/context/CsrfContext';
import { useEstablishment } from '@/context/EstablishmentContext'; import { useEstablishment } from '@/context/EstablishmentContext';
import Popup from '@/components/Popup';
export default function FileUploadDocuSeal({ export default function FileUploadDocuSeal({
handleCreateTemplateMaster, handleCreateTemplateMaster,
@ -24,6 +25,9 @@ export default function FileUploadDocuSeal({
const [selectedGroups, setSelectedGroups] = useState([]); const [selectedGroups, setSelectedGroups] = useState([]);
const [guardianDetails, setGuardianDetails] = useState([]); const [guardianDetails, setGuardianDetails] = useState([]);
const [popupVisible, setPopupVisible] = useState(false);
const [popupMessage, setPopupMessage] = useState('');
const csrfToken = useCsrfToken(); const csrfToken = useCsrfToken();
const { selectedEstablishmentId } = useEstablishment(); const { selectedEstablishmentId } = useEstablishment();
@ -84,6 +88,17 @@ export default function FileUploadDocuSeal({
}; };
const handleSubmit = (data) => { const handleSubmit = (data) => {
// Vérifier si au moins un champ a la propriété "required" à true
const hasRequiredField = data.fields.some(
(field) => field.required === true
);
if (!hasRequiredField) {
setPopupMessage('Veuillez définir au moins un champ comme requis.');
setPopupVisible(true);
return;
}
const is_required = data.fields.length > 0; const is_required = data.fields.length > 0;
if (fileToEdit) { if (fileToEdit) {
logger.debug('Modification du template master:', templateMaster?.id); logger.debug('Modification du template master:', templateMaster?.id);
@ -139,6 +154,13 @@ export default function FileUploadDocuSeal({
return ( return (
<div className="h-full flex flex-col mt-4 space-y-6"> <div className="h-full flex flex-col mt-4 space-y-6">
<Popup
visible={popupVisible}
message={popupMessage}
onConfirm={() => setPopupVisible(false)}
onCancel={() => setPopupVisible(false)}
uniqueConfirmButton={true}
/>
{/* Contenu principal */} {/* Contenu principal */}
<div className="grid grid-cols-10 gap-6 items-start"> <div className="grid grid-cols-10 gap-6 items-start">
{/* Sélection des groupes */} {/* Sélection des groupes */}