feat: Affectation des enseignants aux classes et matières
Permet aux administrateurs d'associer un enseignant à une classe pour une matière donnée au sein d'une année scolaire. Cette brique est nécessaire pour construire les emplois du temps et les carnets de notes par la suite. Le modèle impose l'unicité du triplet enseignant × classe × matière par année scolaire, avec réactivation automatique d'une affectation retirée plutôt que duplication. L'isolation multi-tenant est garantie au niveau du repository (findById/get filtrent par tenant_id).
This commit is contained in:
@@ -17,6 +17,28 @@
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
interface TeacherAssignment {
|
||||
id: string;
|
||||
teacherId: string;
|
||||
classId: string;
|
||||
subjectId: string;
|
||||
status: string;
|
||||
startDate: string;
|
||||
}
|
||||
|
||||
interface Subject {
|
||||
id: string;
|
||||
name: string;
|
||||
code: string;
|
||||
color: string | null;
|
||||
}
|
||||
|
||||
interface User {
|
||||
id: string;
|
||||
firstName: string;
|
||||
lastName: string;
|
||||
}
|
||||
|
||||
// State
|
||||
let schoolClass = $state<SchoolClass | null>(null);
|
||||
let isLoading = $state(true);
|
||||
@@ -24,6 +46,13 @@
|
||||
let error = $state<string | null>(null);
|
||||
let successMessage = $state<string | null>(null);
|
||||
|
||||
// Teacher assignments state
|
||||
let classTeachers = $state<TeacherAssignment[]>([]);
|
||||
let allSubjects = $state<Subject[]>([]);
|
||||
let allTeachers = $state<User[]>([]);
|
||||
let isLoadingTeachers = $state(false);
|
||||
let teachersError = $state<string | null>(null);
|
||||
|
||||
// Form state (bound to schoolClass)
|
||||
let formName = $state('');
|
||||
let formLevel = $state<string | null>(null);
|
||||
@@ -37,10 +66,11 @@
|
||||
|
||||
const classId = $derived(page.params.id);
|
||||
|
||||
// Load class on mount
|
||||
// Load class and teachers on mount
|
||||
$effect(() => {
|
||||
if (classId) {
|
||||
loadClass(classId);
|
||||
loadClassTeachers(classId);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -127,7 +157,7 @@
|
||||
originalDescription = updatedClass.description;
|
||||
|
||||
// Clear success message after 3 seconds
|
||||
window.setTimeout(() => {
|
||||
globalThis.setTimeout(() => {
|
||||
successMessage = null;
|
||||
}, 3000);
|
||||
} catch (e) {
|
||||
@@ -137,6 +167,54 @@
|
||||
}
|
||||
}
|
||||
|
||||
async function loadClassTeachers(id: string) {
|
||||
try {
|
||||
isLoadingTeachers = true;
|
||||
teachersError = null;
|
||||
const apiUrl = getApiBaseUrl();
|
||||
|
||||
const [teachersRes, subjectsRes, usersRes] = await Promise.all([
|
||||
authenticatedFetch(`${apiUrl}/classes/${id}/teachers`),
|
||||
authenticatedFetch(`${apiUrl}/subjects`),
|
||||
authenticatedFetch(`${apiUrl}/users?role=ROLE_PROF`)
|
||||
]);
|
||||
|
||||
if (!teachersRes.ok) throw new Error('Erreur lors du chargement des enseignants');
|
||||
|
||||
const teachersData = await teachersRes.json();
|
||||
classTeachers = (teachersData['hydra:member'] ?? teachersData['member'] ?? (Array.isArray(teachersData) ? teachersData : []))
|
||||
.filter((a: TeacherAssignment) => a.status === 'active');
|
||||
|
||||
if (subjectsRes.ok) {
|
||||
const subjectsData = await subjectsRes.json();
|
||||
allSubjects = subjectsData['hydra:member'] ?? subjectsData['member'] ?? (Array.isArray(subjectsData) ? subjectsData : []);
|
||||
}
|
||||
if (usersRes.ok) {
|
||||
const usersData = await usersRes.json();
|
||||
allTeachers = usersData['hydra:member'] ?? usersData['member'] ?? (Array.isArray(usersData) ? usersData : []);
|
||||
}
|
||||
} catch (e) {
|
||||
teachersError = e instanceof Error ? e.message : 'Erreur inconnue';
|
||||
} finally {
|
||||
isLoadingTeachers = false;
|
||||
}
|
||||
}
|
||||
|
||||
function getTeacherName(teacherId: string): string {
|
||||
const teacher = allTeachers.find((t) => t.id === teacherId);
|
||||
return teacher ? `${teacher.firstName} ${teacher.lastName}` : teacherId;
|
||||
}
|
||||
|
||||
function getSubjectName(subjectId: string): string {
|
||||
const subject = allSubjects.find((s) => s.id === subjectId);
|
||||
return subject ? subject.name : subjectId;
|
||||
}
|
||||
|
||||
function getSubjectColor(subjectId: string): string | null {
|
||||
const subject = allSubjects.find((s) => s.id === subjectId);
|
||||
return subject?.color ?? null;
|
||||
}
|
||||
|
||||
function goBack() {
|
||||
goto('/admin/classes');
|
||||
}
|
||||
@@ -251,13 +329,53 @@
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- Teachers Section (AC5) -->
|
||||
<section class="teachers-section">
|
||||
<div class="section-header">
|
||||
<h2>Enseignants affectés</h2>
|
||||
<a href="/admin/assignments" class="btn-link">Gérer les affectations</a>
|
||||
</div>
|
||||
|
||||
{#if teachersError}
|
||||
<div class="alert alert-error">
|
||||
{teachersError}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if isLoadingTeachers}
|
||||
<p class="section-loading">Chargement des enseignants...</p>
|
||||
{:else if classTeachers.length === 0}
|
||||
<p class="section-empty">Aucun enseignant affecté à cette classe.</p>
|
||||
{:else}
|
||||
<ul class="teacher-list">
|
||||
{#each classTeachers as assignment (assignment.id)}
|
||||
<li class="teacher-item">
|
||||
<span class="teacher-name">{getTeacherName(assignment.teacherId)}</span>
|
||||
{#if getSubjectColor(assignment.subjectId)}
|
||||
<span
|
||||
class="subject-tag"
|
||||
style="background-color: {getSubjectColor(assignment.subjectId)}; color: white"
|
||||
>
|
||||
{getSubjectName(assignment.subjectId)}
|
||||
</span>
|
||||
{:else}
|
||||
<span class="subject-tag subject-tag-default">
|
||||
{getSubjectName(assignment.subjectId)}
|
||||
</span>
|
||||
{/if}
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
{/if}
|
||||
</section>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.edit-page {
|
||||
padding: 1.5rem;
|
||||
max-width: 600px;
|
||||
max-width: 700px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
@@ -484,4 +602,81 @@
|
||||
.btn-secondary:hover:not(:disabled) {
|
||||
background: #f3f4f6;
|
||||
}
|
||||
|
||||
/* Teachers Section */
|
||||
.teachers-section {
|
||||
margin-top: 1.5rem;
|
||||
background: white;
|
||||
border-radius: 0.75rem;
|
||||
border: 1px solid #e5e7eb;
|
||||
padding: 1.5rem;
|
||||
}
|
||||
|
||||
.section-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.section-header h2 {
|
||||
margin: 0;
|
||||
font-size: 1.125rem;
|
||||
font-weight: 600;
|
||||
color: #1f2937;
|
||||
}
|
||||
|
||||
.btn-link {
|
||||
font-size: 0.875rem;
|
||||
color: #3b82f6;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.btn-link:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.section-loading,
|
||||
.section-empty {
|
||||
color: #6b7280;
|
||||
font-size: 0.875rem;
|
||||
text-align: center;
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.teacher-list {
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.teacher-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
padding: 0.75rem 1rem;
|
||||
background: #f9fafb;
|
||||
border-radius: 0.5rem;
|
||||
}
|
||||
|
||||
.teacher-name {
|
||||
font-weight: 500;
|
||||
color: #1f2937;
|
||||
}
|
||||
|
||||
.subject-tag {
|
||||
display: inline-block;
|
||||
padding: 0.125rem 0.5rem;
|
||||
border-radius: 9999px;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.subject-tag-default {
|
||||
background: #f3f4f6;
|
||||
color: #374151;
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user