feat: Pre cablage du dashboard [#]

This commit is contained in:
Luc SORIGNET
2025-02-22 17:06:11 +01:00
parent c7723eceee
commit 1911f79f45
8 changed files with 132 additions and 86 deletions

View File

@ -1,6 +1,7 @@
from rest_framework import serializers from rest_framework import serializers
from .models import Teacher, Speciality, SchoolClass, Planning, LEVEL_CHOICES, Discount, Fee, PaymentPlan, PaymentMode, Establishment from .models import Teacher, Speciality, SchoolClass, Planning, LEVEL_CHOICES, Discount, Fee, PaymentPlan, PaymentMode, Establishment
from Auth.models import Profile from Auth.models import Profile
from Subscriptions.models import Student
from N3wtSchool import settings, bdd from N3wtSchool import settings, bdd
from django.utils import timezone from django.utils import timezone
import pytz import pytz
@ -89,6 +90,7 @@ class SchoolClassSerializer(serializers.ModelSerializer):
teachers = serializers.PrimaryKeyRelatedField(queryset=Teacher.objects.all(), many=True, required=False) teachers = serializers.PrimaryKeyRelatedField(queryset=Teacher.objects.all(), many=True, required=False)
establishment = serializers.PrimaryKeyRelatedField(queryset=Establishment.objects.all(), required=False) establishment = serializers.PrimaryKeyRelatedField(queryset=Establishment.objects.all(), required=False)
teachers_details = serializers.SerializerMethodField() teachers_details = serializers.SerializerMethodField()
students = serializers.PrimaryKeyRelatedField(queryset=Student.objects.all(), many=True, required=False)
class Meta: class Meta:
model = SchoolClass model = SchoolClass

View File

@ -1,9 +1,10 @@
{ {
"dashboard": "Dashboard", "dashboard": "Dashboard",
"totalStudents": "Total Students", "totalStudents": "Total Students",
"averageInscriptionTime": "Average Registration Time", "pendingRegistrations": "Pending Registration",
"reInscriptionRate": "Re-enrollment Rate", "reInscriptionRate": "Re-enrollment Rate",
"structureCapacity": "Structure Capacity", "structureCapacity": "Structure Capacity",
"capacityRate": "Capacity Rate",
"inscriptionTrends": "Enrollment Trends", "inscriptionTrends": "Enrollment Trends",
"upcomingEvents": "Upcoming Events" "upcomingEvents": "Upcoming Events"
} }

View File

@ -1,9 +1,10 @@
{ {
"dashboard": "Tableau de bord", "dashboard": "Tableau de bord",
"totalStudents": "Total des étudiants", "totalStudents": "Total des étudiants",
"averageInscriptionTime": "Temps moyen d'inscription", "pendingRegistrations": "Inscriptions en attente",
"reInscriptionRate": "Taux de réinscription", "reInscriptionRate": "Taux de réinscription",
"structureCapacity": "Remplissage de la structure", "structureCapacity": "Capacité de la structure",
"capacityRate": "Remplissage de la structure",
"inscriptionTrends": "Tendances d'inscription", "inscriptionTrends": "Tendances d'inscription",
"upcomingEvents": "Événements à venir" "upcomingEvents": "Événements à venir"
} }

View File

@ -6,26 +6,10 @@ import { Users, Clock, CalendarCheck, School, TrendingUp, UserCheck } from 'luci
import Loader from '@/components/Loader'; import Loader from '@/components/Loader';
import ClasseDetails from '@/components/ClasseDetails'; import ClasseDetails from '@/components/ClasseDetails';
import { fetchClasses } from '@/app/actions/schoolAction'; import { fetchClasses } from '@/app/actions/schoolAction';
import StatCard from '@/components/StatCard';
import logger from '@/utils/logger';
import { fetchRegisterForms } from '@/app/actions/subscriptionAction';
// Composant StatCard pour afficher une statistique
const StatCard = ({ title, value, icon, change, color = "blue" }) => (
<div className="bg-white p-6 rounded-lg shadow-sm border border-gray-100">
<div className="flex justify-between items-start">
<div>
<h3 className="text-gray-500 text-sm font-medium">{title}</h3>
<p className="text-2xl font-semibold mt-1">{value}</p>
{change && (
<p className={`text-sm ${change > 0 ? 'text-green-500' : 'text-red-500'}`}>
{change > 0 ? '+' : ''}{change}% depuis le mois dernier
</p>
)}
</div>
<div className={`p-3 rounded-full bg-${color}-100`}>
{icon}
</div>
</div>
</div>
);
// Composant EventCard pour afficher les événements // Composant EventCard pour afficher les événements
const EventCard = ({ title, date, description, type }) => ( const EventCard = ({ title, date, description, type }) => (
@ -44,18 +28,16 @@ const EventCard = ({ title, date, description, type }) => (
export default function DashboardPage() { export default function DashboardPage() {
const t = useTranslations('dashboard'); const t = useTranslations('dashboard');
const [isLoading, setIsLoading] = useState(true); const [isLoading, setIsLoading] = useState(true);
const [stats, setStats] = useState({ const [totalStudents, setTotalStudents] = useState(0);
totalStudents: 0, const [pendingRegistration, setPendingRegistration] = useState(0);
averageInscriptionTime: 0, const [structureCapacity, setStructureCapacity] = useState(0);
reInscriptionRate: 0, const [upcomingEvents, setUpcomingEvents] = useState([]);
structureCapacity: 0, const [monthlyStats, setMonthlyStats] = useState({
upcomingEvents: [], inscriptions: [],
monthlyStats: { completionRate: 0
inscriptions: [],
completionRate: 0
}
}); });
const [classes, setClasses] = useState([]); const [classes, setClasses] = useState([]);
@ -63,40 +45,50 @@ export default function DashboardPage() {
// Fetch data for classes // Fetch data for classes
fetchClasses().then(data => { fetchClasses().then(data => {
setClasses(data); setClasses(data);
logger.info('Classes fetched:', data);
const nbMaxStudents = data.reduce((acc, classe) => acc + classe.number_of_students, 0);
const nbStudents = data.reduce((acc, classe) => acc + classe.students.length, 0);
setStructureCapacity(nbMaxStudents);
setTotalStudents(nbStudents);
}) })
.catch(error => { .catch(error => {
console.error('Error fetching classes:', error); logger.error('Error fetching classes:', error);
});
fetchRegisterForms().then(data => {
logger.info('Pending registrations fetched:', data);
setPendingRegistration(data.count);
})
.catch(error => {
logger.error('Error fetching pending registrations:', error);
}); });
// Simulation de chargement des données // Simulation de chargement des données
setTimeout(() => { setTimeout(() => {
setStats({ setUpcomingEvents([
totalStudents: 245, {
averageInscriptionTime: 3.5, title: "Réunion de rentrée",
reInscriptionRate: 85, date: "2024-09-01",
structureCapacity: 300, description: "Présentation de l'année scolaire",
upcomingEvents: [ type: "meeting"
{ },
title: "Réunion de rentrée", {
date: "2024-09-01", title: "Date limite inscriptions",
description: "Présentation de l'année scolaire", date: "2024-08-15",
type: "meeting" description: "Clôture des inscriptions",
}, type: "deadline"
{
title: "Date limite inscriptions",
date: "2024-08-15",
description: "Clôture des inscriptions",
type: "deadline"
}
],
monthlyStats: {
inscriptions: [150, 180, 210, 245],
completionRate: 78
} }
]);
setMonthlyStats({
inscriptions: [150, 180, 210, 245],
completionRate: 78
}); });
setIsLoading(false); setIsLoading(false);
}, 1000); }, 1000);
}, []); }
, []);
if (isLoading) return <Loader />; if (isLoading) return <Loader />;
@ -108,25 +100,24 @@ export default function DashboardPage() {
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4 mb-6"> <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4 mb-6">
<StatCard <StatCard
title={t('totalStudents')} title={t('totalStudents')}
value={stats.totalStudents} value={totalStudents}
icon={<Users className="text-blue-500" size={24} />} icon={<Users className="text-blue-500" size={24} />}
change={12}
/> />
<StatCard <StatCard
title={t('averageInscriptionTime')} title={t('pendingRegistrations')}
value={`${stats.averageInscriptionTime} jours`} value={`${pendingRegistration} `}
icon={<Clock className="text-green-500" size={24} />} icon={<Clock className="text-green-500" size={24} />}
color="green" color="green"
/> />
<StatCard
title={t('reInscriptionRate')}
value={`${stats.reInscriptionRate}%`}
icon={<UserCheck className="text-purple-500" size={24} />}
color="purple"
/>
<StatCard <StatCard
title={t('structureCapacity')} title={t('structureCapacity')}
value={`${(stats.totalStudents/stats.structureCapacity * 100).toFixed(1)}%`} value={`${structureCapacity}`}
icon={<School className="text-green-500" size={24} />}
color="emerald"
/>
<StatCard
title={t('capacityRate')}
value={`${(totalStudents/structureCapacity * 100).toFixed(1)}%`}
icon={<School className="text-orange-500" size={24} />} icon={<School className="text-orange-500" size={24} />}
color="orange" color="orange"
/> />
@ -146,7 +137,7 @@ export default function DashboardPage() {
{/* Événements à venir */} {/* Événements à venir */}
<div className="bg-white p-6 rounded-lg shadow-sm border border-gray-100"> <div className="bg-white p-6 rounded-lg shadow-sm border border-gray-100">
<h2 className="text-lg font-semibold mb-4">{t('upcomingEvents')}</h2> <h2 className="text-lg font-semibold mb-4">{t('upcomingEvents')}</h2>
{stats.upcomingEvents.map((event, index) => ( {upcomingEvents.map((event, index) => (
<EventCard key={index} {...event} /> <EventCard key={index} {...event} />
))} ))}
</div> </div>

View File

@ -58,11 +58,11 @@ const ClasseDetails = ({ classe }) => {
<div className="bg-white rounded-lg border border-gray-200 shadow-md"> <div className="bg-white rounded-lg border border-gray-200 shadow-md">
<Table <Table
columns={[ columns={[
{ name: 'NOM', transform: (row) => row.nom }, { name: 'NOM', transform: (row) => row.name },
{ name: 'PRENOM', transform: (row) => row.prenom }, { name: 'PRENOM', transform: (row) => row.first_name },
{ name: 'AGE', transform: (row) => `${row.age}` } { name: 'AGE', transform: (row) => `${row.age}` }
]} ]}
data={classe.eleves} data={classe.students}
/> />
</div> </div>
</div> </div>

View File

@ -4,21 +4,41 @@ import ReactDOM from 'react-dom';
const Popup = ({ visible, message, onConfirm, onCancel, uniqueConfirmButton = false }) => { const Popup = ({ visible, message, onConfirm, onCancel, uniqueConfirmButton = false }) => {
if (!visible) return null; if (!visible) return null;
// Diviser le message en lignes // Vérifier si le message est une chaîne de caractères
const messageLines = message.split('\n'); const isStringMessage = typeof message === 'string';
// Diviser le message en lignes seulement si c'est une chaîne
const messageLines = isStringMessage ? message.split('\n') : null;
return ReactDOM.createPortal( return ReactDOM.createPortal(
<div className="fixed inset-0 flex items-center justify-center bg-black bg-opacity-50"> <div className="fixed inset-0 flex items-center justify-center bg-black bg-opacity-50">
<div className="bg-white p-6 rounded-md shadow-md"> <div className="bg-white p-6 rounded-lg shadow-xl max-w-md w-full">
{messageLines.map((line, index) => ( <div className="mb-4">
<p key={index} className="mb-4">{line}</p> {isStringMessage ? (
))} // Afficher le message sous forme de lignes si c'est une chaîne
<div className={`flex ${uniqueConfirmButton ? 'justify-center' : 'justify-end'} gap-4`}> messageLines.map((line, index) => (
{!uniqueConfirmButton && ( <p key={index} className="text-gray-700">
<button className="px-4 py-2 bg-gray-200 rounded-md" onClick={onCancel}>Annuler</button> {line}
</p>
))
) : (
// Sinon, afficher directement le contenu React
message
)} )}
<button className="px-4 py-2 bg-emerald-500 text-white rounded-md" onClick={onConfirm}> </div>
{uniqueConfirmButton ? 'Compris !' : 'Confirmer'} <div className="flex justify-end space-x-2">
{!uniqueConfirmButton && (
<button
className="px-4 py-2 bg-red-500 text-white rounded hover:bg-red-600"
onClick={onCancel}
>
Annuler
</button>
)}
<button
className="px-4 py-2 bg-emerald-500 text-white rounded hover:bg-emerald-600"
onClick={onConfirm}
>
{uniqueConfirmButton ? 'Fermer' : 'Confirmer'}
</button> </button>
</div> </div>
</div> </div>

View File

@ -0,0 +1,16 @@
// Composant StatCard pour afficher une statistique
const StatCard = ({ title, value, icon, color = "blue" }) => (
<div className="bg-white p-6 rounded-lg shadow-sm border border-gray-100">
<div className="flex justify-between items-start">
<div>
<h3 className="text-gray-500 text-sm font-medium">{title}</h3>
<p className="text-2xl font-semibold mt-1">{value}</p>
</div>
<div className={`p-3 rounded-full bg-${color}-100`}>
{icon}
</div>
</div>
</div>
);
export default StatCard;

View File

@ -11,6 +11,7 @@ import { DndProvider, useDrop } from 'react-dnd';
import { HTML5Backend } from 'react-dnd-html5-backend'; import { HTML5Backend } from 'react-dnd-html5-backend';
import { ESTABLISHMENT_ID } from '@/utils/Url'; import { ESTABLISHMENT_ID } from '@/utils/Url';
import logger from '@/utils/logger'; import logger from '@/utils/logger';
import ClasseDetails from '@/components/ClasseDetails';
const ItemTypes = { const ItemTypes = {
TEACHER: 'teacher', TEACHER: 'teacher',
@ -100,6 +101,8 @@ const ClassesSection = ({ classes, setClasses, teachers, handleCreate, handleEdi
const [removePopupVisible, setRemovePopupVisible] = useState(false); const [removePopupVisible, setRemovePopupVisible] = useState(false);
const [removePopupMessage, setRemovePopupMessage] = useState(""); const [removePopupMessage, setRemovePopupMessage] = useState("");
const [removePopupOnConfirm, setRemovePopupOnConfirm] = useState(() => {}); const [removePopupOnConfirm, setRemovePopupOnConfirm] = useState(() => {});
const [detailsModalVisible, setDetailsModalVisible] = useState(false);
const [selectedClass, setSelectedClass] = useState(null);
const niveauxPremierCycle = [ const niveauxPremierCycle = [
{ id: 1, name: 'TPS', age: 2 }, { id: 1, name: 'TPS', age: 2 },
@ -252,6 +255,11 @@ const ClassesSection = ({ classes, setClasses, teachers, handleCreate, handleEdi
} }
}; };
const openEditModalDetails = (classe) => {
setSelectedClass(classe);
setDetailsModalVisible(true);
};
const renderClassCell = (classe, column) => { const renderClassCell = (classe, column) => {
const isEditing = editingClass === classe.id; const isEditing = editingClass === classe.id;
const isCreating = newClass && newClass.id === classe.id; const isCreating = newClass && newClass.id === classe.id;
@ -449,6 +457,13 @@ const ClassesSection = ({ classes, setClasses, teachers, handleCreate, handleEdi
columns={columns} columns={columns}
renderCell={renderClassCell} renderCell={renderClassCell}
/> />
<Popup
visible={detailsModalVisible}
message={selectedClass ? <ClasseDetails classe={selectedClass} /> : null}
onConfirm={() => setDetailsModalVisible(false)}
onCancel={() => setDetailsModalVisible(false)}
uniqueConfirmButton={true}
/>
<Popup <Popup
visible={popupVisible} visible={popupVisible}
message={popupMessage} message={popupMessage}