Index: frontend/src/lib/admin-api.ts
===================================================================
--- frontend/src/lib/admin-api.ts	(revision b8093a091d45f33bab33deb8ff2fffc4734972c0)
+++ frontend/src/lib/admin-api.ts	(revision b8093a091d45f33bab33deb8ff2fffc4734972c0)
@@ -0,0 +1,90 @@
+import { apiUrl } from "@/lib/api";
+import { getAccessToken } from "@/lib/auth";
+
+export type AdminResult = { ok: boolean; message?: string; id?: number | null };
+
+export type SubjectRef = { id: number; name?: string; code?: string | null };
+
+export type SubjectMajor = {
+  majorId: number;
+  majorName?: string;
+  mandatorySemester: number;
+};
+
+export type AdminSubject = {
+  id: number;
+  name?: string;
+  code?: string;
+  awardedCredits: number;
+  dependencyCredit?: number | null;
+  majors: SubjectMajor[];
+  prerequisites: SubjectRef[];
+  enrolledCount: number;
+};
+
+export type AdminSemester = {
+  id: number;
+  year: number;
+  type?: string;
+  name?: string;
+  enrolmentCount: number;
+  uncoveredSubjects: SubjectRef[];
+};
+
+export type ScheduleRow = {
+  subjectId: number;
+  subjectName?: string;
+  subjectCode?: string;
+  professorId: number;
+  professorName?: string;
+};
+
+export type ProfessorLoad = {
+  professorId: number;
+  professorName?: string;
+  subjects: number;
+};
+
+export type Schedule = {
+  semesterId: number;
+  semesterName?: string;
+  assignments: ScheduleRow[];
+  load: ProfessorLoad[];
+  uncoveredSubjects: SubjectRef[];
+  allSubjects: SubjectRef[];
+  professors: { id: number; name?: string }[];
+};
+
+function authHeaders(): Record<string, string> {
+  const token = getAccessToken();
+  if (!token) throw new Error("You are not signed in.");
+  return { Authorization: `Bearer ${token}`, "Content-Type": "application/json" };
+}
+
+/** GET that throws with the server message rather than a bare status code. */
+export async function adminGet<T>(path: string): Promise<T> {
+  const res = await fetch(apiUrl(path), { cache: "no-store", headers: authHeaders() });
+  if (!res.ok) {
+    const body = await res.json().catch(() => null);
+    throw new Error(body?.message || `Request failed (${res.status})`);
+  }
+  return (await res.json()) as T;
+}
+
+/** POST/PUT/DELETE returning the standard {ok, message} envelope. */
+export async function adminSend(
+  path: string,
+  method: "POST" | "PUT" | "DELETE",
+  body?: unknown,
+): Promise<AdminResult> {
+  const res = await fetch(apiUrl(path), {
+    method,
+    headers: authHeaders(),
+    body: body === undefined ? undefined : JSON.stringify(body),
+  });
+  const result = (await res.json().catch(() => null)) as AdminResult | null;
+  if (!res.ok || !result?.ok) {
+    throw new Error(result?.message || `Request failed (${res.status})`);
+  }
+  return result;
+}
Index: frontend/src/lib/api.ts
===================================================================
--- frontend/src/lib/api.ts	(revision b8093a091d45f33bab33deb8ff2fffc4734972c0)
+++ frontend/src/lib/api.ts	(revision b8093a091d45f33bab33deb8ff2fffc4734972c0)
@@ -0,0 +1,15 @@
+/**
+ * Base URL of the iknow-api backend.
+ *
+ * Set NEXT_PUBLIC_API_BASE_URL in .env.local to point at a local API run
+ * (see .env.example). The faculty database is only reachable through the SSH
+ * tunnel on the developer machine, so the deployed backend cannot read it -
+ * working against real data means running iknow-api locally.
+ */
+export const API_BASE_URL =
+  process.env.NEXT_PUBLIC_API_BASE_URL ?? 'https://iknow-api.onrender.com';
+
+/** Builds an absolute API URL from a path such as `/api/user/getUser`. */
+export function apiUrl(path: string): string {
+  return `${API_BASE_URL}${path}`;
+}
Index: frontend/src/lib/auth.ts
===================================================================
--- frontend/src/lib/auth.ts	(revision b8093a091d45f33bab33deb8ff2fffc4734972c0)
+++ frontend/src/lib/auth.ts	(revision b8093a091d45f33bab33deb8ff2fffc4734972c0)
@@ -0,0 +1,142 @@
+import { API_BASE_URL } from './api';
+export type AuthTokens = {
+  accessToken: string;
+  refreshToken: string;
+};
+
+export type UserRole = 'Professor' | 'Student' | 'Admin' | string;
+
+export type AuthSession = AuthTokens & {
+  role: UserRole;
+};
+
+type StoredToken = {
+  value: string;
+  expiresAt: number; // epoch ms
+};
+
+const STORAGE_KEYS = {
+  access: 'iknow.auth.access',
+  refresh: 'iknow.auth.refresh',
+  role: 'iknow.auth.role',
+} as const;
+
+const ACCESS_TOKEN_TTL_MS = 60 * 60 * 1000; // 1 hour
+const REFRESH_TOKEN_TTL_MS = 30 * 24 * 60 * 60 * 1000; // ~1 month (30 days)
+
+function isBrowser() {
+  return typeof window !== 'undefined' && typeof window.localStorage !== 'undefined';
+}
+
+function readStoredToken(key: string): StoredToken | null {
+  if (!isBrowser()) return null;
+  const raw = window.localStorage.getItem(key);
+  if (!raw) return null;
+
+  try {
+    const parsed = JSON.parse(raw) as StoredToken;
+    if (!parsed?.value || typeof parsed.expiresAt !== 'number') {
+      window.localStorage.removeItem(key);
+      return null;
+    }
+
+    if (Date.now() >= parsed.expiresAt) {
+      window.localStorage.removeItem(key);
+      return null;
+    }
+
+    return parsed;
+  } catch {
+    window.localStorage.removeItem(key);
+    return null;
+  }
+}
+
+function writeStoredToken(key: string, value: string, ttlMs: number) {
+  if (!isBrowser()) return;
+  const payload: StoredToken = {
+    value,
+    expiresAt: Date.now() + ttlMs,
+  };
+  window.localStorage.setItem(key, JSON.stringify(payload));
+}
+
+export function setAuthTokens(tokens: AuthTokens) {
+  writeStoredToken(STORAGE_KEYS.access, tokens.accessToken, ACCESS_TOKEN_TTL_MS);
+  writeStoredToken(STORAGE_KEYS.refresh, tokens.refreshToken, REFRESH_TOKEN_TTL_MS);
+}
+
+export function setUserRole(role: UserRole) {
+  // Keep role TTL aligned with access token TTL.
+  writeStoredToken(STORAGE_KEYS.role, role, ACCESS_TOKEN_TTL_MS);
+}
+
+export function getAccessToken(): string | null {
+  return readStoredToken(STORAGE_KEYS.access)?.value ?? null;
+}
+
+export function getRefreshToken(): string | null {
+  return readStoredToken(STORAGE_KEYS.refresh)?.value ?? null;
+}
+
+export function getUserRole(): UserRole | null {
+  return readStoredToken(STORAGE_KEYS.role)?.value ?? null;
+}
+
+export function clearAuthTokens() {
+  if (!isBrowser()) return;
+  window.localStorage.removeItem(STORAGE_KEYS.access);
+  window.localStorage.removeItem(STORAGE_KEYS.refresh);
+  window.localStorage.removeItem(STORAGE_KEYS.role);
+}
+
+export async function login(params: { email: string; password: string }): Promise<AuthSession> {
+  const baseUrl = API_BASE_URL;
+
+  const response = await fetch(`${baseUrl}/api/auth/login`, {
+    method: 'POST',
+    headers: {
+      'Content-Type': 'application/json',
+    },
+    body: JSON.stringify({
+      email: params.email,
+      password: params.password,
+      GenerateRefreshToken: true,
+    }),
+  });
+
+  if (!response.ok) {
+    let message = `Login failed (${response.status})`;
+    try {
+      const text = await response.text();
+      if (text) message = text;
+    } catch {
+      // ignore
+    }
+    throw new Error(message);
+  }
+
+  const data = (await response.json()) as {
+    token?: string;
+    refreshToken?: string;
+    role?: string;
+  };
+
+  const accessToken = data?.token;
+  const refreshToken = data?.refreshToken;
+  const role = data?.role;
+
+  if (!accessToken || !refreshToken) {
+    throw new Error('Login response did not contain tokens.');
+  }
+
+  const session: AuthSession = {
+    accessToken,
+    refreshToken,
+    role: role ?? 'Student',
+  };
+
+  setAuthTokens(session);
+  setUserRole(session.role);
+  return session;
+}
Index: frontend/src/lib/pdf-fonts.ts
===================================================================
--- frontend/src/lib/pdf-fonts.ts	(revision b8093a091d45f33bab33deb8ff2fffc4734972c0)
+++ frontend/src/lib/pdf-fonts.ts	(revision b8093a091d45f33bab33deb8ff2fffc4734972c0)
@@ -0,0 +1,32 @@
+import { jsPDF } from 'jspdf';
+
+let fontsLoaded = false;
+let regularFontBase64: string | null = null;
+let boldFontBase64: string | null = null;
+
+async function fetchFontAsBase64(url: string): Promise<string> {
+  const response = await fetch(url);
+  const buffer = await response.arrayBuffer();
+  const bytes = new Uint8Array(buffer);
+  let binary = '';
+  for (let i = 0; i < bytes.length; i++) {
+    binary += String.fromCharCode(bytes[i]);
+  }
+  return btoa(binary);
+}
+
+export async function loadCyrillicFonts(doc: jsPDF): Promise<void> {
+  if (!fontsLoaded) {
+    regularFontBase64 = await fetchFontAsBase64('/fonts/Roboto-Regular.ttf');
+    boldFontBase64 = await fetchFontAsBase64('/fonts/Roboto-Bold.ttf');
+    fontsLoaded = true;
+  }
+
+  doc.addFileToVFS('Roboto-Regular.ttf', regularFontBase64!);
+  doc.addFont('Roboto-Regular.ttf', 'Roboto', 'normal');
+
+  doc.addFileToVFS('Roboto-Bold.ttf', boldFontBase64!);
+  doc.addFont('Roboto-Bold.ttf', 'Roboto', 'bold');
+
+  doc.setFont('Roboto', 'normal');
+}
Index: frontend/src/lib/pdf-generators.ts
===================================================================
--- frontend/src/lib/pdf-generators.ts	(revision b8093a091d45f33bab33deb8ff2fffc4734972c0)
+++ frontend/src/lib/pdf-generators.ts	(revision b8093a091d45f33bab33deb8ff2fffc4734972c0)
@@ -0,0 +1,1207 @@
+import { jsPDF } from 'jspdf';
+import autoTable from 'jspdf-autotable';
+import { loadCyrillicFonts } from './pdf-fonts';
+import { apiUrl } from './api';
+
+/* ---------- shared types ---------- */
+
+export type StudentInfo = {
+  firstName: string;
+  middleName: string;
+  lastName: string;
+  index: string;
+  embg: string;
+  dateOfBirth: string;
+  gender: string;
+  nationality: string;
+  citizenship: string;
+  currentPlan: string;
+  registryNumber: string;
+  studyGroup: string;
+  scholarship: string;
+};
+
+export type EnrollmentInfo = {
+  enrollmentYear: string | number;
+  status: string;
+  cycle: string;
+  program: string;
+  quota: string;
+};
+
+export type BirthInfo = {
+  placeOfBirth: string;
+  municipalityOfBirth: string;
+  country: string;
+};
+
+export type ContactInfo = {
+  placeOfResidence: string;
+  municipalityOfResidence: string;
+  address: string;
+  phone: string;
+  mobilePhone: string;
+  email: string;
+};
+
+export type PassedExam = {
+  id: number;
+  code: string;
+  subject: string;
+  credits: number;
+  grade: number;
+  date: string;
+  semester: string;
+  professor: string;
+};
+
+export type PreviousEducation = {
+  type: string;
+  profession: string;
+  average: string | number;
+  language: string;
+  country: string;
+  previousUniversity: string;
+  previousFaculty: string;
+};
+
+/* ---------- helpers ---------- */
+
+const PAGE_WIDTH = 210; // A4 mm
+const MARGIN = 20;
+const CONTENT_WIDTH = PAGE_WIDTH - 2 * MARGIN;
+
+function drawUniversityHeader(doc: jsPDF, y: number): number {
+  doc.setFont('Roboto', 'bold');
+
+  doc.setFontSize(11);
+  doc.text('УНИВЕРЗИТЕТ „СВ. КИРИЛ И МЕТОДИЈ" - СКОПЈЕ', PAGE_WIDTH / 2, y, { align: 'center' });
+  y += 6;
+
+  doc.setFontSize(10);
+  doc.text('ФАКУЛТЕТ ЗА ИНФОРМАТИЧКИ НАУКИ И', PAGE_WIDTH / 2, y, { align: 'center' });
+  y += 5;
+  doc.text('КОМПЈУТЕРСКО ИНЖЕНЕРСТВО', PAGE_WIDTH / 2, y, { align: 'center' });
+  y += 5;
+
+  // Horizontal line under header
+  doc.setDrawColor(0, 114, 209); // primary blue
+  doc.setLineWidth(0.5);
+  doc.line(MARGIN, y, PAGE_WIDTH - MARGIN, y);
+  y += 8;
+
+  return y;
+}
+
+function drawDocumentTitle(doc: jsPDF, title: string, y: number): number {
+  doc.setFont('Roboto', 'bold');
+  doc.setFontSize(14);
+  doc.text(title, PAGE_WIDTH / 2, y, { align: 'center' });
+  y += 10;
+  return y;
+}
+
+function drawInfoRow(doc: jsPDF, label: string, value: string, x: number, y: number, labelWidth = 50): number {
+  doc.setFont('Roboto', 'bold');
+  doc.setFontSize(9);
+  doc.text(`${label}:`, x, y);
+  doc.setFont('Roboto', 'normal');
+  doc.text(String(value || '—'), x + labelWidth, y);
+  return y + 5.5;
+}
+
+function drawFooter(doc: jsPDF, archiveNumber: string, date: string): void {
+  const pageHeight = doc.internal.pageSize.getHeight();
+  let y = pageHeight - 40;
+
+  doc.setDrawColor(0, 114, 209);
+  doc.setLineWidth(0.3);
+  doc.line(MARGIN, y, PAGE_WIDTH - MARGIN, y);
+  y += 8;
+
+  doc.setFont('Roboto', 'normal');
+  doc.setFontSize(9);
+
+  doc.text(`Бр.: ${archiveNumber}`, MARGIN, y);
+  doc.text(`Датум: ${date}`, PAGE_WIDTH / 2, y, { align: 'center' });
+  y += 12;
+
+  // Signature placeholders
+  doc.text('Потпис на студентот:', MARGIN, y);
+  doc.text('М.П.', PAGE_WIDTH / 2, y, { align: 'center' });
+  doc.text('Одговорно лице:', PAGE_WIDTH - MARGIN - 35, y);
+  y += 8;
+
+  doc.setDrawColor(150, 150, 150);
+  doc.setLineWidth(0.2);
+  doc.line(MARGIN, y, MARGIN + 45, y);
+  doc.line(PAGE_WIDTH / 2 - 15, y, PAGE_WIDTH / 2 + 15, y);
+  doc.line(PAGE_WIDTH - MARGIN - 45, y, PAGE_WIDTH - MARGIN, y);
+}
+
+/* ==================================================================
+   PDF 1 — Уверение за положени испити (Certificate of Passed Exams)
+   ================================================================== */
+
+export async function generatePassedExamsCertificate(
+  student: StudentInfo,
+  enrollment: EnrollmentInfo,
+  passedExams: PassedExam[],
+  archiveNumber: string,
+  date: string,
+): Promise<void> {
+  const doc = new jsPDF({ orientation: 'portrait', unit: 'mm', format: 'a4' });
+  await loadCyrillicFonts(doc);
+
+  let y = 20;
+
+  // --- Header ---
+  y = drawUniversityHeader(doc, y);
+
+  // --- Title ---
+  y = drawDocumentTitle(doc, 'УВЕРЕНИЕ ЗА ПОЛОЖЕНИ ИСПИТИ', y);
+
+  // --- Student Info Block ---
+  doc.setFont('Roboto', 'normal');
+  doc.setFontSize(9);
+
+  y = drawInfoRow(doc, 'Студент', `${student.firstName} ${student.middleName} ${student.lastName}`, MARGIN, y);
+  y = drawInfoRow(doc, 'Индекс', student.index, MARGIN, y);
+  y = drawInfoRow(doc, 'Студиска програма', enrollment.program, MARGIN, y);
+  y = drawInfoRow(doc, 'Циклус на студии', enrollment.cycle, MARGIN, y);
+  y = drawInfoRow(doc, 'Статус', enrollment.status, MARGIN, y);
+  y += 4;
+
+  // --- Passed Exams Table ---
+  const tableBody = passedExams.map((exam, idx) => [
+    String(idx + 1),
+    exam.code,
+    exam.subject,
+    String(exam.credits),
+    String(exam.grade),
+    exam.date,
+    exam.semester,
+    exam.professor || '—',
+  ]);
+
+  autoTable(doc, {
+    startY: y,
+    head: [['Р.Б.', 'Шифра', 'Предмет', 'Кредити', 'Оцена', 'Датум', 'Семестар', 'Професор']],
+    body: tableBody,
+    theme: 'grid',
+    styles: {
+      font: 'Roboto',
+      fontSize: 7.5,
+      cellPadding: 2,
+      textColor: [30, 30, 30],
+      lineColor: [200, 200, 200],
+      lineWidth: 0.2,
+    },
+    headStyles: {
+      fillColor: [0, 114, 209],
+      textColor: [255, 255, 255],
+      fontStyle: 'bold',
+      fontSize: 7.5,
+      halign: 'center',
+    },
+    columnStyles: {
+      0: { halign: 'center', cellWidth: 10 },
+      1: { halign: 'center', cellWidth: 16 },
+      2: { cellWidth: 50 },
+      3: { halign: 'center', cellWidth: 14 },
+      4: { halign: 'center', cellWidth: 12 },
+      5: { halign: 'center', cellWidth: 20 },
+      6: { cellWidth: 22 },
+      7: { cellWidth: 26 },
+    },
+    margin: { left: MARGIN, right: MARGIN },
+    didParseCell: (data) => {
+      if (data.section === 'body' && data.row.index % 2 === 0) {
+        data.cell.styles.fillColor = [245, 247, 250];
+      }
+    },
+  });
+
+  // eslint-disable-next-line @typescript-eslint/no-explicit-any
+  y = (doc as any).lastAutoTable.finalY + 8;
+
+  // --- Summary ---
+  const totalCredits = passedExams.reduce((sum, e) => sum + e.credits, 0);
+  const grades = passedExams.map((e) => e.grade).filter((g) => g > 0);
+  const average = grades.length > 0 ? (grades.reduce((s, g) => s + g, 0) / grades.length).toFixed(2) : '0.00';
+
+  doc.setFont('Roboto', 'bold');
+  doc.setFontSize(10);
+  doc.text(`Вкупно положени предмети: ${passedExams.length}`, MARGIN, y);
+  y += 6;
+  doc.text(`Вкупно кредити: ${totalCredits}`, MARGIN, y);
+  y += 6;
+  doc.text(`Просечна оцена: ${average}`, MARGIN, y);
+
+  // --- Footer ---
+  drawFooter(doc, archiveNumber, date);
+
+  doc.save(`Уверение_положени_испити_${student.index}.pdf`);
+}
+
+/* ==================================================================
+   PDF 2 — УППИ образец (UPPI Form / Student Record)
+   ================================================================== */
+
+export async function generateUPPIForm(
+  student: StudentInfo,
+  enrollment: EnrollmentInfo,
+  birth: BirthInfo,
+  contact: ContactInfo,
+  previousEdu: PreviousEducation,
+  passedExams: PassedExam[],
+  archiveNumber: string,
+  date: string,
+): Promise<void> {
+  const doc = new jsPDF({ orientation: 'portrait', unit: 'mm', format: 'a4' });
+  await loadCyrillicFonts(doc);
+
+  let y = 20;
+
+  // --- Header ---
+  y = drawUniversityHeader(doc, y);
+
+  // --- Title ---
+  y = drawDocumentTitle(doc, 'УППИ ОБРАЗЕЦ', y);
+
+  doc.setFont('Roboto', 'bold');
+  doc.setFontSize(9);
+  doc.text('УНИВЕРЗАЛНА ПРИЈАВА ЗА ПРАКТИЧНА ИНДИВИДУАЛНА РАБОТА', PAGE_WIDTH / 2, y, { align: 'center' });
+  y += 10;
+
+  // --- Section: Personal Information ---
+  doc.setFillColor(0, 114, 209);
+  doc.rect(MARGIN, y - 4, CONTENT_WIDTH, 7, 'F');
+  doc.setFont('Roboto', 'bold');
+  doc.setFontSize(9);
+  doc.setTextColor(255, 255, 255);
+  doc.text('ЛИЧНИ ПОДАТОЦИ', MARGIN + 3, y + 0.5);
+  doc.setTextColor(30, 30, 30);
+  y += 10;
+
+  const col2X = MARGIN + CONTENT_WIDTH / 2;
+
+  y = drawInfoRow(doc, 'Име', student.firstName, MARGIN, y, 35);
+  y -= 5.5;
+  y = drawInfoRow(doc, 'Презиме', student.lastName, col2X, y, 35);
+  y = drawInfoRow(doc, 'Татково име', student.middleName, MARGIN, y, 35);
+  y -= 5.5;
+  y = drawInfoRow(doc, 'ЕМБГ', student.embg, col2X, y, 35);
+  y = drawInfoRow(doc, 'Датум на раѓање', student.dateOfBirth, MARGIN, y, 35);
+  y -= 5.5;
+  y = drawInfoRow(doc, 'Пол', student.gender, col2X, y, 35);
+  y = drawInfoRow(doc, 'Националност', student.nationality, MARGIN, y, 35);
+  y -= 5.5;
+  y = drawInfoRow(doc, 'Државјанство', student.citizenship, col2X, y, 35);
+  y += 3;
+
+  // --- Section: Birth Info ---
+  doc.setFillColor(0, 114, 209);
+  doc.rect(MARGIN, y - 4, CONTENT_WIDTH, 7, 'F');
+  doc.setFont('Roboto', 'bold');
+  doc.setFontSize(9);
+  doc.setTextColor(255, 255, 255);
+  doc.text('ПОДАТОЦИ ЗА РАЃАЊЕ', MARGIN + 3, y + 0.5);
+  doc.setTextColor(30, 30, 30);
+  y += 10;
+
+  y = drawInfoRow(doc, 'Место на раѓање', birth.placeOfBirth, MARGIN, y, 40);
+  y = drawInfoRow(doc, 'Општина', birth.municipalityOfBirth, MARGIN, y, 40);
+  y = drawInfoRow(doc, 'Држава', birth.country, MARGIN, y, 40);
+  y += 3;
+
+  // --- Section: Contact ---
+  doc.setFillColor(0, 114, 209);
+  doc.rect(MARGIN, y - 4, CONTENT_WIDTH, 7, 'F');
+  doc.setFont('Roboto', 'bold');
+  doc.setFontSize(9);
+  doc.setTextColor(255, 255, 255);
+  doc.text('КОНТАКТ ИНФОРМАЦИИ', MARGIN + 3, y + 0.5);
+  doc.setTextColor(30, 30, 30);
+  y += 10;
+
+  y = drawInfoRow(doc, 'Место на живеење', contact.placeOfResidence, MARGIN, y, 40);
+  y = drawInfoRow(doc, 'Адреса', contact.address, MARGIN, y, 40);
+  y = drawInfoRow(doc, 'Телефон', contact.phone || contact.mobilePhone, MARGIN, y, 40);
+  y = drawInfoRow(doc, 'Е-пошта', contact.email, MARGIN, y, 40);
+  y += 3;
+
+  // --- Section: Enrollment ---
+  doc.setFillColor(0, 114, 209);
+  doc.rect(MARGIN, y - 4, CONTENT_WIDTH, 7, 'F');
+  doc.setFont('Roboto', 'bold');
+  doc.setFontSize(9);
+  doc.setTextColor(255, 255, 255);
+  doc.text('СТУДИСКИ ИНФОРМАЦИИ', MARGIN + 3, y + 0.5);
+  doc.setTextColor(30, 30, 30);
+  y += 10;
+
+  y = drawInfoRow(doc, 'Индекс', student.index, MARGIN, y, 40);
+  y -= 5.5;
+  y = drawInfoRow(doc, 'Мат. број', student.registryNumber, col2X, y, 35);
+  y = drawInfoRow(doc, 'Студиска програма', enrollment.program, MARGIN, y, 40);
+  y = drawInfoRow(doc, 'Циклус', enrollment.cycle, MARGIN, y, 40);
+  y -= 5.5;
+  y = drawInfoRow(doc, 'Статус', enrollment.status, col2X, y, 35);
+  y = drawInfoRow(doc, 'Год. на запишување', String(enrollment.enrollmentYear), MARGIN, y, 40);
+  y -= 5.5;
+  y = drawInfoRow(doc, 'Квота', enrollment.quota, col2X, y, 35);
+  y += 3;
+
+  // --- Section: Previous Education ---
+  doc.setFillColor(0, 114, 209);
+  doc.rect(MARGIN, y - 4, CONTENT_WIDTH, 7, 'F');
+  doc.setFont('Roboto', 'bold');
+  doc.setFontSize(9);
+  doc.setTextColor(255, 255, 255);
+  doc.text('ПРЕТХОДНО ОБРАЗОВАНИЕ', MARGIN + 3, y + 0.5);
+  doc.setTextColor(30, 30, 30);
+  y += 10;
+
+  y = drawInfoRow(doc, 'Вид', previousEdu.type, MARGIN, y, 40);
+  y = drawInfoRow(doc, 'Просек', String(previousEdu.average), MARGIN, y, 40);
+  y = drawInfoRow(doc, 'Јазик', previousEdu.language, MARGIN, y, 40);
+  y += 3;
+
+  // --- Section: Passed Exams mini-table ---
+  doc.setFillColor(0, 114, 209);
+  doc.rect(MARGIN, y - 4, CONTENT_WIDTH, 7, 'F');
+  doc.setFont('Roboto', 'bold');
+  doc.setFontSize(9);
+  doc.setTextColor(255, 255, 255);
+  doc.text('ПОЛОЖЕНИ ИСПИТИ', MARGIN + 3, y + 0.5);
+  doc.setTextColor(30, 30, 30);
+  y += 8;
+
+  const examTableBody = passedExams.map((exam, idx) => [
+    String(idx + 1),
+    exam.code,
+    exam.subject,
+    String(exam.credits),
+    String(exam.grade),
+    exam.date,
+  ]);
+
+  autoTable(doc, {
+    startY: y,
+    head: [['#', 'Шифра', 'Предмет', 'Кредити', 'Оцена', 'Датум']],
+    body: examTableBody,
+    theme: 'grid',
+    styles: {
+      font: 'Roboto',
+      fontSize: 7,
+      cellPadding: 1.5,
+      textColor: [30, 30, 30],
+      lineColor: [200, 200, 200],
+      lineWidth: 0.2,
+    },
+    headStyles: {
+      fillColor: [0, 114, 209],
+      textColor: [255, 255, 255],
+      fontStyle: 'bold',
+      fontSize: 7,
+      halign: 'center',
+    },
+    columnStyles: {
+      0: { halign: 'center', cellWidth: 8 },
+      1: { halign: 'center', cellWidth: 18 },
+      2: { cellWidth: 70 },
+      3: { halign: 'center', cellWidth: 16 },
+      4: { halign: 'center', cellWidth: 14 },
+      5: { halign: 'center', cellWidth: 22 },
+    },
+    margin: { left: MARGIN, right: MARGIN },
+    didParseCell: (data) => {
+      if (data.section === 'body' && data.row.index % 2 === 0) {
+        data.cell.styles.fillColor = [245, 247, 250];
+      }
+    },
+  });
+
+  // eslint-disable-next-line @typescript-eslint/no-explicit-any
+  y = (doc as any).lastAutoTable.finalY + 8;
+
+  // Summary
+  const totalCredits = passedExams.reduce((sum, e) => sum + e.credits, 0);
+  const grades = passedExams.map((e) => e.grade).filter((g) => g > 0);
+  const average = grades.length > 0 ? (grades.reduce((s, g) => s + g, 0) / grades.length).toFixed(2) : '0.00';
+
+  doc.setFont('Roboto', 'bold');
+  doc.setFontSize(9);
+  doc.text(`Вкупно кредити: ${totalCredits}    |    Просечна оцена: ${average}`, MARGIN, y);
+
+  // Footer
+  drawFooter(doc, archiveNumber, date);
+
+  doc.save(`УППИ_образец_${student.index}.pdf`);
+}
+
+/* ==================================================================
+   PDF 3 — Уверение за редовен студент (Regular Student Certificate)
+   ================================================================== */
+
+export async function generateRegularStudentCertificate(
+  student: StudentInfo,
+  enrollment: EnrollmentInfo,
+  passedExams: PassedExam[],
+  archiveNumber: string,
+  date: string,
+): Promise<void> {
+  const doc = new jsPDF({ orientation: 'portrait', unit: 'mm', format: 'a4' });
+  await loadCyrillicFonts(doc);
+
+  let y = 20;
+
+  // --- Header ---
+  y = drawUniversityHeader(doc, y);
+
+  // --- Title ---
+  y = drawDocumentTitle(doc, 'УВЕРЕНИЕ', y);
+  doc.setFont('Roboto', 'normal');
+  doc.setFontSize(10);
+  doc.text('за редовен студент', PAGE_WIDTH / 2, y, { align: 'center' });
+  y += 12;
+
+  // --- Body text ---
+  doc.setFont('Roboto', 'normal');
+  doc.setFontSize(10);
+
+  const totalCredits = passedExams.reduce((sum, e) => sum + e.credits, 0);
+  const grades = passedExams.map((e) => e.grade).filter((g) => g > 0);
+  const average = grades.length > 0 ? (grades.reduce((s, g) => s + g, 0) / grades.length).toFixed(2) : '0.00';
+
+  const bodyText = [
+    `Со ова се потврдува дека студентот/студентката`,
+    ``,
+    `${student.firstName} ${student.middleName} ${student.lastName}`,
+    ``,
+    `со индекс број ${student.index}, ЕМБГ ${student.embg},`,
+    `е редовен студент на Факултетот за информатички науки и компјутерско`,
+    `инженерство при Универзитетот „Св. Кирил и Методиј" - Скопје.`,
+    ``,
+    `Студиска програма: ${enrollment.program}`,
+    `Циклус на студии: ${enrollment.cycle}`,
+    `Статус: ${enrollment.status}`,
+    `Година на запишување: ${enrollment.enrollmentYear}`,
+    ``,
+    `Студентот/студентката до сега има положено ${passedExams.length} испити,`,
+    `со вкупно ${totalCredits} кредити и просечна оцена ${average}.`,
+    ``,
+    `Ова уверение се издава за потребите на студентот/студентката`,
+    `и може да послужи за секоја законска употреба.`,
+  ];
+
+  for (const line of bodyText) {
+    if (line === '') {
+      y += 4;
+    } else {
+      if (line.startsWith(`${student.firstName}`)) {
+        doc.setFont('Roboto', 'bold');
+        doc.setFontSize(12);
+        doc.text(line, PAGE_WIDTH / 2, y, { align: 'center' });
+        doc.setFont('Roboto', 'normal');
+        doc.setFontSize(10);
+      } else {
+        doc.text(line, MARGIN, y);
+      }
+      y += 6;
+    }
+  }
+
+  // Footer
+  drawFooter(doc, archiveNumber, date);
+
+  doc.save(`Уверение_редовен_студент_${student.index}.pdf`);
+}
+
+/* ==================================================================
+   Generic document — fallback for other document types
+   ================================================================== */
+
+export async function generateGenericDocument(
+  student: StudentInfo,
+  enrollment: EnrollmentInfo,
+  documentTitle: string,
+  archiveNumber: string,
+  date: string,
+): Promise<void> {
+  const doc = new jsPDF({ orientation: 'portrait', unit: 'mm', format: 'a4' });
+  await loadCyrillicFonts(doc);
+
+  let y = 20;
+
+  y = drawUniversityHeader(doc, y);
+  y = drawDocumentTitle(doc, documentTitle.toUpperCase(), y);
+
+  doc.setFont('Roboto', 'normal');
+  doc.setFontSize(10);
+
+  y = drawInfoRow(doc, 'Студент', `${student.firstName} ${student.middleName} ${student.lastName}`, MARGIN, y);
+  y = drawInfoRow(doc, 'Индекс', student.index, MARGIN, y);
+  y = drawInfoRow(doc, 'ЕМБГ', student.embg, MARGIN, y);
+  y = drawInfoRow(doc, 'Студиска програма', enrollment.program, MARGIN, y);
+  y = drawInfoRow(doc, 'Циклус', enrollment.cycle, MARGIN, y);
+  y = drawInfoRow(doc, 'Статус', enrollment.status, MARGIN, y);
+  y += 8;
+
+  doc.setFont('Roboto', 'normal');
+  doc.setFontSize(10);
+  doc.text('Документот е издаден по барање на студентот.', MARGIN, y);
+
+  drawFooter(doc, archiveNumber, date);
+
+  doc.save(`${documentTitle.replace(/\s+/g, '_')}_${student.index}.pdf`);
+}
+
+/* ==================================================================
+   Themed request generators — one for each document type
+   ================================================================== */
+
+async function generateRequestDocument(
+  student: StudentInfo,
+  enrollment: EnrollmentInfo,
+  title: string,
+  bodyLines: string[],
+  archiveNumber: string,
+  date: string,
+  filename: string,
+): Promise<void> {
+  const doc = new jsPDF({ orientation: 'portrait', unit: 'mm', format: 'a4' });
+  await loadCyrillicFonts(doc);
+
+  let y = 20;
+
+  y = drawUniversityHeader(doc, y);
+  y = drawDocumentTitle(doc, title, y);
+
+  // Student info block
+  doc.setFont('Roboto', 'normal');
+  doc.setFontSize(9);
+  y = drawInfoRow(doc, 'Студент', `${student.firstName} ${student.middleName} ${student.lastName}`, MARGIN, y);
+  y = drawInfoRow(doc, 'Индекс', student.index, MARGIN, y);
+  y = drawInfoRow(doc, 'ЕМБГ', student.embg, MARGIN, y);
+  y = drawInfoRow(doc, 'Студиска програма', enrollment.program, MARGIN, y);
+  y = drawInfoRow(doc, 'Циклус', enrollment.cycle, MARGIN, y);
+  y = drawInfoRow(doc, 'Статус', enrollment.status, MARGIN, y);
+  y = drawInfoRow(doc, 'Година на упис', String(enrollment.enrollmentYear), MARGIN, y);
+  y += 8;
+
+  // Separator
+  doc.setDrawColor(200, 200, 200);
+  doc.setLineWidth(0.3);
+  doc.line(MARGIN, y, PAGE_WIDTH - MARGIN, y);
+  y += 8;
+
+  // Body text
+  doc.setFont('Roboto', 'normal');
+  doc.setFontSize(10);
+  for (const line of bodyLines) {
+    if (line === '') {
+      y += 5;
+    } else {
+      const wrapped = doc.splitTextToSize(line, CONTENT_WIDTH);
+      for (const wl of wrapped) {
+        doc.text(wl, MARGIN, y);
+        y += 5.5;
+      }
+    }
+  }
+
+  drawFooter(doc, archiveNumber, date);
+  doc.save(`${filename}_${student.index}.pdf`);
+}
+
+// --- Administrative Regulation ---
+export async function generateAdministrativeRegulation(
+  student: StudentInfo, enrollment: EnrollmentInfo, archiveNumber: string, date: string,
+): Promise<void> {
+  await generateRequestDocument(student, enrollment,
+    'БАРАЊЕ ЗА АДМИНИСТРАТИВНО РЕГУЛИРАЊЕ НА РЕТРОАКТИВЕН СЕМЕСТАР',
+    [
+      `До: Деканат на Факултетот за информатички науки и компјутерско инженерство`,
+      '',
+      `Јас, ${student.firstName} ${student.middleName} ${student.lastName}, студент/ка со индекс ${student.index},`,
+      `запишан/а на студиската програма ${enrollment.program}, ${enrollment.cycle} циклус студии,`,
+      `со ова барање побарувам административно регулирање на ретроактивен семестар.`,
+      '',
+      `Молам да ми се одобри административна регулација на семестар кој не е запишан во предвидениот рок,`,
+      `согласно одлуката на Факултетот и важечката регулатива.`,
+      '',
+      `Прилози: Уплатница за административна такса од 1000 ден.`,
+    ],
+    archiveNumber, date, 'Административно_регулирање');
+}
+
+// --- Late Exam Registration ---
+export async function generateExamApplicationSatisfaction(
+  student: StudentInfo, enrollment: EnrollmentInfo, archiveNumber: string, date: string,
+): Promise<void> {
+  await generateRequestDocument(student, enrollment,
+    'БАРАЊЕ ЗА ЗАДОЧНО ПРИЈАВУВАЊЕ НА ИСПИТ',
+    [
+      `До: Деканат на ФИНКИ`,
+      '',
+      `Јас, ${student.firstName} ${student.middleName} ${student.lastName}, студент/ка со индекс ${student.index},`,
+      `запишан/а на студиската програма ${enrollment.program},`,
+      `побарувам задочно пријавување на испит кој не е пријавен во предвидениот рок.`,
+      '',
+      `Причина за задоцнување: ___________________________________`,
+      '',
+      `Молам за одобрување на ова барање.`,
+      '',
+      `Прилози: Уплатница за административна такса од 1000 ден.`,
+    ],
+    archiveNumber, date, 'Задочно_пријавување_испит');
+}
+
+// --- Semester Enrollment After Deadline ---
+export async function generateSemesterEnrollmentAfterDeadline(
+  student: StudentInfo, enrollment: EnrollmentInfo, archiveNumber: string, date: string,
+): Promise<void> {
+  await generateRequestDocument(student, enrollment,
+    'БАРАЊЕ ЗА ЗАПИШУВАЊЕ НА СЕМЕСТАР ПО ИСТЕК НА РОК',
+    [
+      `До: Деканат на ФИНКИ`,
+      '',
+      `Јас, ${student.firstName} ${student.middleName} ${student.lastName}, студент/ка со индекс ${student.index},`,
+      `побарувам да ми се одобри запишување на нареден семестар по истекот на официјалниот рок.`,
+      '',
+      `Причина за задоцнето запишување: ___________________________________`,
+      '',
+      `Молам за одобрување.`,
+      '',
+      `Прилози: Уплатница за административна такса од 1500 ден.`,
+    ],
+    archiveNumber, date, 'Запишување_семестар_по_рок');
+}
+
+// --- Certificate Issuance ---
+export async function generateCertificateIssuanceVarious(
+  student: StudentInfo, enrollment: EnrollmentInfo, archiveNumber: string, date: string,
+): Promise<void> {
+  await generateRequestDocument(student, enrollment,
+    'БАРАЊЕ ЗА ИЗДАВАЊЕ НА ПОТВРДИ ПО РАЗНИ ОСНОВИ',
+    [
+      `До: Деканат на ФИНКИ`,
+      '',
+      `Јас, ${student.firstName} ${student.middleName} ${student.lastName}, студент/ка со индекс ${student.index},`,
+      `побарувам издавање на потврда за следната намена:`,
+      '',
+      `Намена: ___________________________________`,
+      '',
+      `Молам да ми биде издадена бараната потврда.`,
+      '',
+      `Прилози: Уплатница за административна такса од 1500 ден.`,
+    ],
+    archiveNumber, date, 'Издавање_потврди');
+}
+
+// --- Study Suspension ---
+export async function generateStudySuspension(
+  student: StudentInfo, enrollment: EnrollmentInfo, archiveNumber: string, date: string,
+): Promise<void> {
+  await generateRequestDocument(student, enrollment,
+    'БАРАЊЕ ЗА МИРУВАЊЕ НА СТУДИИТЕ',
+    [
+      `До: Деканат на ФИНКИ`,
+      '',
+      `Јас, ${student.firstName} ${student.middleName} ${student.lastName}, студент/ка со индекс ${student.index},`,
+      `запишан/а на студиската програма ${enrollment.program},`,
+      `побарувам мирување на студиите за период од ______ семестри.`,
+      '',
+      `Причина за мирување: ___________________________________`,
+      '',
+      `Изјавувам дека сум запознаен/а со правилата за продолжување на студиите`,
+      `по истекот на периодот на мирување.`,
+      '',
+      `Прилози: Уплатница за административна такса од 2000 ден.`,
+    ],
+    archiveNumber, date, 'Мирување_студии');
+}
+
+// --- Diploma Thesis Cancellation ---
+export async function generateDiplomaThesisCancellation(
+  student: StudentInfo, enrollment: EnrollmentInfo, archiveNumber: string, date: string,
+): Promise<void> {
+  await generateRequestDocument(student, enrollment,
+    'БАРАЊЕ ЗА ОТКАЖУВАЊЕ НА ПРИЈАВЕНА ТЕМА ЗА ДИПЛОМСКА РАБОТА',
+    [
+      `До: Деканат на ФИНКИ`,
+      '',
+      `Јас, ${student.firstName} ${student.middleName} ${student.lastName}, студент/ка со индекс ${student.index},`,
+      `побарувам откажување на претходно пријавената тема за дипломска работа.`,
+      '',
+      `Назив на темата: ___________________________________`,
+      `Ментор: ___________________________________`,
+      '',
+      `Причина за откажување: ___________________________________`,
+      '',
+      `Прилози: Уплатница за административна такса од 1000 ден.`,
+    ],
+    archiveNumber, date, 'Откажување_дипломска');
+}
+
+// --- Graduation Package ---
+export async function generateDiplomaPackage(
+  student: StudentInfo, enrollment: EnrollmentInfo, archiveNumber: string, date: string,
+): Promise<void> {
+  await generateRequestDocument(student, enrollment,
+    'БАРАЊЕ ЗА ПАКЕТ ЗА ДИПЛОМИРАЊЕ',
+    [
+      `До: Деканат на ФИНКИ`,
+      '',
+      `Јас, ${student.firstName} ${student.middleName} ${student.lastName}, студент/ка со индекс ${student.index},`,
+      `запишан/а на студиската програма ${enrollment.program}, ${enrollment.cycle} циклус,`,
+      `побарувам издавање на пакет за дипломирање.`,
+      '',
+      `Со ова потврдувам дека ги имам исполнето сите услови за дипломирање`,
+      `согласно студиската програма и правилникот на Факултетот.`,
+      '',
+      `Пакетот за дипломирање ги вклучува: диплома, додаток на диплома и свечена промоција.`,
+      '',
+      `Прилози: Уплатница за административна такса од 6200 ден.`,
+    ],
+    archiveNumber, date, 'Пакет_дипломирање');
+}
+
+// --- Exam Cancellation ---
+export async function generateExamCancellation(
+  student: StudentInfo, enrollment: EnrollmentInfo, archiveNumber: string, date: string,
+): Promise<void> {
+  await generateRequestDocument(student, enrollment,
+    'БАРАЊЕ ЗА ПОНИШТУВАЊЕ НА ИСПИТ',
+    [
+      `До: Деканат на ФИНКИ`,
+      '',
+      `Јас, ${student.firstName} ${student.middleName} ${student.lastName}, студент/ка со индекс ${student.index},`,
+      `побарувам поништување на испит по предметот:`,
+      '',
+      `Предмет: ___________________________________`,
+      `Датум на полагање: ___________________________________`,
+      '',
+      `Причина за поништување: ___________________________________`,
+      '',
+      `Прилози: Уплатница за административна такса од 2000 ден.`,
+    ],
+    archiveNumber, date, 'Поништување_испит');
+}
+
+// --- Study Continuation ---
+export async function generateStudyContinuation(
+  student: StudentInfo, enrollment: EnrollmentInfo, archiveNumber: string, date: string,
+): Promise<void> {
+  await generateRequestDocument(student, enrollment,
+    'БАРАЊЕ ЗА ПРОДОЛЖУВАЊЕ НА СТУДИИ ВО МИРУВАЊЕ',
+    [
+      `До: Деканат на ФИНКИ`,
+      '',
+      `Јас, ${student.firstName} ${student.middleName} ${student.lastName}, студент/ка со индекс ${student.index},`,
+      `побарувам продолжување на студиите по периодот на мирување.`,
+      '',
+      `Период на мирување: од ____________ до ____________`,
+      '',
+      `Молам да ми се одобри продолжување на студиите согласно тековната`,
+      `студиска програма и важечкиот правилник.`,
+      '',
+      `Прилози: Уплатница за административна такса од 2000 ден.`,
+    ],
+    archiveNumber, date, 'Продолжување_студии');
+}
+
+// --- Elective Subject Change ---
+export async function generateElectiveSubjectChange(
+  student: StudentInfo, enrollment: EnrollmentInfo, archiveNumber: string, date: string,
+): Promise<void> {
+  await generateRequestDocument(student, enrollment,
+    'БАРАЊЕ ЗА ПРОМЕНА НА ИЗБОРЕН ПРЕДМЕТ',
+    [
+      `До: Деканат на ФИНКИ`,
+      '',
+      `Јас, ${student.firstName} ${student.middleName} ${student.lastName}, студент/ка со индекс ${student.index},`,
+      `побарувам промена на изборен предмет.`,
+      '',
+      `Предмет што се менува: ___________________________________`,
+      `Нов изборен предмет: ___________________________________`,
+      '',
+      `Причина за промена: ___________________________________`,
+      '',
+      `Прилози: Уплатница за административна такса од 1500 ден.`,
+    ],
+    archiveNumber, date, 'Промена_изборен_предмет');
+}
+
+// --- Passed Subject Change ---
+export async function generatePassedSubjectChange(
+  student: StudentInfo, enrollment: EnrollmentInfo, archiveNumber: string, date: string,
+): Promise<void> {
+  await generateRequestDocument(student, enrollment,
+    'БАРАЊЕ ЗА ПРОМЕНА НА ПОЛОЖЕН ПРЕДМЕТ',
+    [
+      `До: Деканат на ФИНКИ`,
+      '',
+      `Јас, ${student.firstName} ${student.middleName} ${student.lastName}, студент/ка со индекс ${student.index},`,
+      `побарувам промена / корекција на запис за положен предмет.`,
+      '',
+      `Предмет: ___________________________________`,
+      `Датум на полагање: ___________________________________`,
+      `Оцена: ___________________________________`,
+      '',
+      `Детали за промена: ___________________________________`,
+    ],
+    archiveNumber, date, 'Промена_положен_предмет');
+}
+
+// --- Study Program Change (Same Accreditation) ---
+export async function generateProgramChangeSameAccreditation(
+  student: StudentInfo, enrollment: EnrollmentInfo, archiveNumber: string, date: string,
+): Promise<void> {
+  await generateRequestDocument(student, enrollment,
+    'БАРАЊЕ ЗА ПРОМЕНА НА СТУДИСКА ПРОГРАМА ОД ИСТА АКРЕДИТАЦИЈА',
+    [
+      `До: Деканат на ФИНКИ`,
+      '',
+      `Јас, ${student.firstName} ${student.middleName} ${student.lastName}, студент/ка со индекс ${student.index},`,
+      `тековно запишан/а на програмата ${enrollment.program},`,
+      `побарувам промена на студиска програма во рамки на истата акредитација.`,
+      '',
+      `Нова студиска програма: ___________________________________`,
+      '',
+      `Причина за промена: ___________________________________`,
+      '',
+      `Прилози: Уплатница за административна такса од 2000 ден.`,
+    ],
+    archiveNumber, date, 'Промена_програма_иста_акредитација');
+}
+
+// --- Study Program Change (New Accreditation) ---
+export async function generateProgramChangeNewAccreditation(
+  student: StudentInfo, enrollment: EnrollmentInfo, archiveNumber: string, date: string,
+): Promise<void> {
+  await generateRequestDocument(student, enrollment,
+    'БАРАЊЕ ЗА ПРОМЕНА НА СТУДИСКА ПРОГРАМА ОД ПОНОВА АКРЕДИТАЦИЈА',
+    [
+      `До: Деканат на ФИНКИ`,
+      '',
+      `Јас, ${student.firstName} ${student.middleName} ${student.lastName}, студент/ка со индекс ${student.index},`,
+      `тековно запишан/а на програмата ${enrollment.program},`,
+      `побарувам промена на студиска програма во рамки на нова акредитација.`,
+      '',
+      `Нова студиска програма: ___________________________________`,
+      `Нова акредитација: ___________________________________`,
+      '',
+      `Причина за промена: ___________________________________`,
+      '',
+      `Прилози: Уплатница за административна такса од 3000 ден.`,
+    ],
+    archiveNumber, date, 'Промена_програма_нова_акредитација');
+}
+
+// --- Diploma Supplement ---
+export async function generateDiplomaSupplement(
+  student: StudentInfo, enrollment: EnrollmentInfo, archiveNumber: string, date: string,
+): Promise<void> {
+  await generateRequestDocument(student, enrollment,
+    'ДОДАТОК НА ДИПЛОМА',
+    [
+      `До: Деканат на ФИНКИ`,
+      '',
+      `Јас, ${student.firstName} ${student.middleName} ${student.lastName}, студент/ка со индекс ${student.index},`,
+      `запишан/а на студиската програма ${enrollment.program}, ${enrollment.cycle} циклус,`,
+      `побарувам издавање на додаток на диплома.`,
+      '',
+      `Додатокот на дипломата се издава како придружен документ`,
+      `кон дипломата за завршените студии.`,
+    ],
+    archiveNumber, date, 'Додаток_диплома');
+}
+
+// --- Diploma Supplement Second Cycle ---
+export async function generateDiplomaSupplementSecondCycle(
+  student: StudentInfo, enrollment: EnrollmentInfo, archiveNumber: string, date: string,
+): Promise<void> {
+  await generateRequestDocument(student, enrollment,
+    'ДОДАТОК НА ДИПЛОМА (ВТОР ЦИКЛУС)',
+    [
+      `До: Деканат на ФИНКИ`,
+      '',
+      `Јас, ${student.firstName} ${student.middleName} ${student.lastName}, студент/ка со индекс ${student.index},`,
+      `запишан/а на студиската програма ${enrollment.program}, втор циклус на студии,`,
+      `побарувам издавање на додаток на диплома за вториот циклус студии.`,
+      '',
+      `Додатокот на дипломата се издава како придружен документ`,
+      `кон магистерската диплома.`,
+    ],
+    archiveNumber, date, 'Додаток_диплома_втор_циклус');
+}
+
+// --- MKSA ---
+export async function generateMKSA(
+  student: StudentInfo, enrollment: EnrollmentInfo, archiveNumber: string, date: string,
+): Promise<void> {
+  await generateRequestDocument(student, enrollment,
+    'МКСА - МАКЕДОНСКА КВАЛИФИКАЦИСКА РАМКА',
+    [
+      `До: Деканат на ФИНКИ`,
+      '',
+      `Јас, ${student.firstName} ${student.middleName} ${student.lastName}, студент/ка со индекс ${student.index},`,
+      `побарувам издавање на МКСА (Македонска Квалификациска Рамка) документ.`,
+      '',
+      `Студиска програма: ${enrollment.program}`,
+      `Циклус: ${enrollment.cycle}`,
+      '',
+      `Документот е потребен за целите на: ___________________________________`,
+      '',
+      `Прилози: Уплатница за административна такса од 750 ден.`,
+    ],
+    archiveNumber, date, 'МКСА');
+}
+
+// --- Exam Recognition ---
+export async function generateExamRecognition(
+  student: StudentInfo, enrollment: EnrollmentInfo, archiveNumber: string, date: string,
+): Promise<void> {
+  await generateRequestDocument(student, enrollment,
+    'ПРИЗНАВАЊЕ НА ПОЛОЖЕНИ ИСПИТИ',
+    [
+      `До: Деканат на ФИНКИ`,
+      '',
+      `Јас, ${student.firstName} ${student.middleName} ${student.lastName}, студент/ка со индекс ${student.index},`,
+      `побарувам признавање на положени испити од претходна институција / програма.`,
+      '',
+      `Претходна институција: ___________________________________`,
+      `Претходна програма: ___________________________________`,
+      '',
+      `Листа на предмети за признавање:`,
+      `1. ___________________________________`,
+      `2. ___________________________________`,
+      `3. ___________________________________`,
+      '',
+      `Прилози: Уверение за положени испити од претходна институција.`,
+    ],
+    archiveNumber, date, 'Признавање_испити');
+}
+
+// --- Student Card (Old) ---
+export async function generateStudentCardOld(
+  student: StudentInfo, enrollment: EnrollmentInfo, archiveNumber: string, date: string,
+): Promise<void> {
+  await generateRequestDocument(student, enrollment,
+    'СТУДЕНТСКИ КАРТОН (СТАРО)',
+    [
+      `До: Деканат на ФИНКИ`,
+      '',
+      `Јас, ${student.firstName} ${student.middleName} ${student.lastName}, студент/ка со индекс ${student.index},`,
+      `побарувам издавање на студентски картон (стар формат).`,
+      '',
+      `Студиска програма: ${enrollment.program}`,
+      `Година на упис: ${enrollment.enrollmentYear}`,
+      '',
+      `Молам да ми биде издаден студентски картон.`,
+    ],
+    archiveNumber, date, 'Студентски_картон');
+}
+
+/* ==================================================================
+   Main dispatcher — called from the Documents page
+   ================================================================== */
+
+// Map document type IDs → generator function names
+const DOCUMENT_TYPE_MAP: Record<string, string> = {
+  'административно регулирање': 'administrative_regulation',
+  'задочно пријавување': 'exam_application_satisfaction',
+  'запишување на семестар по истек': 'semester_enrollment_after_deadline',
+  'издавање потврди': 'certificate_issuance_various',
+  'мирување на студиите': 'student_status_verification',
+  'откажување на пријавена тема': 'diploma_thesis_postponement',
+  'пакет за дипломирање': 'diploma_package',
+  'поништување на испит': 'exam_postponement',
+  'потврда за редовен студент': 'regular_student_certificate',
+  'продолжување на студии во мирување': 'study_continuation_verification',
+  'промена на изборен предмет': 'failed_subject_grade_change',
+  'промена на положен предмет': 'passed_subject_grade_change',
+  'промена на студиска програма од иста': 'study_program_change_same_accreditation',
+  'промена на студиска програма од понова': 'study_program_change_new_accreditation',
+  'додаток на диплома (втор циклус)': 'diploma_supplement_second_cycle',
+  'додаток на диплома': 'diploma_supplement',
+  'мкса': 'mksa',
+  'признавање на положени испити': 'exam_recognition',
+  'студентски картон': 'student_card_old',
+};
+
+function identifyDocumentType(request: string): string | null {
+  const lower = request.toLowerCase();
+
+  // Exact-match patterns (check longer patterns first)
+  if (lower.includes('положени испити')) return 'passed_exams_certificate';
+  if (lower.includes('уппи')) return 'uppi_form';
+  if (lower.includes('редовен студент')) return 'regular_student_certificate';
+
+  // Match by document type keywords
+  for (const [keyword, docType] of Object.entries(DOCUMENT_TYPE_MAP)) {
+    if (lower.includes(keyword)) return docType;
+  }
+
+  return null;
+}
+
+export async function downloadDocumentPDF(
+  documentRequest: string,
+  archiveNumber: string,
+  documentDate: string,
+  accessToken: string,
+): Promise<void> {
+  // Fetch student profile
+  const profileRes = await fetch(apiUrl('/api/user/getUser'), {
+    headers: { Authorization: `Bearer ${accessToken}` },
+  });
+  if (!profileRes.ok) throw new Error('Не може да се вчита профилот на студентот');
+
+  const profile = await profileRes.json();
+  const { personalInfo, enrollmentInfo, birthInfo, contact, previousEducation } = profile;
+
+  const student: StudentInfo = {
+    firstName: personalInfo.firstName,
+    middleName: personalInfo.middleName,
+    lastName: personalInfo.lastName,
+    index: personalInfo.index,
+    embg: personalInfo.embg,
+    dateOfBirth: personalInfo.dateOfBirth,
+    gender: personalInfo.gender,
+    nationality: personalInfo.nationality,
+    citizenship: personalInfo.citizenship,
+    currentPlan: personalInfo.currentPlan,
+    registryNumber: personalInfo.registryNumber,
+    studyGroup: personalInfo.studyGroup,
+    scholarship: personalInfo.scholarship,
+  };
+
+  const enrollment: EnrollmentInfo = {
+    enrollmentYear: enrollmentInfo.enrollmentYear,
+    status: enrollmentInfo.status,
+    cycle: enrollmentInfo.cycle,
+    program: enrollmentInfo.program,
+    quota: enrollmentInfo.quota,
+  };
+
+  const docType = identifyDocumentType(documentRequest);
+
+  // Types that need passed exams data
+  const needsExams = docType === 'passed_exams_certificate' || docType === 'uppi_form' || docType === 'regular_student_certificate';
+
+  let passedExams: PassedExam[] = [];
+  if (needsExams) {
+    const examsRes = await fetch(apiUrl('/api/user/getPassedSubjects'), {
+      headers: { Authorization: `Bearer ${accessToken}` },
+    });
+    if (examsRes.ok) {
+      const examsData = await examsRes.json();
+      passedExams = (examsData?.passedSubjects ?? []).map((s: Record<string, unknown>) => ({
+        id: s.id as number,
+        code: s.code as string,
+        subject: s.subject as string,
+        credits: s.credits as number,
+        grade: s.grade as number,
+        date: s.date as string,
+        semester: s.semester as string,
+        professor: (s.professor as string) || '—',
+      }));
+    }
+  }
+
+  switch (docType) {
+    case 'passed_exams_certificate':
+      await generatePassedExamsCertificate(student, enrollment, passedExams, archiveNumber, documentDate);
+      break;
+    case 'uppi_form': {
+      const birth: BirthInfo = {
+        placeOfBirth: birthInfo.placeOfBirth,
+        municipalityOfBirth: birthInfo.municipalityOfBirth,
+        country: birthInfo.country,
+      };
+      const contactInfo: ContactInfo = {
+        placeOfResidence: contact.placeOfResidence,
+        municipalityOfResidence: contact.municipalityOfResidence,
+        address: contact.address,
+        phone: contact.phone,
+        mobilePhone: contact.mobilePhone,
+        email: contact.email,
+      };
+      const prevEdu: PreviousEducation = {
+        type: previousEducation.type,
+        profession: previousEducation.profession,
+        average: previousEducation.average,
+        language: previousEducation.language,
+        country: previousEducation.country,
+        previousUniversity: previousEducation.previousUniversity,
+        previousFaculty: previousEducation.previousFaculty,
+      };
+      await generateUPPIForm(student, enrollment, birth, contactInfo, prevEdu, passedExams, archiveNumber, documentDate);
+      break;
+    }
+    case 'regular_student_certificate':
+      await generateRegularStudentCertificate(student, enrollment, passedExams, archiveNumber, documentDate);
+      break;
+    case 'administrative_regulation':
+      await generateAdministrativeRegulation(student, enrollment, archiveNumber, documentDate);
+      break;
+    case 'exam_application_satisfaction':
+      await generateExamApplicationSatisfaction(student, enrollment, archiveNumber, documentDate);
+      break;
+    case 'semester_enrollment_after_deadline':
+      await generateSemesterEnrollmentAfterDeadline(student, enrollment, archiveNumber, documentDate);
+      break;
+    case 'certificate_issuance_various':
+      await generateCertificateIssuanceVarious(student, enrollment, archiveNumber, documentDate);
+      break;
+    case 'student_status_verification':
+      await generateStudySuspension(student, enrollment, archiveNumber, documentDate);
+      break;
+    case 'diploma_thesis_postponement':
+      await generateDiplomaThesisCancellation(student, enrollment, archiveNumber, documentDate);
+      break;
+    case 'diploma_package':
+      await generateDiplomaPackage(student, enrollment, archiveNumber, documentDate);
+      break;
+    case 'exam_postponement':
+      await generateExamCancellation(student, enrollment, archiveNumber, documentDate);
+      break;
+    case 'study_continuation_verification':
+      await generateStudyContinuation(student, enrollment, archiveNumber, documentDate);
+      break;
+    case 'failed_subject_grade_change':
+      await generateElectiveSubjectChange(student, enrollment, archiveNumber, documentDate);
+      break;
+    case 'passed_subject_grade_change':
+      await generatePassedSubjectChange(student, enrollment, archiveNumber, documentDate);
+      break;
+    case 'study_program_change_same_accreditation':
+      await generateProgramChangeSameAccreditation(student, enrollment, archiveNumber, documentDate);
+      break;
+    case 'study_program_change_new_accreditation':
+      await generateProgramChangeNewAccreditation(student, enrollment, archiveNumber, documentDate);
+      break;
+    case 'diploma_supplement':
+      await generateDiplomaSupplement(student, enrollment, archiveNumber, documentDate);
+      break;
+    case 'diploma_supplement_second_cycle':
+      await generateDiplomaSupplementSecondCycle(student, enrollment, archiveNumber, documentDate);
+      break;
+    case 'mksa':
+      await generateMKSA(student, enrollment, archiveNumber, documentDate);
+      break;
+    case 'exam_recognition':
+      await generateExamRecognition(student, enrollment, archiveNumber, documentDate);
+      break;
+    case 'student_card_old':
+      await generateStudentCardOld(student, enrollment, archiveNumber, documentDate);
+      break;
+    default:
+      await generateGenericDocument(student, enrollment, documentRequest, archiveNumber, documentDate);
+      break;
+  }
+}
Index: frontend/src/lib/utils.ts
===================================================================
--- frontend/src/lib/utils.ts	(revision b8093a091d45f33bab33deb8ff2fffc4734972c0)
+++ frontend/src/lib/utils.ts	(revision b8093a091d45f33bab33deb8ff2fffc4734972c0)
@@ -0,0 +1,6 @@
+import { clsx, type ClassValue } from "clsx"
+import { twMerge } from "tailwind-merge"
+
+export function cn(...inputs: ClassValue[]) {
+  return twMerge(clsx(inputs))
+}
