source: frontend/src/app/admin/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: 7.3 KB
RevLine 
[8496f3c]1"use client";
2
3import { useCallback, useEffect, useState } from "react";
4import { useTranslation } from "react-i18next";
5import { adminGet, adminSend, type AdminSemester } from "@/lib/admin-api";
6
7export default function AdminSemestersPage() {
8 const { t } = useTranslation();
9
10 const [semesters, setSemesters] = useState<AdminSemester[]>([]);
11 const [loading, setLoading] = useState(true);
12 const [busy, setBusy] = useState(false);
13 const [error, setError] = useState<string | null>(null);
14 const [notice, setNotice] = useState<string | null>(null);
15
16 const [year, setYear] = useState(new Date().getFullYear());
17 const [type, setType] = useState<"winter" | "summer">("winter");
18 const [expandedId, setExpandedId] = useState<number | null>(null);
19
20 const refresh = useCallback(async () => {
21 setLoading(true);
22 setError(null);
23 try {
24 setSemesters(await adminGet<AdminSemester[]>("/api/admin/semesters"));
25 } catch (e) {
26 setError(e instanceof Error ? e.message : String(e));
27 } finally {
28 setLoading(false);
29 }
30 }, []);
31
32 useEffect(() => {
33 void refresh();
34 }, [refresh]);
35
36 async function create() {
37 setBusy(true);
38 setError(null);
39 setNotice(null);
40 try {
41 const result = await adminSend("/api/admin/semesters", "POST", { Year: year, Type: type });
42 setNotice(result.message ?? null);
43 await refresh();
44 } catch (e) {
45 setError(e instanceof Error ? e.message : String(e));
46 } finally {
47 setBusy(false);
48 }
49 }
50
51 return (
52 <div className="min-h-screen pb-8">
53 <div className="bg-primary text-white rounded-xl p-8 mb-8">
54 <h1 className="text-3xl font-bold mb-2">{t("admin_semesters")}</h1>
55 <p className="text-lg opacity-90">{t("admin_semesters_intro")}</p>
56 </div>
57
58 {error && (
59 <div className="mb-4 rounded-lg border border-red-200 bg-red-50 px-4 py-3 text-sm text-red-800">{error}</div>
60 )}
61 {notice && (
62 <div className="mb-4 rounded-lg border border-green-200 bg-green-50 px-4 py-3 text-sm text-green-800">{notice}</div>
63 )}
64
65 <div className="bg-card rounded-xl shadow-sm border border-border p-6 mb-6">
66 <h2 className="text-xl font-semibold mb-4 text-card-foreground">{t("admin_new_semester")}</h2>
67 <div className="flex flex-wrap items-end gap-4">
68 <label className="flex flex-col">
69 <span className="text-sm text-muted-foreground">{t("admin_year")}</span>
70 <input
71 type="number"
72 min={2000}
73 max={2100}
74 className="rounded-lg border border-border bg-background px-3 py-2"
75 value={year}
76 onChange={(e) => setYear(Number(e.target.value))}
77 />
78 </label>
79 <label className="flex flex-col">
80 <span className="text-sm text-muted-foreground">{t("admin_type")}</span>
81 <select
82 className="rounded-lg border border-border bg-background px-3 py-2"
83 value={type}
84 onChange={(e) => setType(e.target.value as "winter" | "summer")}
85 >
86 <option value="winter">{t("admin_winter")}</option>
87 <option value="summer">{t("admin_summer")}</option>
88 </select>
89 </label>
90 <button
91 disabled={busy}
92 className="rounded-lg bg-primary px-4 py-2 text-sm font-medium text-white disabled:opacity-50"
93 onClick={() => void create()}
94 >
95 {t("admin_open_semester")}
96 </button>
97 </div>
98 </div>
99
100 <div className="bg-card rounded-xl shadow-sm border border-border overflow-hidden">
101 <div className="bg-primary text-white px-6 py-4">
102 <h2 className="text-xl font-bold">{t("admin_open_semesters")}</h2>
103 </div>
104 <div className="overflow-x-auto">
105 <table className="w-full">
106 <thead className="bg-accent">
107 <tr>
108 <th className="px-4 py-3 text-left text-xs font-medium uppercase text-muted-foreground">{t("semester")}</th>
109 <th className="px-4 py-3 text-left text-xs font-medium uppercase text-muted-foreground">{t("admin_enrolments")}</th>
110 <th className="px-4 py-3 text-left text-xs font-medium uppercase text-muted-foreground">{t("admin_coverage")}</th>
111 <th className="px-4 py-3 text-left text-xs font-medium uppercase text-muted-foreground">{t("actions")}</th>
112 </tr>
113 </thead>
114 <tbody>
115 {loading ? (
116 <tr><td className="px-4 py-4 text-muted-foreground" colSpan={4}>{t("loading")}</td></tr>
117 ) : semesters.length === 0 ? (
118 <tr><td className="px-4 py-4 text-muted-foreground" colSpan={4}>{t("admin_no_semesters")}</td></tr>
119 ) : (
120 semesters.map((s) => {
121 const uncovered = s.uncoveredSubjects.length;
122 return (
123 <>
124 <tr key={s.id} className="hover:bg-accent">
125 <td className="px-4 py-3 border-b text-card-foreground">{s.name}</td>
126 <td className="px-4 py-3 border-b text-card-foreground">{s.enrolmentCount}</td>
127 <td className="px-4 py-3 border-b">
128 {uncovered === 0 ? (
129 <span className="rounded-full bg-green-100 px-2 py-0.5 text-xs text-green-800">
130 {t("admin_fully_covered")}
131 </span>
132 ) : (
133 <span className="rounded-full bg-yellow-100 px-2 py-0.5 text-xs text-yellow-900">
134 {t("admin_uncovered_count", { count: uncovered })}
135 </span>
136 )}
137 </td>
138 <td className="px-4 py-3 border-b">
139 {uncovered > 0 && (
140 <button
141 className="rounded-lg border border-border px-3 py-1.5 text-sm"
142 onClick={() => setExpandedId(expandedId === s.id ? null : s.id)}
143 >
144 {expandedId === s.id ? t("admin_close") : t("admin_show_uncovered")}
145 </button>
146 )}
147 </td>
148 </tr>
149 {expandedId === s.id && uncovered > 0 && (
150 <tr key={`${s.id}-detail`}>
151 <td colSpan={4} className="border-b bg-accent/40 px-4 py-4 text-sm">
152 <p className="mb-2 text-muted-foreground">{t("admin_uncovered_hint")}</p>
153 <ul className="grid gap-1 sm:grid-cols-2 lg:grid-cols-3">
154 {s.uncoveredSubjects.map((u) => (
155 <li key={u.id} className="text-card-foreground">
156 <span className="font-mono">{u.code}</span> — {u.name}
157 </li>
158 ))}
159 </ul>
160 </td>
161 </tr>
162 )}
163 </>
164 );
165 })
166 )}
167 </tbody>
168 </table>
169 </div>
170 </div>
171 </div>
172 );
173}
Note: See TracBrowser for help on using the repository browser.