admin dashboard
This commit is contained in:
parent
f2127b01b8
commit
f829e608bf
2 changed files with 874 additions and 150 deletions
|
@ -2,182 +2,690 @@
|
|||
// Admin Dashboard Component
|
||||
import { Authentication } from "../../scripts/pocketbase/Authentication";
|
||||
import { Get } from "../../scripts/pocketbase/Get";
|
||||
import { SendLog } from "../../scripts/pocketbase/SendLog";
|
||||
import {
|
||||
Collections,
|
||||
type User,
|
||||
type Event,
|
||||
type Officer,
|
||||
type Reimbursement,
|
||||
} from "../../schemas/pocketbase";
|
||||
import { Icon } from "astro-icon/components";
|
||||
import AdminSystemActivity from "./AdminDashboard/AdminSystemActivity";
|
||||
|
||||
const auth = Authentication.getInstance();
|
||||
const get = Get.getInstance();
|
||||
|
||||
// Fetch some basic stats for the admin dashboard
|
||||
// Fetch initial data for the dashboard
|
||||
let users: User[] = [];
|
||||
let officers: Officer[] = [];
|
||||
let events: Event[] = [];
|
||||
let reimbursements: Reimbursement[] = [];
|
||||
let userCount = 0;
|
||||
let officerCount = 0;
|
||||
let eventCount = 0;
|
||||
let reimbursementCount = 0;
|
||||
let pendingReimbursements = 0;
|
||||
let upcomingEvents = 0;
|
||||
|
||||
// Interface for expanded reimbursement data
|
||||
interface ExpandedReimbursement extends Reimbursement {
|
||||
expand?: {
|
||||
submitted_by?: User;
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
if (auth.isAuthenticated()) {
|
||||
const userResponse = await get.getList("users", 1, 1);
|
||||
userCount = userResponse.totalItems;
|
||||
if (auth.isAuthenticated()) {
|
||||
// Get users with pagination
|
||||
const userResponse = await get.getList<User>(
|
||||
Collections.USERS,
|
||||
1,
|
||||
50,
|
||||
"",
|
||||
"-created"
|
||||
);
|
||||
users = userResponse.items;
|
||||
userCount = userResponse.totalItems;
|
||||
|
||||
const officerResponse = await get.getList("officers", 1, 1);
|
||||
officerCount = officerResponse.totalItems;
|
||||
// Get officers with user expansion
|
||||
const officerResponse = await get.getList<Officer>(
|
||||
Collections.OFFICERS,
|
||||
1,
|
||||
50,
|
||||
"",
|
||||
"-created",
|
||||
{ expand: "user" }
|
||||
);
|
||||
officers = officerResponse.items;
|
||||
officerCount = officerResponse.totalItems;
|
||||
|
||||
const eventResponse = await get.getList("events", 1, 1);
|
||||
eventCount = eventResponse.totalItems;
|
||||
// Get events
|
||||
const eventResponse = await get.getList<Event>(
|
||||
Collections.EVENTS,
|
||||
1,
|
||||
50,
|
||||
"",
|
||||
"-start_date"
|
||||
);
|
||||
events = eventResponse.items;
|
||||
eventCount = eventResponse.totalItems;
|
||||
|
||||
const reimbursementResponse = await get.getList("reimbursement", 1, 1);
|
||||
reimbursementCount = reimbursementResponse.totalItems;
|
||||
}
|
||||
// Get upcoming events
|
||||
const now = new Date().toISOString();
|
||||
const upcomingEventsResponse = await get.getList<Event>(
|
||||
Collections.EVENTS,
|
||||
1,
|
||||
1,
|
||||
`start_date > "${now}" && published = true`,
|
||||
"start_date"
|
||||
);
|
||||
upcomingEvents = upcomingEventsResponse.totalItems;
|
||||
|
||||
// Get reimbursements with user expansion
|
||||
const reimbursementResponse = await get.getList<ExpandedReimbursement>(
|
||||
Collections.REIMBURSEMENTS,
|
||||
1,
|
||||
50,
|
||||
"",
|
||||
"-created",
|
||||
{ expand: "submitted_by" }
|
||||
);
|
||||
reimbursements = reimbursementResponse.items;
|
||||
reimbursementCount = reimbursementResponse.totalItems;
|
||||
|
||||
// Get pending reimbursements
|
||||
const pendingReimbursementsResponse = await get.getList(
|
||||
Collections.REIMBURSEMENTS,
|
||||
1,
|
||||
1,
|
||||
`status = "submitted" || status = "under_review"`,
|
||||
"-created"
|
||||
);
|
||||
pendingReimbursements = pendingReimbursementsResponse.totalItems;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error fetching admin dashboard data:", error);
|
||||
console.error("Error fetching admin dashboard data:", error);
|
||||
}
|
||||
|
||||
// Format date for display
|
||||
const formatDate = (dateString: string) => {
|
||||
const date = new Date(dateString);
|
||||
return date.toLocaleDateString("en-US", {
|
||||
year: "numeric",
|
||||
month: "short",
|
||||
day: "numeric",
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
});
|
||||
};
|
||||
|
||||
// Get user name from reimbursement
|
||||
const getUserName = (reimbursement: ExpandedReimbursement) => {
|
||||
if (reimbursement.expand?.submitted_by?.name) {
|
||||
return reimbursement.expand.submitted_by.name;
|
||||
}
|
||||
|
||||
// Try to find user by ID if expansion failed
|
||||
const user = users.find((u) => u.id === reimbursement.submitted_by);
|
||||
return user?.name || "Unknown User";
|
||||
};
|
||||
---
|
||||
|
||||
<div class="card bg-base-100 shadow-xl">
|
||||
<div class="card-body">
|
||||
<h2 class="card-title text-2xl mb-4">Administrator Dashboard</h2>
|
||||
<div class="card-body">
|
||||
<h2 class="card-title text-2xl mb-4 flex items-center gap-2">
|
||||
<Icon name="heroicons:shield-check" class="h-6 w-6 text-primary" />
|
||||
Administrator Dashboard
|
||||
<div class="badge badge-primary badge-sm">Real-time</div>
|
||||
</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>
|
||||
<!-- Stats Overview -->
|
||||
<div class="grid gap-4 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-6">
|
||||
<!-- User Stats -->
|
||||
<div
|
||||
class="card bg-base-200 hover:shadow-md transition-all duration-300 hover:-translate-y-1"
|
||||
>
|
||||
<div class="card-body p-4">
|
||||
<div class="flex justify-between items-start">
|
||||
<h3 class="card-title text-lg">Users</h3>
|
||||
<div class="p-2 bg-primary/10 rounded-full">
|
||||
<Icon
|
||||
name="heroicons:users"
|
||||
class="h-5 w-5 text-primary"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<p class="text-primary text-3xl font-semibold">
|
||||
{userCount}
|
||||
</p>
|
||||
<div class="flex justify-between items-center">
|
||||
<p class="text-sm opacity-70">Total registered</p>
|
||||
<div class="badge badge-primary badge-sm">Active</div>
|
||||
</div>
|
||||
</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>
|
||||
<!-- Officer Stats -->
|
||||
<div
|
||||
class="card bg-base-200 hover:shadow-md transition-all duration-300 hover:-translate-y-1"
|
||||
>
|
||||
<div class="card-body p-4">
|
||||
<div class="flex justify-between items-start">
|
||||
<h3 class="card-title text-lg">Officers</h3>
|
||||
<div class="p-2 bg-secondary/10 rounded-full">
|
||||
<Icon
|
||||
name="heroicons:user-group"
|
||||
class="h-5 w-5 text-secondary"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<p class="text-secondary text-3xl font-semibold">
|
||||
{officerCount}
|
||||
</p>
|
||||
<div class="flex justify-between items-center">
|
||||
<p class="text-sm opacity-70">Active officers</p>
|
||||
<button
|
||||
class="text-xs text-secondary hover:underline"
|
||||
id="viewOfficersBtn">View all</button
|
||||
>
|
||||
</div>
|
||||
</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>
|
||||
<!-- Event Stats -->
|
||||
<div
|
||||
class="card bg-base-200 hover:shadow-md transition-all duration-300 hover:-translate-y-1"
|
||||
>
|
||||
<div class="card-body p-4">
|
||||
<div class="flex justify-between items-start">
|
||||
<h3 class="card-title text-lg">Events</h3>
|
||||
<div class="p-2 bg-accent/10 rounded-full">
|
||||
<Icon
|
||||
name="heroicons:calendar"
|
||||
class="h-5 w-5 text-accent"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<p class="text-accent text-3xl font-semibold">
|
||||
{eventCount}
|
||||
</p>
|
||||
<div class="flex justify-between items-center">
|
||||
<p class="text-sm opacity-70">Total events</p>
|
||||
<button
|
||||
class="text-xs text-accent hover:underline"
|
||||
id="viewEventsBtn">View all</button
|
||||
>
|
||||
</div>
|
||||
</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>
|
||||
<!-- Upcoming Events -->
|
||||
<div
|
||||
class="card bg-base-200 hover:shadow-md transition-all duration-300 hover:-translate-y-1"
|
||||
>
|
||||
<div class="card-body p-4">
|
||||
<div class="flex justify-between items-start">
|
||||
<h3 class="card-title text-lg">Upcoming</h3>
|
||||
<div class="p-2 bg-success/10 rounded-full">
|
||||
<Icon
|
||||
name="heroicons:calendar-days"
|
||||
class="h-5 w-5 text-success"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<p class="text-success text-3xl font-semibold">
|
||||
{upcomingEvents}
|
||||
</p>
|
||||
<div class="flex justify-between items-center">
|
||||
<p class="text-sm opacity-70">Future events</p>
|
||||
<div class="badge badge-success badge-sm">Active</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Reimbursement Stats -->
|
||||
<div
|
||||
class="card bg-base-200 hover:shadow-md transition-all duration-300 hover:-translate-y-1"
|
||||
>
|
||||
<div class="card-body p-4">
|
||||
<div class="flex justify-between items-start">
|
||||
<h3 class="card-title text-lg">Reimbursements</h3>
|
||||
<div class="p-2 bg-info/10 rounded-full">
|
||||
<Icon
|
||||
name="heroicons:banknotes"
|
||||
class="h-5 w-5 text-info"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<p class="text-info text-3xl font-semibold">
|
||||
{reimbursementCount}
|
||||
</p>
|
||||
<div class="flex justify-between items-center">
|
||||
<p class="text-sm opacity-70">Total requests</p>
|
||||
<button
|
||||
class="text-xs text-info hover:underline"
|
||||
id="viewReimbursementsBtn">View all</button
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Pending Reimbursements -->
|
||||
<div
|
||||
class="card bg-base-200 hover:shadow-md transition-all duration-300 hover:-translate-y-1"
|
||||
>
|
||||
<div class="card-body p-4">
|
||||
<div class="flex justify-between items-start">
|
||||
<h3 class="card-title text-lg">Pending</h3>
|
||||
<div class="p-2 bg-warning/10 rounded-full">
|
||||
<Icon
|
||||
name="heroicons:clock"
|
||||
class="h-5 w-5 text-warning"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<p class="text-warning text-3xl font-semibold">
|
||||
{pendingReimbursements}
|
||||
</p>
|
||||
<div class="flex justify-between items-center">
|
||||
<p class="text-sm opacity-70">Awaiting review</p>
|
||||
{
|
||||
pendingReimbursements > 0 && (
|
||||
<div class="badge badge-warning badge-sm">
|
||||
Action needed
|
||||
</div>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Quick Actions -->
|
||||
<div class="mt-6">
|
||||
<h3 class="text-xl font-semibold mb-4 flex items-center gap-2">
|
||||
<Icon name="heroicons:bolt" class="h-5 w-5 text-warning" />
|
||||
Administrative Actions
|
||||
</h3>
|
||||
<div class="tabs tabs-boxed">
|
||||
<button class="tab tab-active" data-tab="users">
|
||||
<Icon name="heroicons:users" class="h-5 w-5 mr-2" />
|
||||
Manage Users
|
||||
</button>
|
||||
<button class="tab" data-tab="events">
|
||||
<Icon name="heroicons:calendar" class="h-5 w-5 mr-2" />
|
||||
Manage Events
|
||||
</button>
|
||||
<button class="tab" data-tab="finances">
|
||||
<Icon name="heroicons:banknotes" class="h-5 w-5 mr-2" />
|
||||
Manage Finances
|
||||
</button>
|
||||
<button class="tab" data-tab="logs">
|
||||
<Icon name="heroicons:document-text" class="h-5 w-5 mr-2" />
|
||||
System Logs
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Management Sections -->
|
||||
<div id="adminSections" class="mt-6">
|
||||
<!-- User Management Section -->
|
||||
<div id="userSection" class="tab-content active">
|
||||
<div class="card bg-base-200 p-4">
|
||||
<h3
|
||||
class="text-xl font-semibold mb-4 flex items-center gap-2"
|
||||
>
|
||||
<Icon
|
||||
name="heroicons:users"
|
||||
class="h-5 w-5 text-primary"
|
||||
/>
|
||||
User Management
|
||||
</h3>
|
||||
<div class="overflow-x-auto">
|
||||
<table class="table table-zebra w-full">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Email</th>
|
||||
<th>Role</th>
|
||||
<th>Last Login</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{
|
||||
users.map((user) => (
|
||||
<tr>
|
||||
<td>{user.name}</td>
|
||||
<td>{user.email}</td>
|
||||
<td>
|
||||
{user.member_id || "Member"}
|
||||
</td>
|
||||
<td>
|
||||
{user.last_login
|
||||
? formatDate(
|
||||
user.last_login
|
||||
)
|
||||
: "Never"}
|
||||
</td>
|
||||
<td>
|
||||
<div class="flex gap-2">
|
||||
<button
|
||||
class="btn btn-xs btn-primary"
|
||||
data-user-id={user.id}
|
||||
>
|
||||
Edit
|
||||
</button>
|
||||
<button
|
||||
class="btn btn-xs btn-error"
|
||||
data-user-id={user.id}
|
||||
>
|
||||
Delete
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
))
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Event Management Section -->
|
||||
<div id="eventSection" class="tab-content hidden">
|
||||
<div class="card bg-base-200 p-4">
|
||||
<h3
|
||||
class="text-xl font-semibold mb-4 flex items-center gap-2"
|
||||
>
|
||||
<Icon
|
||||
name="heroicons:calendar"
|
||||
class="h-5 w-5 text-secondary"
|
||||
/>
|
||||
Event Management
|
||||
</h3>
|
||||
<div class="overflow-x-auto">
|
||||
<table class="table table-zebra w-full">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Event Name</th>
|
||||
<th>Date</th>
|
||||
<th>Location</th>
|
||||
<th>Status</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{
|
||||
events.map((event) => (
|
||||
<tr>
|
||||
<td>{event.event_name}</td>
|
||||
<td>
|
||||
{formatDate(event.start_date)}
|
||||
</td>
|
||||
<td>{event.location}</td>
|
||||
<td>
|
||||
<div
|
||||
class={`badge ${event.published ? "badge-success" : "badge-warning"}`}
|
||||
>
|
||||
{event.published
|
||||
? "Published"
|
||||
: "Draft"}
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<div class="flex gap-2">
|
||||
<button
|
||||
class="btn btn-xs btn-primary"
|
||||
data-event-id={event.id}
|
||||
>
|
||||
Edit
|
||||
</button>
|
||||
<button
|
||||
class="btn btn-xs btn-error"
|
||||
data-event-id={event.id}
|
||||
>
|
||||
Delete
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
))
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Finance Management Section -->
|
||||
<div id="financeSection" class="tab-content hidden">
|
||||
<div class="card bg-base-200 p-4">
|
||||
<h3
|
||||
class="text-xl font-semibold mb-4 flex items-center gap-2"
|
||||
>
|
||||
<Icon
|
||||
name="heroicons:banknotes"
|
||||
class="h-5 w-5 text-accent"
|
||||
/>
|
||||
Finance Management
|
||||
</h3>
|
||||
<div class="overflow-x-auto">
|
||||
<table class="table table-zebra w-full">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Title</th>
|
||||
<th>Amount</th>
|
||||
<th>Submitted By</th>
|
||||
<th>Status</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{
|
||||
reimbursements.map((reimbursement) => (
|
||||
<tr>
|
||||
<td>{reimbursement.title}</td>
|
||||
<td>
|
||||
$
|
||||
{reimbursement.total_amount.toFixed(
|
||||
2
|
||||
)}
|
||||
</td>
|
||||
<td>
|
||||
{getUserName(
|
||||
reimbursement as ExpandedReimbursement
|
||||
)}
|
||||
</td>
|
||||
<td>
|
||||
<div
|
||||
class={`badge ${
|
||||
reimbursement.status ===
|
||||
"approved"
|
||||
? "badge-success"
|
||||
: reimbursement.status ===
|
||||
"rejected"
|
||||
? "badge-error"
|
||||
: reimbursement.status ===
|
||||
"paid"
|
||||
? "badge-info"
|
||||
: "badge-warning"
|
||||
}`}
|
||||
>
|
||||
{reimbursement.status}
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<div class="flex gap-2">
|
||||
<button
|
||||
class="btn btn-xs btn-primary"
|
||||
data-reimbursement-id={
|
||||
reimbursement.id
|
||||
}
|
||||
>
|
||||
Review
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
))
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- System Logs Section -->
|
||||
<div id="logsSection" class="tab-content hidden">
|
||||
<div class="card bg-base-200 p-4">
|
||||
<h3
|
||||
class="text-xl font-semibold mb-4 flex items-center gap-2"
|
||||
>
|
||||
<Icon
|
||||
name="heroicons:document-text"
|
||||
class="h-5 w-5 text-info"
|
||||
/>
|
||||
System Logs
|
||||
</h3>
|
||||
<AdminSystemActivity client:load limit={20} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Recent System Activity -->
|
||||
<div class="mt-6">
|
||||
<h3 class="text-xl font-semibold mb-4 flex items-center gap-2">
|
||||
<Icon name="heroicons:chart-bar" class="h-5 w-5 text-info" />
|
||||
Recent System Activity
|
||||
<div class="badge badge-ghost text-xs font-normal">
|
||||
Live updates
|
||||
</div>
|
||||
</h3>
|
||||
<div class="card bg-base-200 p-4">
|
||||
<AdminSystemActivity
|
||||
client:load
|
||||
limit={5}
|
||||
refreshInterval={60000}
|
||||
/>
|
||||
</div>
|
||||
</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
|
||||
// Client-side functionality for the admin dashboard
|
||||
document.addEventListener("DOMContentLoaded", () => {
|
||||
const tabs = document.querySelectorAll(".tab");
|
||||
const tabContents = document.querySelectorAll(".tab-content");
|
||||
|
||||
// Function to switch tabs
|
||||
const switchTab = (tabId: string) => {
|
||||
// Update tab buttons
|
||||
tabs.forEach((tab) => {
|
||||
if (tab.getAttribute("data-tab") === tabId) {
|
||||
tab.classList.add("tab-active");
|
||||
} else {
|
||||
tab.classList.remove("tab-active");
|
||||
}
|
||||
});
|
||||
|
||||
// Update content sections
|
||||
tabContents.forEach((content) => {
|
||||
if (content.id === `${tabId}Section`) {
|
||||
content.classList.remove("hidden");
|
||||
} else {
|
||||
content.classList.add("hidden");
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// Add click handlers to tabs
|
||||
tabs.forEach((tab) => {
|
||||
tab.addEventListener("click", () => {
|
||||
const tabId = tab.getAttribute("data-tab");
|
||||
if (tabId) switchTab(tabId);
|
||||
});
|
||||
});
|
||||
|
||||
// Handle "View all" button clicks
|
||||
document
|
||||
.getElementById("viewOfficersBtn")
|
||||
?.addEventListener("click", () => switchTab("users"));
|
||||
document
|
||||
.getElementById("viewEventsBtn")
|
||||
?.addEventListener("click", () => switchTab("events"));
|
||||
document
|
||||
.getElementById("viewReimbursementsBtn")
|
||||
?.addEventListener("click", () => switchTab("finances"));
|
||||
|
||||
// Refresh data periodically
|
||||
const refreshData = async () => {
|
||||
try {
|
||||
const { Authentication } = await import(
|
||||
"../../scripts/pocketbase/Authentication"
|
||||
);
|
||||
const { Get } = await import("../../scripts/pocketbase/Get");
|
||||
const { Collections } = await import(
|
||||
"../../schemas/pocketbase"
|
||||
);
|
||||
|
||||
const auth = Authentication.getInstance();
|
||||
const get = Get.getInstance();
|
||||
|
||||
if (!auth.isAuthenticated()) return;
|
||||
|
||||
// Update stats
|
||||
const stats = await Promise.all([
|
||||
get.getList(Collections.USERS, 1, 1),
|
||||
get.getList(Collections.OFFICERS, 1, 1),
|
||||
get.getList(Collections.EVENTS, 1, 1),
|
||||
get.getList(
|
||||
Collections.EVENTS,
|
||||
1,
|
||||
1,
|
||||
`start_date > "${new Date().toISOString()}" && published = true`
|
||||
),
|
||||
get.getList(Collections.REIMBURSEMENTS, 1, 1),
|
||||
get.getList(
|
||||
Collections.REIMBURSEMENTS,
|
||||
1,
|
||||
1,
|
||||
`status = "submitted" || status = "under_review"`
|
||||
),
|
||||
]);
|
||||
|
||||
// Update UI with new stats
|
||||
document.querySelector(".text-primary.text-3xl")!.textContent =
|
||||
stats[0].totalItems.toString();
|
||||
document.querySelector(
|
||||
".text-secondary.text-3xl"
|
||||
)!.textContent = stats[1].totalItems.toString();
|
||||
document.querySelector(".text-accent.text-3xl")!.textContent =
|
||||
stats[2].totalItems.toString();
|
||||
document.querySelector(".text-success.text-3xl")!.textContent =
|
||||
stats[3].totalItems.toString();
|
||||
document.querySelector(".text-info.text-3xl")!.textContent =
|
||||
stats[4].totalItems.toString();
|
||||
document.querySelector(".text-warning.text-3xl")!.textContent =
|
||||
stats[5].totalItems.toString();
|
||||
} catch (error) {
|
||||
console.error("Error refreshing dashboard data:", error);
|
||||
}
|
||||
};
|
||||
|
||||
// Refresh every 5 minutes
|
||||
setInterval(refreshData, 5 * 60 * 1000);
|
||||
});
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.tab-content {
|
||||
transition: all 0.3s ease-in-out;
|
||||
}
|
||||
.tab-content.hidden {
|
||||
display: none;
|
||||
}
|
||||
</style>
|
||||
|
|
216
src/components/dashboard/AdminDashboard/AdminSystemActivity.tsx
Normal file
216
src/components/dashboard/AdminDashboard/AdminSystemActivity.tsx
Normal file
|
@ -0,0 +1,216 @@
|
|||
import { useEffect, useState } from "react";
|
||||
import { Authentication } from "../../../scripts/pocketbase/Authentication";
|
||||
import { Get } from "../../../scripts/pocketbase/Get";
|
||||
import { Collections } from "../../../schemas/pocketbase";
|
||||
import { Icon } from "astro-icon/components";
|
||||
import type { Log } from "../../../schemas/pocketbase";
|
||||
|
||||
// Extend the Log type to include expand property
|
||||
interface ExtendedLog extends Log {
|
||||
expand?: {
|
||||
user?: {
|
||||
name: string;
|
||||
email: string;
|
||||
id: string;
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
interface AdminSystemActivityProps {
|
||||
limit?: number;
|
||||
autoRefresh?: boolean;
|
||||
refreshInterval?: number;
|
||||
}
|
||||
|
||||
export default function AdminSystemActivity({
|
||||
limit = 10,
|
||||
autoRefresh = true,
|
||||
refreshInterval = 30000, // 30 seconds
|
||||
}: AdminSystemActivityProps) {
|
||||
const [logs, setLogs] = useState<ExtendedLog[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
const fetchLogs = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const auth = Authentication.getInstance();
|
||||
const get = Get.getInstance();
|
||||
|
||||
if (!auth.isAuthenticated()) {
|
||||
setError("Not authenticated");
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
// Fetch logs with user expansion
|
||||
const logsResponse = await get.getList<ExtendedLog>(
|
||||
Collections.LOGS,
|
||||
1,
|
||||
limit,
|
||||
"",
|
||||
"-created",
|
||||
{ expand: "user" }
|
||||
);
|
||||
|
||||
setLogs(logsResponse.items);
|
||||
setError(null);
|
||||
} catch (err) {
|
||||
console.error("Error fetching logs:", err);
|
||||
setError("Failed to load system logs");
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
// Initial fetch
|
||||
fetchLogs();
|
||||
|
||||
// Set up auto-refresh if enabled
|
||||
let intervalId: number | undefined;
|
||||
if (autoRefresh && typeof window !== 'undefined') {
|
||||
intervalId = window.setInterval(fetchLogs, refreshInterval);
|
||||
}
|
||||
|
||||
// Cleanup interval on unmount
|
||||
return () => {
|
||||
if (intervalId !== undefined && typeof window !== 'undefined') {
|
||||
window.clearInterval(intervalId);
|
||||
}
|
||||
};
|
||||
}, [limit, autoRefresh, refreshInterval]);
|
||||
|
||||
// Format date to a readable format
|
||||
const formatDate = (dateString: string) => {
|
||||
const date = new Date(dateString);
|
||||
|
||||
// Check if the date is today
|
||||
const today = new Date();
|
||||
const isToday = date.toDateString() === today.toDateString();
|
||||
|
||||
if (isToday) {
|
||||
// If today, show time only
|
||||
return date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
|
||||
} else {
|
||||
// Otherwise show date and time
|
||||
return date.toLocaleString([], {
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit'
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// Get appropriate icon for log type
|
||||
const getLogTypeIcon = (type: string) => {
|
||||
switch (type.toLowerCase()) {
|
||||
case "login":
|
||||
return "heroicons:login";
|
||||
case "logout":
|
||||
return "heroicons:logout";
|
||||
case "create":
|
||||
return "heroicons:plus-circle";
|
||||
case "update":
|
||||
return "heroicons:pencil";
|
||||
case "delete":
|
||||
return "heroicons:trash";
|
||||
case "error":
|
||||
return "heroicons:exclamation-circle";
|
||||
default:
|
||||
return "heroicons:information-circle";
|
||||
}
|
||||
};
|
||||
|
||||
// Get appropriate color for log type
|
||||
const getLogTypeColor = (type: string) => {
|
||||
switch (type.toLowerCase()) {
|
||||
case "login":
|
||||
return "text-success";
|
||||
case "logout":
|
||||
return "text-info";
|
||||
case "create":
|
||||
return "text-primary";
|
||||
case "update":
|
||||
return "text-secondary";
|
||||
case "delete":
|
||||
return "text-error";
|
||||
case "error":
|
||||
return "text-error";
|
||||
default:
|
||||
return "text-base-content";
|
||||
}
|
||||
};
|
||||
|
||||
if (loading && logs.length === 0) {
|
||||
return (
|
||||
<div className="flex justify-center items-center p-4">
|
||||
<div className="loading loading-spinner loading-md"></div>
|
||||
<span className="ml-2">Loading system logs...</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<div className="alert alert-error">
|
||||
<Icon name="heroicons:exclamation-circle" class="h-5 w-5" />
|
||||
<span>{error}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (logs.length === 0) {
|
||||
return (
|
||||
<div className="alert alert-info">
|
||||
<Icon name="heroicons:information-circle" class="h-5 w-5" />
|
||||
<span>No system logs found</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="overflow-x-auto">
|
||||
<table className="table table-zebra w-full">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Time</th>
|
||||
<th>User</th>
|
||||
<th>Action</th>
|
||||
<th>Details</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{logs.map((log) => (
|
||||
<tr key={log.id} className="hover:bg-base-200 transition-colors">
|
||||
<td className="text-sm whitespace-nowrap">
|
||||
{formatDate(log.created)}
|
||||
</td>
|
||||
<td>
|
||||
{log.expand?.user?.name || "System"}
|
||||
</td>
|
||||
<td>
|
||||
<div className="flex items-center gap-1">
|
||||
<Icon
|
||||
name={getLogTypeIcon(log.type)}
|
||||
class={`h-4 w-4 ${getLogTypeColor(log.type)}`}
|
||||
/>
|
||||
<span className="capitalize">{log.type}</span>
|
||||
</div>
|
||||
</td>
|
||||
<td className="max-w-md truncate">{log.message}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
{loading && logs.length > 0 && (
|
||||
<div className="flex justify-center items-center p-2 text-sm text-base-content/70">
|
||||
<div className="loading loading-spinner loading-xs"></div>
|
||||
<span className="ml-2">Refreshing...</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
Loading…
Reference in a new issue