mirror of
https://git.v0id.ovh/n3wt-innov/n3wt-school.git
synced 2026-01-28 23:43:22 +00:00
feat: Ajout de la photo pour le dossier de l'élève + correction
sauvegarde des datas des responsables
This commit is contained in:
@ -48,6 +48,9 @@ class Sibling(models.Model):
|
||||
def __str__(self):
|
||||
return "SIBLING"
|
||||
|
||||
def registration_photo_upload_to(instance, filename):
|
||||
return f"registration_files/dossier_rf_{instance.pk}/parent/{filename}"
|
||||
|
||||
class Student(models.Model):
|
||||
"""
|
||||
Représente l’élève inscrit ou en cours d’inscription.
|
||||
@ -64,6 +67,11 @@ class Student(models.Model):
|
||||
MS = 3, _('MS - Moyenne 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="")
|
||||
first_name = models.CharField(max_length=200, default="")
|
||||
gender = models.IntegerField(choices=StudentGender, default=StudentGender.NONE, blank=True)
|
||||
@ -239,12 +247,6 @@ class RegistrationForm(models.Model):
|
||||
# Appeler la méthode save originale
|
||||
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 ########################
|
||||
#############################################################
|
||||
@ -269,6 +271,12 @@ class RegistrationParentFileMaster(models.Model):
|
||||
####################### 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) #######
|
||||
class RegistrationSchoolFileTemplate(models.Model):
|
||||
master = models.ForeignKey(RegistrationSchoolFileMaster, on_delete=models.CASCADE, related_name='school_file_templates', blank=True)
|
||||
|
||||
@ -91,7 +91,25 @@
|
||||
{% load myTemplateTag %}
|
||||
<div class="container">
|
||||
<div class="header">
|
||||
<h1 class="title">{{ pdf_title }}</h1>
|
||||
<div style="position: relative;">
|
||||
<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 class="section">
|
||||
|
||||
@ -229,25 +229,37 @@ class RegisterFormWithIdView(APIView):
|
||||
"""
|
||||
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)
|
||||
if isinstance(_status, list): # Cas Multipart/data, les données sont envoyées sous forme de liste, c'est nul
|
||||
_status = int(_status[0])
|
||||
else:
|
||||
_status = int(_status)
|
||||
# Extraire le fichier photo
|
||||
photo_file = request.FILES.get('photo')
|
||||
|
||||
# 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)
|
||||
|
||||
# Récupérer le dossier d'inscription
|
||||
registerForm = bdd.getObject(_objectName=RegistrationForm, _columnName='student__id', _value=id)
|
||||
if not registerForm:
|
||||
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():
|
||||
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:
|
||||
return JsonResponse(studentForm_serializer.errors, safe=False, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
|
||||
@ -5,6 +5,7 @@
|
||||
"pending": "Pending Registrations",
|
||||
"subscribed": "Subscribed",
|
||||
"archived": "Archived",
|
||||
"photo": "Photo",
|
||||
"name": "Name",
|
||||
"class": "Class",
|
||||
"status": "Status",
|
||||
|
||||
@ -5,6 +5,7 @@
|
||||
"pending": "Inscriptions en attente",
|
||||
"subscribed": "Inscrits",
|
||||
"archived": "Archivés",
|
||||
"photo": "Photo",
|
||||
"name": "Nom",
|
||||
"class": "Classe",
|
||||
"status": "Statut",
|
||||
|
||||
@ -5,7 +5,7 @@ import InscriptionFormShared from '@/components/Inscription/InscriptionFormShare
|
||||
import { FE_ADMIN_SUBSCRIPTIONS_URL } from '@/utils/Url';
|
||||
import { useCsrfToken } from '@/context/CsrfContext';
|
||||
import { useEstablishment } from '@/context/EstablishmentContext';
|
||||
import { editRegisterForm } from '@/app/actions/subscriptionAction';
|
||||
import { editRegisterFormWithBinaryFile } from '@/app/actions/subscriptionAction';
|
||||
import logger from '@/utils/logger';
|
||||
import Loader from '@/components/Loader';
|
||||
|
||||
@ -21,7 +21,7 @@ export default function Page() {
|
||||
|
||||
const handleSubmit = (data) => {
|
||||
setIsLoading(true);
|
||||
editRegisterForm(studentId, data, csrfToken)
|
||||
editRegisterFormWithBinaryFile(studentId, data, csrfToken)
|
||||
.then((result) => {
|
||||
setIsLoading(false);
|
||||
logger.debug('Success:', result);
|
||||
|
||||
@ -60,6 +60,7 @@ import { fetchProfiles } from '@/app/actions/authAction';
|
||||
import {
|
||||
FE_ADMIN_SUBSCRIPTIONS_EDIT_URL,
|
||||
FE_ADMIN_SUBSCRIPTIONS_VALIDATE_URL,
|
||||
BASE_URL,
|
||||
} from '@/utils/Url';
|
||||
|
||||
import DjangoCSRFToken from '@/components/DjangoCSRFToken';
|
||||
@ -743,17 +744,6 @@ export default function Page({ params: { locale } }) {
|
||||
const getActionsByStatus = (row) => {
|
||||
const actions = {
|
||||
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: (
|
||||
<span title="Envoyer le dossier">
|
||||
@ -865,6 +855,33 @@ export default function Page({ params: { locale } }) {
|
||||
};
|
||||
|
||||
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('studentFistName'), transform: (row) => row.student.first_name },
|
||||
{
|
||||
|
||||
@ -42,7 +42,7 @@ export default function FileUpload({
|
||||
{/* Icône de cloud */}
|
||||
<input
|
||||
type="file"
|
||||
accept=".pdf"
|
||||
accept=".pdf, .png, .jpg, .jpeg, .gif, .bmp"
|
||||
onChange={handleFileChange}
|
||||
className="hidden"
|
||||
ref={fileInputRef} // Attachement de la référence
|
||||
|
||||
@ -43,6 +43,7 @@ export default function InscriptionFormShared({
|
||||
// États pour gérer les données du formulaire
|
||||
const [formData, setFormData] = useState({
|
||||
id: '',
|
||||
photo: null,
|
||||
last_name: '',
|
||||
first_name: '',
|
||||
address: '',
|
||||
@ -55,7 +56,6 @@ export default function InscriptionFormShared({
|
||||
registration_payment: '',
|
||||
tuition_payment: '',
|
||||
});
|
||||
|
||||
const [guardians, setGuardians] = 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
|
||||
const isSepaPayment =
|
||||
formData.registration_payment === '1' || formData.tuition_payment === '1';
|
||||
const data = {
|
||||
|
||||
// Préparer les données JSON
|
||||
const jsonData = {
|
||||
student: {
|
||||
...formData,
|
||||
guardians,
|
||||
guardians: guardians,
|
||||
},
|
||||
establishment: selectedEstablishmentId,
|
||||
status: isSepaPayment ? 8 : 3,
|
||||
@ -344,7 +346,19 @@ export default function InscriptionFormShared({
|
||||
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 = () => {
|
||||
|
||||
@ -4,6 +4,7 @@ import React, { useEffect } from 'react';
|
||||
import { useTranslations } from 'next-intl';
|
||||
import { Trash2, Plus, Users } from 'lucide-react';
|
||||
import SectionHeader from '@/components/SectionHeader';
|
||||
import { useEstablishment } from '@/context/EstablishmentContext';
|
||||
|
||||
export default function ResponsableInputFields({
|
||||
guardians,
|
||||
@ -12,6 +13,7 @@ export default function ResponsableInputFields({
|
||||
setIsPageValid,
|
||||
}) {
|
||||
const t = useTranslations('ResponsableInputFields');
|
||||
const { selectedEstablishmentId } = useEstablishment();
|
||||
|
||||
useEffect(() => {
|
||||
const isValid =
|
||||
@ -38,7 +40,7 @@ export default function ResponsableInputFields({
|
||||
(field === 'first_name' &&
|
||||
(!guardians[index].first_name ||
|
||||
guardians[index].first_name.trim() === '')) ||
|
||||
(field === 'email' &&
|
||||
(field === 'associated_profile_email' &&
|
||||
(!guardians[index].associated_profile_email ||
|
||||
guardians[index].associated_profile_email.trim() === '')) ||
|
||||
(field === 'birth_date' &&
|
||||
@ -56,16 +58,45 @@ export default function ResponsableInputFields({
|
||||
};
|
||||
|
||||
const onGuardiansChange = (id, field, value) => {
|
||||
const updatedGuardians = guardians.map((guardian) =>
|
||||
guardian.id === id ? { ...guardian, [field]: value } : guardian
|
||||
);
|
||||
const updatedGuardians = guardians.map((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);
|
||||
};
|
||||
|
||||
const addGuardian = () => {
|
||||
setGuardians([
|
||||
...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
|
||||
errorMsg={
|
||||
getError(index, 'email') || getLocalError(index, 'email')
|
||||
getError(index, 'associated_profile_email') ||
|
||||
getLocalError(index, 'associated_profile_email')
|
||||
}
|
||||
/>
|
||||
<InputPhone
|
||||
|
||||
@ -6,6 +6,7 @@ import { fetchRegisterForm } from '@/app/actions/subscriptionAction';
|
||||
import logger from '@/utils/logger';
|
||||
import SectionHeader from '@/components/SectionHeader';
|
||||
import { User } from 'lucide-react';
|
||||
import FileUpload from '@/components/FileUpload';
|
||||
|
||||
const levels = [
|
||||
{ value: '1', label: 'TPS - Très Petite Section' },
|
||||
@ -38,6 +39,7 @@ export default function StudentInfoForm({
|
||||
|
||||
setFormData({
|
||||
id: data?.student?.id || '',
|
||||
photo: data?.student?.photo || null,
|
||||
last_name: data?.student?.last_name || '',
|
||||
first_name: data?.student?.first_name || '',
|
||||
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
|
||||
if (isLoading) return <Loader />;
|
||||
|
||||
@ -208,54 +220,18 @@ export default function StudentInfoForm({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* <div className="bg-white p-6 rounded-lg shadow-sm border border-gray-200">
|
||||
<h2 className="text-xl font-bold mb-4 text-gray-800">Responsables</h2>
|
||||
<ResponsableInputFields
|
||||
guardians={guardians}
|
||||
onGuardiansChange={(id, field, value) => {
|
||||
const updatedGuardians = guardians.map((resp) =>
|
||||
resp.id === id ? { ...resp, [field]: value } : resp
|
||||
);
|
||||
setGuardians(updatedGuardians);
|
||||
}}
|
||||
addGuardian={(e) => {
|
||||
e.preventDefault();
|
||||
setGuardians([...guardians, { id: Date.now() }]);
|
||||
}}
|
||||
deleteGuardian={(index) => {
|
||||
const newArray = [...guardians];
|
||||
newArray.splice(index, 1);
|
||||
setGuardians(newArray);
|
||||
}}
|
||||
errors={errors?.student?.guardians || []}
|
||||
{/* Section pour l'upload des fichiers */}
|
||||
<div className="bg-white p-6 rounded-lg shadow-sm border border-gray-200 mt-6">
|
||||
<SectionHeader
|
||||
icon={User}
|
||||
title={`Photo de l'élève`}
|
||||
description={`Ajoutez une photo de votre enfant`}
|
||||
/>
|
||||
<FileUpload
|
||||
selectionMessage="Sélectionnez une photo à uploader"
|
||||
onFileSelect={(file) => handlePhotoUpload(file)}
|
||||
/>
|
||||
</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}
|
||||
/> */}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@ -10,6 +10,7 @@ import logger from '@/utils/logger';
|
||||
import MultiSelect from '@/components/MultiSelect'; // Import du composant MultiSelect
|
||||
import { useCsrfToken } from '@/context/CsrfContext';
|
||||
import { useEstablishment } from '@/context/EstablishmentContext';
|
||||
import Popup from '@/components/Popup';
|
||||
|
||||
export default function FileUploadDocuSeal({
|
||||
handleCreateTemplateMaster,
|
||||
@ -24,6 +25,9 @@ export default function FileUploadDocuSeal({
|
||||
const [selectedGroups, setSelectedGroups] = useState([]);
|
||||
const [guardianDetails, setGuardianDetails] = useState([]);
|
||||
|
||||
const [popupVisible, setPopupVisible] = useState(false);
|
||||
const [popupMessage, setPopupMessage] = useState('');
|
||||
|
||||
const csrfToken = useCsrfToken();
|
||||
|
||||
const { selectedEstablishmentId } = useEstablishment();
|
||||
@ -84,6 +88,17 @@ export default function FileUploadDocuSeal({
|
||||
};
|
||||
|
||||
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;
|
||||
if (fileToEdit) {
|
||||
logger.debug('Modification du template master:', templateMaster?.id);
|
||||
@ -139,6 +154,13 @@ export default function FileUploadDocuSeal({
|
||||
|
||||
return (
|
||||
<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 */}
|
||||
<div className="grid grid-cols-10 gap-6 items-start">
|
||||
{/* Sélection des groupes */}
|
||||
|
||||
Reference in New Issue
Block a user