diff --git a/.gitignore b/.gitignore index c221017..ed2a635 100644 --- a/.gitignore +++ b/.gitignore @@ -1,71 +1,64 @@ -# Python -__pycache__/ -*.py[cod] -*$py.class -*.so -.Python -env/ -.env -build/ -develop-eggs/ -dist/ -downloads/ -eggs/ -.eggs/ -lib/ -!frontend/src/lib/ -lib64/ -parts/ -sdist/ -var/ -wheels/ -*.egg-info/ -.installed.cfg -*.egg -venv/ -ENV/ -migrations/ +``` +# Dependencies +frontend/node_modules/ -# Flask -instance/ -.webassets-cache -.env +# Build artifacts +frontend/node_modules/**/dist/ +frontend/node_modules/**/build/ +frontend/node_modules/**/target/ -# Vite & Frontend -dist/ -dist-ssr/ -.vite/ -*.local -node_modules/ -npm-debug.log* -yarn-debug.log* -yarn-error.log* -pnpm-debug.log* -.eslintcache +# Package manager specific +package-lock.json +yarn.lock -# Editor & OS -.vscode/* -!.vscode/extensions.json +# IDE files +.vscode/ .idea/ -*.suo -*.ntvs* -*.njsproj -*.sln -*.sw? +*.swp +*.swo + +# OS files .DS_Store Thumbs.db -# Agent AI -.gemini/ -.agents/ -.agent/ -_agents/ -_agent/ +# Logs *.log -tmp/ -*.tmp -.pytest_cache/ -pytest-cache-files-*/ -backend/pytest-cache-files-*/ -backend/.pytest_cache/ +# Environment +.env +.env.local +*.env.* + +# Coverage +coverage/ +htmlcov/ +.coverage + +# TypeScript +*.tsbuildinfo + +# Compressed files +*.zip +*.gz +*.tar +*.tgz +*.bz2 +*.xz +*.7z +*.rar +*.zst +*.lz4 +*.lzh +*.cab +*.arj +*.rpm +*.deb +*.Z +*.lz +*.lzo +*.tar.gz +*.tar.bz2 +*.tar.xz +*.tar.zst + +``` \ No newline at end of file diff --git a/backend/app/routes/scheduling.py b/backend/app/routes/scheduling.py index d81e927..31e8b99 100644 --- a/backend/app/routes/scheduling.py +++ b/backend/app/routes/scheduling.py @@ -575,7 +575,7 @@ def gap_penalty(section_id, day, slot): # Find consecutive slots for the needed duration for i in range(len(TIMESLOTS) - needed + 1): - block_slots = TIMESLOTS[i:i + needed] + block_slots = TIMESLOTS[i : i + needed] # Check teacher availability if any(not is_teacher_available(teacher, day, s) for s in block_slots): diff --git a/backend/app/routes/workload.py b/backend/app/routes/workload.py index 0efb979..3b6c63c 100644 --- a/backend/app/routes/workload.py +++ b/backend/app/routes/workload.py @@ -322,21 +322,25 @@ def get_workload_summary(): # Calculate total hours (L+T+P) total_hours = sum(a.course.get_hours_needed() for a in allocations if a.course) - summary.append({ - "teacher_id": t.id, - "teacher_name": t.name, - "teacher_email": t.email, - "course_count": course_count, - "total_hours": total_hours, - "departments": [d.name for d in t.departments], - "assignments": [ - { - "id": a.id, - "course_code": a.course.code, - "course_name": a.course.name, - "section_name": a.section.name, - "batch_name": a.section.batch.name if a.section.batch else "Unknown" - } for a in allocations if a.course and a.section - ] - }) + summary.append( + { + "teacher_id": t.id, + "teacher_name": t.name, + "teacher_email": t.email, + "course_count": course_count, + "total_hours": total_hours, + "departments": [d.name for d in t.departments], + "assignments": [ + { + "id": a.id, + "course_code": a.course.code, + "course_name": a.course.name, + "section_name": a.section.name, + "batch_name": a.section.batch.name if a.section.batch else "Unknown", + } + for a in allocations + if a.course and a.section + ], + } + ) return jsonify(summary), 200 diff --git a/backend/app/scheduler_new/data_loader.py b/backend/app/scheduler_new/data_loader.py index 4eb85f9..6a68032 100644 --- a/backend/app/scheduler_new/data_loader.py +++ b/backend/app/scheduler_new/data_loader.py @@ -270,9 +270,7 @@ def load_sections( query = Section.query.options(joinedload(Section.batch).joinedload(Batch.program)) if self.department_id: - query = query.join(Section.batch).join(Batch.program).filter( - Program.department_id == self.department_id - ) + query = query.join(Section.batch).join(Batch.program).filter(Program.department_id == self.department_id) sections = self._deduplicate_sections(query.all()) if not sections: @@ -432,13 +430,15 @@ def load_problem(self) -> "SchedulingProblem": for c in all_courses: if c.id not in active_course_ids: if (c.program_id, self._resolve_course_semester(c)) in active_section_semesters: - unassigned_curriculum.append({ - "id": c.id, - "code": c.code, - "name": c.name, - "type": c.course_type, - "reason": "Missing from Workload Page" - }) + unassigned_curriculum.append( + { + "id": c.id, + "code": c.code, + "name": c.name, + "type": c.course_type, + "reason": "Missing from Workload Page", + } + ) return SchedulingProblem( sections=sections, diff --git a/backend/app/scheduler_new/genetic_engine.py b/backend/app/scheduler_new/genetic_engine.py index 566880d..7b6fb5e 100644 --- a/backend/app/scheduler_new/genetic_engine.py +++ b/backend/app/scheduler_new/genetic_engine.py @@ -114,7 +114,7 @@ def _precompute_domains(self): try: idx = day_slots.index(t) if idx + hours <= len(day_slots): - consecutive = day_slots[idx:idx + hours] + consecutive = day_slots[idx : idx + hours] self.slot_keys_cache[(t, hours)] = [(s.day, s.start_time) for s in consecutive] else: self.slot_keys_cache[(t, hours)] = None @@ -160,7 +160,7 @@ def _get_random_domain_sample(self, var, max_samples=10, forced_teacher=None) -> samples = [] for _ in range(max_samples): - opt = random.choice(domain) + opt = random.choice(domain) # nosec B311 if forced_teacher is not None: # Only use this option if it contains the forced teacher @@ -168,10 +168,12 @@ def _get_random_domain_sample(self, var, max_samples=10, forced_teacher=None) -> continue faculty_id = forced_teacher else: - faculty_id = random.choice(opt["faculties"]) + faculty_id = random.choice(opt["faculties"]) # nosec B311 samples.append( - DomainValue(faculty_id=faculty_id, room_id=random.choice(opt["rooms"]), timeslot=opt["timeslot"]) + DomainValue( + faculty_id=faculty_id, room_id=random.choice(opt["rooms"]), timeslot=opt["timeslot"] + ) # nosec B311 ) return samples @@ -328,7 +330,9 @@ def _enforce_consistency_in_individual( if assigned_teacher in opt["faculties"]: # Found a matching option - update to use correct teacher fixed[i] = DomainValue( - faculty_id=assigned_teacher, room_id=random.choice(opt["rooms"]), timeslot=opt["timeslot"] + faculty_id=assigned_teacher, + room_id=random.choice(opt["rooms"]), + timeslot=opt["timeslot"], # nosec B311 ) found = True break @@ -357,10 +361,12 @@ def _generate_random_individual(self) -> List[Optional[DomainValue]]: # Use the already-assigned teacher for this (section, course) valid_opts = [opt for opt in domain if assigned_teacher in opt["faculties"]] if valid_opts: - opt = random.choice(valid_opts) + opt = random.choice(valid_opts) # nosec B311 individual.append( DomainValue( - faculty_id=assigned_teacher, room_id=random.choice(opt["rooms"]), timeslot=opt["timeslot"] + faculty_id=assigned_teacher, + room_id=random.choice(opt["rooms"]), + timeslot=opt["timeslot"], # nosec B311 ) ) else: @@ -368,11 +374,13 @@ def _generate_random_individual(self) -> List[Optional[DomainValue]]: individual.append(None) else: # First occurrence - randomly select and record teacher - opt = random.choice(domain) - teacher = random.choice(opt["faculties"]) + opt = random.choice(domain) # nosec B311 + teacher = random.choice(opt["faculties"]) # nosec B311 section_course_faculty[sc_key] = teacher individual.append( - DomainValue(faculty_id=teacher, room_id=random.choice(opt["rooms"]), timeslot=opt["timeslot"]) + DomainValue( + faculty_id=teacher, room_id=random.choice(opt["rooms"]), timeslot=opt["timeslot"] + ) # nosec B311 ) return individual @@ -383,7 +391,7 @@ def _crossover_single_point( """Single-point crossover""" if len(self.variables) < 2: return list(parent1), list(parent2) - point = random.randint(1, len(self.variables) - 1) + point = random.randint(1, len(self.variables) - 1) # nosec B311 child1 = parent1[:point] + parent2[point:] child2 = parent2[:point] + parent1[point:] return child1, child2 @@ -394,8 +402,8 @@ def _crossover_two_point( """Two-point crossover""" if len(self.variables) < 3: return self._crossover_single_point(parent1, parent2) - point1 = random.randint(1, len(self.variables) - 2) - point2 = random.randint(point1 + 1, len(self.variables) - 1) + point1 = random.randint(1, len(self.variables) - 2) # nosec B311 + point2 = random.randint(point1 + 1, len(self.variables) - 1) # nosec B311 child1 = parent1[:point1] + parent2[point1:point2] + parent1[point2:] child2 = parent2[:point1] + parent1[point1:point2] + parent2[point2:] return child1, child2 @@ -407,7 +415,7 @@ def _crossover_uniform( child1 = [] child2 = [] for i in range(len(self.variables)): - if random.random() < 0.5: + if random.random() < 0.5: # nosec B311 child1.append(parent1[i]) child2.append(parent2[i]) else: @@ -419,7 +427,7 @@ def _crossover( self, parent1: List[Optional[DomainValue]], parent2: List[Optional[DomainValue]] ) -> Tuple[List[Optional[DomainValue]], List[Optional[DomainValue]]]: """Adaptive crossover: randomly select operator""" - operator = random.choice(["single", "two_point", "uniform"]) + operator = random.choice(["single", "two_point", "uniform"]) # nosec B311 if operator == "single": return self._crossover_single_point(parent1, parent2) elif operator == "two_point": @@ -441,19 +449,19 @@ def _mutate(self, individual: List[Optional[DomainValue]]) -> List[Optional[Doma section_course_faculty[sc_key] = val.faculty_id for i, var in enumerate(self.variables): - if random.random() < self.current_mutation_rate: + if random.random() < self.current_mutation_rate: # nosec B311 sc_key = (var.section_id, var.course_id) assigned_teacher = section_course_faculty.get(sc_key) sampled = self._get_random_domain_sample(var, 5, forced_teacher=assigned_teacher) if sampled: - if len(sampled) > 1 and random.random() < 0.5: + if len(sampled) > 1 and random.random() < 0.5: # nosec B311 current_val = mutated[i] available = [d for d in sampled if d != current_val] if available: - mutated[i] = random.choice(available) + mutated[i] = random.choice(available) # nosec B311 else: - mutated[i] = random.choice(sampled) + mutated[i] = random.choice(sampled) # nosec B311 return mutated @@ -721,15 +729,19 @@ def solve(self, progress_callback: Optional[Callable[[int, str], None]] = None) for i in range(self.elite_size): elite = fitness_scores[i][1] # Apply local search to elite individuals - if self.use_local_search and random.random() < 0.3: + if self.use_local_search and random.random() < 0.3: # nosec B311 elite = self._local_search_hill_climbing(elite, self.local_search_intensity // 2) new_population.append(elite) # Generate rest of new population while len(new_population) < self.population_size: # Tournament selection - tournament1 = random.sample(fitness_scores, min(self.tournament_size, len(fitness_scores))) - tournament2 = random.sample(fitness_scores, min(self.tournament_size, len(fitness_scores))) + tournament1 = random.sample( + fitness_scores, min(self.tournament_size, len(fitness_scores)) + ) # nosec B311 + tournament2 = random.sample( + fitness_scores, min(self.tournament_size, len(fitness_scores)) + ) # nosec B311 parent1 = min(tournament1, key=lambda x: x[0])[1] parent2 = min(tournament2, key=lambda x: x[0])[1] @@ -740,9 +752,9 @@ def solve(self, progress_callback: Optional[Callable[[int, str], None]] = None) child2 = self._mutate(child2) # Apply repair to offspring - if random.random() < 0.2: + if random.random() < 0.2: # nosec B311 child1 = self._repair_individual(child1) - if random.random() < 0.2: + if random.random() < 0.2: # nosec B311 child2 = self._repair_individual(child2) new_population.append(child1) diff --git a/backend/app/scheduler_new/greedy_engine.py b/backend/app/scheduler_new/greedy_engine.py index f09fecf..5bdffbd 100644 --- a/backend/app/scheduler_new/greedy_engine.py +++ b/backend/app/scheduler_new/greedy_engine.py @@ -57,7 +57,7 @@ def solve(self, progress_callback: Optional[Callable[[int, str], None]] = None) for h in range(1, 8): for i in range(num_timeslots): if i + h <= num_timeslots: - consecutive_slots = timeslot_list[i:i + h] + consecutive_slots = timeslot_list[i : i + h] valid = True for j in range(h - 1): if ( diff --git a/backend/app/scheduler_new/hybrid_engine.py b/backend/app/scheduler_new/hybrid_engine.py index ff8a8e1..d008c98 100644 --- a/backend/app/scheduler_new/hybrid_engine.py +++ b/backend/app/scheduler_new/hybrid_engine.py @@ -73,7 +73,8 @@ def _get_room_candidates(self, section, course): if matched_keywords: # Filter candidates that match any of these keywords in their room name/type specialized = [ - r for r in candidates + r + for r in candidates if any(k in (r.name or "").lower() or k in (r.room_type or "").lower() for k in matched_keywords) ] if specialized: @@ -84,7 +85,8 @@ def _get_room_candidates(self, section, course): candidates = [ room for room in self.problem.rooms - if room.can_accommodate(section.student_count) and room.can_be_used_by_program(None, section_dept_id) + if room.can_accommodate(section.student_count) + and room.can_be_used_by_program(None, section_dept_id) and not ("lab" in (room.room_type or "").lower() if course_type_lower != "lab" else False) ] @@ -191,9 +193,7 @@ def solve(self, progress_callback: Optional[Callable[[int, str], None]] = None) if demand > weekly_capacity: section = self.problem.section_map.get(sid) sname = section.get_full_name() if section else f"Section ID {sid}" - overloaded_reports.append( - f"{sname}: {demand} weekly hours requested (Max {weekly_capacity} capacity)" - ) + overloaded_reports.append(f"{sname}: {demand} weekly hours requested (Max {weekly_capacity} capacity)") if overloaded_reports: error_msg = ( @@ -336,7 +336,7 @@ def _compute_valid_starts(self, timeslot_list): if start_idx + hours > num_timeslots: continue - consecutive_slots = timeslot_list[start_idx:start_idx + hours] + consecutive_slots = timeslot_list[start_idx : start_idx + hours] is_valid = True for offset in range(hours - 1): current = consecutive_slots[offset] diff --git a/backend/app/scheduler_new/ortools_engine.py b/backend/app/scheduler_new/ortools_engine.py index 0bd2815..6606f1c 100644 --- a/backend/app/scheduler_new/ortools_engine.py +++ b/backend/app/scheduler_new/ortools_engine.py @@ -52,7 +52,7 @@ def solve(self, progress_callback: Optional[Callable[[int, str], None]] = None) for h in range(1, 8): for i in range(num_timeslots): if i + h <= num_timeslots: - consecutive_slots = timeslot_list[i:i + h] + consecutive_slots = timeslot_list[i : i + h] valid = True for j in range(h - 1): if ( @@ -160,14 +160,18 @@ def solve(self, progress_callback: Optional[Callable[[int, str], None]] = None) f_per_slot = max(1, min_per_faculty // len(f_slots)) f_selected = [] for ts_idx in f_slots: - f_selected.extend(random.sample(f_by_slot[ts_idx], min(len(f_by_slot[ts_idx]), f_per_slot))) + f_selected.extend( + random.sample(f_by_slot[ts_idx], min(len(f_by_slot[ts_idx]), f_per_slot)) + ) # nosec B311 if len(f_selected) < min_per_faculty: rem = [c for c in f_combos if c not in f_selected] - f_selected.extend(random.sample(rem, min(len(rem), min_per_faculty - len(f_selected)))) + f_selected.extend( + random.sample(rem, min(len(rem), min_per_faculty - len(f_selected))) + ) # nosec B311 selected.extend(f_selected) if len(selected) > MAX_COMBINATIONS_PER_CLASS: - all_combinations = random.sample(selected, MAX_COMBINATIONS_PER_CLASS) + all_combinations = random.sample(selected, MAX_COMBINATIONS_PER_CLASS) # nosec B311 else: all_combinations = selected diff --git a/backend/app/scheduler_new/scheduler_engine.py b/backend/app/scheduler_new/scheduler_engine.py index a4d6ba1..6eb638f 100644 --- a/backend/app/scheduler_new/scheduler_engine.py +++ b/backend/app/scheduler_new/scheduler_engine.py @@ -194,7 +194,7 @@ def _find_consecutive_slots( if start_idx + hours_needed > len(day_slots): return None - consecutive = day_slots[start_idx:start_idx + hours_needed] + consecutive = day_slots[start_idx : start_idx + hours_needed] # Verify they're consecutive for i in range(len(consecutive) - 1): if consecutive[i].end_time != consecutive[i + 1].start_time: diff --git a/frontend/src/pages/BatchesPage.tsx b/frontend/src/pages/BatchesPage.tsx index 4346fbe..32e2812 100644 --- a/frontend/src/pages/BatchesPage.tsx +++ b/frontend/src/pages/BatchesPage.tsx @@ -235,7 +235,7 @@ function BatchesTab() { }, { key: 'current_semester', label: 'Semester', sortable: true, render: row => { - const sem = row.current_semester ?? (row as any).currentSemester; + const sem = row.current_semester ?? (row as unknown as { currentSemester?: number }).currentSemester; return ( {sem !== undefined && sem !== null ? String(sem) : '—'} @@ -484,7 +484,7 @@ function SectionsTab() { { key: 'semester', label: 'Semester', render: row => { const batch = batchMap[row.batch_id as number]; - const sem = batch?.current_semester ?? (batch as any)?.currentSemester; + const sem = batch?.current_semester ?? (batch as unknown as { currentSemester?: number })?.currentSemester; return {sem !== undefined && sem !== null ? String(sem) : '—'}; } }, diff --git a/frontend/src/pages/WorkloadPage.tsx b/frontend/src/pages/WorkloadPage.tsx index 1092985..0af230f 100644 --- a/frontend/src/pages/WorkloadPage.tsx +++ b/frontend/src/pages/WorkloadPage.tsx @@ -15,7 +15,7 @@ */ import React, { useEffect, useState, useMemo, useCallback } from 'react' -import { Check, ClipboardList, Search, User, UserPlus, X, GraduationCap as BatchIcon, Filter, ArrowLeft, Upload, Zap, RefreshCw, BarChart3, LayoutGrid, Plus, BookOpen } from 'lucide-react' +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' @@ -66,19 +66,19 @@ export function WorkloadPage() { const [searchQuery, setSearchQuery] = useState('') const [importModalOpen, setImportModalOpen] = useState(false) const [activeTab, setActiveTab] = useState<'sections' | 'summary'>('sections') - const [summaryData, setSummaryData] = useState([]) + const [summaryData, setSummaryData] = useState([]) const [loadingSummary, setLoadingSummary] = useState(false) const [quickAddOpen, setQuickAddOpen] = useState(false) - const summaryTable = useTable({ data: summaryData as any, searchFields: ['teacher_name', 'teacher_email'] as any, defaultSortKey: 'teacher_name' }) + const summaryTable = useTable({ data: summaryData as unknown[], searchFields: ['teacher_name', 'teacher_email'] as string[], defaultSortKey: 'teacher_name' }) // Manual Add State - const [allBatches, setAllBatches] = useState([]) + const [allBatches, setAllBatches] = useState([]) const [selectedBatchId, setSelectedBatchId] = useState('') const [selectedSectionIdManual, setSelectedSectionIdManual] = useState('') const [selectedCourseIdManual, setSelectedCourseIdManual] = useState('') const [selectedTeacherIdManual, setSelectedTeacherIdManual] = useState('') - const [availableCourses, setAvailableCourses] = useState([]) + const [availableCourses, setAvailableCourses] = useState([]) const [submitting, setSubmitting] = useState(false) // ── Load initial data ────────────────────────────────────────────────────── @@ -410,48 +410,51 @@ export function WorkloadPage() { ( + key: 'teacher_name', label: 'Teacher', sortable: true, render: (row: unknown) => (
-
{row.teacher_name}
-
{row.teacher_email}
+
{(row as Record).teacher_name}
+
{(row as Record).teacher_email}
) }, { - key: 'departments', label: 'Departments', render: (row: any) => ( + key: 'departments', label: 'Departments', render: (row: unknown) => (
- {(row.departments || []).map((d: string) => {d})} + {((row as Record).departments as string[] || []).map((d: string) => {d})}
) }, { - key: 'course_count', label: 'Courses', sortable: true, render: (row: any) => ( - {row.course_count} Courses + key: 'course_count', label: 'Courses', sortable: true, render: (row: unknown) => ( + {(row as Record).course_count} Courses ) }, { - key: 'total_hours', label: 'Total Hours', sortable: true, render: (row: any) => ( - {row.total_hours} hrs / week + key: 'total_hours', label: 'Total Hours', sortable: true, render: (row: unknown) => ( + {(row as Record).total_hours} hrs / week ) }, { - key: 'assignments', label: 'Teaching Assignments', render: (row: any) => ( + key: 'assignments', label: 'Teaching Assignments', render: (row: unknown) => (
- {(row.assignments || []).slice(0, 3).map((a: any) => ( -
- - {a.course_code} - ({a.section_name}) -
- ))} - {(row.assignments || []).length > 3 && ( - + {row.assignments.length - 3} more... + {((row as Record).assignments as unknown[] || []).slice(0, 3).map((a: unknown, idx: number) => { + const assignment = a as Record; + return ( +
+ + {assignment.course_code} + ({assignment.section_name}) +
+ ); + })} + {((row as Record).assignments as unknown[] || []).length > 3 && ( + + {(row as Record).assignments.length - 3} more... )}
) } ]} - data={summaryTable.paginated as any} + data={summaryTable.paginated as unknown[]} search={summaryTable.search} onSearch={summaryTable.setSearch} page={summaryTable.page} @@ -460,7 +463,7 @@ export function WorkloadPage() { total={summaryTable.total} sortKey={summaryTable.sortKey as string} sortDir={summaryTable.sortDir} - onSort={(k) => summaryTable.toggleSort(k as any)} + onSort={(k) => summaryTable.toggleSort(k as string)} emptyTitle="No workload data available" />