add tab management
This commit is contained in:
parent
00ab7aa1a3
commit
b127429c39
10 changed files with 669 additions and 190 deletions
|
@ -6,7 +6,7 @@ import EventCheckIn from "./EventsSection/EventCheckIn";
|
||||||
import EventLoad from "./EventsSection/EventLoad";
|
import EventLoad from "./EventsSection/EventLoad";
|
||||||
---
|
---
|
||||||
|
|
||||||
<div id="eventsSection" class="dashboard-section hidden">
|
<div id="" class="">
|
||||||
<div class="mb-4 sm:mb-6 px-4 sm:px-6">
|
<div class="mb-4 sm:mb-6 px-4 sm:px-6">
|
||||||
<h2 class="text-xl sm:text-2xl font-bold">Events</h2>
|
<h2 class="text-xl sm:text-2xl font-bold">Events</h2>
|
||||||
<p class="opacity-70 text-sm sm:text-base">
|
<p class="opacity-70 text-sm sm:text-base">
|
||||||
|
|
|
@ -70,7 +70,7 @@ const totalPages = eventResponse.totalPages;
|
||||||
const currentPage = eventResponse.page;
|
const currentPage = eventResponse.page;
|
||||||
---
|
---
|
||||||
|
|
||||||
<div id="eventManagementSection" class="dashboard-section hidden">
|
<div id=" class=" ">
|
||||||
<div
|
<div
|
||||||
class="mb-4 md:mb-6 flex flex-col md:flex-row md:justify-between md:items-center gap-2"
|
class="mb-4 md:mb-6 flex flex-col md:flex-row md:justify-between md:items-center gap-2"
|
||||||
>
|
>
|
||||||
|
|
|
@ -4,7 +4,7 @@ import ShowProfileLogs from "./ProfileSection/ShowProfileLogs";
|
||||||
import { Stats } from "./ProfileSection/Stats";
|
import { Stats } from "./ProfileSection/Stats";
|
||||||
---
|
---
|
||||||
|
|
||||||
<div id="profileSection" class="dashboard-section">
|
<div id="" class="">
|
||||||
<div class="mb-6">
|
<div class="mb-6">
|
||||||
<h2 class="text-2xl font-bold">Dashboard Overview</h2>
|
<h2 class="text-2xl font-bold">Dashboard Overview</h2>
|
||||||
<p class="opacity-70">Welcome to your IEEE UCSD dashboard</p>
|
<p class="opacity-70">Welcome to your IEEE UCSD dashboard</p>
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
import { Icon } from "astro-icon/components";
|
import { Icon } from "astro-icon/components";
|
||||||
---
|
---
|
||||||
|
|
||||||
<div id="reimbursementSection" class="dashboard-section hidden">
|
<div id="" class="">
|
||||||
<div class="mb-6">
|
<div class="mb-6">
|
||||||
<h2 class="text-2xl font-bold">Reimbursement</h2>
|
<h2 class="text-2xl font-bold">Reimbursement</h2>
|
||||||
<p class="opacity-70">Manage your reimbursement requests</p>
|
<p class="opacity-70">Manage your reimbursement requests</p>
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
import { Icon } from "astro-icon/components";
|
import { Icon } from "astro-icon/components";
|
||||||
---
|
---
|
||||||
|
|
||||||
<div id="settingsSection" class="dashboard-section hidden">
|
<div id="" class="">
|
||||||
<div class="mb-6">
|
<div class="mb-6">
|
||||||
<h2 class="text-2xl font-bold">Settings</h2>
|
<h2 class="text-2xl font-bold">Settings</h2>
|
||||||
<p class="opacity-70">Manage your account settings</p>
|
<p class="opacity-70">Manage your account settings</p>
|
||||||
|
|
99
src/components/dashboard/SponsorAnalytics.astro
Normal file
99
src/components/dashboard/SponsorAnalytics.astro
Normal file
|
@ -0,0 +1,99 @@
|
||||||
|
---
|
||||||
|
// Sponsor Analytics Component
|
||||||
|
---
|
||||||
|
|
||||||
|
<div class="card bg-base-100 shadow-xl">
|
||||||
|
<div class="card-body">
|
||||||
|
<h2 class="card-title text-2xl mb-4">Analytics Dashboard</h2>
|
||||||
|
|
||||||
|
<!-- Metrics Overview -->
|
||||||
|
<div class="grid gap-4 md:grid-cols-2 lg:grid-cols-4 mb-6">
|
||||||
|
<div class="stat bg-base-200 rounded-box p-4">
|
||||||
|
<div class="stat-title">Resume Downloads</div>
|
||||||
|
<div class="stat-value text-primary">89</div>
|
||||||
|
<div class="stat-desc">↗︎ 14 (30 days)</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="stat bg-base-200 rounded-box p-4">
|
||||||
|
<div class="stat-title">Event Attendance</div>
|
||||||
|
<div class="stat-value">45</div>
|
||||||
|
<div class="stat-desc">↘︎ 5 (30 days)</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="stat bg-base-200 rounded-box p-4">
|
||||||
|
<div class="stat-title">Student Interactions</div>
|
||||||
|
<div class="stat-value text-secondary">124</div>
|
||||||
|
<div class="stat-desc">↗︎ 32 (30 days)</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="stat bg-base-200 rounded-box p-4">
|
||||||
|
<div class="stat-title">Workshop Engagement</div>
|
||||||
|
<div class="stat-value">92%</div>
|
||||||
|
<div class="stat-desc">↗︎ 8% (30 days)</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Detailed Analytics -->
|
||||||
|
<div class="grid gap-6 md:grid-cols-2">
|
||||||
|
<!-- Event Performance -->
|
||||||
|
<div class="card bg-base-200">
|
||||||
|
<div class="card-body">
|
||||||
|
<h3 class="card-title text-lg">Event Performance</h3>
|
||||||
|
<div class="overflow-x-auto">
|
||||||
|
<table class="table table-sm">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Event</th>
|
||||||
|
<th>Attendance</th>
|
||||||
|
<th>Rating</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td>Tech Talk</td>
|
||||||
|
<td>32</td>
|
||||||
|
<td>4.8/5</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Workshop</td>
|
||||||
|
<td>28</td>
|
||||||
|
<td>4.6/5</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Resume Analytics -->
|
||||||
|
<div class="card bg-base-200">
|
||||||
|
<div class="card-body">
|
||||||
|
<h3 class="card-title text-lg">Resume Analytics</h3>
|
||||||
|
<div class="overflow-x-auto">
|
||||||
|
<table class="table table-sm">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Major</th>
|
||||||
|
<th>Downloads</th>
|
||||||
|
<th>Trend</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td>Computer Science</td>
|
||||||
|
<td>45</td>
|
||||||
|
<td>↗︎</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Electrical Engineering</td>
|
||||||
|
<td>32</td>
|
||||||
|
<td>↗︎</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
78
src/components/dashboard/SponsorDashboard.astro
Normal file
78
src/components/dashboard/SponsorDashboard.astro
Normal file
|
@ -0,0 +1,78 @@
|
||||||
|
---
|
||||||
|
// Sponsor Dashboard Component
|
||||||
|
---
|
||||||
|
|
||||||
|
<div class="card bg-base-100 shadow-xl">
|
||||||
|
<div class="card-body">
|
||||||
|
<h2 class="card-title text-2xl mb-4">Sponsor Dashboard</h2>
|
||||||
|
<div class="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
|
||||||
|
<!-- Sponsorship Status -->
|
||||||
|
<div class="card bg-base-200">
|
||||||
|
<div class="card-body">
|
||||||
|
<h3 class="card-title text-lg">Sponsorship Status</h3>
|
||||||
|
<p class="text-primary font-semibold">Active</p>
|
||||||
|
<p class="text-sm opacity-70">Valid until: Dec 31, 2024</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Partnership Level -->
|
||||||
|
<div class="card bg-base-200">
|
||||||
|
<div class="card-body">
|
||||||
|
<h3 class="card-title text-lg">Partnership Level</h3>
|
||||||
|
<p class="text-primary font-semibold">Platinum</p>
|
||||||
|
<p class="text-sm opacity-70">All benefits included</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Quick Actions -->
|
||||||
|
<div class="card bg-base-200">
|
||||||
|
<div class="card-body">
|
||||||
|
<h3 class="card-title text-lg">Quick Actions</h3>
|
||||||
|
<div class="flex gap-2 mt-2">
|
||||||
|
<button class="btn btn-primary btn-sm"
|
||||||
|
>Contact Us</button
|
||||||
|
>
|
||||||
|
<button class="btn btn-outline btn-sm"
|
||||||
|
>View Contract</button
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Recent Activity -->
|
||||||
|
<div class="mt-6">
|
||||||
|
<h3 class="text-xl font-semibold mb-4">Recent Activity</h3>
|
||||||
|
<div class="overflow-x-auto">
|
||||||
|
<table class="table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Date</th>
|
||||||
|
<th>Activity</th>
|
||||||
|
<th>Status</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td>2024-01-15</td>
|
||||||
|
<td>Resume Book Access</td>
|
||||||
|
<td
|
||||||
|
><span class="badge badge-success"
|
||||||
|
>Completed</span
|
||||||
|
></td
|
||||||
|
>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>2024-01-10</td>
|
||||||
|
<td>Workshop Scheduling</td>
|
||||||
|
<td
|
||||||
|
><span class="badge badge-warning">Pending</span
|
||||||
|
></td
|
||||||
|
>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
|
@ -1,29 +1,91 @@
|
||||||
member:
|
sections:
|
||||||
tabs:
|
# Base Menu (accessible to all except sponsors)
|
||||||
- "Dashboard"
|
profile:
|
||||||
- "Events"
|
title: "Dashboard"
|
||||||
- "Reimbursements"
|
icon: "heroicons:home"
|
||||||
|
role: "none"
|
||||||
|
component: "ProfileSection"
|
||||||
|
class: "text-primary hover:text-primary-focus"
|
||||||
|
|
||||||
officer:
|
events:
|
||||||
tabs:
|
title: "Events"
|
||||||
- "Event Management"
|
icon: "heroicons:calendar"
|
||||||
include:
|
role: "none"
|
||||||
- "member"
|
component: "EventsSection"
|
||||||
roles:
|
class: "text-secondary hover:text-secondary-focus"
|
||||||
- "IEEE Officer"
|
|
||||||
|
|
||||||
executive:
|
reimbursement:
|
||||||
include:
|
title: "Reimbursement"
|
||||||
- "officer"
|
icon: "heroicons:credit-card"
|
||||||
roles:
|
role: "none"
|
||||||
- "IEEE Executive"
|
component: "ReimbursementSection"
|
||||||
|
class: "text-accent hover:text-accent-focus"
|
||||||
|
|
||||||
administrator:
|
# Officer Menu
|
||||||
include:
|
eventManagement:
|
||||||
- "executive"
|
title: "Event Management"
|
||||||
roles:
|
icon: "heroicons:cog-6-tooth"
|
||||||
- "IEEE Administrator"
|
role: "general"
|
||||||
|
component: "Officer_EventManagement"
|
||||||
|
class: "text-info hover:text-info-focus"
|
||||||
|
|
||||||
sponsor:
|
# Sponsor Menu
|
||||||
roles:
|
sponsorDashboard:
|
||||||
- "IEEE Sponsor"
|
title: "Sponsor Dashboard"
|
||||||
|
icon: "heroicons:briefcase"
|
||||||
|
role: "sponsor"
|
||||||
|
component: "SponsorDashboard"
|
||||||
|
class: "text-warning hover:text-warning-focus"
|
||||||
|
|
||||||
|
sponsorAnalytics:
|
||||||
|
title: "Analytics"
|
||||||
|
icon: "heroicons:chart-bar"
|
||||||
|
role: "sponsor"
|
||||||
|
component: "SponsorAnalytics"
|
||||||
|
class: "text-warning hover:text-warning-focus"
|
||||||
|
|
||||||
|
# Settings (accessible to all except sponsors)
|
||||||
|
settings:
|
||||||
|
title: "Settings"
|
||||||
|
icon: "heroicons:cog-6-tooth"
|
||||||
|
role: "none"
|
||||||
|
component: "SettingsSection"
|
||||||
|
class: "text-neutral hover:text-neutral-focus"
|
||||||
|
|
||||||
|
logout:
|
||||||
|
title: "Logout"
|
||||||
|
icon: "heroicons:arrow-left-on-rectangle"
|
||||||
|
role: "none"
|
||||||
|
class: "text-error hover:text-error-focus"
|
||||||
|
|
||||||
|
# Menu Categories
|
||||||
|
categories:
|
||||||
|
main:
|
||||||
|
title: "Main Menu"
|
||||||
|
sections: ["profile", "events", "reimbursement"]
|
||||||
|
role: "none"
|
||||||
|
|
||||||
|
officer:
|
||||||
|
title: "Officer Menu"
|
||||||
|
sections: ["eventManagement"]
|
||||||
|
role: "general"
|
||||||
|
|
||||||
|
executive:
|
||||||
|
title: "Executive Menu"
|
||||||
|
sections: []
|
||||||
|
role: "executive"
|
||||||
|
|
||||||
|
admin:
|
||||||
|
title: "Admin Menu"
|
||||||
|
sections: []
|
||||||
|
role: "admin"
|
||||||
|
|
||||||
|
sponsor:
|
||||||
|
title: "Sponsor Portal"
|
||||||
|
sections: ["sponsorDashboard", "sponsorAnalytics"]
|
||||||
|
role: "sponsor"
|
||||||
|
|
||||||
|
account:
|
||||||
|
title: "Account"
|
||||||
|
sections: ["settings", "logout"]
|
||||||
|
role: "none"
|
||||||
|
|
|
@ -1,12 +1,32 @@
|
||||||
---
|
---
|
||||||
import yaml from "js-yaml";
|
import yaml from "js-yaml";
|
||||||
import { Icon } from "astro-icon/components";
|
import { Icon } from "astro-icon/components";
|
||||||
import ProfileSection from "../components/dashboard/ProfileSection.astro";
|
import { hasAccess, type OfficerStatus } from "../utils/roleAccess";
|
||||||
import EventsSection from "../components/dashboard/EventsSection.astro";
|
import fs from "node:fs";
|
||||||
import ReimbursementSection from "../components/dashboard/ReimbursementSection.astro";
|
import path from "node:path";
|
||||||
import SettingsSection from "../components/dashboard/SettingsSection.astro";
|
|
||||||
import Officer_EventManagement from "../components/dashboard/Officer_EventManagement.astro";
|
|
||||||
const title = "Dashboard";
|
const title = "Dashboard";
|
||||||
|
|
||||||
|
// Load and parse dashboard config
|
||||||
|
const configPath = path.join(process.cwd(), "src", "config", "dashboard.yaml");
|
||||||
|
const dashboardConfig = yaml.load(fs.readFileSync(configPath, "utf8")) as any;
|
||||||
|
|
||||||
|
// Dynamically import all dashboard components
|
||||||
|
const components = Object.fromEntries(
|
||||||
|
await Promise.all(
|
||||||
|
Object.values(dashboardConfig.sections)
|
||||||
|
.filter((section: any) => section.component) // Only process sections with components
|
||||||
|
.map(async (section: any) => {
|
||||||
|
const component = await import(
|
||||||
|
`../components/dashboard/${section.component}.astro`
|
||||||
|
);
|
||||||
|
console.log(`Loaded component: ${section.component}`); // Debug log
|
||||||
|
return [section.component, component.default];
|
||||||
|
})
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
console.log("Available components:", Object.keys(components)); // Debug log
|
||||||
---
|
---
|
||||||
|
|
||||||
<!doctype html>
|
<!doctype html>
|
||||||
|
@ -38,9 +58,57 @@ const title = "Dashboard";
|
||||||
|
|
||||||
<!-- User Profile -->
|
<!-- User Profile -->
|
||||||
<div class="p-6 border-b border-base-200">
|
<div class="p-6 border-b border-base-200">
|
||||||
|
<!-- Loading State -->
|
||||||
<div
|
<div
|
||||||
|
id="userProfileSkeleton"
|
||||||
class="flex items-center gap-4"
|
class="flex items-center gap-4"
|
||||||
|
>
|
||||||
|
<div class="avatar flex items-center justify-center">
|
||||||
|
<div
|
||||||
|
class="w-12 h-12 rounded-xl bg-base-300 animate-pulse"
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex-1">
|
||||||
|
<div
|
||||||
|
class="h-6 w-32 bg-base-300 animate-pulse rounded mb-2"
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="h-5 w-20 bg-base-300 animate-pulse rounded"
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Signed Out State -->
|
||||||
|
<div
|
||||||
|
id="userProfileSignedOut"
|
||||||
|
class="flex items-center gap-4 hidden"
|
||||||
|
>
|
||||||
|
<div class="avatar flex items-center justify-center">
|
||||||
|
<div
|
||||||
|
class="w-12 h-12 rounded-xl bg-base-300 text-base-content/30 flex items-center justify-center"
|
||||||
|
>
|
||||||
|
<Icon name="heroicons:user" class="h-6 w-6" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h3
|
||||||
|
class="font-medium text-lg text-base-content/70"
|
||||||
|
>
|
||||||
|
Signed Out
|
||||||
|
</h3>
|
||||||
|
<div class="badge badge-outline mt-1 opacity-50">
|
||||||
|
Guest
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Actual Profile -->
|
||||||
|
<div
|
||||||
id="userProfileSummary"
|
id="userProfileSummary"
|
||||||
|
class="flex items-center gap-4 hidden"
|
||||||
>
|
>
|
||||||
<div class="avatar flex items-center justify-center">
|
<div class="avatar flex items-center justify-center">
|
||||||
<div
|
<div
|
||||||
|
@ -69,87 +137,92 @@ const title = "Dashboard";
|
||||||
<!-- Navigation -->
|
<!-- Navigation -->
|
||||||
<nav class="flex-1 overflow-y-auto scrollbar-hide py-6">
|
<nav class="flex-1 overflow-y-auto scrollbar-hide py-6">
|
||||||
<ul class="menu gap-2 px-4 text-base-content/80">
|
<ul class="menu gap-2 px-4 text-base-content/80">
|
||||||
<!-- All Members -->
|
<!-- Loading Skeleton -->
|
||||||
<li class="menu-title font-medium opacity-70">
|
<div id="menuLoadingSkeleton">
|
||||||
<span>Main Menu</span>
|
{
|
||||||
</li>
|
[1, 2, 3].map((group) => (
|
||||||
<li>
|
<>
|
||||||
<button
|
<li class="menu-title font-medium opacity-70">
|
||||||
class="dashboard-nav-btn gap-4 transition-all duration-200 outline-none focus:outline-none hover:bg-opacity-5 active:bg-base-200 active:font-medium active:text-primary"
|
<div class="h-4 w-24 bg-base-300 animate-pulse rounded" />
|
||||||
data-section="profile"
|
</li>
|
||||||
>
|
{[1, 2, 3].map((item) => (
|
||||||
<Icon
|
<li>
|
||||||
name="heroicons:home"
|
<div class="flex items-center gap-4 py-2">
|
||||||
class="h-5 w-5 group-[.active]:text-primary"
|
<div class="h-5 w-5 bg-base-300 animate-pulse rounded" />
|
||||||
/>
|
<div class="h-4 w-32 bg-base-300 animate-pulse rounded" />
|
||||||
Dashboard
|
</div>
|
||||||
</button>
|
</li>
|
||||||
</li>
|
))}
|
||||||
<li>
|
</>
|
||||||
<button
|
))
|
||||||
class="dashboard-nav-btn gap-4 transition-all duration-200 outline-none focus:outline-none hover:bg-opacity-5"
|
}
|
||||||
data-section="events"
|
</div>
|
||||||
>
|
|
||||||
<Icon
|
|
||||||
name="heroicons:calendar"
|
|
||||||
class="h-5 w-5"
|
|
||||||
/>
|
|
||||||
Events
|
|
||||||
</button>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<button
|
|
||||||
class="dashboard-nav-btn gap-4 transition-all duration-200 outline-none focus:outline-none hover:bg-opacity-5"
|
|
||||||
data-section="reimbursement"
|
|
||||||
>
|
|
||||||
<Icon
|
|
||||||
name="heroicons:credit-card"
|
|
||||||
class="h-5 w-5"
|
|
||||||
/>
|
|
||||||
Reimbursement
|
|
||||||
</button>
|
|
||||||
</li>
|
|
||||||
<!-- Officers Only -->
|
|
||||||
<li>
|
|
||||||
<button
|
|
||||||
class="dashboard-nav-btn gap-4 transition-all duration-200 outline-none focus:outline-none hover:bg-opacity-5 active:bg-base-200 active:font-medium active:text-primary"
|
|
||||||
data-section="eventManagement"
|
|
||||||
>
|
|
||||||
<Icon
|
|
||||||
name="heroicons:home"
|
|
||||||
class="h-5 w-5 group-[.active]:text-primary"
|
|
||||||
/>
|
|
||||||
Event Management
|
|
||||||
</button>
|
|
||||||
</li>
|
|
||||||
|
|
||||||
<li class="menu-title mt-6 font-medium opacity-70">
|
<!-- Actual Menu -->
|
||||||
<span>Account</span>
|
<div id="actualMenu" class="hidden">
|
||||||
</li>
|
{
|
||||||
<li>
|
Object.entries(dashboardConfig.categories).map(
|
||||||
<button
|
([categoryKey, category]: [
|
||||||
class="dashboard-nav-btn gap-4 transition-all duration-200 outline-none focus:outline-none hover:bg-opacity-5"
|
string,
|
||||||
data-section="settings"
|
any,
|
||||||
>
|
]) => (
|
||||||
<Icon
|
<>
|
||||||
name="heroicons:cog-6-tooth"
|
<li
|
||||||
class="h-5 w-5"
|
class={`menu-title font-medium opacity-70 ${
|
||||||
/>
|
category.role &&
|
||||||
Settings
|
category.role !== "none"
|
||||||
</button>
|
? "hidden"
|
||||||
</li>
|
: ""
|
||||||
<li>
|
}`}
|
||||||
<button
|
data-role-required={
|
||||||
id="logoutButton"
|
category.role || "none"
|
||||||
class="gap-4 text-error hover:bg-error/10 transition-all duration-200 outline-none focus:outline-none"
|
}
|
||||||
>
|
>
|
||||||
<Icon
|
<span>{category.title}</span>
|
||||||
name="heroicons:arrow-right-on-rectangle"
|
</li>
|
||||||
class="h-5 w-5"
|
{category.sections.map(
|
||||||
/>
|
(sectionKey: string) => {
|
||||||
Logout
|
const section =
|
||||||
</button>
|
dashboardConfig
|
||||||
</li>
|
.sections[
|
||||||
|
sectionKey
|
||||||
|
];
|
||||||
|
return (
|
||||||
|
<li
|
||||||
|
class={
|
||||||
|
section.role &&
|
||||||
|
section.role !==
|
||||||
|
"none"
|
||||||
|
? "hidden"
|
||||||
|
: ""
|
||||||
|
}
|
||||||
|
data-role-required={
|
||||||
|
section.role
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
class={`dashboard-nav-btn gap-4 transition-all duration-200 outline-none focus:outline-none hover:bg-opacity-5 ${section.class || ""}`}
|
||||||
|
data-section={
|
||||||
|
sectionKey
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Icon
|
||||||
|
name={
|
||||||
|
section.icon
|
||||||
|
}
|
||||||
|
class="h-5 w-5"
|
||||||
|
/>
|
||||||
|
{section.title}
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
</div>
|
||||||
</ul>
|
</ul>
|
||||||
</nav>
|
</nav>
|
||||||
</aside>
|
</aside>
|
||||||
|
@ -257,12 +330,32 @@ const title = "Dashboard";
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Main Content -->
|
<!-- Main Content -->
|
||||||
<div id="mainContent" class="hidden space-y-4 sm:space-y-6">
|
<div id="mainContent" class="space-y-4 sm:space-y-6">
|
||||||
<ProfileSection />
|
{
|
||||||
<EventsSection />
|
Object.entries(dashboardConfig.sections).map(
|
||||||
<ReimbursementSection />
|
([sectionKey, section]: [string, any]) => {
|
||||||
<SettingsSection />
|
// Skip if no component is defined
|
||||||
<Officer_EventManagement />
|
if (!section.component) return null;
|
||||||
|
|
||||||
|
const Component =
|
||||||
|
components[section.component];
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
id={`${sectionKey}Section`}
|
||||||
|
class={`dashboard-section hidden ${
|
||||||
|
section.role &&
|
||||||
|
section.role !== "none"
|
||||||
|
? "role-restricted"
|
||||||
|
: ""
|
||||||
|
}`}
|
||||||
|
data-role-required={section.role}
|
||||||
|
>
|
||||||
|
<Component />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
|
@ -274,6 +367,7 @@ const title = "Dashboard";
|
||||||
import { Authentication } from "../scripts/pocketbase/Authentication";
|
import { Authentication } from "../scripts/pocketbase/Authentication";
|
||||||
import { Get } from "../scripts/pocketbase/Get";
|
import { Get } from "../scripts/pocketbase/Get";
|
||||||
import { SendLog } from "../scripts/pocketbase/SendLog";
|
import { SendLog } from "../scripts/pocketbase/SendLog";
|
||||||
|
import { hasAccess, type OfficerStatus } from "../utils/roleAccess";
|
||||||
|
|
||||||
const auth = Authentication.getInstance();
|
const auth = Authentication.getInstance();
|
||||||
const get = Get.getInstance();
|
const get = Get.getInstance();
|
||||||
|
@ -293,29 +387,132 @@ const title = "Dashboard";
|
||||||
const userName = document.getElementById("userName");
|
const userName = document.getElementById("userName");
|
||||||
const userRole = document.getElementById("userRole");
|
const userRole = document.getElementById("userRole");
|
||||||
|
|
||||||
// Display user profile information
|
// Function to update section visibility based on role
|
||||||
const updateUserProfile = async (user: any) => {
|
const updateSectionVisibility = (officerStatus: OfficerStatus) => {
|
||||||
|
// Special handling for sponsor role
|
||||||
|
if (officerStatus === "sponsor") {
|
||||||
|
// Hide all sections first
|
||||||
|
document
|
||||||
|
.querySelectorAll("[data-role-required]")
|
||||||
|
.forEach((element) => {
|
||||||
|
element.classList.add("hidden");
|
||||||
|
});
|
||||||
|
|
||||||
|
// Only show sponsor sections
|
||||||
|
document
|
||||||
|
.querySelectorAll('[data-role-required="sponsor"]')
|
||||||
|
.forEach((element) => {
|
||||||
|
element.classList.remove("hidden");
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// For non-sponsor roles, handle normally
|
||||||
|
document.querySelectorAll("[data-role-required]").forEach((element) => {
|
||||||
|
const requiredRole = element.getAttribute(
|
||||||
|
"data-role-required"
|
||||||
|
) as OfficerStatus;
|
||||||
|
|
||||||
|
// Skip elements that don't have a role requirement
|
||||||
|
if (!requiredRole || requiredRole === "none") {
|
||||||
|
element.classList.remove("hidden");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if user has permission for this role
|
||||||
|
const hasPermission = hasAccess(officerStatus, requiredRole);
|
||||||
|
|
||||||
|
// Only show elements if user has permission
|
||||||
|
element.classList.toggle("hidden", !hasPermission);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// Handle navigation
|
||||||
|
const handleNavigation = () => {
|
||||||
|
const navButtons = document.querySelectorAll(".dashboard-nav-btn");
|
||||||
|
const sections = document.querySelectorAll(".dashboard-section");
|
||||||
|
const mainContentDiv = document.getElementById("mainContent");
|
||||||
|
|
||||||
|
// Ensure mainContent is visible
|
||||||
|
if (mainContentDiv) {
|
||||||
|
mainContentDiv.classList.remove("hidden");
|
||||||
|
}
|
||||||
|
|
||||||
|
navButtons.forEach((button) => {
|
||||||
|
button.addEventListener("click", () => {
|
||||||
|
const sectionKey = button.getAttribute("data-section");
|
||||||
|
|
||||||
|
// Handle logout button
|
||||||
|
if (sectionKey === "logout") {
|
||||||
|
auth.logout();
|
||||||
|
window.location.reload();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove active class from all buttons
|
||||||
|
navButtons.forEach((btn) => {
|
||||||
|
btn.classList.remove("active", "bg-base-200");
|
||||||
|
});
|
||||||
|
|
||||||
|
// Add active class to clicked button
|
||||||
|
button.classList.add("active", "bg-base-200");
|
||||||
|
|
||||||
|
// Hide all sections
|
||||||
|
sections.forEach((section) => {
|
||||||
|
section.classList.add("hidden");
|
||||||
|
});
|
||||||
|
|
||||||
|
// Show selected section
|
||||||
|
const sectionId = `${sectionKey}Section`;
|
||||||
|
const targetSection = document.getElementById(sectionId);
|
||||||
|
if (targetSection) {
|
||||||
|
targetSection.classList.remove("hidden");
|
||||||
|
console.log(`Showing section: ${sectionId}`); // Debug log
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close mobile sidebar if needed
|
||||||
|
if (window.innerWidth < 1024 && sidebar) {
|
||||||
|
sidebar.classList.add("-translate-x-full");
|
||||||
|
document.body.classList.remove("overflow-hidden");
|
||||||
|
const overlay = document.getElementById("sidebarOverlay");
|
||||||
|
overlay?.remove();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// Display user profile information and handle role-based access
|
||||||
|
const updateUserProfile = async (user: { id: string }) => {
|
||||||
if (!user) return;
|
if (!user) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Get user information including member type
|
|
||||||
const extendedUser = await get.getOne("users", user.id, {
|
const extendedUser = await get.getOne("users", user.id, {
|
||||||
fields: ["id", "name", "member_type", "expand.member_type"],
|
fields: [
|
||||||
|
"id",
|
||||||
|
"name",
|
||||||
|
"member_type",
|
||||||
|
"officer_status",
|
||||||
|
"expand.member_type",
|
||||||
|
],
|
||||||
});
|
});
|
||||||
|
|
||||||
// Update display elements
|
|
||||||
const displayName = extendedUser.name || "Unknown User";
|
const displayName = extendedUser.name || "Unknown User";
|
||||||
const displayRole = extendedUser.member_type || "Member";
|
const displayRole = extendedUser.member_type || "Member";
|
||||||
|
const officerStatus = (extendedUser.officer_status ||
|
||||||
|
"") as OfficerStatus;
|
||||||
const initials = (extendedUser.name || "U")
|
const initials = (extendedUser.name || "U")
|
||||||
.split(" ")
|
.split(" ")
|
||||||
.map((n: string) => n[0])
|
.map((n: string) => n[0])
|
||||||
.join("")
|
.join("")
|
||||||
.toUpperCase();
|
.toUpperCase();
|
||||||
|
|
||||||
// Update elements
|
// Update profile display
|
||||||
if (userName) userName.textContent = displayName;
|
if (userName) userName.textContent = displayName;
|
||||||
if (userRole) userRole.textContent = displayRole;
|
if (userRole) userRole.textContent = displayRole;
|
||||||
if (userInitials) userInitials.textContent = initials;
|
if (userInitials) userInitials.textContent = initials;
|
||||||
|
|
||||||
|
// Update section visibility based on role
|
||||||
|
updateSectionVisibility(officerStatus);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error fetching user profile:", error);
|
console.error("Error fetching user profile:", error);
|
||||||
const fallbackValues = {
|
const fallbackValues = {
|
||||||
|
@ -324,15 +521,16 @@ const title = "Dashboard";
|
||||||
initials: "?",
|
initials: "?",
|
||||||
};
|
};
|
||||||
|
|
||||||
// Update elements with fallback values
|
|
||||||
if (userName) userName.textContent = fallbackValues.name;
|
if (userName) userName.textContent = fallbackValues.name;
|
||||||
if (userRole) userRole.textContent = fallbackValues.role;
|
if (userRole) userRole.textContent = fallbackValues.role;
|
||||||
if (userInitials)
|
if (userInitials)
|
||||||
userInitials.textContent = fallbackValues.initials;
|
userInitials.textContent = fallbackValues.initials;
|
||||||
|
|
||||||
|
updateSectionVisibility("" as OfficerStatus);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Mobile sidebar toggle with overlay
|
// Mobile sidebar toggle
|
||||||
const mobileSidebarToggle = document.getElementById("mobileSidebarToggle");
|
const mobileSidebarToggle = document.getElementById("mobileSidebarToggle");
|
||||||
if (mobileSidebarToggle && sidebar) {
|
if (mobileSidebarToggle && sidebar) {
|
||||||
const toggleSidebar = () => {
|
const toggleSidebar = () => {
|
||||||
|
@ -341,13 +539,11 @@ const title = "Dashboard";
|
||||||
if (isOpen) {
|
if (isOpen) {
|
||||||
sidebar.classList.add("-translate-x-full");
|
sidebar.classList.add("-translate-x-full");
|
||||||
document.body.classList.remove("overflow-hidden");
|
document.body.classList.remove("overflow-hidden");
|
||||||
// Remove overlay if it exists
|
|
||||||
const overlay = document.getElementById("sidebarOverlay");
|
const overlay = document.getElementById("sidebarOverlay");
|
||||||
overlay?.remove();
|
overlay?.remove();
|
||||||
} else {
|
} else {
|
||||||
sidebar.classList.remove("-translate-x-full");
|
sidebar.classList.remove("-translate-x-full");
|
||||||
document.body.classList.add("overflow-hidden");
|
document.body.classList.add("overflow-hidden");
|
||||||
// Add overlay
|
|
||||||
const overlay = document.createElement("div");
|
const overlay = document.createElement("div");
|
||||||
overlay.id = "sidebarOverlay";
|
overlay.id = "sidebarOverlay";
|
||||||
overlay.className =
|
overlay.className =
|
||||||
|
@ -360,51 +556,6 @@ const title = "Dashboard";
|
||||||
mobileSidebarToggle.addEventListener("click", toggleSidebar);
|
mobileSidebarToggle.addEventListener("click", toggleSidebar);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Close sidebar on window resize if screen becomes larger
|
|
||||||
window.addEventListener("resize", () => {
|
|
||||||
if (window.innerWidth >= 1024) {
|
|
||||||
const overlay = document.getElementById("sidebarOverlay");
|
|
||||||
if (overlay) {
|
|
||||||
overlay.remove();
|
|
||||||
document.body.classList.remove("overflow-hidden");
|
|
||||||
}
|
|
||||||
if (sidebar) {
|
|
||||||
sidebar.classList.remove("-translate-x-full");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Handle navigation
|
|
||||||
const handleNavigation = () => {
|
|
||||||
const navButtons = document.querySelectorAll(".dashboard-nav-btn");
|
|
||||||
const sections = document.querySelectorAll(".dashboard-section");
|
|
||||||
|
|
||||||
navButtons.forEach((button) => {
|
|
||||||
button.addEventListener("click", () => {
|
|
||||||
// Remove active class from all buttons
|
|
||||||
navButtons.forEach((btn) =>
|
|
||||||
btn.classList.remove("active", "bg-base-200")
|
|
||||||
);
|
|
||||||
// Add active class to clicked button
|
|
||||||
button.classList.add("active", "bg-base-200");
|
|
||||||
|
|
||||||
// Hide all sections
|
|
||||||
sections.forEach((section) => section.classList.add("hidden"));
|
|
||||||
// Show selected section
|
|
||||||
const sectionId = `${button.getAttribute("data-section")}Section`;
|
|
||||||
document.getElementById(sectionId)?.classList.remove("hidden");
|
|
||||||
|
|
||||||
// Close sidebar and cleanup overlay on mobile
|
|
||||||
if (window.innerWidth < 1024 && sidebar) {
|
|
||||||
sidebar.classList.add("-translate-x-full");
|
|
||||||
document.body.classList.remove("overflow-hidden");
|
|
||||||
const overlay = document.getElementById("sidebarOverlay");
|
|
||||||
overlay?.remove();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
// Initialize page
|
// Initialize page
|
||||||
const initializePage = async () => {
|
const initializePage = async () => {
|
||||||
try {
|
try {
|
||||||
|
@ -412,23 +563,98 @@ const title = "Dashboard";
|
||||||
if (pageErrorState) pageErrorState.classList.add("hidden");
|
if (pageErrorState) pageErrorState.classList.add("hidden");
|
||||||
if (notAuthenticatedState)
|
if (notAuthenticatedState)
|
||||||
notAuthenticatedState.classList.add("hidden");
|
notAuthenticatedState.classList.add("hidden");
|
||||||
if (mainContent) mainContent.classList.add("hidden");
|
|
||||||
|
// Show loading states
|
||||||
|
const userProfileSkeleton = document.getElementById(
|
||||||
|
"userProfileSkeleton"
|
||||||
|
);
|
||||||
|
const userProfileSignedOut = document.getElementById(
|
||||||
|
"userProfileSignedOut"
|
||||||
|
);
|
||||||
|
const userProfileSummary =
|
||||||
|
document.getElementById("userProfileSummary");
|
||||||
|
const menuLoadingSkeleton = document.getElementById(
|
||||||
|
"menuLoadingSkeleton"
|
||||||
|
);
|
||||||
|
const actualMenu = document.getElementById("actualMenu");
|
||||||
|
|
||||||
|
if (userProfileSkeleton)
|
||||||
|
userProfileSkeleton.classList.remove("hidden");
|
||||||
|
if (userProfileSummary) userProfileSummary.classList.add("hidden");
|
||||||
|
if (userProfileSignedOut)
|
||||||
|
userProfileSignedOut.classList.add("hidden");
|
||||||
|
if (menuLoadingSkeleton)
|
||||||
|
menuLoadingSkeleton.classList.remove("hidden");
|
||||||
|
if (actualMenu) actualMenu.classList.add("hidden");
|
||||||
|
|
||||||
// Check authentication
|
// Check authentication
|
||||||
if (!auth.isAuthenticated()) {
|
if (!auth.isAuthenticated()) {
|
||||||
if (pageLoadingState) pageLoadingState.classList.add("hidden");
|
if (pageLoadingState) pageLoadingState.classList.add("hidden");
|
||||||
if (notAuthenticatedState)
|
if (notAuthenticatedState)
|
||||||
notAuthenticatedState.classList.remove("hidden");
|
notAuthenticatedState.classList.remove("hidden");
|
||||||
|
if (userProfileSkeleton)
|
||||||
|
userProfileSkeleton.classList.add("hidden");
|
||||||
|
if (userProfileSignedOut)
|
||||||
|
userProfileSignedOut.classList.remove("hidden");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const user = auth.getCurrentUser();
|
const user = auth.getCurrentUser();
|
||||||
await updateUserProfile(user);
|
await updateUserProfile(user);
|
||||||
|
|
||||||
|
// Show actual profile and hide skeleton
|
||||||
|
if (userProfileSkeleton)
|
||||||
|
userProfileSkeleton.classList.add("hidden");
|
||||||
|
if (userProfileSummary)
|
||||||
|
userProfileSummary.classList.remove("hidden");
|
||||||
|
|
||||||
|
// Hide all sections first
|
||||||
|
document
|
||||||
|
.querySelectorAll(".dashboard-section")
|
||||||
|
.forEach((section) => {
|
||||||
|
section.classList.add("hidden");
|
||||||
|
});
|
||||||
|
|
||||||
|
// Show appropriate default section based on role
|
||||||
|
const extendedUser = await get.getOne("users", user.id, {
|
||||||
|
fields: ["officer_status"],
|
||||||
|
});
|
||||||
|
const officerStatus = (extendedUser.officer_status ||
|
||||||
|
"") as OfficerStatus;
|
||||||
|
|
||||||
|
let defaultSection;
|
||||||
|
let defaultButton;
|
||||||
|
|
||||||
|
if (officerStatus === "sponsor") {
|
||||||
|
defaultSection = document.getElementById(
|
||||||
|
"sponsorDashboardSection"
|
||||||
|
);
|
||||||
|
defaultButton = document.querySelector(
|
||||||
|
'[data-section="sponsorDashboard"]'
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
defaultSection = document.getElementById("profileSection");
|
||||||
|
defaultButton = document.querySelector(
|
||||||
|
'[data-section="profile"]'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (defaultSection) {
|
||||||
|
defaultSection.classList.remove("hidden");
|
||||||
|
}
|
||||||
|
if (defaultButton) {
|
||||||
|
defaultButton.classList.add("active", "bg-base-200");
|
||||||
|
}
|
||||||
|
|
||||||
// Initialize navigation
|
// Initialize navigation
|
||||||
handleNavigation();
|
handleNavigation();
|
||||||
|
|
||||||
// Show main content
|
// Show actual menu and hide skeleton
|
||||||
|
if (menuLoadingSkeleton)
|
||||||
|
menuLoadingSkeleton.classList.add("hidden");
|
||||||
|
if (actualMenu) actualMenu.classList.remove("hidden");
|
||||||
|
|
||||||
|
// Show main content and hide loading
|
||||||
if (mainContent) mainContent.classList.remove("hidden");
|
if (mainContent) mainContent.classList.remove("hidden");
|
||||||
if (pageLoadingState) pageLoadingState.classList.add("hidden");
|
if (pageLoadingState) pageLoadingState.classList.add("hidden");
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
@ -466,21 +692,19 @@ const title = "Dashboard";
|
||||||
|
|
||||||
// Handle responsive sidebar
|
// Handle responsive sidebar
|
||||||
if (sidebar) {
|
if (sidebar) {
|
||||||
// Hide sidebar by default on mobile
|
|
||||||
if (window.innerWidth < 1024) {
|
if (window.innerWidth < 1024) {
|
||||||
sidebar.classList.add("-translate-x-full");
|
sidebar.classList.add("-translate-x-full");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add transition class
|
window.addEventListener("resize", () => {
|
||||||
sidebar.classList.add(
|
if (window.innerWidth >= 1024) {
|
||||||
"transition-transform",
|
const overlay = document.getElementById("sidebarOverlay");
|
||||||
"duration-300",
|
if (overlay) {
|
||||||
"ease-in-out",
|
overlay.remove();
|
||||||
"lg:translate-x-0",
|
document.body.classList.remove("overflow-hidden");
|
||||||
"fixed",
|
}
|
||||||
"lg:relative",
|
sidebar.classList.remove("-translate-x-full");
|
||||||
"h-full",
|
}
|
||||||
"z-50"
|
});
|
||||||
);
|
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
16
src/utils/roleAccess.ts
Normal file
16
src/utils/roleAccess.ts
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
export type OfficerStatus = "admin" | "executive" | "general" | "past" | "sponsor" | "none" | "";
|
||||||
|
type RoleHierarchy = Record<OfficerStatus, OfficerStatus[]>;
|
||||||
|
|
||||||
|
export function hasAccess(userRole: OfficerStatus, requiredRole: OfficerStatus): boolean {
|
||||||
|
const roleHierarchy: RoleHierarchy = {
|
||||||
|
"admin": ["admin", "sponsor", "executive", "general", "past", "none", ""],
|
||||||
|
"executive": ["executive", "general", "past", "none", ""],
|
||||||
|
"general": ["general", "past", "none", ""],
|
||||||
|
"past": ["past", "none", ""],
|
||||||
|
"sponsor": ["sponsor"], // Sponsor can only access sponsor-specific content
|
||||||
|
"none": ["none", ""],
|
||||||
|
"": [""]
|
||||||
|
};
|
||||||
|
|
||||||
|
return roleHierarchy[userRole]?.includes(requiredRole) || false;
|
||||||
|
}
|
Loading…
Reference in a new issue