- Timestamp:
- 09/15/26 21:16:02 (8 days ago)
- Branches:
- master
- Children:
- b8093a0
- Parents:
- 4fe4582
- Location:
- src
- Files:
-
- 9 added
- 5 deleted
- 12 edited
-
app/admin/layout.tsx (added)
-
app/admin/page.tsx (added)
-
app/admin/schedule/page.tsx (added)
-
app/admin/semesters/page.tsx (added)
-
app/admin/subjects/page.tsx (added)
-
app/api/prof/grade/add/route.ts (deleted)
-
app/api/prof/grade/edit/route.ts (deleted)
-
app/api/prof/grade/remove/route.ts (deleted)
-
app/api/prof/students/route.ts (deleted)
-
app/page.tsx (modified) (2 diffs)
-
app/professor/profile/page.tsx (modified) (2 diffs)
-
app/professor/students/page.tsx (modified) (15 diffs)
-
app/students/profile/page.tsx (modified) (2 diffs)
-
app/students/semesters/page.tsx (modified) (6 diffs)
-
app/students/subjects/page.tsx (modified) (2 diffs)
-
components/admin-navbar.tsx (added)
-
components/enroll-semester-dialog.tsx (added)
-
components/exams.tsx (modified) (2 diffs)
-
components/header.tsx (modified) (2 diffs)
-
lib/admin-api.ts (added)
-
lib/api.ts (added)
-
lib/auth.ts (modified) (3 diffs)
-
lib/pdf-generators.ts (modified) (3 diffs)
-
lib/prof-demo-store.ts (deleted)
-
locales/en/translation.json (modified) (2 diffs)
-
locales/mk/translation.json (modified) (2 diffs)
Legend:
- Unmodified
- Added
- Removed
-
src/app/page.tsx
r4fe4582 r8496f3c 39 39 try { 40 40 const session = await login({ email: formData.email, password: formData.password }); 41 window.location.href = session.role === 'Professor' ? '/professor' : '/students'; 41 // Admins previously fell through to /students, where they have no 42 // enrolments and every page renders empty. 43 const home = 44 session.role === 'Professor' ? '/professor' 45 : session.role === 'Admin' ? '/admin' 46 : '/students'; 47 window.location.href = home; 42 48 } catch (err) { 43 49 setErrorMessage(err instanceof Error ? err.message : 'Login failed.'); … … 70 76 <div className="text-xs text-card-foreground whitespace-pre-wrap"> 71 77 {t('login_professor_example')} 78 </div> 79 <div className="mt-3 text-xs text-blue-600 font-medium"> 80 {t('login_demo_note')} 81 </div> 82 </div> 83 84 <div className="bg-yellow-500/10 border border-yellow-500/20 rounded-xl shadow-sm p-5"> 85 <div className="text-sm font-bold text-card-foreground mb-2">{t('login_admin_title')}</div> 86 <div className="text-xs text-card-foreground whitespace-pre-wrap"> 87 {t('login_admin_example')} 72 88 </div> 73 89 <div className="mt-3 text-xs text-blue-600 font-medium"> -
src/app/professor/profile/page.tsx
r4fe4582 r8496f3c 19 19 import { IconDefinition } from "@fortawesome/fontawesome-svg-core"; 20 20 import { useTranslation } from 'react-i18next'; 21 import { apiUrl } from '@/lib/api'; 21 22 22 23 type PersonalInfo = { … … 152 153 153 154 try { 154 const response = await fetch( "https://iknow-api.onrender.com/api/user/getUser", {155 const response = await fetch(apiUrl("/api/user/getUser"), { 155 156 method: "GET", 156 157 headers: { -
src/app/professor/students/page.tsx
r4fe4582 r8496f3c 3 3 import { useEffect, useMemo, useState } from "react"; 4 4 import { useTranslation } from 'react-i18next'; 5 import { apiUrl } from '@/lib/api'; 6 import { getAccessToken } from '@/lib/auth'; 5 7 6 8 type UsersBySubject = { 7 Id?: string; 8 Name?: string; 9 Grade: number; 9 /** users.id - what the grading endpoints expect as StudentId. */ 10 id: number; 11 name?: string; 12 /** users.index - the number shown to the professor and searched on. */ 13 index?: string; 14 grade: number; 15 semester?: string; 10 16 }; 11 17 12 18 type SubjectsAndUsers = { 13 Name?: string; 14 Id?: number; 15 Users: UsersBySubject[]; 19 name?: string; 20 id?: number; 21 code?: string; 22 users: UsersBySubject[]; 16 23 }; 17 24 … … 19 26 StudentId: number; 20 27 SubjectId: number; 21 grade: number;28 Grade: number; 22 29 }; 23 30 24 31 type FlatRow = { 25 studentId Str: string;26 studentI dNum: number | null;32 studentIdNum: number; 33 studentIndex: string; 27 34 studentName: string; 28 35 subjectId: number; 29 36 subjectName: string; 37 semester: string; 30 38 grade: number; 31 39 }; 32 33 function toInt(value: string): number | null {34 const n = Number.parseInt(value, 10);35 return Number.isFinite(n) ? n : null;36 }37 40 38 41 export default function ProfessorStudentsPage() { … … 52 55 setError(null); 53 56 try { 54 const res = await fetch("/api/prof/students", { cache: "no-store" }); 57 const token = getAccessToken(); 58 if (!token) { 59 throw new Error("You are not signed in."); 60 } 61 62 const res = await fetch(apiUrl("/api/prof/students"), { 63 cache: "no-store", 64 headers: { Authorization: `Bearer ${token}` }, 65 }); 55 66 if (!res.ok) { 56 67 throw new Error(`Failed to fetch students (${res.status})`); … … 61 72 const nextSelections: Record<string, number> = {}; 62 73 for (const subj of data) { 63 if (!subj. Id) continue;64 for (const u of subj. Users) {65 const key = `${subj. Id}:${u.Id ?? ""}`;66 const current = u.Grade;67 nextSelections[key] = current >= 5 && current <= 10 ? current : 5;74 if (!subj.id) continue; 75 for (const u of subj.users) { 76 const key = `${subj.id}:${u.id}`; 77 // grade_type only declares 6..10, so an ungraded row starts at 6. 78 nextSelections[key] = u.grade >= 6 && u.grade <= 10 ? u.grade : 6; 68 79 } 69 80 } … … 83 94 const rows: FlatRow[] = []; 84 95 for (const subj of subjects) { 85 if (!subj. Id) continue;86 for (const u of subj. Users) {96 if (!subj.id) continue; 97 for (const u of subj.users) { 87 98 rows.push({ 88 studentIdStr: u.Id ?? "", 89 studentIdNum: toInt(u.Id ?? ""), 90 studentName: u.Name ?? "", 91 subjectId: subj.Id, 92 subjectName: subj.Name ?? "", 93 grade: u.Grade, 99 studentIdNum: u.id, 100 studentIndex: u.index ?? "", 101 studentName: u.name ?? "", 102 subjectId: subj.id, 103 subjectName: subj.name ?? "", 104 semester: u.semester ?? "", 105 grade: u.grade, 94 106 }); 95 107 } … … 102 114 return flatRows.filter((r) => { 103 115 if (subjectFilter !== "all" && String(r.subjectId) !== subjectFilter) return false; 104 if (q.length > 0 && !r.studentI dStr.includes(q)) return false;116 if (q.length > 0 && !r.studentIndex.includes(q)) return false; 105 117 return true; 106 118 }); … … 111 123 setError(null); 112 124 try { 113 const res = await fetch(url, { 125 const token = getAccessToken(); 126 if (!token) { 127 throw new Error("You are not signed in."); 128 } 129 130 const res = await fetch(apiUrl(url), { 114 131 method: "POST", 115 headers: { "Content-Type": "application/json" }, 132 headers: { 133 "Content-Type": "application/json", 134 Authorization: `Bearer ${token}`, 135 }, 116 136 body: JSON.stringify(payload), 117 137 }); … … 135 155 <h1 className="text-2xl font-semibold text-card-foreground">{t('prof_students_title')}</h1> 136 156 <p className="text-muted-foreground mt-1"> 137 {t('prof_students_data_note')} <span className="font-mono">/api/prof/students</span> (demo store).157 {t('prof_students_data_note')} <span className="font-mono">/api/prof/students</span>. 138 158 </p> 139 159 </div> … … 149 169 <option value="all">{t('all_subjects')}</option> 150 170 {subjects 151 .filter((s) => typeof s. Id === "number")171 .filter((s) => typeof s.id === "number") 152 172 .map((s) => ( 153 <option key={String(s. Id)} value={String(s.Id)}>154 {s. Name ?? `${t('subject')} ${s.Id}`}173 <option key={String(s.id)} value={String(s.id)}> 174 {s.name ?? `${t('subject')} ${s.id}`} 155 175 </option> 156 176 ))} … … 202 222 ) : ( 203 223 filteredRows.map((r) => { 204 const key = `${r.subjectId}:${r.studentId Str}`;205 const selected = gradeSelection[key] ?? 5;224 const key = `${r.subjectId}:${r.studentIdNum}`; 225 const selected = gradeSelection[key] ?? 6; 206 226 const busy = actionBusyKey === key; 207 227 … … 209 229 <tr key={key} className="hover:bg-accent"> 210 230 <td className="px-4 py-3 border-b text-card-foreground">{r.studentName}</td> 211 <td className="px-4 py-3 border-b text-card-foreground">{r.studentIdStr}</td> 212 <td className="px-4 py-3 border-b text-card-foreground">{r.subjectName}</td> 231 <td className="px-4 py-3 border-b text-card-foreground">{r.studentIndex}</td> 232 <td className="px-4 py-3 border-b text-card-foreground"> 233 {r.subjectName} 234 {r.semester && ( 235 <span className="block text-xs text-muted-foreground">{r.semester}</span> 236 )} 237 </td> 213 238 <td className="px-4 py-3 border-b"> 214 239 <div className="flex items-center gap-3"> … … 223 248 } 224 249 > 225 {[ 5,6, 7, 8, 9, 10].map((g) => (250 {[6, 7, 8, 9, 10].map((g) => ( 226 251 <option key={g} value={g}> 227 252 {g} … … 237 262 <div className="flex flex-wrap gap-2"> 238 263 <button 239 disabled={busy || r.studentIdNum === null}264 disabled={busy} 240 265 className="px-3 py-2 rounded-lg bg-green-600 text-white text-sm font-medium disabled:opacity-50" 241 266 onClick={() => { 242 if (r.studentIdNum === null) return;243 267 void postGrade( 244 268 "/api/prof/grade/add", 245 { StudentId: r.studentIdNum, SubjectId: r.subjectId, grade: selected },269 { StudentId: r.studentIdNum, SubjectId: r.subjectId, Grade: selected }, 246 270 key, 247 271 ); … … 252 276 253 277 <button 254 disabled={busy || r.studentIdNum === null}278 disabled={busy} 255 279 className="px-3 py-2 rounded-lg bg-blue-600 text-white text-sm font-medium disabled:opacity-50" 256 280 onClick={() => { 257 if (r.studentIdNum === null) return;258 281 void postGrade( 259 282 "/api/prof/grade/edit", 260 { StudentId: r.studentIdNum, SubjectId: r.subjectId, grade: selected },283 { StudentId: r.studentIdNum, SubjectId: r.subjectId, Grade: selected }, 261 284 key, 262 285 ); … … 267 290 268 291 <button 269 disabled={busy || r.studentIdNum === null}292 disabled={busy} 270 293 className="px-3 py-2 rounded-lg bg-gray-800 text-white text-sm font-medium disabled:opacity-50" 271 294 onClick={() => { 272 if (r.studentIdNum === null) return;273 295 void postGrade( 274 296 "/api/prof/grade/remove", 275 { StudentId: r.studentIdNum, SubjectId: r.subjectId, grade: 0 },297 { StudentId: r.studentIdNum, SubjectId: r.subjectId, Grade: 0 }, 276 298 key, 277 299 ); -
src/app/students/profile/page.tsx
r4fe4582 r8496f3c 21 21 import { IconDefinition } from '@fortawesome/fontawesome-svg-core'; 22 22 import { useTranslation } from 'react-i18next'; 23 import { apiUrl } from '@/lib/api'; 23 24 24 25 type PersonalInfo = { … … 154 155 155 156 try { 156 const response = await fetch( 'https://iknow-api.onrender.com/api/user/getUser', {157 const response = await fetch(apiUrl('/api/user/getUser'), { 157 158 method: 'GET', 158 159 headers: { -
src/app/students/semesters/page.tsx
r4fe4582 r8496f3c 15 15 import { getAccessToken } from '@/lib/auth'; 16 16 import { useTranslation } from 'react-i18next'; 17 import { apiUrl } from '@/lib/api'; 18 import EnrollSemesterDialog from '@/components/enroll-semester-dialog'; 17 19 18 20 type Semester = { … … 113 115 const [isLoading, setIsLoading] = useState(true); 114 116 const [errorMessage, setErrorMessage] = useState<string | null>(null); 117 const [enrollOpen, setEnrollOpen] = useState(false); 118 // Bumped after a successful enrolment so the table reloads. 119 const [reloadKey, setReloadKey] = useState(0); 115 120 const { t } = useTranslation(); 116 121 … … 130 135 131 136 try { 132 const response = await fetch( 'https://iknow-api.onrender.com/api/user/getSemesters', {137 const response = await fetch(apiUrl('/api/user/getSemesters'), { 133 138 method: 'GET', 134 139 headers: { … … 158 163 cancelled = true; 159 164 }; 160 }, [ ]);165 }, [reloadKey]); 161 166 162 167 if (isLoading) { … … 184 189 {/* Header */} 185 190 <div className="bg-primary text-white rounded-xl p-8 mb-8"> 186 <div className="flex items-center gap-4"> 187 <div className="bg-white rounded-full p-4"> 188 <FontAwesomeIcon icon={faCalendarAlt} className="text-3xl text-[#0272D1]" /> 191 <div className="flex flex-col gap-4 md:flex-row md:items-center md:justify-between"> 192 <div className="flex items-center gap-4"> 193 <div className="bg-white rounded-full p-4"> 194 <FontAwesomeIcon icon={faCalendarAlt} className="text-3xl text-[#0272D1]" /> 195 </div> 196 <div> 197 <h1 className="text-3xl font-bold mb-2">{t('semesters')}</h1> 198 <p className="text-lg opacity-90"> 199 {t('semesters_overview')} 200 </p> 201 </div> 189 202 </div> 190 <div> 191 <h1 className="text-3xl font-bold mb-2">{t('semesters')}</h1> 192 <p className="text-lg opacity-90"> 193 {t('semesters_overview')} 194 </p> 195 </div> 203 204 <button 205 className="self-start rounded-lg bg-white px-5 py-3 font-semibold text-[#0272D1] shadow-sm hover:bg-white/90 md:self-auto" 206 onClick={() => setEnrollOpen(true)} 207 > 208 + {t('enroll_semester')} 209 </button> 196 210 </div> 197 211 </div> … … 373 387 </div> 374 388 </div> 389 390 <EnrollSemesterDialog 391 open={enrollOpen} 392 onClose={() => setEnrollOpen(false)} 393 onEnrolled={() => setReloadKey((k) => k + 1)} 394 /> 375 395 </div> 376 396 ); -
src/app/students/subjects/page.tsx
r4fe4582 r8496f3c 10 10 import { getAccessToken } from '@/lib/auth'; 11 11 import { useTranslation } from 'react-i18next'; 12 import { apiUrl } from '@/lib/api'; 12 13 13 14 interface Subject { … … 193 194 194 195 try { 195 const response = await fetch( 'https://iknow-api.onrender.com/api/user/getSubjects', {196 const response = await fetch(apiUrl('/api/user/getSubjects'), { 196 197 method: 'GET', 197 198 headers: { -
src/components/exams.tsx
r4fe4582 r8496f3c 5 5 import { faSort, faSortUp, faSortDown } from '@fortawesome/free-solid-svg-icons'; 6 6 import { getAccessToken } from '@/lib/auth'; 7 import { apiUrl } from '@/lib/api'; 7 8 8 9 type PassedSubject = { … … 56 57 57 58 try { 58 const response = await fetch( 'https://iknow-api.onrender.com/api/user/getPassedSubjects', {59 const response = await fetch(apiUrl('/api/user/getPassedSubjects'), { 59 60 method: 'GET', 60 61 headers: { -
src/components/header.tsx
r4fe4582 r8496f3c 9 9 import { useTranslation } from 'react-i18next'; 10 10 import LanguageSwitcher from './LanguageSwitcher'; 11 import { apiUrl } from '@/lib/api'; 11 12 12 13 const Header = () => { … … 20 21 if (!token) return; 21 22 try { 22 const response = await fetch( 'https://iknow-api.onrender.com/api/user/getUser', {23 const response = await fetch(apiUrl('/api/user/getUser'), { 23 24 method: 'GET', 24 25 headers: { Authorization: `Bearer ${token}` }, -
src/lib/auth.ts
r4fe4582 r8496f3c 1 import { API_BASE_URL } from './api'; 1 2 export type AuthTokens = { 2 3 accessToken: string; … … 4 5 }; 5 6 6 export type UserRole = 'Professor' | 'Student' | string;7 export type UserRole = 'Professor' | 'Student' | 'Admin' | string; 7 8 8 9 export type AuthSession = AuthTokens & { … … 91 92 92 93 export async function login(params: { email: string; password: string }): Promise<AuthSession> { 93 const baseUrl = process.env.NEXT_PUBLIC_API_BASE_URL ?? 'https://iknow-api.onrender.com';94 const baseUrl = API_BASE_URL; 94 95 95 96 const response = await fetch(`${baseUrl}/api/auth/login`, { -
src/lib/pdf-generators.ts
r4fe4582 r8496f3c 2 2 import autoTable from 'jspdf-autotable'; 3 3 import { loadCyrillicFonts } from './pdf-fonts'; 4 import { apiUrl } from './api'; 4 5 5 6 /* ---------- shared types ---------- */ … … 1056 1057 ): Promise<void> { 1057 1058 // Fetch student profile 1058 const profileRes = await fetch( 'https://iknow-api.onrender.com/api/user/getUser', {1059 const profileRes = await fetch(apiUrl('/api/user/getUser'), { 1059 1060 headers: { Authorization: `Bearer ${accessToken}` }, 1060 1061 }); … … 1095 1096 let passedExams: PassedExam[] = []; 1096 1097 if (needsExams) { 1097 const examsRes = await fetch( 'https://iknow-api.onrender.com/api/user/getPassedSubjects', {1098 const examsRes = await fetch(apiUrl('/api/user/getPassedSubjects'), { 1098 1099 headers: { Authorization: `Bearer ${accessToken}` }, 1099 1100 }); -
src/locales/en/translation.json
r4fe4582 r8496f3c 200 200 "applications_important_note_text": "The serial number of the electronic application must be entered on the paper application that the student will submit physically.", 201 201 "login_student_title": "Login for Student", 202 "login_student_example": "Email: boris@example.com\nPassword: your-password-here",202 "login_student_example": "Email: stefan.saveski@students.finki.ukim.mk\nPassword: Test12345!", 203 203 "login_professor_title": "Login for Professor", 204 "login_professor_example": "Email: andonov@gmail.com\nPassword: 123lol456", 204 "login_professor_example": "Email: vangel.ajanovski@finki.ukim.mk\nPassword: Test12345!", 205 "login_admin_title": "Login for Administrator", 206 "login_admin_example": "Email: igor.adminovski@finki.ukim.mk\nPassword: Test12345!", 205 207 "login_welcome": "Welcome", 206 208 "login_subtitle": "Sign in to your IKnow account", … … 257 259 "remove_grade": "Remove Grade", 258 260 "current": "Current", 259 "login_demo_note": "Use these credentials to explore the system." 261 "login_demo_note": "Use these credentials to explore the system.", 262 "enroll_semester": "Enroll semester", 263 "enroll_confirm": "Enroll", 264 "enroll_no_semesters": "You are already enrolled in every available semester.", 265 "enroll_already_passed": "passed", 266 "enroll_credits": "credits", 267 "enroll_semester_short": "sem.", 268 "enroll_not_signed_in": "You are not signed in.", 269 "enroll_options_failed": "Could not load enrollment options", 270 "enroll_failed": "Enrollment failed", 271 "cancel": "Cancel", 272 "admin_portal": "Admin", 273 "admin_subjects": "Subjects", 274 "admin_semesters": "Semesters", 275 "admin_schedule": "Teaching schedule", 276 "admin_subjects_intro": "Add subjects, map them to study programmes and set prerequisites.", 277 "admin_semesters_intro": "Open an active semester so students can enroll in it.", 278 "admin_schedule_intro": "Decide which professor teaches which subject in which semester.", 279 "admin_new_subject": "New subject", 280 "admin_edit_subject": "Edit subject", 281 "admin_subject_name": "Name", 282 "admin_subject_code": "Code", 283 "admin_credits": "Credits", 284 "admin_dependency_credit": "Required credits", 285 "admin_create": "Create", 286 "admin_save": "Save", 287 "admin_delete": "Delete", 288 "admin_remove": "Remove", 289 "admin_add": "Add", 290 "admin_manage": "Manage", 291 "admin_close": "Close", 292 "admin_majors": "Study programmes", 293 "admin_prerequisites": "Prerequisites", 294 "admin_pick_major": "Pick a study programme", 295 "admin_pick_prerequisite": "Pick a prerequisite", 296 "admin_pick_subject": "Pick a subject", 297 "admin_pick_professor": "Pick a professor", 298 "admin_search_subject": "Search by name or code", 299 "admin_no_subjects": "No subjects found.", 300 "admin_delete_blocked": "Cannot delete: {{count}} enrolment(s) already contain this subject.", 301 "admin_new_semester": "Open a new semester", 302 "admin_year": "Year", 303 "admin_type": "Type", 304 "admin_winter": "Winter", 305 "admin_summer": "Summer", 306 "admin_open_semester": "Open semester", 307 "admin_open_semesters": "Open semesters", 308 "admin_no_semesters": "No semesters yet.", 309 "admin_enrolments": "Enrolments", 310 "admin_coverage": "Coverage", 311 "admin_fully_covered": "all subjects covered", 312 "admin_uncovered_count": "{{count}} subject(s) without a professor", 313 "admin_uncovered_hint": "Students cannot enroll in a subject nobody teaches that semester.", 314 "admin_show_uncovered": "Show", 315 "admin_professor": "Professor", 316 "admin_assign": "Assign", 317 "admin_assignments": "Assignments", 318 "admin_load": "Teaching load", 319 "admin_no_assignments": "Nothing assigned yet.", 320 "admin_no_professors": "No professors." 260 321 } -
src/locales/mk/translation.json
r4fe4582 r8496f3c 231 231 "applications_important_note_text": "Серискиот број на електронската пријава задолжително треба да се впише на хартиената пријава која студентот ќе ја поднесе физички.", 232 232 "login_student_title": "Најава за студент", 233 "login_student_example": "Е-пошта: boris@example.com\nЛозинка: your-password-here",233 "login_student_example": "Е-пошта: stefan.saveski@students.finki.ukim.mk\nЛозинка: Test12345!", 234 234 "login_professor_title": "Најава за професор", 235 "login_professor_example": "Е-пошта: andonov@gmail.com\nЛозинка: 123lol456", 235 "login_professor_example": "Е-пошта: vangel.ajanovski@finki.ukim.mk\nЛозинка: Test12345!", 236 "login_admin_title": "Најава за администратор", 237 "login_admin_example": "Е-пошта: igor.adminovski@finki.ukim.mk\nЛозинка: Test12345!", 236 238 "login_welcome": "Добредојде", 237 239 "login_subtitle": "Најавете се во вашиот IKnow акаунт", … … 261 263 "contact_sent_title": "Пораката е испратена!", 262 264 "contact_sent_desc": "Ќе ви одговориме наскоро.", 263 "login_demo_note": "Користете ги овие податоци за да го истражите системот." 265 "login_demo_note": "Користете ги овие податоци за да го истражите системот.", 266 "enroll_semester": "Запиши семестар", 267 "enroll_confirm": "Запиши", 268 "enroll_no_semesters": "Веќе сте запишани во сите достапни семестри.", 269 "enroll_already_passed": "положен", 270 "enroll_credits": "кредити", 271 "enroll_semester_short": "сем.", 272 "enroll_not_signed_in": "Не сте најавени.", 273 "enroll_options_failed": "Не може да се вчитаат опциите за запишување", 274 "enroll_failed": "Запишувањето не успеа", 275 "cancel": "Откажи", 276 "admin_portal": "Админ", 277 "admin_subjects": "Предмети", 278 "admin_semesters": "Семестри", 279 "admin_schedule": "Распоред", 280 "admin_subjects_intro": "Додавање предмети, поврзување со студиски програми и предуслови.", 281 "admin_semesters_intro": "Отворање активен семестар во кој студентите можат да се запишат.", 282 "admin_schedule_intro": "Определување кој професор кој предмет го предава.", 283 "admin_new_subject": "Нов предмет", 284 "admin_edit_subject": "Измена на предмет", 285 "admin_subject_name": "Назив", 286 "admin_subject_code": "Шифра", 287 "admin_credits": "Кредити", 288 "admin_dependency_credit": "Потребни кредити", 289 "admin_create": "Креирај", 290 "admin_save": "Зачувај", 291 "admin_delete": "Избриши", 292 "admin_remove": "Отстрани", 293 "admin_add": "Додади", 294 "admin_manage": "Уреди", 295 "admin_close": "Затвори", 296 "admin_majors": "Студиски програми", 297 "admin_prerequisites": "Предуслови", 298 "admin_pick_major": "Избери студиска програма", 299 "admin_pick_prerequisite": "Избери предуслов", 300 "admin_pick_subject": "Избери предмет", 301 "admin_pick_professor": "Избери професор", 302 "admin_search_subject": "Пребарај по назив или шифра", 303 "admin_no_subjects": "Нема предмети.", 304 "admin_delete_blocked": "Не може да се избрише: {{count}} запишувања го содржат предметот.", 305 "admin_new_semester": "Отвори нов семестар", 306 "admin_year": "Година", 307 "admin_type": "Тип", 308 "admin_winter": "Зимски", 309 "admin_summer": "Летен", 310 "admin_open_semester": "Отвори семестар", 311 "admin_open_semesters": "Отворени семестри", 312 "admin_no_semesters": "Нема семестри.", 313 "admin_enrolments": "Запишувања", 314 "admin_coverage": "Покриеност", 315 "admin_fully_covered": "сите предмети се покриени", 316 "admin_uncovered_count": "{{count}} предмети без професор", 317 "admin_uncovered_hint": "Студентите не можат да запишат предмет кој никој не го предава.", 318 "admin_show_uncovered": "Прикажи", 319 "admin_professor": "Професор", 320 "admin_assign": "Распореди", 321 "admin_assignments": "Распоред", 322 "admin_load": "Оптовареност", 323 "admin_no_assignments": "Секој нема распоред.", 324 "admin_no_professors": "Нема професори." 264 325 }
Note:
See TracChangeset
for help on using the changeset viewer.
