source: frontend/src/app/admin/schedule/page.tsx@ b8093a0

Last change on this file since b8093a0 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: 9.2 KB
Line 
1"use client";
2
3import { useCallback, useEffect, useState } from "react";
4import { useTranslation } from "react-i18next";
5import { adminGet, adminSend, type AdminSemester, type Schedule } from "@/lib/admin-api";
6
7export default function AdminSchedulePage() {
8 const { t } = useTranslation();
9
10 const [semesters, setSemesters] = useState<AdminSemester[]>([]);
11 const [semesterId, setSemesterId] = useState<number | null>(null);
12 const [schedule, setSchedule] = useState<Schedule | null>(null);
13
14 const [loading, setLoading] = useState(true);
15 const [busy, setBusy] = useState(false);
16 const [error, setError] = useState<string | null>(null);
17 const [notice, setNotice] = useState<string | null>(null);
18
19 const [subjectId, setSubjectId] = useState<number | "">("");
20 const [professorId, setProfessorId] = useState<number | "">("");
21
22 useEffect(() => {
23 (async () => {
24 try {
25 const list = await adminGet<AdminSemester[]>("/api/admin/semesters");
26 setSemesters(list);
27 setSemesterId(list[0]?.id ?? null);
28 } catch (e) {
29 setError(e instanceof Error ? e.message : String(e));
30 } finally {
31 setLoading(false);
32 }
33 })();
34 }, []);
35
36 const loadSchedule = useCallback(async (id: number) => {
37 setLoading(true);
38 setError(null);
39 try {
40 setSchedule(await adminGet<Schedule>(`/api/admin/schedule/${id}`));
41 } catch (e) {
42 setError(e instanceof Error ? e.message : String(e));
43 } finally {
44 setLoading(false);
45 }
46 }, []);
47
48 useEffect(() => {
49 if (semesterId !== null) void loadSchedule(semesterId);
50 }, [semesterId, loadSchedule]);
51
52 async function run(action: () => Promise<{ message?: string }>) {
53 setBusy(true);
54 setError(null);
55 setNotice(null);
56 try {
57 const result = await action();
58 setNotice(result.message ?? null);
59 if (semesterId !== null) await loadSchedule(semesterId);
60 } catch (e) {
61 setError(e instanceof Error ? e.message : String(e));
62 } finally {
63 setBusy(false);
64 }
65 }
66
67 return (
68 <div className="min-h-screen pb-8">
69 <div className="bg-primary text-white rounded-xl p-8 mb-8">
70 <h1 className="text-3xl font-bold mb-2">{t("admin_schedule")}</h1>
71 <p className="text-lg opacity-90">{t("admin_schedule_intro")}</p>
72 </div>
73
74 {error && (
75 <div className="mb-4 rounded-lg border border-red-200 bg-red-50 px-4 py-3 text-sm text-red-800">{error}</div>
76 )}
77 {notice && (
78 <div className="mb-4 rounded-lg border border-green-200 bg-green-50 px-4 py-3 text-sm text-green-800">{notice}</div>
79 )}
80
81 <div className="bg-card rounded-xl shadow-sm border border-border p-6 mb-6">
82 <div className="flex flex-wrap items-end gap-4">
83 <label className="flex flex-col">
84 <span className="text-sm text-muted-foreground">{t("semester")}</span>
85 <select
86 className="rounded-lg border border-border bg-background px-3 py-2"
87 value={semesterId ?? ""}
88 onChange={(e) => setSemesterId(Number(e.target.value))}
89 >
90 {semesters.map((s) => (
91 <option key={s.id} value={s.id}>{s.name}</option>
92 ))}
93 </select>
94 </label>
95
96 <label className="flex flex-col">
97 <span className="text-sm text-muted-foreground">{t("subject")}</span>
98 <select
99 className="rounded-lg border border-border bg-background px-3 py-2"
100 value={subjectId}
101 onChange={(e) => setSubjectId(e.target.value === "" ? "" : Number(e.target.value))}
102 >
103 <option value="">{t("admin_pick_subject")}</option>
104 {schedule?.allSubjects.map((s) => (
105 <option key={s.id} value={s.id}>{s.code} — {s.name}</option>
106 ))}
107 </select>
108 </label>
109
110 <label className="flex flex-col">
111 <span className="text-sm text-muted-foreground">{t("admin_professor")}</span>
112 <select
113 className="rounded-lg border border-border bg-background px-3 py-2"
114 value={professorId}
115 onChange={(e) => setProfessorId(e.target.value === "" ? "" : Number(e.target.value))}
116 >
117 <option value="">{t("admin_pick_professor")}</option>
118 {schedule?.professors.map((p) => (
119 <option key={p.id} value={p.id}>{p.name}</option>
120 ))}
121 </select>
122 </label>
123
124 <button
125 className="rounded-lg bg-green-600 px-4 py-2 text-sm font-medium text-white disabled:opacity-50"
126 disabled={busy || subjectId === "" || professorId === "" || semesterId === null}
127 onClick={() =>
128 void run(() =>
129 adminSend("/api/admin/schedule", "POST", {
130 ProfessorId: professorId,
131 SemesterId: semesterId,
132 SubjectId: subjectId,
133 }),
134 )
135 }
136 >
137 {t("admin_assign")}
138 </button>
139 </div>
140 </div>
141
142 {schedule && schedule.uncoveredSubjects.length > 0 && (
143 <div className="mb-6 rounded-lg border border-yellow-200 bg-yellow-50 px-4 py-3 text-sm text-yellow-900">
144 <strong>{t("admin_uncovered_count", { count: schedule.uncoveredSubjects.length })}</strong>
145 <span className="ml-2">{t("admin_uncovered_hint")}</span>
146 <div className="mt-2 font-mono text-xs">
147 {schedule.uncoveredSubjects.map((u) => u.code).join(", ")}
148 </div>
149 </div>
150 )}
151
152 <div className="grid gap-6 lg:grid-cols-3">
153 <div className="lg:col-span-2 bg-card rounded-xl shadow-sm border border-border overflow-hidden">
154 <div className="bg-primary text-white px-6 py-4">
155 <h2 className="text-xl font-bold">
156 {t("admin_assignments")} {schedule ? `— ${schedule.semesterName}` : ""}
157 </h2>
158 </div>
159 <div className="overflow-x-auto">
160 <table className="w-full">
161 <thead className="bg-accent">
162 <tr>
163 <th className="px-4 py-3 text-left text-xs font-medium uppercase text-muted-foreground">{t("admin_subject_code")}</th>
164 <th className="px-4 py-3 text-left text-xs font-medium uppercase text-muted-foreground">{t("subject")}</th>
165 <th className="px-4 py-3 text-left text-xs font-medium uppercase text-muted-foreground">{t("admin_professor")}</th>
166 <th className="px-4 py-3 text-left text-xs font-medium uppercase text-muted-foreground">{t("actions")}</th>
167 </tr>
168 </thead>
169 <tbody>
170 {loading ? (
171 <tr><td className="px-4 py-4 text-muted-foreground" colSpan={4}>{t("loading")}</td></tr>
172 ) : !schedule || schedule.assignments.length === 0 ? (
173 <tr><td className="px-4 py-4 text-muted-foreground" colSpan={4}>{t("admin_no_assignments")}</td></tr>
174 ) : (
175 schedule.assignments.map((a) => (
176 <tr key={`${a.subjectId}-${a.professorId}`} className="hover:bg-accent">
177 <td className="px-4 py-3 border-b font-mono text-card-foreground">{a.subjectCode}</td>
178 <td className="px-4 py-3 border-b text-card-foreground">{a.subjectName}</td>
179 <td className="px-4 py-3 border-b text-card-foreground">{a.professorName}</td>
180 <td className="px-4 py-3 border-b">
181 <button
182 className="rounded-lg bg-gray-800 px-3 py-1.5 text-sm text-white disabled:opacity-50"
183 disabled={busy}
184 onClick={() =>
185 void run(() =>
186 adminSend("/api/admin/schedule", "DELETE", {
187 ProfessorId: a.professorId,
188 SemesterId: schedule.semesterId,
189 SubjectId: a.subjectId,
190 }),
191 )
192 }
193 >
194 {t("admin_remove")}
195 </button>
196 </td>
197 </tr>
198 ))
199 )}
200 </tbody>
201 </table>
202 </div>
203 </div>
204
205 <div className="bg-card rounded-xl shadow-sm border border-border overflow-hidden">
206 <div className="bg-primary text-white px-6 py-4">
207 <h2 className="text-xl font-bold">{t("admin_load")}</h2>
208 </div>
209 <ul className="divide-y divide-border">
210 {schedule?.load.map((l) => (
211 <li key={l.professorId} className="flex items-center justify-between px-4 py-3">
212 <span className="text-card-foreground">{l.professorName}</span>
213 <span className="rounded-full bg-accent px-2 py-0.5 text-sm text-muted-foreground">{l.subjects}</span>
214 </li>
215 ))}
216 {!schedule?.load.length && (
217 <li className="px-4 py-3 text-muted-foreground">{t("admin_no_professors")}</li>
218 )}
219 </ul>
220 </div>
221 </div>
222 </div>
223 );
224}
Note: See TracBrowser for help on using the repository browser.