source: src/app/students/semesters/page.tsx@ 997d094

Last change on this file since 997d094 was 90f7842, checked in by Stefan-Saveski <stefansaveski19@…>, 9 months ago

fix: Update API endpoints to use production URL for user data retrieval and authentication

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