feat: Bilan de compétence d'un élève [#16]

This commit is contained in:
N3WT DE COMPET
2025-05-18 17:10:49 +02:00
parent e65e31014d
commit 5760c89105
14 changed files with 646 additions and 124 deletions

View File

@ -325,7 +325,7 @@ class RegistrationSchoolFileTemplate(models.Model):
class StudentCompetency(models.Model): class StudentCompetency(models.Model):
student = models.ForeignKey('Subscriptions.Student', on_delete=models.CASCADE, related_name='competency_scores') student = models.ForeignKey('Subscriptions.Student', on_delete=models.CASCADE, related_name='competency_scores')
competency = models.ForeignKey('Common.Competency', on_delete=models.CASCADE, related_name='student_scores') competency = models.ForeignKey('Common.Competency', on_delete=models.CASCADE, related_name='student_scores')
score = models.DecimalField(max_digits=5, decimal_places=2, null=True, blank=True) score = models.IntegerField(null=True, blank=True)
comment = models.TextField(blank=True, null=True) comment = models.TextField(blank=True, null=True)
class Meta: class Meta:

View File

@ -63,12 +63,36 @@ class StudentCompetencyListCreateView(APIView):
"data": result "data": result
}, safe=False, status=200) }, safe=False, status=200)
# def post(self, request): def put(self, request):
# serializer = AbsenceManagementSerializer(data=request.data) """
# if serializer.is_valid(): Met à jour en masse les notes des compétences d'un élève.
# serializer.save() Attend une liste d'objets {"competenceId": ..., "grade": ...}
# return JsonResponse(serializer.data, safe=False, status=status.HTTP_201_CREATED) """
# return JsonResponse(serializer.errors, safe=False, status=status.HTTP_400_BAD_REQUEST) data = request.data
if not isinstance(data, list):
return JsonResponse({"error": "Une liste est attendue."}, status=400)
updated = []
errors = []
for item in data:
comp_id = item.get("competenceId")
grade = item.get("grade")
student_id = item.get('studentId')
print(f'lecture des données : {comp_id} - {grade} - {student_id}')
if comp_id is None or grade is None:
errors.append({"competenceId": comp_id, "error": "champ manquant"})
continue
try:
# Ajoute le filtre student_id
sc = StudentCompetency.objects.get(
competency_id=comp_id,
student_id=student_id
)
sc.score = grade
sc.save()
updated.append(comp_id)
except StudentCompetency.DoesNotExist:
errors.append({"competenceId": comp_id, "error": "not found"})
return JsonResponse({"updated": updated, "errors": errors}, status=200)
@method_decorator(csrf_protect, name='dispatch') @method_decorator(csrf_protect, name='dispatch')
@method_decorator(ensure_csrf_cookie, name='dispatch') @method_decorator(ensure_csrf_cookie, name='dispatch')

View File

@ -1,144 +1,158 @@
'use client'; 'use client';
import React, { useState } from 'react'; import React, { useState, useEffect } from 'react';
import SelectChoice from '@/components/SelectChoice'; import SelectChoice from '@/components/SelectChoice';
import AcademicResults from '@/components/Grades/AcademicResults';
import Attendance from '@/components/Grades/Attendance';
import Remarks from '@/components/Grades/Remarks';
import WorkPlan from '@/components/Grades/WorkPlan';
import Homeworks from '@/components/Grades/Homeworks';
import SpecificEvaluations from '@/components/Grades/SpecificEvaluations';
import Orientation from '@/components/Grades/Orientation';
import Button from '@/components/Button'; import Button from '@/components/Button';
import Table from '@/components/Table'; import { FE_ADMIN_GRADES_STUDENT_COMPETENCIES_URL } from '@/utils/Url';
import logger from '@/utils/logger'; import { useRouter } from 'next/navigation';
import { fetchStudents } from '@/app/actions/subscriptionAction';
import { useEstablishment } from '@/context/EstablishmentContext';
import { useClasses } from '@/context/ClassesContext';
export default function Page() { export default function Page() {
const router = useRouter();
const { selectedEstablishmentId } = useEstablishment();
const { getNiveauLabel } = useClasses();
const [formData, setFormData] = useState({ const [formData, setFormData] = useState({
selectedStudent: null, selectedStudent: null,
absences: [],
competenceReview: [
{ competence: 'Lecture', score: null },
{ competence: 'Écriture', score: null },
{ competence: 'Mathématiques', score: null },
{ competence: 'Sciences', score: null },
],
}); });
const students = [ const [students, setStudents] = useState([]);
{ id: 1, name: 'John Doe', class: 'CM2' },
{ id: 2, name: 'Jane Smith', class: 'CE1' }, const academicResults = [
{ id: 3, name: 'Alice Johnson', class: 'CM1' }, {
subject: 'Mathématiques',
grade: 16,
average: 14,
appreciation: 'Très bon travail',
},
{
subject: 'Français',
grade: 15,
average: 13,
appreciation: 'Bonne participation',
},
]; ];
const absences = [ const absences = [
{ date: '2023-09-01', reason: 'Maladie' }, { date: '2023-09-01', type: 'Absence', reason: 'Maladie', justified: true },
{ date: '2023-09-15', reason: 'Vacances' }, { date: '2023-09-15', type: 'Retard', reason: 'Trafic', justified: false },
{ date: '2023-10-05', reason: 'Retard justifié' },
]; ];
const handleChange = (field, value) => { const remarks = [
setFormData((prev) => ({ ...prev, [field]: value })); {
}; date: '2023-09-10',
teacher: 'Mme Dupont',
comment: 'Participation active en classe.',
},
{
date: '2023-09-20',
teacher: 'M. Martin',
comment: 'Doit améliorer la concentration.',
},
];
const handleScoreChange = (index, score) => { const workPlan = [
const updatedCompetenceReview = [...formData.competenceReview]; {
updatedCompetenceReview[index].score = score; objective: 'Renforcer la lecture',
setFormData((prev) => ({ support: 'Exercices hebdomadaires',
...prev, followUp: 'En cours',
competenceReview: updatedCompetenceReview, },
})); {
}; objective: 'Maîtriser les tables de multiplication',
support: 'Jeux éducatifs',
followUp: 'À démarrer',
},
];
const homeworks = [
{ title: 'Rédaction', dueDate: '2023-10-10', status: 'Rendu' },
{ title: 'Exercices de maths', dueDate: '2023-10-12', status: 'À faire' },
];
const specificEvaluations = [
{
test: 'Bilan de compétences',
date: '2023-09-25',
result: 'Bon niveau général',
},
];
const orientation = [
{
date: '2023-10-01',
counselor: 'Mme Leroy',
advice: 'Poursuivre en filière générale',
},
];
const handleChange = (field, value) =>
setFormData((prev) => ({ ...prev, [field]: value }));
useEffect(() => {
if (selectedEstablishmentId) {
fetchStudents(selectedEstablishmentId)
.then((studentsData) => {
setStudents(studentsData);
})
.catch((error) => logger.error('Error fetching students:', error));
}
}, [selectedEstablishmentId]);
return ( return (
<div className="p-8 space-y-8"> <div className="p-8 space-y-8">
<h1 className="heading-section">Suivi pédagogique</h1>
{/* Sélection de l'élève */} {/* Sélection de l'élève */}
<div className="bg-white p-6 rounded-lg shadow-sm border border-gray-200"> <div className="bg-white p-6 rounded-lg shadow-sm border border-gray-200">
<h2 className="text-xl font-semibold mb-4">Sélectionner un élève</h2> <h2 className="text-xl font-semibold mb-4">Sélectionner un élève</h2>
<div className="flex flex-col sm:flex-row sm:items-end gap-4">
<div className="flex-1">
<SelectChoice <SelectChoice
name="selectedStudent" name="selectedStudent"
label="Élève" label="Élève"
placeHolder="Sélectionnez un élève" placeHolder="Sélectionnez un élève"
selected={formData.selectedStudent} selected={formData.selectedStudent || ''}
callback={(e) => handleChange('selectedStudent', e.target.value)} callback={(e) => handleChange('selectedStudent', e.target.value)}
choices={students.map((student) => ({ choices={students.map((student) => ({
value: student.id, value: student.id,
label: `${student.name} - Classe : ${student.class}`, label: `${student.last_name} ${student.first_name} - ${getNiveauLabel(student.level)} (${student.associated_class_name})`,
}))} }))}
required required
/> />
</div> </div>
{/* Liste des absences */}
{formData.selectedStudent && (
<div className="bg-white p-6 rounded-lg shadow-sm border border-gray-200">
<h2 className="text-xl font-semibold mb-4">Liste des absences</h2>
<ul className="space-y-2">
{absences.map((absence, index) => (
<li
key={index}
className="flex justify-between items-center bg-gray-50 p-4 rounded-md border border-gray-100"
>
<span className="text-gray-800">{absence.date}</span>
<span className="text-gray-500 italic">{absence.reason}</span>
</li>
))}
</ul>
</div>
)}
{/* Bilan de compétence */}
{formData.selectedStudent && (
<div className="bg-white p-6 rounded-lg shadow-sm border border-gray-200">
<h2 className="text-xl font-semibold mb-4">Bilan de compétence</h2>
<Table
data={formData.competenceReview}
columns={[
{
name: 'Compétence',
transform: (row) => row.competence,
},
{
name: '1',
transform: (row, index) => (
<input
type="radio"
name={`score-${index}`}
value="1"
checked={row.score === '1'}
onChange={() => handleScoreChange(index, '1')}
/>
),
},
{
name: '2',
transform: (row, index) => (
<input
type="radio"
name={`score-${index}`}
value="2"
checked={row.score === '2'}
onChange={() => handleScoreChange(index, '2')}
/>
),
},
{
name: '3',
transform: (row, index) => (
<input
type="radio"
name={`score-${index}`}
value="3"
checked={row.score === '3'}
onChange={() => handleScoreChange(index, '3')}
/>
),
},
]}
/>
<div className="mt-4">
<Button <Button
text="Enregistrer" text="Réaliser le bilan de compétences"
onClick={() => logger.debug('FormData:', formData)}
primary primary
className="bg-emerald-500 text-white hover:bg-emerald-600" disabled={!formData.selectedStudent}
onClick={() => {
const url = `${FE_ADMIN_GRADES_STUDENT_COMPETENCIES_URL}?studentId=${formData.selectedStudent}`;
router.push(`${url}`);
}}
className={`px-6 py-2 rounded-md shadow ${
!formData.selectedStudent
? 'bg-gray-300 text-gray-500 cursor-not-allowed'
: 'bg-emerald-500 text-white hover:bg-emerald-600'
}`}
/> />
</div> </div>
</div> </div>
{formData.selectedStudent && (
<>
<AcademicResults results={academicResults} />
<Attendance absences={absences} />
<Remarks remarks={remarks} />
<WorkPlan workPlan={workPlan} />
<Homeworks homeworks={homeworks} />
<SpecificEvaluations specificEvaluations={specificEvaluations} />
<Orientation orientation={orientation} />
</>
)} )}
</div> </div>
); );

View File

@ -0,0 +1,114 @@
'use client';
import React, { useState, useEffect } from 'react';
import { useSearchParams, useRouter } from 'next/navigation';
import Button from '@/components/Button';
import GradeView from '@/components/Grades/GradeView';
import {
fetchStudentCompetencies,
editStudentCompetencies,
} from '@/app/actions/subscriptionAction';
import SectionHeader from '@/components/SectionHeader';
import { Award } from 'lucide-react';
import { useCsrfToken } from '@/context/CsrfContext';
// À remplacer par un fetch réel des compétences selon l'élève
const mockCompetencies = [
{ id: 1, name: 'Lire un texte court', score: null },
{ id: 2, name: 'Résoudre un problème simple', score: null },
{ id: 3, name: 'Exprimer une idée à loral', score: null },
];
export default function StudentCompetenciesPage() {
const searchParams = useSearchParams();
const router = useRouter();
const csrfToken = useCsrfToken();
const [studentCompetencies, setStudentCompetencies] = useState([]);
const [grades, setGrades] = useState({});
const studentId = searchParams.get('studentId');
useEffect(() => {
fetchStudentCompetencies(studentId)
.then((data) => {
setStudentCompetencies(data);
})
.catch((error) =>
logger.error('Error fetching studentCompetencies:', error)
);
}, []);
useEffect(() => {
if (studentCompetencies.data) {
const initialGrades = {};
studentCompetencies.data.forEach((domaine) => {
domaine.categories.forEach((cat) => {
cat.competences.forEach((comp) => {
initialGrades[comp.competence_id] = comp.score ?? 0;
});
});
});
setGrades(initialGrades);
}
}, [studentCompetencies.data]);
const handleScoreChange = (competencyId, score) => {
setCompetencies((prev) =>
prev.map((comp) => (comp.id === competencyId ? { ...comp, score } : comp))
);
};
const handleGradeChange = (competenceId, level) => {
setGrades((prev) => ({
...prev,
[competenceId]: level,
}));
};
const handleSubmit = () => {
const data = Object.entries(grades).map(([competenceId, score]) => ({
studentId,
competenceId,
grade: score,
}));
editStudentCompetencies(data, csrfToken)
.then(() => {
alert('Bilan de compétence enregistré !');
router.back();
})
.catch((error) => {
alert("Erreur lors de l'enregistrement du bilan");
});
};
return (
<div className="h-full flex flex-col p-4">
<SectionHeader
icon={Award}
title="Bilan de compétence"
description="Evaluez les compétence de l'élève"
/>
<div className="flex-1 min-h-0 flex flex-col">
<form
className="flex-1 min-h-0 flex flex-col"
onSubmit={(e) => {
e.preventDefault();
handleSubmit();
}}
>
{/* Zone scrollable pour les compétences */}
<div className="flex-1 min-h-0 overflow-y-auto">
<GradeView
data={studentCompetencies.data}
grades={grades}
onGradeChange={handleGradeChange}
/>
</div>
<div className="mt-6 flex justify-end">
<Button text="Enregistrer le bilan" primary type="submit" />
</div>
</form>
</div>
</div>
);
}

View File

@ -4,11 +4,38 @@ import {
BE_SUBSCRIPTION_REGISTERFORMS_URL, BE_SUBSCRIPTION_REGISTERFORMS_URL,
BE_SUBSCRIPTION_LAST_GUARDIAN_ID_URL, BE_SUBSCRIPTION_LAST_GUARDIAN_ID_URL,
BE_SUBSCRIPTION_ABSENCES_URL, BE_SUBSCRIPTION_ABSENCES_URL,
BE_SUBSCRIPTION_STUDENT_COMPETENCIES_URL,
} from '@/utils/Url'; } from '@/utils/Url';
import { CURRENT_YEAR_FILTER } from '@/utils/constants'; import { CURRENT_YEAR_FILTER } from '@/utils/constants';
import { errorHandler, requestResponseHandler } from './actionsHandlers'; import { errorHandler, requestResponseHandler } from './actionsHandlers';
export const editStudentCompetencies = (data, csrfToken) => {
const request = new Request(`${BE_SUBSCRIPTION_STUDENT_COMPETENCIES_URL}`, {
method: 'PUT',
body: JSON.stringify(data),
headers: {
'X-CSRFToken': csrfToken,
'Content-Type': 'application/json',
},
credentials: 'include',
});
return fetch(request).then(requestResponseHandler).catch(errorHandler);
};
export const fetchStudentCompetencies = (id) => {
const request = new Request(
`${BE_SUBSCRIPTION_STUDENT_COMPETENCIES_URL}?student_id=${id}`,
{
method: 'GET',
headers: {
'Content-Type': 'application/json',
},
}
);
return fetch(request).then(requestResponseHandler).catch(errorHandler);
};
export const fetchRegisterForms = ( export const fetchRegisterForms = (
establishment, establishment,
filter = CURRENT_YEAR_FILTER, filter = CURRENT_YEAR_FILTER,

View File

@ -0,0 +1,28 @@
import React from 'react';
export default function AcademicResults({ results }) {
return (
<div className="bg-white p-6 rounded-lg shadow-sm border border-gray-200">
<h2 className="text-xl font-semibold mb-4">Résultats académiques</h2>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
{results.map((result, idx) => (
<div
key={idx}
className="p-4 rounded-lg bg-emerald-50 flex flex-col gap-2 shadow"
>
<div className="flex justify-between items-center">
<span className="font-medium">{result.subject}</span>
<span className="text-emerald-700 font-bold text-lg">
{result.grade}/20
</span>
</div>
<div className="text-sm text-gray-600">
Moyenne classe : {result.average}
</div>
<div className="italic text-gray-500">{result.appreciation}</div>
</div>
))}
</div>
</div>
);
}

View File

@ -0,0 +1,28 @@
import React from 'react';
export default function Attendance({ absences }) {
return (
<div className="bg-white p-6 rounded-lg shadow-sm border border-gray-200">
<h2 className="text-xl font-semibold mb-4">Présence et assiduité</h2>
<ol className="relative border-l border-emerald-200">
{absences.map((absence, idx) => (
<li key={idx} className="mb-6 ml-4">
<div className="absolute w-3 h-3 bg-emerald-400 rounded-full mt-1.5 -left-1.5 border border-white" />
<time className="mb-1 text-xs font-normal leading-none text-gray-400">
{absence.date}
</time>
<div className="flex items-center gap-2">
<span className="font-medium">{absence.type}</span>
<span
className={`text-xs px-2 py-1 rounded ${absence.justified ? 'bg-emerald-100 text-emerald-700' : 'bg-red-100 text-red-700'}`}
>
{absence.justified ? 'Justifiée' : 'Non justifiée'}
</span>
</div>
<div className="text-sm text-gray-500">{absence.reason}</div>
</li>
))}
</ol>
</div>
);
}

View File

@ -0,0 +1,155 @@
import React, { useState, useMemo, useEffect } from 'react';
import { BookOpen, CheckCircle, AlertCircle, Clock } from 'lucide-react';
import RadioList from '@/components/RadioList';
const LEVELS = [
{ value: 0, label: 'Non évalué' },
{ value: 1, label: '1 - Non acquis' },
{ value: 2, label: "2 - En cours d'acquisition" },
{ value: 3, label: '3 - Acquis' },
];
const getGradeStyle = (grade) => {
switch (grade) {
case 1:
return 'bg-red-50 border-red-200';
case 2:
return 'bg-yellow-50 border-yellow-200';
case 3:
return 'bg-emerald-50 border-emerald-200';
default:
return 'bg-gray-50 border-gray-200';
}
};
export default function GradeView({ data, grades, onGradeChange }) {
const [openDomains, setOpenDomains] = useState({});
const [openCategories, setOpenCategories] = useState({});
// Initialiser tout ouvert au premier rendu ou quand data change
useEffect(() => {
if (data && data.length > 0) {
// Initialisation des domaines et catégories (collapsed)
const domains = {};
const categories = {};
data.forEach((domaine) => {
domains[domaine.domaine_id] = false;
domaine.categories.forEach((cat) => {
categories[cat.categorie_id] = false;
});
});
setOpenDomains(domains);
setOpenCategories(categories);
}
}, [data]);
// Calcul du nombre total de compétences
const totalCompetencies = useMemo(
() =>
(data || []).reduce(
(sum, domaine) =>
sum +
domaine.categories.reduce(
(catSum, cat) => catSum + cat.competences.length,
0
),
0
),
[data]
);
if (!data) return null;
const toggleDomain = (id) =>
setOpenDomains((prev) => ({ ...prev, [id]: !prev[id] }));
const toggleCategory = (id) =>
setOpenCategories((prev) => ({ ...prev, [id]: !prev[id] }));
return (
<div className="w-full">
<div className="mb-4 mr-4 text-right text-emerald-700 font-semibold">
{totalCompetencies} compétence{totalCompetencies > 1 ? 's' : ''} au
total
</div>
{data.map((domaine) => (
<div key={domaine.domaine_id} className="mb-8">
<div
className={'flex items-center justify-between cursor-pointer px-6 py-4 rounded-lg transition bg-emerald-50 border border-emerald-200 shadow-sm hover:bg-emerald-100'}
onClick={() => toggleDomain(domaine.domaine_id)}
>
<div className="flex items-center gap-3">
<BookOpen className="w-7 h-7 text-emerald-600" />
<span className="text-2xl font-bold text-emerald-800">
{domaine.domaine_nom}
</span>
</div>
<span className="text-emerald-700 text-xl">
{openDomains[domaine.domaine_id] ? '▼' : '►'}
</span>
</div>
{openDomains[domaine.domaine_id] && (
<div className="mt-4">
{domaine.categories.map((categorie) => (
<div key={categorie.categorie_id} className="mb-10 mr-4">
<button
type="button"
className="flex items-center gap-2 text-lg font-semibold text-emerald-700 mb-4 hover:underline"
onClick={() => toggleCategory(categorie.categorie_id)}
>
{openCategories[categorie.categorie_id] ? '▼' : '►'}{' '}
{categorie.categorie_nom}
</button>
{openCategories[categorie.categorie_id] && (
<div className="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-4 gap-8 w-full">
{categorie.competences.map((competence) => {
const grade = grades[competence.competence_id];n (
<div
key={competence.competence_id}
className={`border rounded-xl p-6 flex flex-col shadow transition hover:shadow-md ${getGradeStyle(grade)}`}
>
<div className="mb-4 pb-4 border-b border-emerald-800 flex items-center min-h-[48px]">
<span className="text-gray-900 font-semibold text-base">
{competence.nom}
</span>
</div>
<div className="flex-1 flex items-center">
<RadioList
key={`grade-${competence.competence_id}-${grades[competence.competence_id] ?? 0}`}
items={LEVELS.map(({ value, label }) => ({
id: value,
label,
}))}
formData={{
[`grade-${competence.competence_id}`]:
grades[competence.competence_id] !==
undefined
? grades[competence.competence_id]
: 0,
}}
handleChange={(e) =>
onGradeChange(
competence.competence_id,
parseInt(e.target.value, 10)
)
}
fieldName={`grade-${competence.competence_id}`}
disabled={competence.state === 'required'}
className="mt-2"
/>
</div>
</div>
);
})}
</div>
)}
</div>
))}
</div>
)}
<hr className="my-6 border-emerald-100" />
</div>
))}
</div>
);
}

View File

@ -0,0 +1,27 @@
import React from 'react';
export default function Homeworks({ homeworks }) {
return (
<div className="bg-white p-6 rounded-lg shadow-sm border border-gray-200">
<h2 className="text-xl font-semibold mb-4">Suivi des devoirs</h2>
<ul className="divide-y divide-gray-100">
{homeworks.map((hw, idx) => (
<li
key={idx}
className="py-3 flex flex-col sm:flex-row sm:items-center sm:justify-between"
>
<div>
<span className="font-medium">{hw.title}</span>
<span className="ml-2 text-xs text-gray-400">{hw.dueDate}</span>
</div>
<span
className={`text-xs px-2 py-1 rounded ${hw.status === 'Rendu' ? 'bg-emerald-100 text-emerald-700' : 'bg-yellow-100 text-yellow-700'}`}
>
{hw.status}
</span>
</li>
))}
</ul>
</div>
);
}

View File

@ -0,0 +1,23 @@
import React from 'react';
export default function Orientation({ orientation }) {
return (
<div className="bg-white p-6 rounded-lg shadow-sm border border-gray-200">
<h2 className="text-xl font-semibold mb-4">Orientation & conseils</h2>
<ul className="divide-y divide-gray-100">
{orientation.map((item, idx) => (
<li
key={idx}
className="py-3 flex flex-col sm:flex-row sm:items-center sm:justify-between"
>
<div>
<span className="font-medium">{item.counselor}</span>
<span className="ml-2 text-xs text-gray-400">{item.date}</span>
</div>
<div className="text-gray-700 mt-1 sm:mt-0">{item.advice}</div>
</li>
))}
</ul>
</div>
);
}

View File

@ -0,0 +1,23 @@
import React from 'react';
export default function Remarks({ remarks }) {
return (
<div className="bg-white p-6 rounded-lg shadow-sm border border-gray-200">
<h2 className="text-xl font-semibold mb-4">Remarques & observations</h2>
<ul className="divide-y divide-gray-100">
{remarks.map((remark, idx) => (
<li
key={idx}
className="py-3 flex flex-col sm:flex-row sm:items-center sm:justify-between"
>
<div>
<span className="font-medium">{remark.teacher}</span>
<span className="ml-2 text-xs text-gray-400">{remark.date}</span>
</div>
<div className="text-gray-700 mt-1 sm:mt-0">{remark.comment}</div>
</li>
))}
</ul>
</div>
);
}

View File

@ -0,0 +1,27 @@
import React from 'react';
export default function SpecificEvaluations({ specificEvaluations }) {
return (
<div className="bg-white p-6 rounded-lg shadow-sm border border-gray-200">
<h2 className="text-xl font-semibold mb-4">Évaluations spécifiques</h2>
<div className="flex flex-col gap-3">
{specificEvaluations.map((evalItem, idx) => (
<div
key={idx}
className="p-3 rounded border border-blue-100 bg-blue-50 flex flex-col sm:flex-row sm:items-center sm:justify-between"
>
<div>
<span className="font-medium">{evalItem.test}</span>
<span className="ml-2 text-xs text-gray-500">
{evalItem.date}
</span>
</div>
<span className="text-xs px-2 py-1 rounded bg-blue-200 text-blue-800 mt-1 sm:mt-0">
{evalItem.result}
</span>
</div>
))}
</div>
</div>
);
}

View File

@ -0,0 +1,29 @@
import React from 'react';
export default function WorkPlan({ workPlan }) {
return (
<div className="bg-white p-6 rounded-lg shadow-sm border border-gray-200">
<h2 className="text-xl font-semibold mb-4">
Plan de travail personnalisé
</h2>
<div className="flex flex-col gap-3">
{workPlan.map((plan, idx) => (
<div
key={idx}
className="p-3 rounded border border-emerald-100 bg-emerald-50 flex flex-col sm:flex-row sm:items-center sm:justify-between"
>
<div>
<span className="font-medium">{plan.objective}</span>
<span className="ml-2 text-xs text-gray-500">
({plan.support})
</span>
</div>
<span className="text-xs px-2 py-1 rounded bg-emerald-200 text-emerald-800 mt-1 sm:mt-0">
{plan.followUp}
</span>
</div>
))}
</div>
</div>
);
}

View File

@ -32,6 +32,7 @@ export const BE_SUBSCRIPTION_REGISTRATION_PARENT_FILE_MASTERS_URL = `${BASE_URL}
export const BE_SUBSCRIPTION_REGISTRATION_PARENT_FILE_TEMPLATES_URL = `${BASE_URL}/Subscriptions/registrationParentFileTemplates`; export const BE_SUBSCRIPTION_REGISTRATION_PARENT_FILE_TEMPLATES_URL = `${BASE_URL}/Subscriptions/registrationParentFileTemplates`;
export const BE_SUBSCRIPTION_LAST_GUARDIAN_ID_URL = `${BASE_URL}/Subscriptions/lastGuardianId`; export const BE_SUBSCRIPTION_LAST_GUARDIAN_ID_URL = `${BASE_URL}/Subscriptions/lastGuardianId`;
export const BE_SUBSCRIPTION_ABSENCES_URL = `${BASE_URL}/Subscriptions/absences`; export const BE_SUBSCRIPTION_ABSENCES_URL = `${BASE_URL}/Subscriptions/absences`;
export const BE_SUBSCRIPTION_STUDENT_COMPETENCIES_URL = `${BASE_URL}/Subscriptions/studentCompetencies`;
//GESTION ECOLE //GESTION ECOLE
export const BE_SCHOOL_SPECIALITIES_URL = `${BASE_URL}/School/specialities`; export const BE_SCHOOL_SPECIALITIES_URL = `${BASE_URL}/School/specialities`;
@ -96,6 +97,8 @@ export const FE_ADMIN_DIRECTORY_URL = '/admin/directory';
//ADMIN/GRADES URL //ADMIN/GRADES URL
export const FE_ADMIN_GRADES_URL = '/admin/grades'; export const FE_ADMIN_GRADES_URL = '/admin/grades';
export const FE_ADMIN_GRADES_STUDENT_COMPETENCIES_URL =
'/admin/grades/studentCompetencies';
//ADMIN/TEACHERS URL //ADMIN/TEACHERS URL
export const FE_ADMIN_TEACHERS_URL = '/admin/teachers'; export const FE_ADMIN_TEACHERS_URL = '/admin/teachers';