Seth0330 commited on
Commit
6d059dd
·
verified ·
1 Parent(s): 1cbbccd

Update frontend/src/pages/AdminDashboard.jsx

Browse files
Files changed (1) hide show
  1. frontend/src/pages/AdminDashboard.jsx +199 -92
frontend/src/pages/AdminDashboard.jsx CHANGED
@@ -1,6 +1,6 @@
1
  // frontend/src/pages/AdminDashboard.jsx
2
- import React, { useState } from "react";
3
- import { Link, useNavigate } from "react-router-dom";
4
  import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
5
  import {
6
  Users,
@@ -29,6 +29,7 @@ import dojoLogo from "../assets/dojo-logo.png";
29
  export default function AdminDashboard() {
30
  const queryClient = useQueryClient();
31
  const navigate = useNavigate();
 
32
 
33
  const [showPlanForm, setShowPlanForm] = useState(false);
34
  const [editingPlan, setEditingPlan] = useState(null);
@@ -36,6 +37,9 @@ export default function AdminDashboard() {
36
  const [planError, setPlanError] = useState("");
37
  const [mobileNavOpen, setMobileNavOpen] = useState(false);
38
 
 
 
 
39
  const storedAdmin = JSON.parse(sessionStorage.getItem("admin") || "{}");
40
  const adminName = storedAdmin.name || "Admin";
41
  const adminEmail = storedAdmin.email || "admin@dojo.com";
@@ -45,6 +49,17 @@ export default function AdminDashboard() {
45
  navigate("/login");
46
  }
47
 
 
 
 
 
 
 
 
 
 
 
 
48
  // MEMBERSHIPS
49
  const membershipsQuery = useQuery({
50
  queryKey: ["all-memberships"],
@@ -165,11 +180,34 @@ export default function AdminDashboard() {
165
  return window.location.pathname === to;
166
  };
167
 
168
- // NEW: helper for clicking overview cards
169
  const handleMembersFilterClick = (filter) => {
170
- navigate(`/admin/members?filter=${filter}`);
171
  };
172
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
173
  return (
174
  <div className="min-h-screen bg-stone-50 flex flex-col">
175
  {/* TOP HEADER: logo (desktop) + hamburger (mobile) + user menu */}
@@ -262,105 +300,174 @@ export default function AdminDashboard() {
262
  Manage memberships and monitor your dojo
263
  </p>
264
  </div>
265
- <button
266
- type="button"
267
- onClick={() => setShowAIChat(true)}
268
- className="inline-flex items-center gap-2 rounded-lg bg-purple-600 hover:bg-purple-700 text-white text-sm font-medium px-4 py-2 shadow-sm"
269
- >
270
- <Sparkles className="w-4 h-4" />
271
- AI Assistant
272
- </button>
273
- </div>
274
- </div>
275
-
276
- {/* Overview cards */}
277
- <div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4 sm:gap-6 mb-8">
278
- <OverviewCard
279
- title="Active Members"
280
- value={membershipsLoading ? "…" : activeMembers}
281
- icon={Users}
282
- color="blue"
283
- trend={`${memberships.length} total`}
284
- onClick={() => handleMembersFilterClick("active")}
285
- />
286
- <OverviewCard
287
- title="Renewing Soon"
288
- value={membershipsLoading ? "…" : renewingSoon}
289
- icon={Calendar}
290
- color="amber"
291
- trend="Next 14 days"
292
- onClick={() => handleMembersFilterClick("renewing")}
293
- />
294
- <OverviewCard
295
- title="Expired"
296
- value={membershipsLoading ? "…" : expired}
297
- icon={AlertCircle}
298
- color="red"
299
- trend="Needs attention"
300
- onClick={() => handleMembersFilterClick("expired")}
301
- />
302
- </div>
303
 
304
- {/* AI insights */}
305
- <AIInsightBox memberships={memberships} />
306
-
307
- {/* Membership plans */}
308
- <div className="mb-8 bg-white border border-stone-200 rounded-2xl shadow-sm">
309
- <div className="px-4 sm:px-6 py-4 border-b border-stone-100">
310
- <div className="flex flex-col sm:flex-row justify-between items-start sm:items-center gap-4">
311
- <div>
312
- <h2 className="text-base font-semibold text-stone-900">
313
- Membership Plans
314
- </h2>
315
- <p className="text-sm text-stone-500">
316
- Create and manage membership offerings
317
- </p>
318
- </div>
319
  <button
320
  type="button"
321
  onClick={() => {
322
- setEditingPlan(null);
323
- setShowPlanForm((v) => !v);
324
- setPlanError("");
325
  }}
326
- 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"
327
  >
328
- <Plus className="w-4 h-4" />
329
- New Plan
 
 
 
 
 
 
 
 
330
  </button>
331
- </div>
332
- </div>
333
- <div className="px-4 sm:px-6 py-4">
334
- {planError && (
335
- <p className="mb-4 text-sm text-red-600 bg-red-50 border border-red-100 rounded-md px-3 py-2">
336
- {planError}
337
- </p>
338
  )}
339
- {showPlanForm && (
340
- <div className="mb-6">
341
- <PlanForm
342
- plan={editingPlan}
343
- onSave={handleSavePlan}
344
- onCancel={() => {
345
- setShowPlanForm(false);
346
- setEditingPlan(null);
347
- setPlanError("");
348
- }}
349
- isLoading={
350
- createPlanMutation.isPending ||
351
- updatePlanMutation.isPending
352
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
353
  />
354
  </div>
355
- )}
356
- <PlansTable
357
- plans={plans}
358
- isLoading={plansLoading}
359
- onEdit={handleEditPlan}
360
- onDelete={handleDeletePlan}
361
- />
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
362
  </div>
363
- </div>
364
  </main>
365
  </div>
366
  </div>
 
1
  // frontend/src/pages/AdminDashboard.jsx
2
+ import React, { useState, useEffect } from "react";
3
+ import { Link, useNavigate, useLocation } from "react-router-dom";
4
  import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
5
  import {
6
  Users,
 
29
  export default function AdminDashboard() {
30
  const queryClient = useQueryClient();
31
  const navigate = useNavigate();
32
+ const location = useLocation();
33
 
34
  const [showPlanForm, setShowPlanForm] = useState(false);
35
  const [editingPlan, setEditingPlan] = useState(null);
 
37
  const [planError, setPlanError] = useState("");
38
  const [mobileNavOpen, setMobileNavOpen] = useState(false);
39
 
40
+ // which members-filter view is active: "active" | "renewing" | "expired" | null
41
+ const [membersFilter, setMembersFilter] = useState(null);
42
+
43
  const storedAdmin = JSON.parse(sessionStorage.getItem("admin") || "{}");
44
  const adminName = storedAdmin.name || "Admin";
45
  const adminEmail = storedAdmin.email || "admin@dojo.com";
 
49
  navigate("/login");
50
  }
51
 
52
+ // Sync filter with URL (?filter=...)
53
+ useEffect(() => {
54
+ const params = new URLSearchParams(location.search);
55
+ const filter = params.get("filter");
56
+ if (filter === "active" || filter === "renewing" || filter === "expired") {
57
+ setMembersFilter(filter);
58
+ } else {
59
+ setMembersFilter(null);
60
+ }
61
+ }, [location.search]);
62
+
63
  // MEMBERSHIPS
64
  const membershipsQuery = useQuery({
65
  queryKey: ["all-memberships"],
 
180
  return window.location.pathname === to;
181
  };
182
 
183
+ // Clicking overview cards -> stay in /admin, but switch view & update query param
184
  const handleMembersFilterClick = (filter) => {
185
+ navigate(`/admin?filter=${filter}`);
186
  };
187
 
188
+ // Build filtered list for members view
189
+ let filteredMembers = memberships;
190
+ if (membersFilter === "active") {
191
+ filteredMembers = memberships.filter((m) => m.status === "active");
192
+ } else if (membersFilter === "expired") {
193
+ filteredMembers = memberships.filter((m) => m.status === "expired");
194
+ } else if (membersFilter === "renewing") {
195
+ filteredMembers = memberships.filter((m) => {
196
+ if (m.status !== "active" || !m.renewal_date) return false;
197
+ const days = differenceInDays(new Date(m.renewal_date), new Date());
198
+ return days >= 0 && days <= 14;
199
+ });
200
+ }
201
+
202
+ const membersTitle =
203
+ membersFilter === "active"
204
+ ? "Active members"
205
+ : membersFilter === "renewing"
206
+ ? "Renewing soon"
207
+ : membersFilter === "expired"
208
+ ? "Expired members"
209
+ : "";
210
+
211
  return (
212
  <div className="min-h-screen bg-stone-50 flex flex-col">
213
  {/* TOP HEADER: logo (desktop) + hamburger (mobile) + user menu */}
 
300
  Manage memberships and monitor your dojo
301
  </p>
302
  </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
303
 
304
+ {/* Back to overview for members filter view */}
305
+ {membersFilter ? (
 
 
 
 
 
 
 
 
 
 
 
 
 
306
  <button
307
  type="button"
308
  onClick={() => {
309
+ setMembersFilter(null);
310
+ navigate("/admin");
 
311
  }}
312
+ className="inline-flex items-center gap-2 rounded-full border border-stone-200 bg-white text-sm text-stone-700 px-4 py-2 hover:bg-stone-50"
313
  >
314
+ Back to overview
315
+ </button>
316
+ ) : (
317
+ <button
318
+ type="button"
319
+ onClick={() => setShowAIChat(true)}
320
+ className="inline-flex items-center gap-2 rounded-lg bg-purple-600 hover:bg-purple-700 text-white text-sm font-medium px-4 py-2 shadow-sm"
321
+ >
322
+ <Sparkles className="w-4 h-4" />
323
+ AI Assistant
324
  </button>
 
 
 
 
 
 
 
325
  )}
326
+ </div>
327
+ </div>
328
+
329
+ {/* If no filter selected -> original dashboard view */}
330
+ {!membersFilter && (
331
+ <>
332
+ {/* Overview cards */}
333
+ <div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4 sm:gap-6 mb-8">
334
+ <OverviewCard
335
+ title="Active Members"
336
+ value={membershipsLoading ? "…" : activeMembers}
337
+ icon={Users}
338
+ color="blue"
339
+ trend={`${memberships.length} total`}
340
+ onClick={() => handleMembersFilterClick("active")}
341
+ />
342
+ <OverviewCard
343
+ title="Renewing Soon"
344
+ value={membershipsLoading ? "…" : renewingSoon}
345
+ icon={Calendar}
346
+ color="amber"
347
+ trend="Next 14 days"
348
+ onClick={() => handleMembersFilterClick("renewing")}
349
+ />
350
+ <OverviewCard
351
+ title="Expired"
352
+ value={membershipsLoading ? "…" : expired}
353
+ icon={AlertCircle}
354
+ color="red"
355
+ trend="Needs attention"
356
+ onClick={() => handleMembersFilterClick("expired")}
357
+ />
358
+ </div>
359
+
360
+ {/* AI insights */}
361
+ <AIInsightBox memberships={memberships} />
362
+
363
+ {/* Membership plans */}
364
+ <div className="mb-8 bg-white border border-stone-200 rounded-2xl shadow-sm">
365
+ <div className="px-4 sm:px-6 py-4 border-b border-stone-100">
366
+ <div className="flex flex-col sm:flex-row justify-between items-start sm:items-center gap-4">
367
+ <div>
368
+ <h2 className="text-base font-semibold text-stone-900">
369
+ Membership Plans
370
+ </h2>
371
+ <p className="text-sm text-stone-500">
372
+ Create and manage membership offerings
373
+ </p>
374
+ </div>
375
+ <button
376
+ type="button"
377
+ onClick={() => {
378
+ setEditingPlan(null);
379
+ setShowPlanForm((v) => !v);
380
+ setPlanError("");
381
+ }}
382
+ 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"
383
+ >
384
+ <Plus className="w-4 h-4" />
385
+ New Plan
386
+ </button>
387
+ </div>
388
+ </div>
389
+ <div className="px-4 sm:px-6 py-4">
390
+ {planError && (
391
+ <p className="mb-4 text-sm text-red-600 bg-red-50 border border-red-100 rounded-md px-3 py-2">
392
+ {planError}
393
+ </p>
394
+ )}
395
+ {showPlanForm && (
396
+ <div className="mb-6">
397
+ <PlanForm
398
+ plan={editingPlan}
399
+ onSave={handleSavePlan}
400
+ onCancel={() => {
401
+ setShowPlanForm(false);
402
+ setEditingPlan(null);
403
+ setPlanError("");
404
+ }}
405
+ isLoading={
406
+ createPlanMutation.isPending ||
407
+ updatePlanMutation.isPending
408
+ }
409
+ />
410
+ </div>
411
+ )}
412
+ <PlansTable
413
+ plans={plans}
414
+ isLoading={plansLoading}
415
+ onEdit={handleEditPlan}
416
+ onDelete={handleDeletePlan}
417
  />
418
  </div>
419
+ </div>
420
+ </>
421
+ )}
422
+
423
+ {/* If a filter is selected -> inline members list card */}
424
+ {membersFilter && (
425
+ <div className="bg-white border border-stone-200 rounded-2xl shadow-sm">
426
+ <div className="px-4 sm:px-6 py-4 border-b border-stone-100 flex items-center justify-between">
427
+ <div className="text-sm font-semibold text-stone-900">
428
+ {membersTitle}
429
+ </div>
430
+ <div className="text-xs text-stone-500">
431
+ Showing {filteredMembers.length} of {memberships.length} total
432
+ </div>
433
+ </div>
434
+ <div className="px-4 sm:px-6 py-4">
435
+ {membershipsLoading ? (
436
+ <p className="text-sm text-stone-500">Loading members…</p>
437
+ ) : filteredMembers.length === 0 ? (
438
+ <p className="text-sm text-stone-500">
439
+ No members match this filter yet.
440
+ </p>
441
+ ) : (
442
+ <ul className="divide-y divide-stone-100">
443
+ {filteredMembers.map((m) => (
444
+ <li
445
+ key={m.id}
446
+ className="py-3 flex items-center justify-between"
447
+ >
448
+ <div>
449
+ <div className="text-sm font-medium text-stone-900">
450
+ {m.user_name || m.user_email}
451
+ </div>
452
+ <div className="text-xs text-stone-500">
453
+ {m.plan_name || "No plan name"} ·{" "}
454
+ {m.billing_period || "—"} · {m.status}
455
+ </div>
456
+ </div>
457
+ <div className="text-xs text-stone-500 text-right">
458
+ {m.renewal_date
459
+ ? `Renews on ${new Date(
460
+ m.renewal_date
461
+ ).toLocaleDateString()}`
462
+ : ""}
463
+ </div>
464
+ </li>
465
+ ))}
466
+ </ul>
467
+ )}
468
+ </div>
469
  </div>
470
+ )}
471
  </main>
472
  </div>
473
  </div>