Seth0330 commited on
Commit
808185e
·
verified ·
1 Parent(s): 86219a8

Update frontend/src/pages/AdminClasses.jsx

Browse files
Files changed (1) hide show
  1. frontend/src/pages/AdminClasses.jsx +95 -317
frontend/src/pages/AdminClasses.jsx CHANGED
@@ -1,102 +1,57 @@
1
  // frontend/src/pages/AdminClasses.jsx
2
  import React, { useState } from "react";
3
- import { useNavigate } from "react-router-dom";
4
  import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
 
5
  import {
6
- LayoutDashboard,
7
- Users2,
8
- FileText,
9
- Trophy,
10
- ShoppingBag,
11
- Menu,
12
- Plus,
13
- } from "lucide-react";
14
-
15
- import api from "../api/client";
16
- import UserMenu from "../components/UserMenu";
17
  import ClassForm from "../components/admin/ClassForm";
18
  import ClassesTable from "../components/admin/ClassesTable";
19
- import ClassEnrollments from "../components/admin/ClassEnrollments";
20
- import dojoLogo from "../assets/dojo-logo.png";
21
 
22
  export default function AdminClasses() {
23
- const navigate = useNavigate();
24
  const queryClient = useQueryClient();
25
-
26
  const [showForm, setShowForm] = useState(false);
27
  const [editingClass, setEditingClass] = useState(null);
28
- const [selectedClass, setSelectedClass] = useState(null);
29
- const [mobileNavOpen, setMobileNavOpen] = useState(false);
30
-
31
- const storedAdmin = JSON.parse(sessionStorage.getItem("admin") || "{}");
32
- const adminName = storedAdmin.name || "Admin";
33
- const adminEmail = storedAdmin.email || "admin@dojo.com";
34
 
35
- function handleAdminLogout() {
36
- sessionStorage.removeItem("admin");
37
- navigate("/login");
38
- }
39
-
40
- // ---- DATA QUERIES ----
41
-
42
- const {
43
- data: classes = [],
44
- isLoading: classesLoading,
45
- } = useQuery({
46
  queryKey: ["classes"],
47
- queryFn: async () => {
48
- const res = await api.get("/admin/classes");
49
- return Array.isArray(res.data) ? res.data : [];
50
- },
51
- initialData: [],
52
  });
53
 
54
- const { data: plans = [], isLoading: plansLoading } = useQuery({
55
- queryKey: ["membership-plans"],
56
- queryFn: async () => {
57
- const res = await api.get("/admin/plans");
58
- return Array.isArray(res.data) ? res.data : [];
59
- },
60
- initialData: [],
61
- });
62
-
63
- // ---- MUTATIONS ----
64
-
65
  const createClassMutation = useMutation({
66
- mutationFn: async (classData) => {
67
- const res = await api.post("/admin/classes", classData);
68
- return res.data;
69
- },
70
  onSuccess: () => {
71
  queryClient.invalidateQueries({ queryKey: ["classes"] });
72
  setShowForm(false);
73
  setEditingClass(null);
74
- },
75
  });
76
 
77
  const updateClassMutation = useMutation({
78
- mutationFn: async ({ id, classData }) => {
79
- const res = await api.put(`/admin/classes/${id}`, classData);
80
- return res.data;
81
- },
82
  onSuccess: () => {
83
  queryClient.invalidateQueries({ queryKey: ["classes"] });
84
  setShowForm(false);
85
  setEditingClass(null);
86
- },
87
  });
88
 
89
  const deleteClassMutation = useMutation({
90
- mutationFn: async (id) => {
91
- await api.delete(`/admin/classes/${id}`);
92
- },
93
  onSuccess: () => {
94
  queryClient.invalidateQueries({ queryKey: ["classes"] });
95
- },
96
  });
97
 
98
- // ---- HANDLERS ----
99
-
100
  const handleSaveClass = (classData) => {
101
  if (editingClass) {
102
  updateClassMutation.mutate({ id: editingClass.id, classData });
@@ -105,265 +60,88 @@ export default function AdminClasses() {
105
  }
106
  };
107
 
108
- const handleEditClass = (classItem) => {
109
- setEditingClass(classItem);
110
- setShowForm(true);
111
- };
112
-
113
- const handleDeleteClass = (classId) => {
114
- if (window.confirm("Are you sure you want to delete this class?")) {
115
- deleteClassMutation.mutate(classId);
116
- }
117
- };
118
-
119
- const navItems = [
120
- { label: "Dashboard", to: "/admin", icon: LayoutDashboard },
121
- { label: "Classes", to: "/admin/classes", icon: Users2 },
122
- { label: "Exams", to: "/admin/exams", icon: FileText },
123
- { label: "Competitions", to: "/admin/competitions", icon: Trophy },
124
- { label: "Shop", to: "/admin/products", icon: ShoppingBag },
125
- ];
126
-
127
- const handleNavClick = (to) => {
128
- navigate(to);
129
- setMobileNavOpen(false);
130
- };
131
-
132
- const isActivePath = (to) => {
133
- if (to === "/admin") {
134
- return window.location.pathname === "/admin";
135
- }
136
- return window.location.pathname === to;
137
- };
138
-
139
- // ---- RENDER ----
140
-
141
  return (
142
- <div className="min-h-screen bg-stone-50 flex flex-col">
143
- {/* TOP HEADER */}
144
- <header className="border-b border-stone-100 bg-white flex items-center justify-between px-4 sm:px-6 lg:px-10 py-3">
145
- <div className="flex items-center gap-3">
146
- {/* Mobile hamburger */}
147
- <button
148
- type="button"
149
- className="inline-flex md:hidden items-center justify-center h-9 w-9 rounded-full border border-stone-200 text-stone-700 hover:bg-stone-50"
150
- onClick={() => setMobileNavOpen(true)}
151
- aria-label="Open navigation"
152
- >
153
- <Menu className="w-4 h-4" />
154
- </button>
155
- {/* Logo */}
156
- <img
157
- src={dojoLogo}
158
- alt="Dojo logo"
159
- className="h-9 w-9 rounded-full border border-stone-200 bg-white object-cover"
160
- />
161
- <div className="hidden sm:block">
162
- <div className="text-sm font-semibold text-stone-900">
163
- Karate Dojo
164
- </div>
165
- <div className="text-xs text-stone-500">Admin Portal</div>
166
- </div>
167
- </div>
168
- <UserMenu
169
- name={adminName}
170
- email={adminEmail}
171
- onLogout={handleAdminLogout}
172
- />
173
- </header>
174
 
175
- {/* BODY */}
176
- <div className="flex flex-1">
177
- {/* DESKTOP SIDEBAR */}
178
- <aside className="hidden md:flex w-64 flex-col bg-white">
179
- <nav className="flex-1 px-3 pt-4 pb-2 space-y-1">
180
- {navItems.map((item) => {
181
- const Icon = item.icon;
182
- const active = isActivePath(item.to);
183
- return (
184
- <button
185
- key={item.to}
186
- type="button"
187
- onClick={() => handleNavClick(item.to)}
188
- className={`w-full flex items-center gap-3 rounded-xl px-3 py-2 text-sm text-left ${
189
- active
190
- ? "bg-rose-50 text-rose-700 font-semibold"
191
- : "text-stone-600 hover:bg-stone-50"
192
- }`}
193
- >
194
- <span
195
- className={`inline-flex h-8 w-8 items-center justify-center rounded-xl border ${
196
- active
197
- ? "border-rose-100 bg-rose-50 text-rose-600"
198
- : "border-stone-200 bg-white text-stone-500"
199
- }`}
200
- >
201
- <Icon className="w-4 h-4" />
202
- </span>
203
- <span>{item.label}</span>
204
- </button>
205
- );
206
- })}
207
- </nav>
208
- <div className="px-4 pb-5">
209
- <div className="rounded-2xl border border-rose-100 bg-rose-50 px-3 py-2">
210
- <div className="text-[11px] font-semibold text-rose-700">
211
- Admin Mode
212
- </div>
213
- <div className="text-[11px] text-rose-500">Full Access</div>
214
  </div>
 
 
 
 
 
 
 
 
 
 
215
  </div>
216
- </aside>
217
-
218
- {/* MAIN CONTENT */}
219
- <div className="flex-1 border-l border-stone-100">
220
- <main className="px-4 sm:px-6 lg:px-10 py-6 sm:py-8 pb-10">
221
- {/* Page title */}
222
- <div className="mb-8">
223
- <h1 className="text-2xl sm:text-3xl font-bold text-stone-900">
224
- Classes Management
225
- </h1>
226
- <p className="text-stone-600 mt-1">
227
- Create classes and invite students
228
- </p>
229
- </div>
230
-
231
- {/* CLASS SETUP CARD */}
232
- <div className="mb-6 bg-white border border-stone-200 rounded-2xl shadow-sm">
233
- <div className="px-4 sm:px-6 py-4 border-b border-stone-100 flex flex-col sm:flex-row justify-between items-start sm:items-center gap-4">
234
- <div>
235
- <h2 className="text-base font-semibold text-stone-900">
236
- Class Setup
237
- </h2>
238
- <p className="text-sm text-stone-500">
239
- Configure class schedule and invite students
240
- </p>
241
- </div>
242
- <button
243
- type="button"
244
- onClick={() => {
245
- setEditingClass(null);
246
- setShowForm((v) => !v);
247
- }}
248
- className="inline-flex items-center gap-2 rounded-lg bg-red-600 hover:bg-red-700 text-white text-sm font-medium px-4 py-2 shadow-sm"
249
- >
250
- <Plus className="w-4 h-4" />
251
- {showForm ? "Cancel" : "New Class"}
252
- </button>
253
- </div>
254
- {showForm && (
255
- <div className="px-4 sm:px-6 py-4">
256
- <ClassForm
257
- classData={editingClass}
258
- plans={plans}
259
- plansLoading={plansLoading}
260
- onSave={handleSaveClass}
261
- onCancel={() => {
262
- setShowForm(false);
263
- setEditingClass(null);
264
- }}
265
- isLoading={
266
- createClassMutation.isPending ||
267
- updateClassMutation.isPending
268
- }
269
- />
270
- </div>
271
- )}
272
- </div>
273
-
274
- {/* ACTIVE CLASSES CARD */}
275
- <div className="mb-6 bg-white border border-stone-200 rounded-2xl shadow-sm">
276
- <div className="px-4 sm:px-6 py-4 border-b border-stone-100">
277
- <h2 className="text-base font-semibold text-stone-900">
278
- Active Classes
279
- </h2>
280
- <p className="text-sm text-stone-500">
281
- Manage your classes and enrollments
282
- </p>
283
- </div>
284
- <div className="px-4 sm:px-6 py-4">
285
- <ClassesTable
286
- classes={classes}
287
- isLoading={classesLoading}
288
- onEdit={handleEditClass}
289
- onDelete={handleDeleteClass}
290
- onViewEnrollments={setSelectedClass}
291
- />
292
- </div>
293
- </div>
294
-
295
- {/* ENROLLMENTS MODAL / PANEL */}
296
- {selectedClass && (
297
- <ClassEnrollments
298
- classData={selectedClass}
299
- onClose={() => setSelectedClass(null)}
300
- />
301
- )}
302
- </main>
303
  </div>
304
- </div>
305
 
306
- {/* MOBILE DRAWER NAV */}
307
- {mobileNavOpen && (
308
- <>
309
- <div
310
- className="fixed inset-0 z-40 bg-black/20 md:hidden"
311
- onClick={() => setMobileNavOpen(false)}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
312
  />
313
- <div className="fixed inset-y-0 left-0 z-50 w-72 max-w-full bg-white border-r border-stone-100 shadow-lg flex flex-col md:hidden">
314
- <div className="px-5 pt-5 pb-4 flex items-center gap-3 border-b border-stone-100">
315
- <img
316
- src={dojoLogo}
317
- alt="Dojo logo"
318
- className="h-9 w-9 rounded-full border border-stone-200 bg-white object-cover"
319
- />
320
- <div>
321
- <div className="text-sm font-semibold text-stone-900">
322
- Karate Dojo
323
- </div>
324
- <div className="text-xs text-stone-500">Admin Portal</div>
325
- </div>
326
- </div>
327
- <nav className="flex-1 px-3 pt-4 pb-2 space-y-1 overflow-y-auto">
328
- {navItems.map((item) => {
329
- const Icon = item.icon;
330
- const active = isActivePath(item.to);
331
- return (
332
- <button
333
- key={item.to}
334
- type="button"
335
- onClick={() => handleNavClick(item.to)}
336
- className={`w-full flex items-center gap-3 rounded-xl px-3 py-2 text-sm text-left ${
337
- active
338
- ? "bg-rose-50 text-rose-700 font-semibold"
339
- : "text-stone-600 hover:bg-stone-50"
340
- }`}
341
- >
342
- <span
343
- className={`inline-flex h-8 w-8 items-center justify-center rounded-xl border ${
344
- active
345
- ? "border-rose-100 bg-rose-50 text-rose-600"
346
- : "border-stone-200 bg-white text-stone-500"
347
- }`}
348
- >
349
- <Icon className="w-4 h-4" />
350
- </span>
351
- <span>{item.label}</span>
352
- </button>
353
- );
354
- })}
355
- </nav>
356
- <div className="px-4 pb-5">
357
- <div className="rounded-2xl border border-rose-100 bg-rose-50 px-3 py-2">
358
- <div className="text-[11px] font-semibold text-rose-700">
359
- Admin Mode
360
- </div>
361
- <div className="text-[11px] text-rose-500">Full Access</div>
362
- </div>
363
- </div>
364
- </div>
365
- </>
366
- )}
367
  </div>
368
  );
369
  }
 
1
  // frontend/src/pages/AdminClasses.jsx
2
  import React, { useState } from "react";
3
+ import { base44 } from "@/api/base44Client";
4
  import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
5
+ import { Button } from "@/components/ui/button";
6
  import {
7
+ Card,
8
+ CardContent,
9
+ CardHeader,
10
+ CardTitle,
11
+ CardDescription
12
+ } from "@/components/ui/card";
13
+ import { Plus } from "lucide-react";
 
 
 
 
14
  import ClassForm from "../components/admin/ClassForm";
15
  import ClassesTable from "../components/admin/ClassesTable";
16
+ import ClassStudentManager from "../components/admin/ClassStudentManager";
 
17
 
18
  export default function AdminClasses() {
 
19
  const queryClient = useQueryClient();
 
20
  const [showForm, setShowForm] = useState(false);
21
  const [editingClass, setEditingClass] = useState(null);
22
+ const [managingClass, setManagingClass] = useState(null);
 
 
 
 
 
23
 
24
+ const { data: classes = [], isLoading } = useQuery({
 
 
 
 
 
 
 
 
 
 
25
  queryKey: ["classes"],
26
+ queryFn: () => base44.entities.Class.list("-created_date"),
27
+ initialData: []
 
 
 
28
  });
29
 
 
 
 
 
 
 
 
 
 
 
 
30
  const createClassMutation = useMutation({
31
+ mutationFn: (classData) => base44.entities.Class.create(classData),
 
 
 
32
  onSuccess: () => {
33
  queryClient.invalidateQueries({ queryKey: ["classes"] });
34
  setShowForm(false);
35
  setEditingClass(null);
36
+ }
37
  });
38
 
39
  const updateClassMutation = useMutation({
40
+ mutationFn: ({ id, classData }) => base44.entities.Class.update(id, classData),
 
 
 
41
  onSuccess: () => {
42
  queryClient.invalidateQueries({ queryKey: ["classes"] });
43
  setShowForm(false);
44
  setEditingClass(null);
45
+ }
46
  });
47
 
48
  const deleteClassMutation = useMutation({
49
+ mutationFn: (id) => base44.entities.Class.delete(id),
 
 
50
  onSuccess: () => {
51
  queryClient.invalidateQueries({ queryKey: ["classes"] });
52
+ }
53
  });
54
 
 
 
55
  const handleSaveClass = (classData) => {
56
  if (editingClass) {
57
  updateClassMutation.mutate({ id: editingClass.id, classData });
 
60
  }
61
  };
62
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
63
  return (
64
+ <div className="p-6 max-w-7xl mx-auto">
65
+ {/* Header */}
66
+ <div className="mb-8">
67
+ <h1 className="text-3xl font-bold text-stone-900">Classes Management</h1>
68
+ <p className="text-stone-600 mt-1">
69
+ Create classes and manage student enrollments
70
+ </p>
71
+ </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
72
 
73
+ {/* Class Form */}
74
+ <Card className="mb-6">
75
+ <CardHeader>
76
+ <div className="flex justify-between items-center">
77
+ <div>
78
+ <CardTitle>Class Setup</CardTitle>
79
+ <CardDescription>Create classes with flexible schedules</CardDescription>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
80
  </div>
81
+ <Button
82
+ onClick={() => {
83
+ setEditingClass(null);
84
+ setShowForm(!showForm);
85
+ }}
86
+ className="gap-2 bg-red-600 hover:bg-red-700"
87
+ >
88
+ <Plus className="w-4 h-4" />
89
+ {showForm ? "Cancel" : "New Class"}
90
+ </Button>
91
  </div>
92
+ </CardHeader>
93
+ {showForm && (
94
+ <CardContent>
95
+ <ClassForm
96
+ classData={editingClass}
97
+ onSave={handleSaveClass}
98
+ onCancel={() => {
99
+ setShowForm(false);
100
+ setEditingClass(null);
101
+ }}
102
+ isLoading={
103
+ createClassMutation.isPending || updateClassMutation.isPending
104
+ }
105
+ />
106
+ </CardContent>
107
+ )}
108
+ </Card>
109
+
110
+ {/* Student Manager (when a class is selected) */}
111
+ {managingClass && (
112
+ <div className="mb-6">
113
+ <ClassStudentManager
114
+ classData={managingClass}
115
+ onClose={() => setManagingClass(null)}
116
+ />
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
117
  </div>
118
+ )}
119
 
120
+ {/* Classes Table */}
121
+ <Card>
122
+ <CardHeader>
123
+ <CardTitle>All Classes</CardTitle>
124
+ <CardDescription>
125
+ Manage class schedules and enrollments
126
+ </CardDescription>
127
+ </CardHeader>
128
+ <CardContent>
129
+ <ClassesTable
130
+ classes={classes}
131
+ isLoading={isLoading}
132
+ onEdit={(cls) => {
133
+ setEditingClass(cls);
134
+ setShowForm(true);
135
+ }}
136
+ onDelete={(id) => {
137
+ if (confirm("Are you sure you want to delete this class?")) {
138
+ deleteClassMutation.mutate(id);
139
+ }
140
+ }}
141
+ onManageStudents={(cls) => setManagingClass(cls)}
142
  />
143
+ </CardContent>
144
+ </Card>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
145
  </div>
146
  );
147
  }