Changeset b67f274 for src/app/students/subjects/page.tsx
- Timestamp:
- 12/21/25 18:28:25 (9 months ago)
- Branches:
- master
- Children:
- 90f7842
- Parents:
- 82c9c53
- File:
-
- 1 edited
-
src/app/students/subjects/page.tsx (modified) (3 diffs)
Legend:
- Unmodified
- Added
- Removed
-
src/app/students/subjects/page.tsx
r82c9c53 rb67f274 7 7 faFileInvoice 8 8 } from '@fortawesome/free-solid-svg-icons'; 9 import { use State } from 'react';10 import subjectsData from '@/data/subjects.json';9 import { useEffect, useState } from 'react'; 10 import { getAccessToken } from '@/lib/auth'; 11 11 12 12 interface Subject { … … 21 21 group: string; 22 22 professor: string; 23 } 24 25 type SemesterInfo = { 26 id: number; 27 name: string; 28 status: string; 29 serviceNumber: string | number; 30 }; 31 32 type FinancialInfo = { 33 sum: string | number; 34 paid: string; 35 due: string; 36 materialCosts: string; 37 credits: string; 38 MKSA: string; 39 electronicRegistration: string; 40 eUKIM: string; 41 bankProvision: string; 42 total: string; 43 }; 44 45 type CurrentSemester = { 46 id: string; 47 name: string; 48 status: string; 49 serviceNumber: string | number; 50 ticketNumber: string; 51 debt: string; 52 financialInfo: FinancialInfo; 53 }; 54 55 type SubjectsResponse = { 56 currentSemester: CurrentSemester; 57 semesters: SemesterInfo[]; 58 subjectsBySemester: Record<string, Subject[]>; 59 semesterKeyById: Record<number, string>; 60 }; 61 62 type ApiCurrentSemester = { 63 id: string; 64 name: string; 65 status: string; 66 serviceNumber: string | number; 67 ticketNumber: string; 68 debt: string; 69 financialInfo: { 70 sum: string | number; 71 paid: string; 72 due: string; 73 materialCosts: string; 74 credits: string; 75 totalCredits?: string; 76 mksa?: string; 77 MKSA?: string; 78 electronicRegistration: string; 79 eUKIM: string; 80 bankProvision: string; 81 total: string; 82 }; 83 }; 84 85 type ApiSubjectsResponse = { 86 semesters: SemesterInfo[]; 87 currentSemester?: ApiCurrentSemester; 88 currentSemestar?: ApiCurrentSemester; 89 subjectsBySemester: Record<string, Subject[]>; 90 }; 91 92 function normalizeKey(input: string) { 93 return input.toLowerCase().replace(/\s|\(|\)|\.|,/g, ''); 94 } 95 96 function detectSeasonFromName(name: string): 'summer' | 'winter' | null { 97 const n = name.toLowerCase(); 98 if (n.includes('летен')) return 'summer'; 99 if (n.includes('зимски')) return 'winter'; 100 return null; 101 } 102 103 function buildSemesterKeyById(semesters: SemesterInfo[], keys: string[]) { 104 const keyBySeason: Partial<Record<'summer' | 'winter', string>> = {}; 105 for (const key of keys) { 106 const k = key.toLowerCase(); 107 if (k.includes('summer')) keyBySeason.summer = key; 108 if (k.includes('winter')) keyBySeason.winter = key; 109 } 110 111 const mapping: Record<number, string> = {}; 112 for (const s of semesters) { 113 const season = detectSeasonFromName(s.name); 114 const mapped = season ? keyBySeason[season] : undefined; 115 if (mapped) mapping[s.id] = mapped; 116 } 117 118 // Fallback: if we couldn't infer seasons, try matching by normalized names. 119 if (Object.keys(mapping).length === 0) { 120 for (const s of semesters) { 121 const ns = normalizeKey(s.name); 122 const match = keys.find((k) => normalizeKey(k).includes(ns) || ns.includes(normalizeKey(k))); 123 if (match) mapping[s.id] = match; 124 } 125 } 126 127 // Last resort: map in order. 128 if (Object.keys(mapping).length === 0) { 129 semesters.forEach((s, idx) => { 130 if (keys[idx]) mapping[s.id] = keys[idx]; 131 }); 132 } 133 134 return mapping; 23 135 } 24 136 … … 59 171 60 172 export default function SubjectsPage() { 61 const [selectedSemester, setSelectedSemester] = useState(subjectsData.currentSemester.id); 173 const [subjectsData, setSubjectsData] = useState<SubjectsResponse | null>(null); 174 const [selectedSemester, setSelectedSemester] = useState<number | null>(null); 62 175 const [isDropdownOpen, setIsDropdownOpen] = useState(false); 176 const [isLoading, setIsLoading] = useState(true); 177 const [errorMessage, setErrorMessage] = useState<string | null>(null); 178 179 useEffect(() => { 180 let cancelled = false; 181 182 async function load() { 183 setIsLoading(true); 184 setErrorMessage(null); 185 186 const token = getAccessToken(); 187 if (!token) { 188 setErrorMessage('Not authenticated. Please login again.'); 189 setIsLoading(false); 190 return; 191 } 192 193 try { 194 const response = await fetch('http://localhost:5147/api/user/getSubjects', { 195 method: 'GET', 196 headers: { 197 Authorization: `Bearer ${token}`, 198 }, 199 }); 200 201 if (!response.ok) { 202 const text = await response.text().catch(() => ''); 203 throw new Error(text || `Failed to load subjects (${response.status})`); 204 } 205 206 const apiData = (await response.json()) as ApiSubjectsResponse; 207 if (cancelled) return; 208 209 const current = apiData.currentSemester ?? apiData.currentSemestar; 210 if (!current) { 211 throw new Error('Response missing currentSemestar/currentSemester'); 212 } 213 214 const keys = Object.keys(apiData.subjectsBySemester ?? {}); 215 const semesterKeyById = buildSemesterKeyById(apiData.semesters ?? [], keys); 216 217 const normalized: SubjectsResponse = { 218 semesters: apiData.semesters ?? [], 219 subjectsBySemester: apiData.subjectsBySemester ?? {}, 220 semesterKeyById, 221 currentSemester: { 222 id: current.id, 223 name: current.name, 224 status: current.status, 225 serviceNumber: current.serviceNumber, 226 ticketNumber: current.ticketNumber, 227 debt: current.debt, 228 financialInfo: { 229 sum: current.financialInfo.sum, 230 paid: current.financialInfo.paid, 231 due: current.financialInfo.due, 232 materialCosts: current.financialInfo.materialCosts, 233 credits: current.financialInfo.credits, 234 MKSA: current.financialInfo.MKSA ?? current.financialInfo.mksa ?? '', 235 electronicRegistration: current.financialInfo.electronicRegistration, 236 eUKIM: current.financialInfo.eUKIM, 237 bankProvision: current.financialInfo.bankProvision, 238 total: current.financialInfo.total, 239 }, 240 }, 241 }; 242 243 setSubjectsData(normalized); 244 245 setSelectedSemester((prev) => { 246 if (prev !== null) return prev; 247 const season = detectSeasonFromName(current.name); 248 if (season) { 249 const key = keys.find((k) => k.toLowerCase().includes(season)); 250 if (key) { 251 const matchId = normalized.semesters.find((s) => normalized.semesterKeyById[s.id] === key)?.id; 252 if (typeof matchId === 'number') return matchId; 253 } 254 } 255 return normalized.semesters[0]?.id ?? null; 256 }); 257 } catch (err) { 258 if (!cancelled) { 259 setErrorMessage(err instanceof Error ? err.message : 'Failed to load subjects.'); 260 } 261 } finally { 262 if (!cancelled) setIsLoading(false); 263 } 264 } 265 266 void load(); 267 268 return () => { 269 cancelled = true; 270 }; 271 }, []); 272 273 if (isLoading) { 274 return ( 275 <div className="min-h-screen pb-8"> 276 <div className="bg-white rounded-xl shadow-sm border border-gray-100 p-6"> 277 Loading... 278 </div> 279 </div> 280 ); 281 } 282 283 if (errorMessage || !subjectsData || selectedSemester === null) { 284 return ( 285 <div className="min-h-screen pb-8"> 286 <div className="rounded-lg border border-red-200 bg-red-50 px-4 py-3 text-sm text-red-700"> 287 {errorMessage ?? 'Failed to load subjects.'} 288 </div> 289 </div> 290 ); 291 } 63 292 64 const currentSemesterData = subjectsData.semesters.find(s => s.id === selectedSemester) || subjectsData.currentSemester; 65 const currentSubjects: Subject[] = subjectsData.subjectsBySemester[selectedSemester as keyof typeof subjectsData.subjectsBySemester] || []; 293 const currentSemesterData = 294 subjectsData.semesters.find((s) => s.id === selectedSemester) ?? subjectsData.semesters[0]; 295 const semesterKey = 296 subjectsData.semesterKeyById[selectedSemester] ?? 297 Object.keys(subjectsData.subjectsBySemester)[0]; 298 const currentSubjects: Subject[] = semesterKey ? subjectsData.subjectsBySemester[semesterKey] || [] : []; 66 299 const { currentSemester } = subjectsData; 67 300
Note:
See TracChangeset
for help on using the changeset viewer.
