Add authentication #17

Manually merged
Webmaster merged 225 commits from auth into main 2025-03-08 10:37:06 +00:00
4 changed files with 908 additions and 676 deletions
Showing only changes of commit b7493224d2 - Show all commits

View file

@ -0,0 +1,183 @@
---
// Admin Dashboard Component
import { Authentication } from "../../scripts/pocketbase/Authentication";
import { Get } from "../../scripts/pocketbase/Get";
const auth = Authentication.getInstance();
const get = Get.getInstance();
// Fetch some basic stats for the admin dashboard
let userCount = 0;
let officerCount = 0;
let eventCount = 0;
let reimbursementCount = 0;
try {
if (auth.isAuthenticated()) {
const userResponse = await get.getList("users", 1, 1);
userCount = userResponse.totalItems;
const officerResponse = await get.getList("officers", 1, 1);
officerCount = officerResponse.totalItems;
const eventResponse = await get.getList("events", 1, 1);
eventCount = eventResponse.totalItems;
const reimbursementResponse = await get.getList("reimbursement", 1, 1);
reimbursementCount = reimbursementResponse.totalItems;
}
} catch (error) {
console.error("Error fetching admin dashboard data:", error);
}
---
<div class="card bg-base-100 shadow-xl">
<div class="card-body">
<h2 class="card-title text-2xl mb-4">Administrator Dashboard</h2>
<!-- Stats Overview -->
<div class="grid gap-4 md:grid-cols-2 lg:grid-cols-4">
<!-- User Stats -->
<div class="card bg-base-200">
<div class="card-body">
<h3 class="card-title text-lg">Users</h3>
<p class="text-primary text-3xl font-semibold">{userCount}</p>
<p class="text-sm opacity-70">Total registered users</p>
</div>
</div>
<!-- Officer Stats -->
<div class="card bg-base-200">
<div class="card-body">
<h3 class="card-title text-lg">Officers</h3>
<p class="text-secondary text-3xl font-semibold">{officerCount}</p>
<p class="text-sm opacity-70">Active officers</p>
</div>
</div>
<!-- Event Stats -->
<div class="card bg-base-200">
<div class="card-body">
<h3 class="card-title text-lg">Events</h3>
<p class="text-accent text-3xl font-semibold">{eventCount}</p>
<p class="text-sm opacity-70">Total events</p>
</div>
</div>
<!-- Reimbursement Stats -->
<div class="card bg-base-200">
<div class="card-body">
<h3 class="card-title text-lg">Reimbursements</h3>
<p class="text-info text-3xl font-semibold">{reimbursementCount}</p>
<p class="text-sm opacity-70">Total reimbursements</p>
</div>
</div>
</div>
<!-- Quick Actions -->
<div class="mt-6">
<h3 class="text-xl font-semibold mb-4">Administrative Actions</h3>
<div class="flex flex-wrap gap-3">
<button class="btn btn-primary">
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-5 w-5 mr-2"
viewBox="0 0 20 20"
fill="currentColor"
>
<path
d="M13 6a3 3 0 11-6 0 3 3 0 016 0zM18 8a2 2 0 11-4 0 2 2 0 014 0zM14 15a4 4 0 00-8 0v3h8v-3zM6 8a2 2 0 11-4 0 2 2 0 014 0zM16 18v-3a5.972 5.972 0 00-.75-2.906A3.005 3.005 0 0119 15v3h-3zM4.75 12.094A5.973 5.973 0 004 15v3H1v-3a3 3 0 013.75-2.906z"
></path>
</svg>
Manage Users
</button>
<button class="btn btn-secondary">
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-5 w-5 mr-2"
viewBox="0 0 20 20"
fill="currentColor"
>
<path
fill-rule="evenodd"
d="M6 2a1 1 0 00-1 1v1H4a2 2 0 00-2 2v10a2 2 0 002 2h12a2 2 0 002-2V6a2 2 0 00-2-2h-1V3a1 1 0 10-2 0v1H7V3a1 1 0 00-1-1zm0 5a1 1 0 000 2h8a1 1 0 100-2H6z"
clip-rule="evenodd"></path>
</svg>
Manage Events
</button>
<button class="btn btn-accent">
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-5 w-5 mr-2"
viewBox="0 0 20 20"
fill="currentColor"
>
<path
fill-rule="evenodd"
d="M4 4a2 2 0 00-2 2v4a2 2 0 002 2V6h10a2 2 0 00-2-2H4zm2 6a2 2 0 012-2h8a2 2 0 012 2v4a2 2 0 01-2 2H8a2 2 0 01-2-2v-4zm6 4a2 2 0 100-4 2 2 0 000 4z"
clip-rule="evenodd"></path>
</svg>
Manage Finances
</button>
<button class="btn btn-info">
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-5 w-5 mr-2"
viewBox="0 0 20 20"
fill="currentColor"
>
<path d="M9 2a1 1 0 000 2h2a1 1 0 100-2H9z"></path>
<path
fill-rule="evenodd"
d="M4 5a2 2 0 012-2 3 3 0 003 3h2a3 3 0 003-3 2 2 0 012 2v11a2 2 0 01-2 2H6a2 2 0 01-2-2V5zm3 4a1 1 0 000 2h.01a1 1 0 100-2H7zm3 0a1 1 0 000 2h3a1 1 0 100-2h-3zm-3 4a1 1 0 100 2h.01a1 1 0 100-2H7zm3 0a1 1 0 100 2h3a1 1 0 100-2h-3z"
clip-rule="evenodd"></path>
</svg>
System Logs
</button>
</div>
</div>
<!-- Recent System Activity -->
<div class="mt-6">
<h3 class="text-xl font-semibold mb-4">Recent System Activity</h3>
<div class="overflow-x-auto">
<table class="table">
<thead>
<tr>
<th>Time</th>
<th>User</th>
<th>Action</th>
<th>Details</th>
</tr>
</thead>
<tbody>
<tr>
<td class="text-sm">Just now</td>
<td>Admin</td>
<td>Login</td>
<td>Administrator logged in</td>
</tr>
<tr>
<td class="text-sm">10 min ago</td>
<td>System</td>
<td>Update</td>
<td>Event request status changed</td>
</tr>
<tr>
<td class="text-sm">1 hour ago</td>
<td>Jane Doe</td>
<td>Create</td>
<td>New reimbursement request submitted</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
<script>
// Client-side functionality can be added here if needed
// For example, refreshing data or handling button clicks
</script>

View file

@ -65,6 +65,14 @@ sections:
component: "SponsorAnalytics" component: "SponsorAnalytics"
class: "text-warning hover:text-warning-focus" class: "text-warning hover:text-warning-focus"
# Administrator Menu
adminDashboard:
title: "Admin Dashboard"
icon: "heroicons:shield-check"
role: "administrator"
component: "AdminDashboard"
class: "text-error hover:text-error-focus"
# Settings (accessible to all except sponsors) # Settings (accessible to all except sponsors)
settings: settings:
title: "Settings" title: "Settings"
@ -98,8 +106,8 @@ categories:
admin: admin:
title: "Admin Menu" title: "Admin Menu"
sections: [] sections: ["adminDashboard"]
role: "admin" role: "administrator"
sponsor: sponsor:
title: "Sponsor Portal" title: "Sponsor Portal"

View file

@ -1,9 +1,13 @@
--- ---
import yaml from "js-yaml"; import yaml from "js-yaml";
import { Icon } from "astro-icon/components"; import { Icon } from "astro-icon/components";
import { hasAccess, type OfficerStatus } from "../utils/roleAccess";
import fs from "node:fs"; import fs from "node:fs";
import path from "node:path"; import path from "node:path";
import { Authentication } from "../scripts/pocketbase/Authentication";
import { Get } from "../scripts/pocketbase/Get";
import { SendLog } from "../scripts/pocketbase/SendLog";
import { hasAccess, type OfficerStatus } from "../utils/roleAccess";
import { OfficerTypes } from "../schemas/pocketbase/schema";
const title = "Dashboard"; const title = "Dashboard";
@ -22,8 +26,8 @@ const components = Object.fromEntries(
); );
console.log(`Loaded component: ${section.component}`); // Debug log console.log(`Loaded component: ${section.component}`); // Debug log
return [section.component, component.default]; return [section.component, component.default];
}) }),
) ),
); );
console.log("Available components:", Object.keys(components)); // Debug log console.log("Available components:", Object.keys(components)); // Debug log
@ -59,33 +63,19 @@ console.log("Available components:", Object.keys(components)); // Debug log
<!-- User Profile --> <!-- User Profile -->
<div class="p-6 border-b border-base-200"> <div class="p-6 border-b border-base-200">
<!-- Loading State --> <!-- Loading State -->
<div <div id="userProfileSkeleton" class="flex items-center gap-4">
id="userProfileSkeleton"
class="flex items-center gap-4"
>
<div class="avatar flex items-center justify-center"> <div class="avatar flex items-center justify-center">
<div <div class="w-12 h-12 rounded-xl bg-base-300 animate-pulse"></div>
class="w-12 h-12 rounded-xl bg-base-300 animate-pulse"
>
</div>
</div> </div>
<div class="flex-1"> <div class="flex-1">
<div <div class="h-6 w-32 bg-base-300 animate-pulse rounded mb-2">
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 class="h-5 w-20 bg-base-300 animate-pulse rounded"></div>
</div> </div>
</div> </div>
<!-- Signed Out State --> <!-- Signed Out State -->
<div <div id="userProfileSignedOut" class="flex items-center gap-4 hidden">
id="userProfileSignedOut"
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
class="w-12 h-12 rounded-xl bg-base-300 text-base-content/30 flex items-center justify-center" class="w-12 h-12 rounded-xl bg-base-300 text-base-content/30 flex items-center justify-center"
@ -94,22 +84,15 @@ console.log("Available components:", Object.keys(components)); // Debug log
</div> </div>
</div> </div>
<div> <div>
<h3 <h3 class="font-medium text-lg text-base-content/70">
class="font-medium text-lg text-base-content/70"
>
Signed Out Signed Out
</h3> </h3>
<div class="badge badge-outline mt-1 opacity-50"> <div class="badge badge-outline mt-1 opacity-50">Guest</div>
Guest
</div>
</div> </div>
</div> </div>
<!-- Actual Profile --> <!-- Actual Profile -->
<div <div id="userProfileSummary" class="flex items-center gap-4 hidden">
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
class="w-12 h-12 rounded-xl bg-[#06659d] text-white ring ring-base-200 ring-offset-base-100 ring-offset-2 inline-flex items-center justify-center" class="w-12 h-12 rounded-xl bg-[#06659d] text-white ring ring-base-200 ring-offset-base-100 ring-offset-2 inline-flex items-center justify-center"
@ -121,9 +104,7 @@ console.log("Available components:", Object.keys(components)); // Debug log
</div> </div>
</div> </div>
<div> <div>
<h3 class="font-medium text-lg" id="userName"> <h3 class="font-medium text-lg" id="userName">Loading...</h3>
Loading...
</h3>
<div <div
class="badge badge-outline mt-1 border-[#06659d] text-[#06659d]" class="badge badge-outline mt-1 border-[#06659d] text-[#06659d]"
id="userRole" id="userRole"
@ -162,64 +143,41 @@ console.log("Available components:", Object.keys(components)); // Debug log
<div id="actualMenu" class="hidden"> <div id="actualMenu" class="hidden">
{ {
Object.entries(dashboardConfig.categories).map( Object.entries(dashboardConfig.categories).map(
([categoryKey, category]: [ ([categoryKey, category]: [string, any]) => (
string,
any,
]) => (
<> <>
<li <li
class={`menu-title font-medium opacity-70 ${ class={`menu-title font-medium opacity-70 ${
category.role && category.role && category.role !== "none"
category.role !== "none"
? "hidden" ? "hidden"
: "" : ""
}`} }`}
data-role-required={ data-role-required={category.role || "none"}
category.role || "none"
}
> >
<span>{category.title}</span> <span>{category.title}</span>
</li> </li>
{category.sections.map( {category.sections.map((sectionKey: string) => {
(sectionKey: string) => { const section = dashboardConfig.sections[sectionKey];
const section =
dashboardConfig
.sections[
sectionKey
];
return ( return (
<li <li
class={ class={
section.role && section.role && section.role !== "none"
section.role !==
"none"
? "hidden" ? "hidden"
: "" : ""
} }
data-role-required={ data-role-required={section.role}
section.role
}
> >
<button <button
class={`dashboard-nav-btn gap-4 transition-all duration-200 outline-none focus:outline-none hover:bg-opacity-5 ${section.class || ""}`} class={`dashboard-nav-btn gap-4 transition-all duration-200 outline-none focus:outline-none hover:bg-opacity-5 ${section.class || ""}`}
data-section={ data-section={sectionKey}
sectionKey
}
> >
<Icon <Icon name={section.icon} class="h-5 w-5" />
name={
section.icon
}
class="h-5 w-5"
/>
{section.title} {section.title}
</button> </button>
</li> </li>
); );
} })}
)}
</> </>
) ),
) )
} }
</div> </div>
@ -232,15 +190,10 @@ console.log("Available components:", Object.keys(components)); // Debug log
class="flex-1 overflow-x-hidden overflow-y-auto bg-base-200 w-full xl:w-[calc(100%-20rem)]" class="flex-1 overflow-x-hidden overflow-y-auto bg-base-200 w-full xl:w-[calc(100%-20rem)]"
> >
<!-- Mobile Header --> <!-- Mobile Header -->
<header <header class="bg-base-100 p-4 shadow-md xl:hidden sticky top-0 z-40">
class="bg-base-100 p-4 shadow-md xl:hidden sticky top-0 z-40"
>
<div class="flex items-center justify-between"> <div class="flex items-center justify-between">
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<button <button id="mobileSidebarToggle" class="btn btn-square btn-ghost">
id="mobileSidebarToggle"
class="btn btn-square btn-ghost"
>
<Icon name="heroicons:bars-3" class="h-6 w-6" /> <Icon name="heroicons:bars-3" class="h-6 w-6" />
</button> </button>
<h1 class="text-xl font-bold">IEEE UCSD</h1> <h1 class="text-xl font-bold">IEEE UCSD</h1>
@ -252,11 +205,8 @@ console.log("Available components:", Object.keys(components)); // Debug log
<div class="p-4 md:p-6 max-w-[1600px] mx-auto"> <div class="p-4 md:p-6 max-w-[1600px] mx-auto">
<!-- Loading State --> <!-- Loading State -->
<div id="pageLoadingState" class="w-full"> <div id="pageLoadingState" class="w-full">
<div <div class="flex flex-col items-center justify-center p-4 sm:p-8">
class="flex flex-col items-center justify-center p-4 sm:p-8" <div class="loading loading-spinner loading-lg"></div>
>
<div class="loading loading-spinner loading-lg">
</div>
<p class="mt-4 opacity-70">Loading dashboard...</p> <p class="mt-4 opacity-70">Loading dashboard...</p>
</div> </div>
</div> </div>
@ -282,9 +232,7 @@ console.log("Available components:", Object.keys(components)); // Debug log
<!-- Not Authenticated State --> <!-- Not Authenticated State -->
<div id="notAuthenticatedState" class="hidden w-full"> <div id="notAuthenticatedState" class="hidden w-full">
<div class="card bg-base-100 shadow-xl mx-2 sm:mx-0"> <div class="card bg-base-100 shadow-xl mx-2 sm:mx-0">
<div <div class="card-body items-center text-center p-4 sm:p-8">
class="card-body items-center text-center p-4 sm:p-8"
>
<div class="mb-4 sm:mb-6"> <div class="mb-4 sm:mb-6">
<svg <svg
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
@ -301,11 +249,9 @@ console.log("Available components:", Object.keys(components)); // Debug log
<h2 class="card-title text-xl sm:text-2xl mb-2"> <h2 class="card-title text-xl sm:text-2xl mb-2">
Sign in to Access Dashboard Sign in to Access Dashboard
</h2> </h2>
<p <p class="opacity-70 mb-4 sm:mb-6 text-sm sm:text-base">
class="opacity-70 mb-4 sm:mb-6 text-sm sm:text-base" Please sign in with your IEEE UCSD account to access the
> dashboard.
Please sign in with your IEEE UCSD account
to access the dashboard.
</p> </p>
<button <button
class="login-button btn btn-primary btn-lg gap-2 w-full sm:w-auto" class="login-button btn btn-primary btn-lg gap-2 w-full sm:w-auto"
@ -337,14 +283,12 @@ console.log("Available components:", Object.keys(components)); // Debug log
// Skip if no component is defined // Skip if no component is defined
if (!section.component) return null; if (!section.component) return null;
const Component = const Component = components[section.component];
components[section.component];
return ( return (
<div <div
id={`${sectionKey}Section`} id={`${sectionKey}Section`}
class={`dashboard-section hidden ${ class={`dashboard-section hidden ${
section.role && section.role && section.role !== "none"
section.role !== "none"
? "role-restricted" ? "role-restricted"
: "" : ""
}`} }`}
@ -353,7 +297,7 @@ console.log("Available components:", Object.keys(components)); // Debug log
<Component /> <Component />
</div> </div>
); );
} },
) )
} }
</div> </div>
@ -368,6 +312,7 @@ console.log("Available components:", Object.keys(components)); // Debug log
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"; import { hasAccess, type OfficerStatus } from "../utils/roleAccess";
import { OfficerTypes } from "../schemas/pocketbase/schema";
const auth = Authentication.getInstance(); const auth = Authentication.getInstance();
const get = Get.getInstance(); const get = Get.getInstance();
@ -377,7 +322,7 @@ console.log("Available components:", Object.keys(components)); // Debug log
const pageLoadingState = document.getElementById("pageLoadingState"); const pageLoadingState = document.getElementById("pageLoadingState");
const pageErrorState = document.getElementById("pageErrorState"); const pageErrorState = document.getElementById("pageErrorState");
const notAuthenticatedState = document.getElementById( const notAuthenticatedState = document.getElementById(
"notAuthenticatedState" "notAuthenticatedState",
); );
const mainContent = document.getElementById("mainContent"); const mainContent = document.getElementById("mainContent");
const sidebar = document.querySelector("aside"); const sidebar = document.querySelector("aside");
@ -392,9 +337,7 @@ console.log("Available components:", Object.keys(components)); // Debug log
// Special handling for sponsor role // Special handling for sponsor role
if (officerStatus === "sponsor") { if (officerStatus === "sponsor") {
// Hide all sections first // Hide all sections first
document document.querySelectorAll("[data-role-required]").forEach((element) => {
.querySelectorAll("[data-role-required]")
.forEach((element) => {
element.classList.add("hidden"); element.classList.add("hidden");
}); });
@ -410,7 +353,7 @@ console.log("Available components:", Object.keys(components)); // Debug log
// For non-sponsor roles, handle normally // For non-sponsor roles, handle normally
document.querySelectorAll("[data-role-required]").forEach((element) => { document.querySelectorAll("[data-role-required]").forEach((element) => {
const requiredRole = element.getAttribute( const requiredRole = element.getAttribute(
"data-role-required" "data-role-required",
) as OfficerStatus; ) as OfficerStatus;
// Skip elements that don't have a role requirement // Skip elements that don't have a role requirement
@ -498,8 +441,51 @@ console.log("Available components:", Object.keys(components)); // Debug log
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; // Map the officer type from the database to our OfficerStatus type
let officerStatus: OfficerStatus = "";
// Get the officer record for this user if it exists
const officerRecords = await get.getList(
"officers",
1,
50,
`user="${user.id}"`,
"",
{
fields: ["id", "type"],
},
);
if (officerRecords && officerRecords.items.length > 0) {
const officerType = officerRecords.items[0].type;
// Map the officer type to our OfficerStatus
switch (officerType) {
case OfficerTypes.ADMINISTRATOR:
officerStatus = "administrator";
break;
case OfficerTypes.EXECUTIVE:
officerStatus = "executive";
break;
case OfficerTypes.GENERAL:
officerStatus = "general";
break;
case OfficerTypes.HONORARY:
officerStatus = "honorary";
break;
case OfficerTypes.PAST:
officerStatus = "past";
break;
default:
officerStatus = "";
}
} else if (extendedUser.member_type === "Sponsor") {
officerStatus = "sponsor";
} else {
officerStatus = "none";
}
const initials = (extendedUser.name || "U") const initials = (extendedUser.name || "U")
.split(" ") .split(" ")
.map((n: string) => n[0]) .map((n: string) => n[0])
@ -523,8 +509,7 @@ console.log("Available components:", Object.keys(components)); // Debug log
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); updateSectionVisibility("" as OfficerStatus);
} }
@ -561,30 +546,25 @@ console.log("Available components:", Object.keys(components)); // Debug log
try { try {
if (pageLoadingState) pageLoadingState.classList.remove("hidden"); if (pageLoadingState) pageLoadingState.classList.remove("hidden");
if (pageErrorState) pageErrorState.classList.add("hidden"); if (pageErrorState) pageErrorState.classList.add("hidden");
if (notAuthenticatedState) if (notAuthenticatedState) notAuthenticatedState.classList.add("hidden");
notAuthenticatedState.classList.add("hidden");
// Show loading states // Show loading states
const userProfileSkeleton = document.getElementById( const userProfileSkeleton = document.getElementById(
"userProfileSkeleton" "userProfileSkeleton",
); );
const userProfileSignedOut = document.getElementById( const userProfileSignedOut = document.getElementById(
"userProfileSignedOut" "userProfileSignedOut",
); );
const userProfileSummary = const userProfileSummary = document.getElementById("userProfileSummary");
document.getElementById("userProfileSummary");
const menuLoadingSkeleton = document.getElementById( const menuLoadingSkeleton = document.getElementById(
"menuLoadingSkeleton" "menuLoadingSkeleton",
); );
const actualMenu = document.getElementById("actualMenu"); const actualMenu = document.getElementById("actualMenu");
if (userProfileSkeleton) if (userProfileSkeleton) userProfileSkeleton.classList.remove("hidden");
userProfileSkeleton.classList.remove("hidden");
if (userProfileSummary) userProfileSummary.classList.add("hidden"); if (userProfileSummary) userProfileSummary.classList.add("hidden");
if (userProfileSignedOut) if (userProfileSignedOut) userProfileSignedOut.classList.add("hidden");
userProfileSignedOut.classList.add("hidden"); if (menuLoadingSkeleton) menuLoadingSkeleton.classList.remove("hidden");
if (menuLoadingSkeleton)
menuLoadingSkeleton.classList.remove("hidden");
if (actualMenu) actualMenu.classList.add("hidden"); if (actualMenu) actualMenu.classList.add("hidden");
// Check authentication // Check authentication
@ -592,8 +572,7 @@ console.log("Available components:", Object.keys(components)); // Debug log
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) if (userProfileSkeleton) userProfileSkeleton.classList.add("hidden");
userProfileSkeleton.classList.add("hidden");
if (userProfileSignedOut) if (userProfileSignedOut)
userProfileSignedOut.classList.remove("hidden"); userProfileSignedOut.classList.remove("hidden");
return; return;
@ -603,40 +582,83 @@ console.log("Available components:", Object.keys(components)); // Debug log
await updateUserProfile(user); await updateUserProfile(user);
// Show actual profile and hide skeleton // Show actual profile and hide skeleton
if (userProfileSkeleton) if (userProfileSkeleton) userProfileSkeleton.classList.add("hidden");
userProfileSkeleton.classList.add("hidden"); if (userProfileSummary) userProfileSummary.classList.remove("hidden");
if (userProfileSummary)
userProfileSummary.classList.remove("hidden");
// Hide all sections first // Hide all sections first
document document.querySelectorAll(".dashboard-section").forEach((section) => {
.querySelectorAll(".dashboard-section")
.forEach((section) => {
section.classList.add("hidden"); section.classList.add("hidden");
}); });
// Show appropriate default section based on role // Show appropriate default section based on role
// Get the officer record for this user if it exists
let officerStatus: OfficerStatus = "none";
try {
const officerRecords = await get.getList(
"officers",
1,
50,
`user="${user.id}"`,
"",
{
fields: ["id", "type"],
},
);
if (officerRecords && officerRecords.items.length > 0) {
const officerType = officerRecords.items[0].type;
// Map the officer type to our OfficerStatus
switch (officerType) {
case OfficerTypes.ADMINISTRATOR:
officerStatus = "administrator";
break;
case OfficerTypes.EXECUTIVE:
officerStatus = "executive";
break;
case OfficerTypes.GENERAL:
officerStatus = "general";
break;
case OfficerTypes.HONORARY:
officerStatus = "honorary";
break;
case OfficerTypes.PAST:
officerStatus = "past";
break;
default:
officerStatus = "none";
}
} else {
const extendedUser = await get.getOne("users", user.id, { const extendedUser = await get.getOne("users", user.id, {
fields: ["officer_status"], fields: ["member_type"],
}); });
const officerStatus = (extendedUser.officer_status ||
"") as OfficerStatus; if (extendedUser.member_type === "Sponsor") {
officerStatus = "sponsor";
}
}
} catch (error) {
console.error("Error determining officer status:", error);
officerStatus = "none";
}
let defaultSection; let defaultSection;
let defaultButton; let defaultButton;
if (officerStatus === "sponsor") { if (officerStatus === "sponsor") {
defaultSection = document.getElementById( defaultSection = document.getElementById("sponsorDashboardSection");
"sponsorDashboardSection"
);
defaultButton = document.querySelector( defaultButton = document.querySelector(
'[data-section="sponsorDashboard"]' '[data-section="sponsorDashboard"]',
);
} else if (officerStatus === "administrator") {
defaultSection = document.getElementById("adminDashboardSection");
defaultButton = document.querySelector(
'[data-section="adminDashboard"]',
); );
} else { } else {
defaultSection = document.getElementById("profileSection"); defaultSection = document.getElementById("profileSection");
defaultButton = document.querySelector( defaultButton = document.querySelector('[data-section="profile"]');
'[data-section="profile"]'
);
} }
if (defaultSection) { if (defaultSection) {
@ -650,8 +672,7 @@ console.log("Available components:", Object.keys(components)); // Debug log
handleNavigation(); handleNavigation();
// Show actual menu and hide skeleton // Show actual menu and hide skeleton
if (menuLoadingSkeleton) if (menuLoadingSkeleton) menuLoadingSkeleton.classList.add("hidden");
menuLoadingSkeleton.classList.add("hidden");
if (actualMenu) actualMenu.classList.remove("hidden"); if (actualMenu) actualMenu.classList.remove("hidden");
// Show main content and hide loading // Show main content and hide loading
@ -672,8 +693,7 @@ console.log("Available components:", Object.keys(components)); // Debug log
.querySelector(".login-button") .querySelector(".login-button")
?.addEventListener("click", async () => { ?.addEventListener("click", async () => {
try { try {
if (pageLoadingState) if (pageLoadingState) pageLoadingState.classList.remove("hidden");
pageLoadingState.classList.remove("hidden");
if (notAuthenticatedState) if (notAuthenticatedState)
notAuthenticatedState.classList.add("hidden"); notAuthenticatedState.classList.add("hidden");
await auth.login(); await auth.login();

View file

@ -1,15 +1,36 @@
export type OfficerStatus = "admin" | "executive" | "general" | "past" | "sponsor" | "none" | ""; export type OfficerStatus =
| "administrator"
| "executive"
| "general"
| "honorary"
| "past"
| "sponsor"
| "none"
| "";
type RoleHierarchy = Record<OfficerStatus, OfficerStatus[]>; type RoleHierarchy = Record<OfficerStatus, OfficerStatus[]>;
export function hasAccess(userRole: OfficerStatus, requiredRole: OfficerStatus): boolean { export function hasAccess(
userRole: OfficerStatus,
requiredRole: OfficerStatus,
): boolean {
const roleHierarchy: RoleHierarchy = { const roleHierarchy: RoleHierarchy = {
"admin": ["admin", "sponsor", "executive", "general", "past", "none", ""], administrator: [
"executive": ["executive", "general", "past", "none", ""], "administrator",
"general": ["general", "past", "none", ""], "executive",
"past": ["past", "none", ""], "general",
"sponsor": ["sponsor"], // Sponsor can only access sponsor-specific content "honorary",
"none": ["none", ""], "past",
"": [""] "sponsor",
"none",
"",
],
executive: ["executive", "general", "honorary", "past", "none", ""],
general: ["general", "honorary", "past", "none", ""],
honorary: ["honorary", "none", ""],
past: ["past", "none", ""],
sponsor: ["sponsor"], // Sponsor can only access sponsor-specific content
none: ["none", ""],
"": [""],
}; };
return roleHierarchy[userRole]?.includes(requiredRole) || false; return roleHierarchy[userRole]?.includes(requiredRole) || false;