feat: Ajout de la configuration des tarifs de l'école [#18]

This commit is contained in:
N3WT DE COMPET
2025-01-19 21:00:58 +01:00
committed by Luc SORIGNET
parent 147a70135d
commit 5a0e65bb75
45 changed files with 2089 additions and 376 deletions

View File

@ -1,28 +1,22 @@
'use client'
import React, { useState, useEffect } from 'react';
import { School, Calendar } from 'lucide-react';
import TabsStructure from '@/components/Structure/Configuration/TabsStructure';
import ScheduleManagement from '@/components/Structure/Planning/ScheduleManagement'
import StructureManagement from '@/components/Structure/Configuration/StructureManagement'
import { BE_SCHOOL_SPECIALITIES_URL,
BE_SCHOOL_SCHOOLCLASSES_URL,
BE_SCHOOL_TEACHERS_URL,
BE_SCHOOL_PLANNINGS_URL } from '@/utils/Url';
import DjangoCSRFToken from '@/components/DjangoCSRFToken'
import { School, Calendar, DollarSign } from 'lucide-react'; // Import de l'icône DollarSign
import StructureManagement from '@/components/Structure/Configuration/StructureManagement';
import ScheduleManagement from '@/components/Structure/Planning/ScheduleManagement';
import FeesManagement from '@/components/Structure/Configuration/FeesManagement';
import DjangoCSRFToken from '@/components/DjangoCSRFToken';
import useCsrfToken from '@/hooks/useCsrfToken';
import { ClassesProvider } from '@/context/ClassesContext';
import { fetchSpecialities, fetchTeachers, fetchClasses, fetchSchedules } from '@/app/lib/schoolAction';
import { fetchSpecialities, fetchTeachers, fetchClasses, fetchSchedules, fetchDiscounts, fetchFees, fetchTuitionFees } from '@/app/lib/schoolAction';
import SidebarTabs from '@/components/SidebarTabs';
export default function Page() {
const [specialities, setSpecialities] = useState([]);
const [classes, setClasses] = useState([]);
const [teachers, setTeachers] = useState([]);
const [schedules, setSchedules] = useState([]);
const [activeTab, setActiveTab] = useState('Configuration');
const tabs = [
{ id: 'Configuration', title: "Configuration de l'école", icon: School },
{ id: 'Schedule', title: "Gestion de l'emploi du temps", icon: Calendar },
];
const [fees, setFees] = useState([]);
const [discounts, setDiscounts] = useState([]);
const [tuitionFees, setTuitionFees] = useState([]);
const csrfToken = useCsrfToken();
@ -38,6 +32,15 @@ export default function Page() {
// Fetch data for schedules
handleSchedules();
// Fetch data for fees
handleFees();
// Fetch data for discounts
handleDiscounts();
// Fetch data for TuitionFee
handleTuitionFees();
}, []);
const handleSpecialities = () => {
@ -45,9 +48,7 @@ export default function Page() {
.then(data => {
setSpecialities(data);
})
.catch(error => {
console.error('Error fetching specialities:', error);
});
.catch(error => console.error('Error fetching specialities:', error));
};
const handleTeachers = () => {
@ -55,9 +56,7 @@ export default function Page() {
.then(data => {
setTeachers(data);
})
.catch(error => {
console.error('Error fetching teachers:', error);
});
.catch(error => console.error('Error fetching teachers:', error));
};
const handleClasses = () => {
@ -65,9 +64,7 @@ export default function Page() {
.then(data => {
setClasses(data);
})
.catch(error => {
console.error('Error fetching classes:', error);
});
.catch(error => console.error('Error fetching classes:', error));
};
const handleSchedules = () => {
@ -75,13 +72,35 @@ export default function Page() {
.then(data => {
setSchedules(data);
})
.catch(error => {
console.error('Error fetching classes:', error);
});
.catch(error => console.error('Error fetching schedules:', error));
};
const handleCreate = (url, newData, setDatas) => {
fetch(url, {
const handleFees = () => {
fetchFees()
.then(data => {
setFees(data);
})
.catch(error => console.error('Error fetching fees:', error));
};
const handleDiscounts = () => {
fetchDiscounts()
.then(data => {
setDiscounts(data);
})
.catch(error => console.error('Error fetching discounts:', error));
};
const handleTuitionFees = () => {
fetchTuitionFees()
.then(data => {
setTuitionFees(data);
})
.catch(error => console.error('Error fetching tuition fees', error));
};
const handleCreate = (url, newData, setDatas, setErrors) => {
return fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
@ -90,18 +109,28 @@ export default function Page() {
body: JSON.stringify(newData),
credentials: 'include'
})
.then(response => response.json())
.then(response => {
if (!response.ok) {
return response.json().then(errorData => {
throw errorData;
});
}
return response.json();
})
.then(data => {
console.log('Succes :', data);
setDatas(prevState => [...prevState, data]);
setErrors({});
return data;
})
.catch(error => {
console.error('Erreur :', error);
setErrors(error);
console.error('Error creating data:', error);
throw error;
});
};
const handleEdit = (url, id, updatedData, setDatas) => {
fetch(`${url}/${id}`, {
const handleEdit = (url, id, updatedData, setDatas, setErrors) => {
return fetch(`${url}/${id}`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
@ -110,15 +139,41 @@ export default function Page() {
body: JSON.stringify(updatedData),
credentials: 'include'
})
.then(response => response.json())
.then(response => {
if (!response.ok) {
return response.json().then(errorData => {
throw errorData;
});
}
return response.json();
})
.then(data => {
setDatas(prevState => prevState.map(item => item.id === id ? data : item));
setErrors({});
return data;
})
.catch(error => {
console.error('Erreur :', error);
setErrors(error);
console.error('Error editing data:', error);
throw error;
});
};
const handleDelete = (url, id, setDatas) => {
fetch(`${url}/${id}`, {
method: 'DELETE',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': csrfToken
},
credentials: 'include'
})
.then(response => response.json())
.then(data => {
setDatas(prevState => prevState.filter(item => item.id !== id));
})
.catch(error => console.error('Error deleting data:', error));
};
const handleUpdatePlanning = (url, planningId, updatedData) => {
fetch(`${url}/${planningId}`, {
method: 'PUT',
@ -139,35 +194,11 @@ export default function Page() {
});
};
const handleDelete = (url, id, setDatas) => {
fetch(`${url}/${id}`, {
method:'DELETE',
headers: {
'Content-Type':'application/json',
'X-CSRFToken': csrfToken
},
credentials: 'include'
})
.then(response => response.json())
.then(data => {
console.log('Success:', data);
setDatas(prevState => prevState.filter(item => item.id !== id));
})
.catch(error => {
console.error('Error fetching data:', error);
error = error.errorMessage;
console.log(error);
});
};
return (
<div className='p-8'>
<DjangoCSRFToken csrfToken={csrfToken} />
<TabsStructure activeTab={activeTab} setActiveTab={setActiveTab} tabs={tabs} />
{activeTab === 'Configuration' && (
<>
const tabs = [
{
id: 'Configuration',
label: "Configuration de l'école",
content: (
<StructureManagement
specialities={specialities}
setSpecialities={setSpecialities}
@ -177,18 +208,49 @@ export default function Page() {
setClasses={setClasses}
handleCreate={handleCreate}
handleEdit={handleEdit}
handleDelete={handleDelete} />
</>
)}
{activeTab === 'Schedule' && (
handleDelete={handleDelete}
/>
)
},
{
id: 'Schedule',
label: "Gestion de l'emploi du temps",
content: (
<ClassesProvider>
<ScheduleManagement
handleUpdatePlanning={handleUpdatePlanning}
classes={classes}
/>
</ClassesProvider>
)}
)
},
{
id: 'Fees',
label: 'Tarifications',
content: (
<FeesManagement
fees={fees}
setFees={setFees}
discounts={discounts}
setDiscounts={setDiscounts}
tuitionFees={tuitionFees}
setTuitionFees={setTuitionFees}
handleCreate={handleCreate}
handleEdit={handleEdit}
handleDelete={handleDelete}
/>
)
}
];
return (
<div className='p-8'>
<DjangoCSRFToken csrfToken={csrfToken} />
<div className="w-full p-4">
<SidebarTabs tabs={tabs} />
</div>
</div>
);
};
}

View File

@ -0,0 +1,50 @@
import React, { useState } from 'react';
import { Upload } from 'lucide-react';
export default function DraggableFileUpload({ fileName, onFileSelect }) {
const [dragActive, setDragActive] = useState(false);
const handleDragOver = (event) => {
event.preventDefault();
setDragActive(true);
};
const handleDragLeave = () => {
setDragActive(false);
};
const handleFileChosen = (selectedFile) => {
onFileSelect && onFileSelect(selectedFile);
};
const handleDrop = (event) => {
event.preventDefault();
setDragActive(false);
const droppedFile = event.dataTransfer.files[0];
handleFileChosen(droppedFile);
};
const handleFileChange = (event) => {
const selectedFile = event.target.files[0];
handleFileChosen(selectedFile);
};
return (
<div>
<div
onDragOver={handleDragOver}
onDragLeave={handleDragLeave}
onDrop={handleDrop}
className={`border-2 border-dashed p-8 rounded-md ${dragActive ? 'border-blue-500' : 'border-gray-300'} flex flex-col items-center justify-center`}
style={{ height: '200px' }}
>
<input type="file" onChange={handleFileChange} className="hidden" id="fileInput" />
<label htmlFor="fileInput" className="cursor-pointer flex flex-col items-center">
<Upload size={48} className="text-gray-400 mb-2" />
<p className="text-center">{fileName || 'Glissez et déposez un fichier ici ou cliquez ici pour sélectionner un fichier'}</p>
</label>
</div>
</div>
);
}

View File

@ -1,61 +1,47 @@
import React, { useState } from 'react';
import { Upload } from 'lucide-react';
import React, { useState, useEffect } from 'react';
import ToggleSwitch from '@/components/ToggleSwitch'; // Import du composant ToggleSwitch
import DraggableFileUpload from './DraggableFileUpload';
export default function FileUpload({ onFileUpload }) {
const [dragActive, setDragActive] = useState(false);
export default function FileUpload({ onFileUpload, fileToEdit = null }) {
const [fileName, setFileName] = useState('');
const [file, setFile] = useState(null);
const [isRequired, setIsRequired] = useState(false); // État pour le toggle isRequired
const [order, setOrder] = useState(0);
const handleDragOver = (event) => {
event.preventDefault();
setDragActive(true);
};
const handleDragLeave = () => {
setDragActive(false);
};
const handleDrop = (event) => {
event.preventDefault();
setDragActive(false);
const droppedFile = event.dataTransfer.files[0];
setFile(droppedFile);
setFileName(droppedFile.name.replace(/\.[^/.]+$/, ""));
};
const handleFileChange = (event) => {
const selectedFile = event.target.files[0];
setFile(selectedFile);
setFileName(selectedFile.name.replace(/\.[^/.]+$/, ""));
};
useEffect(() => {
if (fileToEdit) {
setFileName(fileToEdit.name || '');
setIsRequired(fileToEdit.is_required || false);
setOrder(fileToEdit.fusion_order || 0);
}
}, [fileToEdit]);
const handleFileNameChange = (event) => {
setFileName(event.target.value);
};
const handleUpload = () => {
onFileUpload(file, fileName);
setFile(null);
setFileName('');
onFileUpload({
file,
name: fileName,
is_required: isRequired,
order: parseInt(order, 10),
});
setFile(null);
setFileName('');
setIsRequired(false);
setOrder(0);
};
return (
<div>
<div
onDragOver={handleDragOver}
onDragLeave={handleDragLeave}
onDrop={handleDrop}
className={`border-2 border-dashed p-8 rounded-md ${dragActive ? 'border-blue-500' : 'border-gray-300'} flex flex-col items-center justify-center`}
style={{ height: '200px' }}
>
<input type="file" onChange={handleFileChange} className="hidden" id="fileInput" />
<label htmlFor="fileInput" className="cursor-pointer flex flex-col items-center">
<Upload size={48} className="text-gray-400 mb-2" />
<p className="text-center">{fileName || 'Glissez et déposez un fichier ici ou cliquez ici pour sélectionner un fichier'}</p>
</label>
</div>
<DraggableFileUpload
fileName={fileName}
onFileSelect={(selectedFile) => {
setFile(selectedFile);
setFileName(selectedFile.name.replace(/\.[^/.]+$/, ""));
}}
/>
<div className="flex mt-2">
<input
type="text"
@ -64,14 +50,28 @@ export default function FileUpload({ onFileUpload }) {
onChange={handleFileNameChange}
className="flex-grow p-2 border border-gray-200 rounded-md"
/>
<input
type="number"
value={order}
onChange={(e) => setOrder(e.target.value)}
placeholder="Ordre de fusion"
className="p-2 border border-gray-200 rounded-md ml-2 w-20"
/>
<button
onClick={handleUpload}
className={`p-2 rounded-md shadow transition duration-200 ml-2 ${fileName!="" ? 'bg-emerald-600 text-white hover:bg-emerald-900' : 'bg-gray-300 text-gray-500 cursor-not-allowed'}`}
disabled={fileName==""}
className={`p-2 rounded-md shadow transition duration-200 ml-2 ${fileName !== "" ? 'bg-emerald-600 text-white hover:bg-emerald-900' : 'bg-gray-300 text-gray-500 cursor-not-allowed'}`}
disabled={fileName === ""}
>
Ajouter
</button>
</div>
<div className="flex items-center mt-4">
<ToggleSwitch
label="Fichier à remplir obligatoirement"
checked={isRequired}
onChange={() => setIsRequired(!isRequired)}
/>
</div>
</div>
);
}

View File

@ -17,6 +17,7 @@ export default function Page() {
const [initialData, setInitialData] = useState(null);
const [isLoading, setIsLoading] = useState(true);
const [formErrors, setFormErrors] = useState({});
const csrfToken = useCsrfToken();
useEffect(() => {
@ -55,9 +56,8 @@ export default function Page() {
console.error('Error:', error.message);
if (error.details) {
console.error('Form errors:', error.details);
// Handle form errors (e.g., display them to the user)
setFormErrors(error.details);
}
alert('Une erreur est survenue lors de la mise à jour des données');
});
};
@ -69,6 +69,7 @@ export default function Page() {
onSubmit={handleSubmit}
cancelUrl={FE_ADMIN_SUBSCRIPTIONS_URL}
isLoading={isLoading}
errors={formErrors}
/>
);
}

View File

@ -28,6 +28,7 @@ import {
fetchRegisterFormFileTemplate,
deleteRegisterFormFileTemplate,
createRegistrationFormFileTemplate,
editRegistrationFormFileTemplate,
fetchStudents,
editRegisterForm } from "@/app/lib/subscriptionAction"
@ -40,6 +41,7 @@ import {
import DjangoCSRFToken from '@/components/DjangoCSRFToken'
import useCsrfToken from '@/hooks/useCsrfToken';
import { formatDate } from '@/utils/Date';
const useFakeData = process.env.NEXT_PUBLIC_USE_FAKE_DATA === 'true';
@ -69,6 +71,9 @@ export default function Page({ params: { locale } }) {
const [classes, setClasses] = useState([]);
const [students, setEleves] = useState([]);
const [reloadFetch, setReloadFetch] = useState(false);
const [isModalOpen, setIsModalOpen] = useState(false);
const [isEditing, setIsEditing] = useState(false);
const [fileToEdit, setFileToEdit] = useState(null);
const csrfToken = useCsrfToken();
@ -185,7 +190,11 @@ const registerFormArchivedDataHandler = (data) => {
.then(registerFormArchivedDataHandler)
.catch(requestErrorHandler)
fetchRegisterFormFileTemplate()
.then((data)=> {setFichiers(data)})
.then((data)=> {
console.log(data);
setFichiers(data)
})
.catch((err)=>{ err = err.message; console.log(err);});
} else {
setTimeout(() => {
@ -548,9 +557,17 @@ const handleFileDelete = (fileId) => {
});
};
const handleFileEdit = (file) => {
setIsEditing(true);
setFileToEdit(file);
setIsModalOpen(true);
};
const columnsFiles = [
{ name: 'Nom du fichier', transform: (row) => row.name },
{ name: 'Date de création', transform: (row) => row.last_update },
{ name: 'Date de création', transform: (row) => formatDate(new Date (row.date_added),"DD/MM/YYYY hh:mm:ss") },
{ name: 'Fichier Obligatoire', transform: (row) => row.is_required ? 'Oui' : 'Non' },
{ name: 'Ordre de fusion', transform: (row) => row.order },
{ name: 'Actions', transform: (row) => (
<div className="flex items-center justify-center gap-2">
{
@ -559,6 +576,9 @@ const columnsFiles = [
<Download size={16} />
</a>)
}
<button onClick={() => handleFileEdit(row)} className="text-blue-500 hover:text-blue-700">
<Edit size={16} />
</button>
<button onClick={() => handleFileDelete(row.id)} className="text-red-500 hover:text-red-700">
<Trash2 size={16} />
</button>
@ -566,27 +586,43 @@ const columnsFiles = [
) },
];
const handleFileUpload = (file, fileName) => {
if ( !fileName) {
const handleFileUpload = ({file, name, is_required, order}) => {
if (!name) {
alert('Veuillez entrer un nom de fichier.');
return;
}
const formData = new FormData();
if(file){
formData.append('file', file);
}
formData.append('name', fileName);
createRegistrationFormFileTemplate(formData,csrfToken)
.then(data => {
console.log('Success:', data);
setFichiers([...fichiers, data]);
closeUploadModal();
})
.catch(error => {
console.error('Error uploading file:', error);
});
formData.append('name', name);
formData.append('is_required', is_required);
formData.append('order', order);
if (isEditing && fileToEdit) {
editRegistrationFormFileTemplate(fileToEdit.id, formData, csrfToken)
.then(data => {
setFichiers(prevFichiers =>
prevFichiers.map(f => f.id === fileToEdit.id ? data : f)
);
setIsModalOpen(false);
setFileToEdit(null);
setIsEditing(false);
})
.catch(error => {
console.error('Error editing file:', error);
});
} else {
createRegistrationFormFileTemplate(formData, csrfToken)
.then(data => {
setFichiers([...fichiers, data]);
setIsModalOpen(false);
})
.catch(error => {
console.error('Error uploading file:', error);
});
}
};
if (isLoading) {
@ -699,7 +735,23 @@ const handleFileUpload = (file, fileName) => {
{/*SI STATE == subscribeFiles */}
{activeTab === 'subscribeFiles' && (
<div>
<FileUpload onFileUpload={handleFileUpload} className="mb-4" />
<button
onClick={() => { setIsModalOpen(true); setIsEditing(false); }}
className="flex items-center bg-emerald-600 text-white p-2 rounded-full shadow hover:bg-emerald-900 transition duration-200 ml-4"
>
<Plus className="w-5 h-5" />
</button>
<Modal
isOpen={isModalOpen}
setIsOpen={setIsModalOpen}
title={isEditing ? 'Modifier un fichier' : 'Ajouter un fichier'}
ContentComponent={() => (
<FileUpload
onFileUpload={handleFileUpload}
fileToEdit={fileToEdit}
/>
)}
/>
<div className="mt-8">
<Table
data={fichiers}