source: frontend/src/app/students/semesters/page.tsx

Last change on this file was b8093a0, checked in by imbrsk <boris696boris@…>, 4 days ago

Merge iknow-remaster into frontend/

Bring the Next.js frontend into this repository as a monorepo subdirectory,
preserving its full commit history via a subtree merge.

  • Property mode set to 100644
File size: 16.0 KB
RevLine 
[636f86c]1"use client"
2
3import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
4import {
5 faCalendarAlt,
6 faCheck,
7 faTimes,
8 faFileAlt,
9 faMoneyBillWave,
10 faSignature,
11 faCheckCircle,
[82c9c53]12 faTimesCircle
[636f86c]13} from '@fortawesome/free-solid-svg-icons';
[b67f274]14import { useEffect, useState } from 'react';
15import { getAccessToken } from '@/lib/auth';
[50f2fb4]16import { useTranslation } from 'react-i18next';
[8496f3c]17import { apiUrl } from '@/lib/api';
18import EnrollSemesterDialog from '@/components/enroll-semester-dialog';
[b67f274]19
20type Semester = {
21 id: number | string;
22 semester: string;
23 direction: string;
24 quota: string;
25 note: string;
26 studentCom: string;
27 sum: string;
28 paid: string;
29 ukim: string;
30 createdOn: string;
31 dateChanged: string;
32 credits: string;
33 type: string;
34 doc: string;
35 doc1: string;
36 verified: string;
37 taxes: string;
38 signatures: string;
39 status: string;
40 completed: string;
41};
42
43type SemestersResponse = {
44 semesters: Semester[];
45};
[636f86c]46
47interface TableCellProps {
48 children: React.ReactNode;
49 className?: string;
50}
51
52const TableCell = ({ children, className = "" }: TableCellProps) => (
[ba52069]53 <td className={`px-4 py-3 text-sm border-b border-border ${className}`}>
[636f86c]54 {children}
55 </td>
56);
57
58const StatusBadge = ({ status }: { status: string }) => {
[50f2fb4]59 const { t } = useTranslation();
[636f86c]60 const baseClasses = "px-2 py-1 rounded-full text-xs font-medium";
[4fe4582]61 if (status === 'Валидно') {
[636f86c]62 return (
63 <span className={`${baseClasses} bg-green-100 text-green-800 flex items-center gap-1`}>
64 <FontAwesomeIcon icon={faCheckCircle} className="w-3 h-3" />
[4fe4582]65 {t('Валидно', 'Валидно')}
[636f86c]66 </span>
67 );
68 }
69 return (
[ba52069]70 <span className={`${baseClasses} bg-accent text-gray-800`}>
[4fe4582]71 {t(status, status)}
[636f86c]72 </span>
73 );
74};
75
76const YesNoBadge = ({ value }: { value: string }) => {
[50f2fb4]77 const { t } = useTranslation();
[4fe4582]78 if (value === 'Да') {
[636f86c]79 return (
80 <span className="inline-flex items-center justify-center w-6 h-6 bg-green-100 text-green-600 rounded-full">
81 <FontAwesomeIcon icon={faCheck} className="w-3 h-3" />
82 </span>
83 );
[4fe4582]84 } else if (value === 'Не') {
[636f86c]85 return (
86 <span className="inline-flex items-center justify-center w-6 h-6 bg-red-100 text-red-600 rounded-full">
87 <FontAwesomeIcon icon={faTimes} className="w-3 h-3" />
88 </span>
89 );
90 }
[ba52069]91 return <span className="text-muted-foreground">—</span>;
[636f86c]92};
93
94const SignatureBadge = ({ signatures }: { signatures: string }) => {
95 const [completed, total] = signatures.split('/').map(Number);
96 const percentage = total > 0 ? (completed / total) * 100 : 0;
97
98 let colorClass = "text-red-600 bg-red-100";
99 if (percentage === 100) {
100 colorClass = "text-green-600 bg-green-100";
101 } else if (percentage >= 50) {
102 colorClass = "text-yellow-600 bg-yellow-100";
103 }
104
105 return (
106 <span className={`px-2 py-1 rounded-full text-xs font-medium flex items-center gap-1 ${colorClass}`}>
107 <FontAwesomeIcon icon={faSignature} className="w-3 h-3" />
108 {signatures}
109 </span>
110 );
111};
112
113export default function SemestersPage() {
[b67f274]114 const [semesters, setSemesters] = useState<Semester[]>([]);
115 const [isLoading, setIsLoading] = useState(true);
116 const [errorMessage, setErrorMessage] = useState<string | null>(null);
[8496f3c]117 const [enrollOpen, setEnrollOpen] = useState(false);
118 // Bumped after a successful enrolment so the table reloads.
119 const [reloadKey, setReloadKey] = useState(0);
[50f2fb4]120 const { t } = useTranslation();
[b67f274]121
122 useEffect(() => {
123 let cancelled = false;
124
125 async function load() {
126 setIsLoading(true);
127 setErrorMessage(null);
128
129 const token = getAccessToken();
130 if (!token) {
131 setErrorMessage('Not authenticated. Please login again.');
132 setIsLoading(false);
133 return;
134 }
135
136 try {
[8496f3c]137 const response = await fetch(apiUrl('/api/user/getSemesters'), {
[b67f274]138 method: 'GET',
139 headers: {
140 Authorization: `Bearer ${token}`,
141 },
142 });
143
144 if (!response.ok) {
145 const text = await response.text().catch(() => '');
146 throw new Error(text || `Failed to load semesters (${response.status})`);
147 }
148
149 const data = (await response.json()) as SemestersResponse;
150 if (!cancelled) setSemesters(Array.isArray(data?.semesters) ? data.semesters : []);
151 } catch (err) {
152 if (!cancelled) {
153 setErrorMessage(err instanceof Error ? err.message : 'Failed to load semesters.');
154 }
155 } finally {
156 if (!cancelled) setIsLoading(false);
157 }
158 }
159
160 void load();
161
162 return () => {
163 cancelled = true;
164 };
[8496f3c]165 }, [reloadKey]);
[b67f274]166
167 if (isLoading) {
168 return (
169 <div className="min-h-screen pb-8">
[ba52069]170 <div className="bg-card rounded-xl shadow-sm border border-border p-6">
[50f2fb4]171 {t('loading')}
[b67f274]172 </div>
173 </div>
174 );
175 }
176
177 if (errorMessage) {
178 return (
179 <div className="min-h-screen pb-8">
180 <div className="rounded-lg border border-red-200 bg-red-50 px-4 py-3 text-sm text-red-700">
181 {errorMessage}
182 </div>
183 </div>
184 );
185 }
[636f86c]186
187 return (
188 <div className="min-h-screen pb-8">
189 {/* Header */}
190 <div className="bg-primary text-white rounded-xl p-8 mb-8">
[8496f3c]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>
[636f86c]202 </div>
[8496f3c]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>
[636f86c]210 </div>
211 </div>
212
213 {/* Table Container */}
[ba52069]214 <div className="bg-card rounded-xl shadow-sm border border-border overflow-hidden">
[636f86c]215 <div className="bg-primary text-white px-6 py-4">
[50f2fb4]216 <h2 className="text-xl font-bold">{t('semesters_list')}</h2>
[636f86c]217 </div>
218
219 <div className="overflow-x-auto">
220 <table className="w-full">
[ba52069]221 <thead className="bg-accent">
[636f86c]222 <tr>
[ba52069]223 <th className="px-4 py-4 text-left text-xs font-medium text-muted-foreground uppercase tracking-wider">#</th>
224 <th className="px-4 py-4 text-left text-xs font-medium text-muted-foreground uppercase tracking-wider">{t('semester')}</th>
225 <th className="px-4 py-4 text-left text-xs font-medium text-muted-foreground uppercase tracking-wider">{t('direction')}</th>
226 <th className="px-4 py-4 text-left text-xs font-medium text-muted-foreground uppercase tracking-wider">{t('quota')}</th>
227 <th className="px-4 py-4 text-left text-xs font-medium text-muted-foreground uppercase tracking-wider">{t('note')}</th>
228 <th className="px-4 py-4 text-left text-xs font-medium text-muted-foreground uppercase tracking-wider">{t('student_com')}</th>
229 <th className="px-4 py-4 text-left text-xs font-medium text-muted-foreground uppercase tracking-wider">{t('sum')}</th>
230 <th className="px-4 py-4 text-left text-xs font-medium text-muted-foreground uppercase tracking-wider">{t('paid')}</th>
231 <th className="px-4 py-4 text-left text-xs font-medium text-muted-foreground uppercase tracking-wider">{t('ukim')}</th>
232 <th className="px-4 py-4 text-left text-xs font-medium text-muted-foreground uppercase tracking-wider">{t('created_on')}</th>
233 <th className="px-4 py-4 text-left text-xs font-medium text-muted-foreground uppercase tracking-wider">{t('date_changed')}</th>
234 <th className="px-4 py-4 text-left text-xs font-medium text-muted-foreground uppercase tracking-wider">{t('credits')}</th>
235 <th className="px-4 py-4 text-left text-xs font-medium text-muted-foreground uppercase tracking-wider">{t('type')}</th>
236 <th className="px-4 py-4 text-left text-xs font-medium text-muted-foreground uppercase tracking-wider">{t('doc')}</th>
237 <th className="px-4 py-4 text-left text-xs font-medium text-muted-foreground uppercase tracking-wider">{t('doc1')}</th>
238 <th className="px-4 py-4 text-left text-xs font-medium text-muted-foreground uppercase tracking-wider">{t('verified')}</th>
239 <th className="px-4 py-4 text-left text-xs font-medium text-muted-foreground uppercase tracking-wider">{t('taxes')}</th>
240 <th className="px-4 py-4 text-left text-xs font-medium text-muted-foreground uppercase tracking-wider">{t('signatures')}</th>
241 <th className="px-4 py-4 text-left text-xs font-medium text-muted-foreground uppercase tracking-wider">{t('status')}</th>
242 <th className="px-4 py-4 text-left text-xs font-medium text-muted-foreground uppercase tracking-wider">{t('completed')}</th>
[636f86c]243 </tr>
244 </thead>
[ba52069]245 <tbody className="divide-y divide-border">
[82c9c53]246 {semesters.map((semester) => (
[ba52069]247 <tr key={semester.id} className="hover:bg-accent transition-colors">
248 <TableCell className="font-medium text-card-foreground">{semester.id}</TableCell>
[636f86c]249 <TableCell className="font-medium text-primary">{semester.semester}</TableCell>
250 <TableCell>{semester.direction}</TableCell>
251 <TableCell className="max-w-xs">
252 <div className="truncate" title={semester.quota}>
253 {semester.quota}
254 </div>
255 </TableCell>
256 <TableCell>
257 {semester.note ? (
258 <span className="text-yellow-600 font-medium" title={semester.note}>
259 {semester.note.length > 10 ? `${semester.note.substring(0, 10)}...` : semester.note}
260 </span>
261 ) : (
[ba52069]262 <span className="text-muted-foreground">—</span>
[636f86c]263 )}
264 </TableCell>
265 <TableCell>
266 {semester.studentCom ? (
267 <span className="text-blue-600 font-medium" title={semester.studentCom}>
268 {semester.studentCom.length > 10 ? `${semester.studentCom.substring(0, 10)}...` : semester.studentCom}
269 </span>
270 ) : (
[ba52069]271 <span className="text-muted-foreground">—</span>
[636f86c]272 )}
273 </TableCell>
274 <TableCell className="font-medium">
275 {semester.sum ? (
276 <span className="text-green-600 flex items-center gap-1">
277 <FontAwesomeIcon icon={faMoneyBillWave} className="w-3 h-3" />
278 {semester.sum}
279 </span>
280 ) : (
[ba52069]281 <span className="text-muted-foreground">—</span>
[636f86c]282 )}
283 </TableCell>
284 <TableCell className="font-medium">
285 {semester.paid ? (
286 <span className="text-green-600 flex items-center gap-1">
287 <FontAwesomeIcon icon={faMoneyBillWave} className="w-3 h-3" />
288 {semester.paid}
289 </span>
290 ) : (
[ba52069]291 <span className="text-muted-foreground">—</span>
[636f86c]292 )}
293 </TableCell>
294 <TableCell className="font-medium">
295 {semester.ukim ? (
296 <span className="text-blue-600">{semester.ukim}</span>
297 ) : (
[ba52069]298 <span className="text-muted-foreground">—</span>
[636f86c]299 )}
300 </TableCell>
[ba52069]301 <TableCell className="text-muted-foreground">{semester.createdOn}</TableCell>
302 <TableCell className="text-muted-foreground">{semester.dateChanged}</TableCell>
[636f86c]303 <TableCell className="font-medium text-primary">{semester.credits}</TableCell>
304 <TableCell>
305 <span className="px-2 py-1 bg-blue-100 text-blue-800 rounded-full text-xs font-medium">
306 {semester.type}
307 </span>
308 </TableCell>
309 <TableCell className="text-center">
310 <YesNoBadge value={semester.doc} />
311 </TableCell>
312 <TableCell className="text-center">
313 <YesNoBadge value={semester.doc1} />
314 </TableCell>
315 <TableCell className="text-center">
316 <YesNoBadge value={semester.verified} />
317 </TableCell>
318 <TableCell className="font-medium text-green-600">{semester.taxes}</TableCell>
319 <TableCell>
320 <SignatureBadge signatures={semester.signatures} />
321 </TableCell>
322 <TableCell>
323 <StatusBadge status={semester.status} />
324 </TableCell>
325 <TableCell>
326 {semester.completed !== "Не" ? (
327 <span className="text-green-600 font-medium flex items-center gap-1">
328 <FontAwesomeIcon icon={faCheckCircle} className="w-3 h-3" />
[4fe4582]329 {t(semester.completed, semester.completed)}
[636f86c]330 </span>
331 ) : (
332 <span className="text-red-600 font-medium flex items-center gap-1">
333 <FontAwesomeIcon icon={faTimesCircle} className="w-3 h-3" />
[4fe4582]334 {t('Не', 'Не')}
[636f86c]335 </span>
336 )}
337 </TableCell>
338 </tr>
339 ))}
340 </tbody>
341 </table>
342 </div>
343 </div>
344
345 {/* Summary Cards */}
346 <div className="grid grid-cols-1 md:grid-cols-3 gap-6 mt-8">
[ba52069]347 <div className="bg-card rounded-xl p-6 shadow-sm border border-border">
[636f86c]348 <div className="flex items-center gap-4">
349 <div className="bg-green-100 text-green-600 rounded-full p-3">
350 <FontAwesomeIcon icon={faCheckCircle} className="text-xl" />
351 </div>
352 <div>
[ba52069]353 <h3 className="text-lg font-bold text-card-foreground">{t('completed_semesters')}</h3>
[636f86c]354 <p className="text-2xl font-bold text-green-600">
355 {semesters.filter(s => s.completed !== "Не").length}
356 </p>
357 </div>
358 </div>
359 </div>
360
[ba52069]361 <div className="bg-card rounded-xl p-6 shadow-sm border border-border">
[636f86c]362 <div className="flex items-center gap-4">
363 <div className="bg-blue-100 text-blue-600 rounded-full p-3">
364 <FontAwesomeIcon icon={faFileAlt} className="text-xl" />
365 </div>
366 <div>
[ba52069]367 <h3 className="text-lg font-bold text-card-foreground">{t('verified_semesters')}</h3>
[636f86c]368 <p className="text-2xl font-bold text-blue-600">
369 {semesters.filter(s => s.verified === "Да").length}
370 </p>
371 </div>
372 </div>
373 </div>
374
[ba52069]375 <div className="bg-card rounded-xl p-6 shadow-sm border border-border">
[636f86c]376 <div className="flex items-center gap-4">
377 <div className="bg-primary text-white rounded-full p-3">
378 <FontAwesomeIcon icon={faCalendarAlt} className="text-xl" />
379 </div>
380 <div>
[ba52069]381 <h3 className="text-lg font-bold text-card-foreground">{t('total_semesters')}</h3>
[636f86c]382 <p className="text-2xl font-bold text-primary">
383 {semesters.length}
384 </p>
385 </div>
386 </div>
387 </div>
388 </div>
[8496f3c]389
390 <EnrollSemesterDialog
391 open={enrollOpen}
392 onClose={() => setEnrollOpen(false)}
393 onEnrolled={() => setReloadKey((k) => k + 1)}
394 />
[636f86c]395 </div>
396 );
397}
Note: See TracBrowser for help on using the repository browser.