feat: ajout des documents d'inscription [#20]

This commit is contained in:
Luc SORIGNET
2025-01-25 12:19:30 +01:00
parent 799e1c6717
commit b8ef34a04b
12 changed files with 449 additions and 321 deletions

View File

@ -1,9 +1,9 @@
import React, { useState } from 'react';
const AffectationClasseForm = ({ eleve, onSubmit, classes }) => {
const AffectationClasseForm = ({ eleve = {}, onSubmit, classes }) => {
const [formData, setFormData] = useState({
classeAssocie_id: eleve.classeAssocie_id || null,
classeAssocie_id: eleve?.classeAssocie_id || null,
});
const handleChange = (e) => {

View File

@ -0,0 +1,33 @@
import React from 'react';
import { Check, Clock } from 'lucide-react';
const FileStatusLabel = ({ status }) => {
const getStatusConfig = () => {
switch (status) {
case 'sent':
return {
label: 'Envoyé',
className: 'bg-green-50 text-green-600',
icon: <Check size={16} className="text-green-600" />
};
case 'pending':
default:
return {
label: 'En attente',
className: 'bg-orange-50 text-orange-600',
icon: <Clock size={16} className="text-orange-600" />
};
}
};
const { label, className, icon } = getStatusConfig();
return (
<div className={`flex items-center justify-center gap-2 px-3 py-1 rounded-md text-sm font-medium ${className}`}>
{icon}
<span>{label}</span>
</div>
);
};
export default FileStatusLabel;

View File

@ -1,3 +1,4 @@
// Import des dépendances nécessaires
import React, { useState, useEffect } from 'react';
import InputText from '@/components/InputText';
import SelectChoice from '@/components/SelectChoice';
@ -6,12 +7,14 @@ import Loader from '@/components/Loader';
import Button from '@/components/Button';
import DjangoCSRFToken from '@/components/DjangoCSRFToken';
import Table from '@/components/Table';
import { fetchRegisterFormFileTemplate, createRegistrationFormFile } from '@/app/lib/subscriptionAction';
import { Download, Upload } from 'lucide-react';
import { fetchRegisterFormFileTemplate, createRegistrationFormFile, fetchRegisterForm, deleteRegisterFormFile } from '@/app/lib/subscriptionAction';
import { Download, Upload, Trash2, Eye } from 'lucide-react';
import { BASE_URL } from '@/utils/Url';
import DraggableFileUpload from '@/app/[locale]/admin/subscriptions/components/DraggableFileUpload';
import Modal from '@/components/Modal';
import FileStatusLabel from '@/components/FileStatusLabel';
// Définition des niveaux scolaires disponibles
const levels = [
{ value:'1', label: 'TPS - Très Petite Section'},
{ value:'2', label: 'PS - Petite Section'},
@ -19,32 +22,28 @@ const levels = [
{ value:'4', label: 'GS - Grande Section'},
];
/**
* Composant de formulaire d'inscription partagé
* @param {string} studentId - ID de l'étudiant
* @param {string} csrfToken - Token CSRF pour la sécurité
* @param {function} onSubmit - Fonction de soumission du formulaire
* @param {string} cancelUrl - URL de redirection en cas d'annulation
* @param {object} errors - Erreurs de validation du formulaire
*/
export default function InscriptionFormShared({
initialData,
studentId,
csrfToken,
onSubmit,
cancelUrl,
isLoading = false,
errors = {} // Nouvelle prop pour les erreurs
}) {
// États pour gérer les données du formulaire
const [isLoading, setIsLoading] = useState(true);
const [formData, setFormData] = useState({});
const [formData, setFormData] = useState(() => ({
id: initialData?.id || '',
last_name: initialData?.last_name || '',
first_name: initialData?.first_name || '',
address: initialData?.address || '',
birth_date: initialData?.birth_date || '',
birth_place: initialData?.birth_place || '',
birth_postal_code: initialData?.birth_postal_code || '',
nationality: initialData?.nationality || '',
attending_physician: initialData?.attending_physician || '',
level: initialData?.level || ''
}));
const [guardians, setGuardians] = useState(() =>
initialData?.guardians || []
);
const [guardians, setGuardians] = useState([]);
// États pour la gestion des fichiers
const [uploadedFiles, setUploadedFiles] = useState([]);
const [fileTemplates, setFileTemplates] = useState([]);
const [fileName, setFileName] = useState("");
@ -52,50 +51,104 @@ export default function InscriptionFormShared({
const [showUploadModal, setShowUploadModal] = useState(false);
const [currentTemplateId, setCurrentTemplateId] = useState(null);
// Chargement initial des données
// Mettre à jour les données quand initialData change
useEffect(() => {
if (initialData) {
setFormData({
id: initialData.id || '',
last_name: initialData.last_name || '',
first_name: initialData.first_name || '',
address: initialData.address || '',
birth_date: initialData.birth_date || '',
birth_place: initialData.birth_place || '',
birth_postal_code: initialData.birth_postal_code || '',
nationality: initialData.nationality || '',
attending_physician: initialData.attending_physician || '',
level: initialData.level || ''
if (studentId) {
fetchRegisterForm(studentId).then((data) => {
console.log(data);
setFormData({
id: data?.student?.id || '',
last_name: data?.student?.last_name || '',
first_name: data?.student?.first_name || '',
address: data?.student?.address || '',
birth_date: data?.student?.birth_date || '',
birth_place: data?.student?.birth_place || '',
birth_postal_code: data?.student?.birth_postal_code || '',
nationality: data?.student?.nationality || '',
attending_physician: data?.student?.attending_physician || '',
level: data?.student?.level || ''
});
setGuardians(data?.student?.guardians || []);
setUploadedFiles(data.registration_files || []);
});
setGuardians(initialData.guardians || []);
fetchRegisterFormFileTemplate().then((data) => {
setFileTemplates(data);
});
setIsLoading(false);
}
}, [initialData]);
}, [studentId]);
// Fonctions de gestion du formulaire et des fichiers
const updateFormField = (field, value) => {
setFormData(prev => ({...prev, [field]: value}));
};
// Gestion du téléversement de fichiers
const handleFileUpload = async (file, fileName) => {
if (!file || !currentTemplateId || !formData.id) {
console.error('Missing required data for upload');
return;
}
const data = new FormData();
data.append('file', file);
data.append('name',fileName);
data.append('name', fileName);
data.append('template', currentTemplateId);
data.append('register_form', formData.id);
try {
await createRegistrationFormFile(data, csrfToken);
// Optionnellement, rafraîchir la liste des fichiers
fetchRegisterFormFileTemplate().then((data) => {
setFileTemplates(data);
});
const response = await createRegistrationFormFile(data, csrfToken);
if (response) {
setUploadedFiles(prev => {
const newFiles = prev.filter(f => parseInt(f.template) !== currentTemplateId);
return [...newFiles, {
name: fileName,
template: currentTemplateId,
file: response.file
}];
});
// Rafraîchir les données du formulaire pour avoir les fichiers à jour
if (studentId) {
fetchRegisterForm(studentId).then((data) => {
setUploadedFiles(data.registration_files || []);
});
}
}
} catch (error) {
console.error('Error uploading file:', error);
}
};
// Vérification si un fichier est déjà uploadé
const isFileUploaded = (templateId) => {
return uploadedFiles.find(template =>
template.template === templateId
);
};
// Récupération d'un fichier uploadé
const getUploadedFile = (templateId) => {
return uploadedFiles.find(file => parseInt(file.template) === templateId);
};
// Suppression d'un fichier
const handleDeleteFile = async (templateId) => {
const fileToDelete = getUploadedFile(templateId);
if (!fileToDelete) return;
try {
await deleteRegisterFormFile(fileToDelete.id, csrfToken);
setUploadedFiles(prev => prev.filter(f => parseInt(f.template) !== templateId));
} catch (error) {
console.error('Error deleting file:', error);
}
};
// Soumission du formulaire
const handleSubmit = (e) => {
e.preventDefault();
const data ={
@ -107,36 +160,70 @@ export default function InscriptionFormShared({
onSubmit(data);
};
// Récupération des messages d'erreur
const getError = (field) => {
return errors?.student?.[field]?.[0];
};
const getGuardianError = (index, field) => {
return errors?.student?.guardians?.[index]?.[field]?.[0];
};
// Configuration des colonnes pour le tableau des fichiers
const columns = [
{ name: 'Nom du fichier', transform: (row) => row.name },
{ name: 'Fichier à Remplir', transform: (row) => row.is_required ? 'Oui' : 'Non' },
{ name: 'Fichier de référence', transform: (row) => row.file && <div className="flex items-center justify-center gap-2"> <a href={`${BASE_URL}${row.file}`} target='_blank' className="text-blue-500 hover:text-blue-700">
<Download size={16} />
</a> </div>},
{ name: 'Actions', transform: (row) => (
<div className="flex items-center justify-center gap-2">
{row.is_required &&
<button className="text-emerald-500 hover:text-emerald-700" type="button" onClick={() => {
setCurrentTemplateId(row.id);
setShowUploadModal(true);
}}>
{ name: 'Statut', transform: (row) =>
row.is_required && (
<FileStatusLabel
status={isFileUploaded(row.id) ? 'sent' : 'pending'}
/>
)
},
{ name: 'Actions', transform: (row) => {
if (!row.is_required) return null;
const uploadedFile = getUploadedFile(row.id);
if (uploadedFile) {
return (
<div className="flex items-center justify-center gap-2">
<a
href={`${BASE_URL}${uploadedFile.file}`}
target="_blank"
className="text-blue-500 hover:text-blue-700"
>
<Eye size={16} />
</a>
<button
className="text-red-500 hover:text-red-700"
onClick={() => handleDeleteFile(row.id)}
type="button"
>
<Trash2 size={16} />
</button>
</div>
);
}
return (
<button
className="text-emerald-500 hover:text-emerald-700"
type="button"
onClick={() => {
setCurrentTemplateId(row.id);
setShowUploadModal(true);
}}
>
<Upload size={16} />
</button>
}
</div>
) },
);
}},
];
// Affichage du loader pendant le chargement
if (isLoading) return <Loader />;
// Rendu du composant
return (
<div className="max-w-4xl mx-auto p-6">
<form onSubmit={handleSubmit} className="space-y-8">
@ -245,17 +332,19 @@ export default function InscriptionFormShared({
</div>
{/* Section Fichiers d'inscription */}
<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">Fichiers à remplir</h2>
<Table
data={fileTemplates}
columns={columns}
itemsPerPage={5}
currentPage={1}
totalPages={1}
onPageChange={() => {}}
/>
</div>
{fileTemplates.length > 0 && (
<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">Fichiers à remplir</h2>
<Table
data={fileTemplates}
columns={columns}
itemsPerPage={5}
currentPage={1}
totalPages={1}
onPageChange={() => {}}
/>
</div>
)}
{/* Boutons de contrôle */}
<div className="flex justify-end space-x-4">
@ -263,44 +352,52 @@ export default function InscriptionFormShared({
<Button type="submit" text="Valider" primary />
</div>
</form>
<Modal
isOpen={showUploadModal}
setIsOpen={setShowUploadModal}
title="Téléverser un fichier"
ContentComponent={() => (
<>
<DraggableFileUpload
className="w-full"
fileName={fileName}
onFileSelect={(selectedFile) => {
setFile(selectedFile);
setFileName(selectedFile.name);
}}
>
<input type="hidden" name="template" value={currentTemplateId} />
<input type="hidden" name="register_form" value={formData.id} />
</DraggableFileUpload>
<div className="mt-4 flex justify-center space-x-4">
<Button
text="Annuler"
onClick={() => {
setShowUploadModal(false);
setCurrentTemplateId(null);
{fileTemplates.length > 0 && (
<Modal
isOpen={showUploadModal}
setIsOpen={setShowUploadModal}
title="Téléverser un fichier"
ContentComponent={() => (
<>
<DraggableFileUpload
className="w-full"
fileName={fileName}
onFileSelect={(selectedFile) => {
if (selectedFile) {
setFile(selectedFile);
setFileName(selectedFile.name);
}
}}
/>
<Button
text="Valider"
onClick={() => {
setShowUploadModal(false);
handleFileUpload(file, fileName);
setCurrentTemplateId(null);
}}
primary={true}
/>
</div>
</>
)}
/>
<div className="mt-4 flex justify-center space-x-4">
<Button
text="Annuler"
onClick={() => {
setShowUploadModal(false);
setCurrentTemplateId(null);
setFile(null);
setFileName("");
}}
/>
<Button
text="Valider"
onClick={() => {
if (file && fileName) {
handleFileUpload(file, fileName);
setShowUploadModal(false);
setCurrentTemplateId(null);
setFile(null);
setFileName("");
}
}}
primary={true}
disabled={!file || !fileName}
/>
</div>
</>
)}
/>
)}
</div>
);
}

View File

@ -4,6 +4,7 @@ import Button from '@/components/Button';
import React from 'react';
import { useTranslations } from 'next-intl';
import 'react-phone-number-input/style.css'
import { Trash2, Plus } from 'lucide-react';
export default function ResponsableInputFields({guardians, onGuardiansChange, addGuardian, deleteGuardian, errors = []}) {
const t = useTranslations('ResponsableInputFields');
@ -19,10 +20,9 @@ export default function ResponsableInputFields({guardians, onGuardiansChange, ad
<div className='flex justify-between items-center mb-4'>
<h3 className='text-xl font-bold'>{t('responsable')} {index+1}</h3>
{guardians.length > 1 && (
<Button
text={t('delete')}
<Trash2
className="w-5 h-5 text-red-500 cursor-pointer hover:text-red-700 transition-colors"
onClick={() => deleteGuardian(index)}
className="w-32"
/>
)}
</div>
@ -102,13 +102,9 @@ export default function ResponsableInputFields({guardians, onGuardiansChange, ad
))}
<div className="flex justify-center">
<Button
text={t('add_responsible')}
<Plus
className="w-8 h-8 text-green-500 cursor-pointer hover:text-green-700 transition-colors border-2 border-green-500 hover:border-green-700 rounded-full p-1"
onClick={(e) => addGuardian(e)}
primary
icon={<i className="icon profile-add" />}
type="button"
className="w-64"
/>
</div>
</div>