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

Last change on this file was b8093a0, checked in by imbrsk <boris696boris@…>, 3 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: 19.9 KB
Line 
1"use client"
2
3import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
4import {
5 faFilePdf,
6 faChevronDown,
7 faCheckCircle,
8 faClock,
9 faMoneyBillWave,
10 faFileText,
11 faChevronLeft,
12 faChevronRight,
13 faAngleDoubleLeft,
14 faAngleDoubleRight,
15 faSpinner
16} from '@fortawesome/free-solid-svg-icons';
17import { useState } from 'react';
18import documentsData from '@/data/documents.json';
19import { useTranslation } from 'react-i18next';
20import { getAccessToken } from '@/lib/auth';
21import { downloadDocumentPDF } from '@/lib/pdf-generators';
22
23interface TableCellProps {
24 children: React.ReactNode;
25 className?: string;
26}
27
28const TableCell = ({ children, className = "" }: TableCellProps) => (
29 <td className={`px-4 py-3 text-sm border-b border-border ${className}`}>
30 {children}
31 </td>
32);
33
34const StatusBadge = ({ status }: { status: string }) => {
35 const { t } = useTranslation();
36 if (status === 'approved' || status === t('approved', 'Одобрено')) {
37 return (
38 <span className="inline-flex items-center justify-center w-8 h-8 bg-green-100 text-green-600 rounded-full">
39 <FontAwesomeIcon icon={faCheckCircle} className="w-5 h-5" />
40 </span>
41 );
42 }
43 if (status === 'pending') {
44 return (
45 <span className="inline-flex items-center gap-1 text-xs font-medium text-yellow-700 bg-yellow-100 px-2 py-1 rounded-full">
46 <FontAwesomeIcon icon={faClock} className="w-3 h-3" />
47 {t('pending', 'Во обработка')}
48 </span>
49 );
50 }
51 return (
52 <span className="inline-flex items-center justify-center w-8 h-8 bg-accent text-muted-foreground rounded-full">
53 <FontAwesomeIcon icon={faFileText} className="w-4 h-4" />
54 </span>
55 );
56};
57
58const PriceBadge = ({ price }: { price: number }) => {
59 const { t } = useTranslation();
60 if (price === 0) {
61 return <span className="font-medium text-green-600">{t('free', '0,00')}</span>;
62 }
63 return <span className="font-medium text-blue-600">{price.toFixed(2)}</span>;
64};
65
66interface DocumentRecord {
67 id: number;
68 archive: string;
69 date: string;
70 request: string;
71 price: number;
72 paid: string;
73 document: string;
74 payOnline: boolean;
75 status: string;
76 comment: string;
77}
78
79export default function DocumentsPage() {
80 const [selectedDocumentType, setSelectedDocumentType] = useState("select_document");
81 const [isDropdownOpen, setIsDropdownOpen] = useState(false);
82 const [comment, setComment] = useState("");
83 const [currentPage, setCurrentPage] = useState(1);
84 const [recordsPerPage, setRecordsPerPage] = useState(15);
85 const [downloadingId, setDownloadingId] = useState<number | null>(null);
86 const [documents, setDocuments] = useState<DocumentRecord[]>(documentsData.documents as DocumentRecord[]);
87 const [isSubmitting, setIsSubmitting] = useState(false);
88 const { t } = useTranslation();
89
90 const handleDownload = async (docId: number, request: string, archive: string, date: string) => {
91 const token = getAccessToken();
92 if (!token) {
93 alert(t('not_authenticated', 'Не сте најавени. Ве молиме најавете се повторно.'));
94 return;
95 }
96 setDownloadingId(docId);
97 try {
98 await downloadDocumentPDF(request, archive, date, token);
99 } catch (err) {
100 console.error('PDF generation error:', err);
101 alert(t('pdf_error', 'Грешка при генерирање на документот. Обидете се повторно.'));
102 } finally {
103 setDownloadingId(null);
104 }
105 };
106
107 const handleSubmit = () => {
108 if (selectedDocumentType === "select_document") return;
109
110 const docType = documentsData.documentTypes.find(d => d.id === selectedDocumentType);
111 if (!docType) return;
112
113 setIsSubmitting(true);
114
115 // Generate archive number (random 5-digit)
116 const archiveNumber = String(90000 + Math.floor(Math.random() * 10000));
117
118 // Current date in DD.MM.YYYY format
119 const now = new Date();
120 const dateStr = `${String(now.getDate()).padStart(2, '0')}.${String(now.getMonth() + 1).padStart(2, '0')}.${now.getFullYear()}`;
121
122 const newDoc: DocumentRecord = {
123 id: documents.length > 0 ? Math.max(...documents.map(d => d.id)) + 1 : 1,
124 archive: archiveNumber,
125 date: dateStr,
126 request: docType.name,
127 price: docType.price,
128 paid: "Не",
129 document: "Преземи",
130 payOnline: docType.price > 0,
131 status: "pending",
132 comment: comment,
133 };
134
135 setDocuments(prev => [newDoc, ...prev]);
136 setSelectedDocumentType("select_document");
137 setComment("");
138 setIsSubmitting(false);
139 alert(t('document_submitted', 'Барањето за документ е успешно поднесено!'));
140 };
141
142 const selectedDocument = documentsData.documentTypes.find(d => d.id === selectedDocumentType);
143 const { paymentInfo } = documentsData;
144
145 const totalPages = Math.ceil(documents.length / recordsPerPage);
146 const startIndex = (currentPage - 1) * recordsPerPage;
147 const endIndex = startIndex + recordsPerPage;
148 const currentDocuments = documents.slice(startIndex, endIndex);
149
150 return (
151 <div className="min-h-screen pb-8">
152 {/* Header */}
153 <div className="bg-primary text-white rounded-xl p-8 mb-8">
154 <div className="flex items-center gap-4">
155 <div className="bg-white rounded-full p-4">
156 <FontAwesomeIcon icon={faFilePdf} className="text-3xl text-[#0272D1]" />
157 </div>
158 <div>
159 <h1 className="text-3xl font-bold mb-2">{t('documents')}</h1>
160 <p className="text-lg opacity-90">
161 {t('documents_overview', 'Преглед на вашите документи и нивниот статус.')}
162 </p>
163 </div>
164 </div>
165 </div>
166
167 {/* Document Request Form */}
168 <div className="bg-card rounded-xl p-6 shadow-sm border border-border mb-8">
169 <h2 className="text-lg font-semibold text-card-foreground mb-6 flex items-center gap-2">
170 <FontAwesomeIcon icon={faFileText} className="text-primary" />
171 {t('new_document_request', 'Ново барање за документ')}
172 </h2>
173
174 <div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
175 {/* Document Type Selection */}
176 <div>
177 <label className="block text-sm font-medium text-muted-foreground mb-2">
178 {t('select_document', 'Избери документ')}:
179 </label>
180 <div className="relative">
181 <button
182 onClick={() => setIsDropdownOpen(!isDropdownOpen)}
183 className="w-full bg-card border border-border rounded-lg px-4 py-3 text-left focus:outline-none focus:ring-2 focus:ring-primary focus:border-primary"
184 >
185 <div className="flex items-center justify-between">
186 <span className="text-sm text-card-foreground truncate">
187 {selectedDocument ? t(selectedDocument.id) : t('select_document', 'Избери документ')}
188 </span>
189 <FontAwesomeIcon
190 icon={faChevronDown}
191 className={`w-4 h-4 text-muted-foreground transition-transform ml-2 flex-shrink-0 ${isDropdownOpen ? 'rotate-180' : ''}`}
192 />
193 </div>
194 </button>
195
196 {isDropdownOpen && (
197 <div className="absolute z-10 w-full mt-1 bg-card border border-border rounded-lg shadow-lg max-h-80 overflow-y-auto">
198 {documentsData.documentTypes.map((docType) => (
199 <button
200 key={docType.id}
201 onClick={() => {
202 setSelectedDocumentType(docType.id);
203 setIsDropdownOpen(false);
204 }}
205 className="w-full px-4 py-3 text-left text-sm hover:bg-accent focus:outline-none focus:bg-accent border-b border-border last:border-b-0"
206 >
207 <div className="font-medium text-card-foreground">{t(docType.id)}</div>
208 {docType.price > 0 && (
209 <div className="text-xs text-blue-600 mt-1">{t('price', 'Цена')}: {docType.price} мкд</div>
210 )}
211 </button>
212 ))}
213 </div>
214 )}
215 </div>
216 </div>
217
218 {/* Comment Section */}
219 <div>
220 <label className="block text-sm font-medium text-muted-foreground mb-2">
221 {t('comment', 'Коментар')}:
222 </label>
223 <textarea
224 value={comment}
225 onChange={(e) => setComment(e.target.value)}
226 rows={4}
227 className="w-full border border-border rounded-lg px-4 py-3 focus:outline-none focus:ring-2 focus:ring-primary focus:border-primary resize-none"
228 placeholder={t('add_comment', 'Додај коментар')}
229 />
230 </div>
231 </div>
232
233 <div className="flex justify-end mt-6">
234 <button
235 onClick={handleSubmit}
236 className="bg-primary hover:bg-blue-700 text-white font-medium py-2 px-6 rounded-lg transition-colors duration-200 flex items-center gap-2 disabled:opacity-50 disabled:cursor-not-allowed"
237 disabled={selectedDocumentType === "select_document" || isSubmitting}
238 >
239 <FontAwesomeIcon icon={isSubmitting ? faSpinner : faFileText} className={`w-4 h-4 ${isSubmitting ? 'animate-spin' : ''}`} />
240 {isSubmitting ? t('submitting', 'Се поднесува...') : t('submit', 'Поднеси')}
241 </button>
242 </div>
243 </div>
244
245 {/* Documents Table */}
246 <div className="bg-card rounded-xl shadow-sm border border-border overflow-hidden">
247 <div className="bg-primary text-white px-6 py-4">
248 <h2 className="text-xl font-bold">{t('my_documents', 'Мои документи')}</h2>
249 </div>
250
251 <div className="overflow-x-auto">
252 <table className="w-full">
253 <thead className="bg-accent">
254 <tr>
255 <th className="px-4 py-4 text-left text-xs font-medium text-muted-foreground uppercase tracking-wider">#</th>
256 <th className="px-4 py-4 text-left text-xs font-medium text-muted-foreground uppercase tracking-wider">{t('archive', 'Архива')}</th>
257 <th className="px-4 py-4 text-left text-xs font-medium text-muted-foreground uppercase tracking-wider">{t('date', 'Датум')}</th>
258 <th className="px-4 py-4 text-left text-xs font-medium text-muted-foreground uppercase tracking-wider">{t('request', 'Барање')}</th>
259 <th className="px-4 py-4 text-right text-xs font-medium text-muted-foreground uppercase tracking-wider">{t('price', 'Цена')}</th>
260 <th className="px-4 py-4 text-center text-xs font-medium text-muted-foreground uppercase tracking-wider">{t('paid', 'Платено')}</th>
261 <th className="px-4 py-4 text-center text-xs font-medium text-muted-foreground uppercase tracking-wider">{t('document', 'Документ')}</th>
262 <th className="px-4 py-4 text-center text-xs font-medium text-muted-foreground uppercase tracking-wider">{t('pay_online', 'Плати онлајн')}</th>
263 <th className="px-4 py-4 text-center text-xs font-medium text-muted-foreground uppercase tracking-wider">{t('status', 'Статус')}</th>
264 <th className="px-4 py-4 text-left text-xs font-medium text-muted-foreground uppercase tracking-wider">{t('comment', 'Коментар')}</th>
265 </tr>
266 </thead>
267 <tbody className="divide-y divide-border">
268 {currentDocuments.map((document) => (
269 <tr key={document.id} className="hover:bg-accent transition-colors">
270 <TableCell className="font-medium text-card-foreground">{document.id}</TableCell>
271 <TableCell className="font-mono text-sm text-primary font-medium">{document.archive}</TableCell>
272 <TableCell className="text-muted-foreground">{document.date}</TableCell>
273 <TableCell className="font-medium text-card-foreground max-w-xs">
274 {t(document.request)}
275 </TableCell>
276 <TableCell className="text-right">
277 <PriceBadge price={document.price} />
278 </TableCell>
279 <TableCell className="text-center">
280 <span className={`text-sm font-medium ${document.paid === 'ДА' ? 'text-green-600' : 'text-red-500'}`}>
281 {document.paid}
282 </span>
283 </TableCell>
284 <TableCell className="text-center">
285 <button
286 onClick={() => handleDownload(document.id, document.request, document.archive, document.date)}
287 disabled={downloadingId === document.id}
288 className="text-primary hover:text-blue-700 font-medium text-sm underline disabled:opacity-50 disabled:cursor-wait inline-flex items-center gap-1"
289 >
290 {downloadingId === document.id ? (
291 <>
292 <FontAwesomeIcon icon={faSpinner} className="w-3 h-3 animate-spin" />
293 {t('generating', 'Генерира...')}
294 </>
295 ) : (
296 t('download', document.document)
297 )}
298 </button>
299 </TableCell>
300 <TableCell className="text-center">
301 {document.payOnline ? (
302 <FontAwesomeIcon icon={faCheckCircle} className="w-5 h-5 text-green-600" />
303 ) : (
304 <span className="text-muted-foreground">{t('none', '—')}</span>
305 )}
306 </TableCell>
307 <TableCell className="text-center">
308 <StatusBadge status={document.status} />
309 </TableCell>
310 <TableCell>
311 {document.comment || (
312 <span className="text-muted-foreground">{t('none', '—')}</span>
313 )}
314 </TableCell>
315 </tr>
316 ))}
317 </tbody>
318 </table>
319 </div>
320
321 {/* Pagination */}
322 <div className="bg-accent px-6 py-4 flex items-center justify-between border-t border-border">
323 <div className="flex items-center gap-4 text-sm text-muted-foreground">
324 <div className="flex items-center gap-2">
325 <span>{t('show_rows', 'Прикажи редови')}:</span>
326 <select
327 value={recordsPerPage}
328 onChange={(e) => setRecordsPerPage(Number(e.target.value))}
329 className="border border-border rounded px-2 py-1 text-sm focus:outline-none focus:ring-1 focus:ring-primary"
330 >
331 <option value={15}>15</option>
332 <option value={25}>25</option>
333 <option value={50}>50</option>
334 </select>
335 </div>
336 <div>
337 {t('page', 'Страница')} <input
338 type="number"
339 min="1"
340 max={totalPages}
341 value={currentPage}
342 onChange={(e) => setCurrentPage(Number(e.target.value))}
343 className="w-12 border border-border rounded px-2 py-1 text-sm text-center focus:outline-none focus:ring-1 focus:ring-primary"
344 /> {t('of', 'од')} {totalPages}
345 </div>
346 </div>
347
348 <div className="flex items-center gap-2">
349 <button
350 onClick={() => setCurrentPage(1)}
351 disabled={currentPage === 1}
352 className="p-2 text-muted-foreground hover:text-primary disabled:opacity-50 disabled:cursor-not-allowed"
353 >
354 <FontAwesomeIcon icon={faAngleDoubleLeft} className="w-4 h-4" />
355 </button>
356 <button
357 onClick={() => setCurrentPage(Math.max(1, currentPage - 1))}
358 disabled={currentPage === 1}
359 className="p-2 text-muted-foreground hover:text-primary disabled:opacity-50 disabled:cursor-not-allowed"
360 >
361 <FontAwesomeIcon icon={faChevronLeft} className="w-4 h-4" />
362 </button>
363 <span className="px-4 py-2 bg-primary text-white rounded text-sm font-medium">
364 {t('first', 'Прва')}
365 </span>
366 <button
367 onClick={() => setCurrentPage(Math.min(totalPages, currentPage + 1))}
368 disabled={currentPage === totalPages}
369 className="p-2 text-muted-foreground hover:text-primary disabled:opacity-50 disabled:cursor-not-allowed"
370 >
371 <FontAwesomeIcon icon={faChevronRight} className="w-4 h-4" />
372 </button>
373 <button
374 onClick={() => setCurrentPage(totalPages)}
375 disabled={currentPage === totalPages}
376 className="p-2 text-muted-foreground hover:text-primary disabled:opacity-50 disabled:cursor-not-allowed"
377 >
378 <FontAwesomeIcon icon={faAngleDoubleRight} className="w-4 h-4" />
379 </button>
380 <span className="ml-4 text-sm text-muted-foreground">
381 {t('last', 'Последна')}
382 </span>
383 </div>
384
385 <div className="text-sm text-muted-foreground">
386 {t('total', 'Вкупно')}: {documents.length}
387 </div>
388 </div>
389
390 {/* Payment Info */}
391 <div className="bg-blue-50 border-t border-blue-100 p-4">
392 <div className="flex items-start gap-3">
393 <FontAwesomeIcon icon={faMoneyBillWave} className="w-5 h-5 text-blue-600 mt-0.5 flex-shrink-0" />
394 <p className="text-sm text-blue-800 leading-relaxed">
395 {t('documents_payment_info', paymentInfo)}
396 </p>
397 </div>
398 </div>
399 </div>
400
401 {/* Statistics Cards */}
402 <div className="grid grid-cols-1 md:grid-cols-3 gap-6 mt-8">
403 <div className="bg-card rounded-xl p-6 shadow-sm border border-border">
404 <div className="flex items-center gap-4">
405 <div className="bg-blue-100 text-blue-600 rounded-full p-3">
406 <FontAwesomeIcon icon={faFileText} className="text-xl" />
407 </div>
408 <div>
409 <h3 className="text-lg font-bold text-card-foreground">{t('total_documents', 'Вкупно документи')}</h3>
410 <p className="text-2xl font-bold text-blue-600">
411 {documents.length}
412 </p>
413 </div>
414 </div>
415 </div>
416
417 <div className="bg-card rounded-xl p-6 shadow-sm border border-border">
418 <div className="flex items-center gap-4">
419 <div className="bg-green-100 text-green-600 rounded-full p-3">
420 <FontAwesomeIcon icon={faCheckCircle} className="text-xl" />
421 </div>
422 <div>
423 <h3 className="text-lg font-bold text-card-foreground">{t('approved', 'Одобрени')}</h3>
424 <p className="text-2xl font-bold text-green-600">
425 {documents.filter(doc => doc.status === "approved").length}
426 </p>
427 </div>
428 </div>
429 </div>
430
431 <div className="bg-card rounded-xl p-6 shadow-sm border border-border">
432 <div className="flex items-center gap-4">
433 <div className="bg-yellow-100 text-yellow-600 rounded-full p-3">
434 <FontAwesomeIcon icon={faMoneyBillWave} className="text-xl" />
435 </div>
436 <div>
437 <h3 className="text-lg font-bold text-card-foreground">{t('total_price', 'Вкупна цена')}</h3>
438 <p className="text-2xl font-bold text-yellow-600">
439 {documents.reduce((sum, doc) => sum + doc.price, 0).toFixed(2)} {t('mkd', 'мкд')}
440 </p>
441 </div>
442 </div>
443 </div>
444 </div>
445 </div>
446 );
447}
Note: See TracBrowser for help on using the repository browser.