- {slot.isBreak
- ? (slot.label && slot.label !== slot.key ? slot.label : 'LUNCH')
+ {slot.isBreak
+ ? (slot.label && slot.label !== slot.key ? slot.label : 'LUNCH')
: `Period ${slot.periodNumber}`}
@@ -426,7 +426,7 @@ function BatchTimetableView({ timetable, batches, sections, teachers, workingDay
)
}
-
+
// Highlight illegal entries in break slots
const isIllegalBreakAssignment = isBreakSlot && entry;
@@ -750,6 +750,14 @@ export function TimetablePage() {
}
setReadiness(nextReadiness)
+
+ // Sync top-level states so the grid and legends can render immediately
+ setSections(sectionData)
+ setBatches(sectionResult.batches)
+ setTeachers(allTeachers)
+ setRooms(deptRooms)
+ setCourses(deptCourses)
+
return nextReadiness
} catch (err) {
const fallback = {
diff --git a/frontend/src/pages/WorkloadPage.tsx b/frontend/src/pages/WorkloadPage.tsx
index c4538c2..facc181 100644
--- a/frontend/src/pages/WorkloadPage.tsx
+++ b/frontend/src/pages/WorkloadPage.tsx
@@ -15,8 +15,10 @@
*/
import React, { useEffect, useState, useMemo, useCallback } from 'react'
-import { Check, ClipboardList, Search, User, UserPlus, X, GraduationCap as BatchIcon, Filter, ArrowLeft, Upload, Zap, RefreshCw } from 'lucide-react'
-import { sectionService, workloadService, departmentService, teacherService } from '@/services/resources.service'
+import { Check, ClipboardList, Search, User, UserPlus, X, GraduationCap as BatchIcon, Filter, ArrowLeft, Upload,RefreshCw, BarChart3, LayoutGrid, Plus, BookOpen } from 'lucide-react'
+import { DataTable } from '@/components/common/DataTable'
+import { sectionService, workloadService, departmentService, teacherService, batchService } from '@/services/resources.service'
+import { useTable } from '@/hooks/useTable'
import { useToast } from '@/context/useToast'
import { PageLoader } from '@/components/ui/Loading'
import { BulkImportModal } from '@/components/common/BulkImportModal'
@@ -41,40 +43,62 @@ interface SectionWorkloadResponse {
section_id: number;
section_name: string;
batch_name: string;
+ current_semester?: number;
courses: WorkloadItem[];
}
export function WorkloadPage() {
const { toast } = useToast()
-
+
// Data states
const [sections, setSections] = useState
([])
const [departments, setDepartments] = useState([])
const [allTeachers, setAllTeachers] = useState([])
-
+
// UI states
const [selectedDeptId, setSelectedDeptId] = useState('all')
const [selectedSectionId, setSelectedSectionId] = useState(null)
const [workloadData, setWorkloadData] = useState(null)
const [viewMode, setViewMode] = useState<'selection' | 'mapping'>('selection')
-
+
const [loading, setLoading] = useState(true)
const [loadingWorkload, setLoadingWorkload] = useState(false)
const [searchQuery, setSearchQuery] = useState('')
const [importModalOpen, setImportModalOpen] = useState(false)
+ const [activeTab, setActiveTab] = useState<'sections' | 'summary'>('sections')
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ const [summaryData, setSummaryData] = useState([])
+ const [loadingSummary, setLoadingSummary] = useState(false)
+ const [quickAddOpen, setQuickAddOpen] = useState(false)
+
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ const summaryTable = useTable({ data: summaryData as any, searchFields: ['teacher_name', 'teacher_email'] as any, defaultSortKey: 'teacher_name' })
+
+ // Manual Add State
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ const [allBatches, setAllBatches] = useState([])
+ const [selectedBatchId, setSelectedBatchId] = useState('')
+ const [selectedSectionIdManual, setSelectedSectionIdManual] = useState('')
+ const [selectedCourseIdManual, setSelectedCourseIdManual] = useState('')
+ const [selectedTeacherIdManual, setSelectedTeacherIdManual] = useState('')
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ const [availableCourses, setAvailableCourses] = useState([])
+ const [submitting, setSubmitting] = useState(false)
// ── Load initial data ──────────────────────────────────────────────────────
const loadResources = useCallback(async () => {
setLoading(true)
try {
- const [secsRes, deptsRes, teachersRes] = await Promise.all([
+ const [secsRes, deptsRes, teachersRes, batchesRes] = await Promise.all([
sectionService.list(),
departmentService.list(),
- teacherService.list()
+ teacherService.list(),
+ batchService.list()
])
setSections(secsRes.data)
setDepartments(deptsRes.data)
setAllTeachers(teachersRes.data)
+ setAllBatches(batchesRes.data)
} catch {
toast('error', 'Failed to load resources')
} finally {
@@ -86,6 +110,24 @@ export function WorkloadPage() {
loadResources()
}, [loadResources])
+ const loadSummary = useCallback(async () => {
+ setLoadingSummary(true)
+ try {
+ const data = await workloadService.getSummary()
+ setSummaryData(data)
+ } catch {
+ toast('error', 'Failed to load workload summary')
+ } finally {
+ setLoadingSummary(false)
+ }
+ }, [toast])
+
+ useEffect(() => {
+ if (activeTab === 'summary') {
+ loadSummary()
+ }
+ }, [activeTab, loadSummary])
+
// ── Load workload for selected section ─────────────────────────────────────
useEffect(() => {
if (!selectedSectionId) {
@@ -116,14 +158,57 @@ export function WorkloadPage() {
const matchesSearch = s.name.toLowerCase().includes(query) ||
(s.batch_name?.toLowerCase().includes(query)) ||
(s.program_name?.toLowerCase().includes(query));
-
+
const matchesDept = selectedDeptId === 'all' || s.department_id === selectedDeptId;
-
+
return matchesSearch && matchesDept;
})
}, [sections, searchQuery, selectedDeptId])
+ useEffect(() => {
+ if (!selectedBatchId || !selectedSectionIdManual) {
+ setAvailableCourses([])
+ return
+ }
+ async function fetchBatchCourses() {
+ try {
+ const res = await workloadService.getSectionWorkload(selectedSectionIdManual as number)
+ setAvailableCourses(res.courses)
+ } catch {
+ toast('error', 'Failed to fetch courses for this section')
+ }
+ }
+ fetchBatchCourses()
+ }, [selectedBatchId, selectedSectionIdManual, toast])
+
// ── Handlers ───────────────────────────────────────────────────────────────
+ const handleManualAdd = async () => {
+ if (!selectedSectionIdManual || !selectedCourseIdManual || !selectedTeacherIdManual) {
+ toast('error', 'Please select all fields')
+ return
+ }
+ setSubmitting(true)
+ try {
+ await workloadService.assign(
+ selectedSectionIdManual as number,
+ selectedCourseIdManual as number,
+ selectedTeacherIdManual as number
+ )
+ toast('success', 'Workload added successfully')
+ setQuickAddOpen(false)
+ // Reset
+ setSelectedBatchId('')
+ setSelectedSectionIdManual('')
+ setSelectedCourseIdManual('')
+ setSelectedTeacherIdManual('')
+ loadResources()
+ if (activeTab === 'summary') loadSummary()
+ } catch {
+ toast('error', 'Failed to add workload')
+ } finally {
+ setSubmitting(false)
+ }
+ }
const handleAssign = async (courseId: number, teacherId: number) => {
if (!selectedSectionId) return
try {
@@ -150,45 +235,18 @@ export function WorkloadPage() {
}
}
- const handleAutoAssign = async () => {
- if (!window.confirm("Auto-assign teachers for ALL batches & sections? (Only unassigned courses will be affected)")) return
- setLoading(true)
- try {
- const res = await workloadService.autoAssignAll()
- toast('success', res.message)
- await loadResources()
- } catch {
- toast('error', 'Auto-assignment failed')
- } finally {
- setLoading(false)
- }
- }
-
- const handleRebalance = async () => {
- if (!window.confirm("🛑 CRITICAL ACTION: This will DELETE ALL current assignments (including those from Bulk Import!) and recreate them evenly across all faculty to solve teacher-overload issues. Is this what you want?")) return
- setLoading(true)
- try {
- const res = await workloadService.rebalanceAll()
- toast('success', res.message || 'Workload rebalanced successfully')
- await loadResources()
- } catch {
- toast('error', 'Rebalance failed')
- } finally {
- setLoading(false)
- }
- }
if (loading) return
return (
-
+
{/* ── Page Title ── */}
-
+
{viewMode === 'mapping' && (
-
+
{viewMode === 'selection' && (
-
-
+
)}
- {/* ── SELECTION MODE: Grid of Sections ── */}
+ {/* ── Tab Switcher (Only in Selection Mode) ── */}
{viewMode === 'selection' && (
+
+
+
+
+ )}
+
+ {/* ── SELECTION MODE: Sections Grid ── */}
+ {viewMode === 'selection' && activeTab === 'sections' && (
-
+
{/* Filters Bar */}
-
-
+
-
setSearchQuery(e.target.value)}
/>
@@ -287,12 +353,12 @@ export function WorkloadPage() {
{/* Sections Grid */}
{filteredSections.map(sec => (
-
setSelectedSectionId(sec.id)}
- style={{
- padding: '1.5rem',
+ style={{
+ padding: '1.5rem',
cursor: 'pointer',
border: '1px solid var(--border)',
display: 'flex',
@@ -303,11 +369,11 @@ export function WorkloadPage() {
}}
>
-
+
{sec.program_name}
-
+
{sec.name}
{sec.batch_name}
@@ -335,29 +401,111 @@ export function WorkloadPage() {
)}
+ {/* ── SELECTION MODE: Summary View ── */}
+ {viewMode === 'selection' && activeTab === 'summary' && (
+
+
+
Faculty Workload Summary
+
+
+
+
(
+
+
{row.teacher_name}
+
{row.teacher_email}
+
+ )
+ },
+ {
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ key: 'departments', label: 'Departments', render: (row: any) => (
+
+ {(row.departments || []).map((d: string) => {d})}
+
+ )
+ },
+ {
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ key: 'course_count', label: 'Courses', sortable: true, render: (row: any) => (
+ {row.course_count} Courses
+ )
+ },
+ {
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ key: 'total_hours', label: 'Total Hours', sortable: true, render: (row: any) => (
+ {row.total_hours} hrs / week
+ )
+ },
+ {
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ key: 'assignments', label: 'Teaching Assignments', render: (row: any) => (
+
+ {/* eslint-disable-next-line @typescript-eslint/no-explicit-any */}
+ {(row.assignments || []).slice(0, 3).map((a: any) => (
+
+
+ {a.course_code}
+ ({a.section_name})
+
+ ))}
+ {(row.assignments || []).length > 3 && (
+
+ {row.assignments.length - 3} more...
+ )}
+
+ )
+ }
+ ]}
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ data={summaryTable.paginated as any}
+ search={summaryTable.search}
+ onSearch={summaryTable.setSearch}
+ page={summaryTable.page}
+ totalPages={summaryTable.totalPages}
+ onPageChange={summaryTable.setPage}
+ total={summaryTable.total}
+ sortKey={summaryTable.sortKey as string}
+ sortDir={summaryTable.sortDir}
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ onSort={(k) => summaryTable.toggleSort(k as any)}
+ emptyTitle="No workload data available"
+ />
+
+ )}
+
{/* ── MAPPING MODE: Allocation Cards ── */}
{viewMode === 'mapping' && (
-
+
{/* Header Info */}
-
{workloadData?.batch_name}
+
+ {workloadData?.batch_name}
+ {workloadData?.current_semester && (
+ Semester {workloadData.current_semester}
+ )}
+
Manage Workload: {workloadData?.section_name}
-
+
-
- {workloadData?.courses.filter(c => c.teacher_id).length} / {workloadData?.courses.length} Assigned
-
-
-
c.teacher_id).length || 0) / (workloadData?.courses.length || 1)) * 100}%`,
- height: '100%',
- background: 'var(--primary)',
- transition: 'width 0.5s ease-out'
- }} />
-
+
+ {workloadData?.courses.filter(c => c.teacher_id).length} / {workloadData?.courses.length} Assigned
+
+
+
c.teacher_id).length || 0) / (workloadData?.courses.length || 1)) * 100}%`,
+ height: '100%',
+ background: 'var(--primary)',
+ transition: 'width 0.5s ease-out'
+ }} />
+
@@ -366,12 +514,12 @@ export function WorkloadPage() {
) : (
{workloadData?.courses.map(course => (
-
-
+
{course.course_code}
@@ -412,8 +560,8 @@ export function WorkloadPage() {
))}
{course.teacher_id && (
-
)
}
diff --git a/frontend/src/services/resources.service.ts b/frontend/src/services/resources.service.ts
index e1b8039..7ec3555 100644
--- a/frontend/src/services/resources.service.ts
+++ b/frontend/src/services/resources.service.ts
@@ -272,6 +272,10 @@ export const roomService = {
// ── Workload ────────────────────────────────────────────────────────────────
export const workloadService = {
+ async getSummary() {
+ const res = await api.get('/workload/summary')
+ return res.data
+ },
async getSectionWorkload(sectionId: number) {
const res = await api.get(`/workload/sections/${sectionId}`)
return res.data
@@ -291,13 +295,5 @@ export const workloadService = {
headers: { 'Content-Type': 'multipart/form-data' },
})
return res.data
- },
- async autoAssignAll() {
- const response = await api.post('/workload/auto-assign-all')
- return response.data
- },
- async rebalanceAll() {
- const response = await api.post('/workload/rebalance-all')
- return response.data
}
}
diff --git a/frontend/src/services/scheduling.service.ts b/frontend/src/services/scheduling.service.ts
index 3fa65d8..33cacc7 100644
--- a/frontend/src/services/scheduling.service.ts
+++ b/frontend/src/services/scheduling.service.ts
@@ -48,7 +48,7 @@ export const schedulingService = {
async generateAllTimetables(payload: Partial
): Promise {
const res = await api.post('/scheduling/generate/all', payload)
- return res.data
+ return res.data.results || []
},
// Timetable viewing
diff --git a/frontend/src/types/index.ts b/frontend/src/types/index.ts
index 5d64ba9..f9814d3 100644
--- a/frontend/src/types/index.ts
+++ b/frontend/src/types/index.ts
@@ -94,6 +94,7 @@ export interface Batch {
code: string;
academic_year: string;
program_id: number;
+ current_semester: number;
program_code?: string;
section_count?: number;
}
@@ -103,6 +104,7 @@ export interface BatchPayload {
code: string;
academic_year: string;
program_id: number;
+ current_semester: number;
}
// ── Section ───────────────────────────────────────────────────────────────