fix stats
This commit is contained in:
parent
f829e608bf
commit
3b1a1d9132
2 changed files with 767 additions and 9506 deletions
9477
package-lock.json
generated
9477
package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
@ -2,7 +2,6 @@
|
||||||
// Admin Dashboard Component
|
// Admin Dashboard Component
|
||||||
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 {
|
import {
|
||||||
Collections,
|
Collections,
|
||||||
type User,
|
type User,
|
||||||
|
@ -138,6 +137,13 @@ const getUserName = (reimbursement: ExpandedReimbursement) => {
|
||||||
<Icon name="heroicons:shield-check" class="h-6 w-6 text-primary" />
|
<Icon name="heroicons:shield-check" class="h-6 w-6 text-primary" />
|
||||||
Administrator Dashboard
|
Administrator Dashboard
|
||||||
<div class="badge badge-primary badge-sm">Real-time</div>
|
<div class="badge badge-primary badge-sm">Real-time</div>
|
||||||
|
<button
|
||||||
|
id="refreshDashboardBtn"
|
||||||
|
class="btn btn-sm btn-ghost ml-auto"
|
||||||
|
>
|
||||||
|
<Icon name="heroicons:arrow-path" class="h-5 w-5" />
|
||||||
|
Refresh
|
||||||
|
</button>
|
||||||
</h2>
|
</h2>
|
||||||
|
|
||||||
<!-- Stats Overview -->
|
<!-- Stats Overview -->
|
||||||
|
@ -309,19 +315,19 @@ const getUserName = (reimbursement: ExpandedReimbursement) => {
|
||||||
Administrative Actions
|
Administrative Actions
|
||||||
</h3>
|
</h3>
|
||||||
<div class="tabs tabs-boxed">
|
<div class="tabs tabs-boxed">
|
||||||
<button class="tab tab-active" data-tab="users">
|
<button class="tab tab-active" data-tab="user">
|
||||||
<Icon name="heroicons:users" class="h-5 w-5 mr-2" />
|
<Icon name="heroicons:users" class="h-5 w-5 mr-2" />
|
||||||
Manage Users
|
Manage Users
|
||||||
</button>
|
</button>
|
||||||
<button class="tab" data-tab="events">
|
<button class="tab" data-tab="event">
|
||||||
<Icon name="heroicons:calendar" class="h-5 w-5 mr-2" />
|
<Icon name="heroicons:calendar" class="h-5 w-5 mr-2" />
|
||||||
Manage Events
|
Manage Events
|
||||||
</button>
|
</button>
|
||||||
<button class="tab" data-tab="finances">
|
<button class="tab" data-tab="finance">
|
||||||
<Icon name="heroicons:banknotes" class="h-5 w-5 mr-2" />
|
<Icon name="heroicons:banknotes" class="h-5 w-5 mr-2" />
|
||||||
Manage Finances
|
Manage Finances
|
||||||
</button>
|
</button>
|
||||||
<button class="tab" data-tab="logs">
|
<button class="tab" data-tab="log">
|
||||||
<Icon name="heroicons:document-text" class="h-5 w-5 mr-2" />
|
<Icon name="heroicons:document-text" class="h-5 w-5 mr-2" />
|
||||||
System Logs
|
System Logs
|
||||||
</button>
|
</button>
|
||||||
|
@ -449,6 +455,20 @@ const getUserName = (reimbursement: ExpandedReimbursement) => {
|
||||||
>
|
>
|
||||||
Delete
|
Delete
|
||||||
</button>
|
</button>
|
||||||
|
<button
|
||||||
|
class={`btn btn-xs ${event.published ? "btn-warning" : "btn-success"}`}
|
||||||
|
data-event-id={event.id}
|
||||||
|
data-action="toggle-publish"
|
||||||
|
data-current-state={
|
||||||
|
event.published
|
||||||
|
? "published"
|
||||||
|
: "draft"
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{event.published
|
||||||
|
? "Unpublish"
|
||||||
|
: "Publish"}
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
@ -539,7 +559,7 @@ const getUserName = (reimbursement: ExpandedReimbursement) => {
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- System Logs Section -->
|
<!-- System Logs Section -->
|
||||||
<div id="logsSection" class="tab-content hidden">
|
<div id="logSection" class="tab-content hidden">
|
||||||
<div class="card bg-base-200 p-4">
|
<div class="card bg-base-200 p-4">
|
||||||
<h3
|
<h3
|
||||||
class="text-xl font-semibold mb-4 flex items-center gap-2"
|
class="text-xl font-semibold mb-4 flex items-center gap-2"
|
||||||
|
@ -550,7 +570,9 @@ const getUserName = (reimbursement: ExpandedReimbursement) => {
|
||||||
/>
|
/>
|
||||||
System Logs
|
System Logs
|
||||||
</h3>
|
</h3>
|
||||||
<AdminSystemActivity client:load limit={20} />
|
<div id="adminSystemActivityLogs">
|
||||||
|
<AdminSystemActivity client:load limit={20} />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -565,11 +587,13 @@ const getUserName = (reimbursement: ExpandedReimbursement) => {
|
||||||
</div>
|
</div>
|
||||||
</h3>
|
</h3>
|
||||||
<div class="card bg-base-200 p-4">
|
<div class="card bg-base-200 p-4">
|
||||||
<AdminSystemActivity
|
<div id="adminSystemActivityRecent">
|
||||||
client:load
|
<AdminSystemActivity
|
||||||
limit={5}
|
client:load
|
||||||
refreshInterval={60000}
|
limit={5}
|
||||||
/>
|
refreshInterval={60000}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -577,7 +601,48 @@ const getUserName = (reimbursement: ExpandedReimbursement) => {
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
// Client-side functionality for the admin dashboard
|
// Client-side functionality for the admin dashboard
|
||||||
document.addEventListener("DOMContentLoaded", () => {
|
document.addEventListener("DOMContentLoaded", async () => {
|
||||||
|
// Check authentication status
|
||||||
|
try {
|
||||||
|
const { Authentication } = await import(
|
||||||
|
"../../scripts/pocketbase/Authentication"
|
||||||
|
);
|
||||||
|
const auth = Authentication.getInstance();
|
||||||
|
|
||||||
|
if (!auth.isAuthenticated()) {
|
||||||
|
// Show authentication error
|
||||||
|
const adminDashboard = document.querySelector(".card-body");
|
||||||
|
if (adminDashboard) {
|
||||||
|
const authError = document.createElement("div");
|
||||||
|
authError.className = "alert alert-error shadow-lg mb-4";
|
||||||
|
authError.innerHTML = `
|
||||||
|
<div>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" class="stroke-current flex-shrink-0 h-6 w-6" fill="none" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||||
|
</svg>
|
||||||
|
<div>
|
||||||
|
<h3 class="font-bold">Authentication Error</h3>
|
||||||
|
<div class="text-xs">You are not authenticated. Please log in to access the admin dashboard.</div>
|
||||||
|
<button class="btn btn-sm btn-primary mt-2" onclick="window.location.href='/login'">Log In</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
adminDashboard.prepend(authError);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.error(
|
||||||
|
"Authentication error: User is not authenticated"
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Debug: Log authentication status
|
||||||
|
console.log("Authentication status: Authenticated");
|
||||||
|
console.log("Current user:", auth.getCurrentUser());
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error checking authentication:", error);
|
||||||
|
}
|
||||||
|
|
||||||
const tabs = document.querySelectorAll(".tab");
|
const tabs = document.querySelectorAll(".tab");
|
||||||
const tabContents = document.querySelectorAll(".tab-content");
|
const tabContents = document.querySelectorAll(".tab-content");
|
||||||
|
|
||||||
|
@ -613,13 +678,620 @@ const getUserName = (reimbursement: ExpandedReimbursement) => {
|
||||||
// Handle "View all" button clicks
|
// Handle "View all" button clicks
|
||||||
document
|
document
|
||||||
.getElementById("viewOfficersBtn")
|
.getElementById("viewOfficersBtn")
|
||||||
?.addEventListener("click", () => switchTab("users"));
|
?.addEventListener("click", () => switchTab("user"));
|
||||||
document
|
document
|
||||||
.getElementById("viewEventsBtn")
|
.getElementById("viewEventsBtn")
|
||||||
?.addEventListener("click", () => switchTab("events"));
|
?.addEventListener("click", () => switchTab("event"));
|
||||||
document
|
document
|
||||||
.getElementById("viewReimbursementsBtn")
|
.getElementById("viewReimbursementsBtn")
|
||||||
?.addEventListener("click", () => switchTab("finances"));
|
?.addEventListener("click", () => switchTab("finance"));
|
||||||
|
|
||||||
|
// Handle user actions (edit, delete, review)
|
||||||
|
const setupActionHandlers = () => {
|
||||||
|
// User edit buttons
|
||||||
|
document.querySelectorAll("[data-user-id]").forEach((button) => {
|
||||||
|
button.addEventListener("click", async (e) => {
|
||||||
|
const target = e.currentTarget as HTMLElement;
|
||||||
|
const userId = target.getAttribute("data-user-id");
|
||||||
|
const isDelete = target.classList.contains("btn-error");
|
||||||
|
|
||||||
|
if (!userId) return;
|
||||||
|
|
||||||
|
if (isDelete) {
|
||||||
|
if (
|
||||||
|
confirm(
|
||||||
|
"Are you sure you want to delete this user? This action cannot be undone."
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
const { Authentication } = await import(
|
||||||
|
"../../scripts/pocketbase/Authentication"
|
||||||
|
);
|
||||||
|
const auth = Authentication.getInstance();
|
||||||
|
const pb = auth.getPocketBase();
|
||||||
|
|
||||||
|
// Delete the user
|
||||||
|
await pb.collection("users").delete(userId);
|
||||||
|
|
||||||
|
// Log the action
|
||||||
|
await logAdminAction(
|
||||||
|
"delete",
|
||||||
|
"users",
|
||||||
|
`Deleted user with ID: ${userId}`
|
||||||
|
);
|
||||||
|
|
||||||
|
// Remove the row from the table
|
||||||
|
const row = target.closest("tr");
|
||||||
|
if (row) row.remove();
|
||||||
|
|
||||||
|
// Show success toast
|
||||||
|
showToast(
|
||||||
|
"User deleted successfully",
|
||||||
|
"success"
|
||||||
|
);
|
||||||
|
|
||||||
|
// Refresh stats
|
||||||
|
refreshData();
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error deleting user:", error);
|
||||||
|
showToast("Failed to delete user", "error");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Redirect to user edit page
|
||||||
|
window.location.href = `/admin/users/edit/${userId}`;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Event edit/delete/publish buttons
|
||||||
|
document.querySelectorAll("[data-event-id]").forEach((button) => {
|
||||||
|
button.addEventListener("click", async (e) => {
|
||||||
|
const target = e.currentTarget as HTMLElement;
|
||||||
|
const eventId = target.getAttribute("data-event-id");
|
||||||
|
const isDelete = target.classList.contains("btn-error");
|
||||||
|
const isTogglePublish =
|
||||||
|
target.getAttribute("data-action") === "toggle-publish";
|
||||||
|
|
||||||
|
if (!eventId) return;
|
||||||
|
|
||||||
|
if (isTogglePublish) {
|
||||||
|
try {
|
||||||
|
const currentState =
|
||||||
|
target.getAttribute("data-current-state");
|
||||||
|
const newPublishedState =
|
||||||
|
currentState !== "published";
|
||||||
|
|
||||||
|
const { Authentication } = await import(
|
||||||
|
"../../scripts/pocketbase/Authentication"
|
||||||
|
);
|
||||||
|
const { Update } = await import(
|
||||||
|
"../../scripts/pocketbase/Update"
|
||||||
|
);
|
||||||
|
const { Collections } = await import(
|
||||||
|
"../../schemas/pocketbase"
|
||||||
|
);
|
||||||
|
|
||||||
|
const auth = Authentication.getInstance();
|
||||||
|
const update = Update.getInstance();
|
||||||
|
|
||||||
|
// Update the event's published state
|
||||||
|
await update.updateFields(
|
||||||
|
Collections.EVENTS,
|
||||||
|
eventId,
|
||||||
|
{ published: newPublishedState }
|
||||||
|
);
|
||||||
|
|
||||||
|
// Log the action
|
||||||
|
await logAdminAction(
|
||||||
|
"update",
|
||||||
|
"events",
|
||||||
|
`${newPublishedState ? "Published" : "Unpublished"} event with ID: ${eventId}`
|
||||||
|
);
|
||||||
|
|
||||||
|
// Update the button and badge
|
||||||
|
const row = target.closest("tr");
|
||||||
|
if (row) {
|
||||||
|
// Update badge
|
||||||
|
const badge = row.querySelector(".badge");
|
||||||
|
if (badge) {
|
||||||
|
badge.className = `badge ${newPublishedState ? "badge-success" : "badge-warning"}`;
|
||||||
|
badge.textContent = newPublishedState
|
||||||
|
? "Published"
|
||||||
|
: "Draft";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update button
|
||||||
|
target.className = `btn btn-xs ${newPublishedState ? "btn-warning" : "btn-success"}`;
|
||||||
|
target.textContent = newPublishedState
|
||||||
|
? "Unpublish"
|
||||||
|
: "Publish";
|
||||||
|
target.setAttribute(
|
||||||
|
"data-current-state",
|
||||||
|
newPublishedState ? "published" : "draft"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show success toast
|
||||||
|
showToast(
|
||||||
|
`Event ${newPublishedState ? "published" : "unpublished"} successfully`,
|
||||||
|
"success"
|
||||||
|
);
|
||||||
|
|
||||||
|
// Refresh stats
|
||||||
|
refreshData();
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error updating event:", error);
|
||||||
|
showToast("Failed to update event", "error");
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isDelete) {
|
||||||
|
if (
|
||||||
|
confirm(
|
||||||
|
"Are you sure you want to delete this event? This action cannot be undone."
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
const { Authentication } = await import(
|
||||||
|
"../../scripts/pocketbase/Authentication"
|
||||||
|
);
|
||||||
|
const { Collections } = await import(
|
||||||
|
"../../schemas/pocketbase"
|
||||||
|
);
|
||||||
|
|
||||||
|
const auth = Authentication.getInstance();
|
||||||
|
const pb = auth.getPocketBase();
|
||||||
|
|
||||||
|
// Delete the event
|
||||||
|
await pb
|
||||||
|
.collection(Collections.EVENTS)
|
||||||
|
.delete(eventId);
|
||||||
|
|
||||||
|
// Log the action
|
||||||
|
await logAdminAction(
|
||||||
|
"delete",
|
||||||
|
"events",
|
||||||
|
`Deleted event with ID: ${eventId}`
|
||||||
|
);
|
||||||
|
|
||||||
|
// Remove the row from the table
|
||||||
|
const row = target.closest("tr");
|
||||||
|
if (row) row.remove();
|
||||||
|
|
||||||
|
// Show success toast
|
||||||
|
showToast(
|
||||||
|
"Event deleted successfully",
|
||||||
|
"success"
|
||||||
|
);
|
||||||
|
|
||||||
|
// Refresh stats
|
||||||
|
refreshData();
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error deleting event:", error);
|
||||||
|
showToast("Failed to delete event", "error");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (!isTogglePublish) {
|
||||||
|
// Redirect to event edit page
|
||||||
|
window.location.href = `/admin/events/edit/${eventId}`;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Reimbursement review buttons
|
||||||
|
document
|
||||||
|
.querySelectorAll("[data-reimbursement-id]")
|
||||||
|
.forEach((button) => {
|
||||||
|
button.addEventListener("click", async (e) => {
|
||||||
|
const target = e.currentTarget as HTMLElement;
|
||||||
|
const reimbursementId = target.getAttribute(
|
||||||
|
"data-reimbursement-id"
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!reimbursementId) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Fetch reimbursement details
|
||||||
|
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();
|
||||||
|
|
||||||
|
// Show loading state
|
||||||
|
showToast(
|
||||||
|
"Loading reimbursement details...",
|
||||||
|
"info"
|
||||||
|
);
|
||||||
|
|
||||||
|
// Get reimbursement with user expansion
|
||||||
|
const reimbursement = await get.getOne(
|
||||||
|
Collections.REIMBURSEMENTS,
|
||||||
|
reimbursementId,
|
||||||
|
{ expand: "submitted_by" }
|
||||||
|
);
|
||||||
|
|
||||||
|
// Open modal with reimbursement details
|
||||||
|
openReimbursementModal(reimbursement);
|
||||||
|
} catch (error) {
|
||||||
|
console.error(
|
||||||
|
"Error fetching reimbursement:",
|
||||||
|
error
|
||||||
|
);
|
||||||
|
showToast(
|
||||||
|
"Failed to load reimbursement details",
|
||||||
|
"error"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Function to open reimbursement modal
|
||||||
|
const openReimbursementModal = (reimbursement: any) => {
|
||||||
|
// Create modal if it doesn't exist
|
||||||
|
let modal = document.getElementById("reimbursement-modal");
|
||||||
|
if (!modal) {
|
||||||
|
modal = document.createElement("div");
|
||||||
|
modal.id = "reimbursement-modal";
|
||||||
|
modal.className = "modal";
|
||||||
|
document.body.appendChild(modal);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Format date
|
||||||
|
const purchaseDate = new Date(reimbursement.date_of_purchase);
|
||||||
|
const formattedDate = purchaseDate.toLocaleDateString("en-US", {
|
||||||
|
year: "numeric",
|
||||||
|
month: "long",
|
||||||
|
day: "numeric",
|
||||||
|
});
|
||||||
|
|
||||||
|
// Get user name
|
||||||
|
const userName =
|
||||||
|
reimbursement.expand?.submitted_by?.name || "Unknown User";
|
||||||
|
|
||||||
|
// Set modal content
|
||||||
|
modal.innerHTML = `
|
||||||
|
<div class="modal-box">
|
||||||
|
<h3 class="font-bold text-lg">Review Reimbursement</h3>
|
||||||
|
<div class="py-4">
|
||||||
|
<div class="grid grid-cols-2 gap-4">
|
||||||
|
<div>
|
||||||
|
<p class="text-sm opacity-70">Title</p>
|
||||||
|
<p class="font-semibold">${reimbursement.title}</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p class="text-sm opacity-70">Amount</p>
|
||||||
|
<p class="font-semibold">$${reimbursement.total_amount.toFixed(2)}</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p class="text-sm opacity-70">Submitted By</p>
|
||||||
|
<p class="font-semibold">${userName}</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p class="text-sm opacity-70">Date of Purchase</p>
|
||||||
|
<p class="font-semibold">${formattedDate}</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p class="text-sm opacity-70">Payment Method</p>
|
||||||
|
<p class="font-semibold">${reimbursement.payment_method}</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p class="text-sm opacity-70">Department</p>
|
||||||
|
<p class="font-semibold">${reimbursement.department}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-4">
|
||||||
|
<p class="text-sm opacity-70">Additional Information</p>
|
||||||
|
<p>${reimbursement.additional_info || "None provided"}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-4">
|
||||||
|
<p class="text-sm opacity-70">Current Status</p>
|
||||||
|
<div class="badge ${
|
||||||
|
reimbursement.status === "approved"
|
||||||
|
? "badge-success"
|
||||||
|
: reimbursement.status === "rejected"
|
||||||
|
? "badge-error"
|
||||||
|
: reimbursement.status === "paid"
|
||||||
|
? "badge-info"
|
||||||
|
: "badge-warning"
|
||||||
|
}">${reimbursement.status}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="divider"></div>
|
||||||
|
|
||||||
|
<div class="form-control">
|
||||||
|
<label class="label">
|
||||||
|
<span class="label-text">Update Status</span>
|
||||||
|
</label>
|
||||||
|
<select id="status-select" class="select select-bordered w-full">
|
||||||
|
<option value="submitted" ${reimbursement.status === "submitted" ? "selected" : ""}>Submitted</option>
|
||||||
|
<option value="under_review" ${reimbursement.status === "under_review" ? "selected" : ""}>Under Review</option>
|
||||||
|
<option value="approved" ${reimbursement.status === "approved" ? "selected" : ""}>Approved</option>
|
||||||
|
<option value="rejected" ${reimbursement.status === "rejected" ? "selected" : ""}>Rejected</option>
|
||||||
|
<option value="in_progress" ${reimbursement.status === "in_progress" ? "selected" : ""}>In Progress</option>
|
||||||
|
<option value="paid" ${reimbursement.status === "paid" ? "selected" : ""}>Paid</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-control mt-4">
|
||||||
|
<label class="label">
|
||||||
|
<span class="label-text">Audit Notes</span>
|
||||||
|
</label>
|
||||||
|
<textarea id="audit-notes" class="textarea textarea-bordered h-24" placeholder="Add notes about this reimbursement">${reimbursement.audit_notes || ""}</textarea>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="modal-action">
|
||||||
|
<button class="btn" onclick="document.getElementById('reimbursement-modal').classList.remove('modal-open')">Cancel</button>
|
||||||
|
<button id="save-reimbursement" class="btn btn-primary" data-id="${reimbursement.id}">Save Changes</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
// Show modal
|
||||||
|
modal.classList.add("modal-open");
|
||||||
|
|
||||||
|
// Add event listener to save button
|
||||||
|
document
|
||||||
|
.getElementById("save-reimbursement")
|
||||||
|
?.addEventListener("click", async () => {
|
||||||
|
const statusSelect = document.getElementById(
|
||||||
|
"status-select"
|
||||||
|
) as HTMLSelectElement;
|
||||||
|
const auditNotes = document.getElementById(
|
||||||
|
"audit-notes"
|
||||||
|
) as HTMLTextAreaElement;
|
||||||
|
const reimbursementId = (
|
||||||
|
document.getElementById(
|
||||||
|
"save-reimbursement"
|
||||||
|
) as HTMLElement
|
||||||
|
).getAttribute("data-id");
|
||||||
|
|
||||||
|
if (!statusSelect || !auditNotes || !reimbursementId)
|
||||||
|
return;
|
||||||
|
|
||||||
|
const newStatus = statusSelect.value;
|
||||||
|
const notes = auditNotes.value;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const { Authentication } = await import(
|
||||||
|
"../../scripts/pocketbase/Authentication"
|
||||||
|
);
|
||||||
|
const { Update } = await import(
|
||||||
|
"../../scripts/pocketbase/Update"
|
||||||
|
);
|
||||||
|
const { Collections } = await import(
|
||||||
|
"../../schemas/pocketbase"
|
||||||
|
);
|
||||||
|
|
||||||
|
const auth = Authentication.getInstance();
|
||||||
|
const update = Update.getInstance();
|
||||||
|
|
||||||
|
// Create audit log entry
|
||||||
|
const currentUser = auth.getCurrentUser();
|
||||||
|
const currentTime = new Date().toISOString();
|
||||||
|
|
||||||
|
// Parse existing audit logs or create new array
|
||||||
|
let auditLogs = [];
|
||||||
|
if (reimbursement.audit_logs) {
|
||||||
|
try {
|
||||||
|
auditLogs = JSON.parse(
|
||||||
|
reimbursement.audit_logs
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
console.warn(
|
||||||
|
"Failed to parse existing audit logs:",
|
||||||
|
e
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add new log entry
|
||||||
|
auditLogs.push({
|
||||||
|
timestamp: currentTime,
|
||||||
|
user_id: currentUser.id,
|
||||||
|
user_name: currentUser.name,
|
||||||
|
action: `Status changed from "${reimbursement.status}" to "${newStatus}"`,
|
||||||
|
notes: notes,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Update reimbursement
|
||||||
|
await update.updateFields(
|
||||||
|
Collections.REIMBURSEMENTS,
|
||||||
|
reimbursementId,
|
||||||
|
{
|
||||||
|
status: newStatus,
|
||||||
|
audit_notes: notes,
|
||||||
|
audit_logs: JSON.stringify(auditLogs),
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// Log the action
|
||||||
|
await logAdminAction(
|
||||||
|
"update",
|
||||||
|
"reimbursements",
|
||||||
|
`Updated reimbursement ${reimbursementId} status to "${newStatus}"`
|
||||||
|
);
|
||||||
|
|
||||||
|
// Close modal
|
||||||
|
modal?.classList.remove("modal-open");
|
||||||
|
|
||||||
|
// Show success toast
|
||||||
|
showToast(
|
||||||
|
"Reimbursement updated successfully",
|
||||||
|
"success"
|
||||||
|
);
|
||||||
|
|
||||||
|
// Mark page for refresh
|
||||||
|
const refreshMarker = document.createElement("div");
|
||||||
|
refreshMarker.setAttribute(
|
||||||
|
"data-refresh-needed",
|
||||||
|
"true"
|
||||||
|
);
|
||||||
|
refreshMarker.style.display = "none";
|
||||||
|
document.body.appendChild(refreshMarker);
|
||||||
|
|
||||||
|
// Refresh data
|
||||||
|
refreshData();
|
||||||
|
} catch (error) {
|
||||||
|
console.error(
|
||||||
|
"Error updating reimbursement:",
|
||||||
|
error
|
||||||
|
);
|
||||||
|
showToast(
|
||||||
|
"Failed to update reimbursement",
|
||||||
|
"error"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
// Setup action handlers when the page loads
|
||||||
|
setupActionHandlers();
|
||||||
|
|
||||||
|
// Function to log admin actions to system logs
|
||||||
|
const logAdminAction = async (
|
||||||
|
type: string,
|
||||||
|
part: string,
|
||||||
|
message: string
|
||||||
|
) => {
|
||||||
|
try {
|
||||||
|
const { Authentication } = await import(
|
||||||
|
"../../scripts/pocketbase/Authentication"
|
||||||
|
);
|
||||||
|
const { Update } = await import(
|
||||||
|
"../../scripts/pocketbase/Update"
|
||||||
|
);
|
||||||
|
const { Collections } = await import(
|
||||||
|
"../../schemas/pocketbase"
|
||||||
|
);
|
||||||
|
|
||||||
|
const auth = Authentication.getInstance();
|
||||||
|
const update = Update.getInstance();
|
||||||
|
|
||||||
|
if (!auth.isAuthenticated()) return;
|
||||||
|
|
||||||
|
const userId = auth.getUserId();
|
||||||
|
if (!userId) return;
|
||||||
|
|
||||||
|
// Create log entry
|
||||||
|
await update.create(Collections.LOGS, {
|
||||||
|
user: userId,
|
||||||
|
type: type,
|
||||||
|
part: part,
|
||||||
|
message: message,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error logging admin action:", error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Function to show toast notifications
|
||||||
|
const showToast = (
|
||||||
|
message: string,
|
||||||
|
type: "success" | "error" | "info" = "info"
|
||||||
|
) => {
|
||||||
|
// Create toast container if it doesn't exist
|
||||||
|
let toastContainer = document.getElementById("toast-container");
|
||||||
|
if (!toastContainer) {
|
||||||
|
toastContainer = document.createElement("div");
|
||||||
|
toastContainer.id = "toast-container";
|
||||||
|
toastContainer.className = "toast toast-top toast-end z-50";
|
||||||
|
document.body.appendChild(toastContainer);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create toast element
|
||||||
|
const toast = document.createElement("div");
|
||||||
|
toast.className = `alert ${type === "success" ? "alert-success" : type === "error" ? "alert-error" : "alert-info"} shadow-lg`;
|
||||||
|
|
||||||
|
// Set toast content
|
||||||
|
toast.innerHTML = `
|
||||||
|
<div>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" class="stroke-current flex-shrink-0 h-6 w-6" fill="none" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="${type === "success" ? "M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" : type === "error" ? "M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z" : "M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"}" />
|
||||||
|
</svg>
|
||||||
|
<span>${message}</span>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
// Add toast to container
|
||||||
|
toastContainer.appendChild(toast);
|
||||||
|
|
||||||
|
// Remove toast after 3 seconds
|
||||||
|
setTimeout(() => {
|
||||||
|
toast.classList.add("fade-out");
|
||||||
|
setTimeout(() => {
|
||||||
|
toast.remove();
|
||||||
|
}, 300);
|
||||||
|
}, 3000);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Debug function to check stats elements
|
||||||
|
const debugStatsElements = () => {
|
||||||
|
const statElements = [
|
||||||
|
{ selector: ".text-primary.text-3xl", label: "Users" },
|
||||||
|
{ selector: ".text-secondary.text-3xl", label: "Officers" },
|
||||||
|
{ selector: ".text-accent.text-3xl", label: "Events" },
|
||||||
|
{
|
||||||
|
selector: ".text-success.text-3xl",
|
||||||
|
label: "Upcoming Events",
|
||||||
|
},
|
||||||
|
{ selector: ".text-info.text-3xl", label: "Reimbursements" },
|
||||||
|
{
|
||||||
|
selector: ".text-warning.text-3xl",
|
||||||
|
label: "Pending Reimbursements",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
console.log("Checking stat elements:");
|
||||||
|
statElements.forEach((item) => {
|
||||||
|
const element = document.querySelector(item.selector);
|
||||||
|
console.log(
|
||||||
|
`${item.label} (${item.selector}): ${element ? "Found" : "Not found"}`
|
||||||
|
);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// Call debug function after DOM is loaded
|
||||||
|
debugStatsElements();
|
||||||
|
|
||||||
|
// Add event listener to refresh button
|
||||||
|
document
|
||||||
|
.getElementById("refreshDashboardBtn")
|
||||||
|
?.addEventListener("click", async () => {
|
||||||
|
console.log("Manual refresh triggered");
|
||||||
|
const button = document.getElementById(
|
||||||
|
"refreshDashboardBtn"
|
||||||
|
) as HTMLButtonElement;
|
||||||
|
if (button) {
|
||||||
|
button.classList.add("loading");
|
||||||
|
button.disabled = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await refreshData();
|
||||||
|
showToast("Dashboard refreshed successfully", "success");
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error refreshing dashboard:", error);
|
||||||
|
showToast("Failed to refresh dashboard", "error");
|
||||||
|
} finally {
|
||||||
|
if (button) {
|
||||||
|
button.classList.remove("loading");
|
||||||
|
button.disabled = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// Refresh data periodically
|
// Refresh data periodically
|
||||||
const refreshData = async () => {
|
const refreshData = async () => {
|
||||||
|
@ -635,7 +1307,14 @@ const getUserName = (reimbursement: ExpandedReimbursement) => {
|
||||||
const auth = Authentication.getInstance();
|
const auth = Authentication.getInstance();
|
||||||
const get = Get.getInstance();
|
const get = Get.getInstance();
|
||||||
|
|
||||||
if (!auth.isAuthenticated()) return;
|
if (!auth.isAuthenticated()) {
|
||||||
|
console.error(
|
||||||
|
"Cannot refresh data: User is not authenticated"
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("Refreshing dashboard data...");
|
||||||
|
|
||||||
// Update stats
|
// Update stats
|
||||||
const stats = await Promise.all([
|
const stats = await Promise.all([
|
||||||
|
@ -657,25 +1336,72 @@ const getUserName = (reimbursement: ExpandedReimbursement) => {
|
||||||
),
|
),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
console.log(
|
||||||
|
"Stats data received:",
|
||||||
|
stats.map((s) => s.totalItems)
|
||||||
|
);
|
||||||
|
|
||||||
// Update UI with new stats
|
// Update UI with new stats
|
||||||
document.querySelector(".text-primary.text-3xl")!.textContent =
|
const userCountElement = document.querySelector(
|
||||||
stats[0].totalItems.toString();
|
".text-primary.text-3xl"
|
||||||
document.querySelector(
|
);
|
||||||
|
if (userCountElement)
|
||||||
|
userCountElement.textContent =
|
||||||
|
stats[0].totalItems.toString();
|
||||||
|
|
||||||
|
const officerCountElement = document.querySelector(
|
||||||
".text-secondary.text-3xl"
|
".text-secondary.text-3xl"
|
||||||
)!.textContent = stats[1].totalItems.toString();
|
);
|
||||||
document.querySelector(".text-accent.text-3xl")!.textContent =
|
if (officerCountElement)
|
||||||
stats[2].totalItems.toString();
|
officerCountElement.textContent =
|
||||||
document.querySelector(".text-success.text-3xl")!.textContent =
|
stats[1].totalItems.toString();
|
||||||
stats[3].totalItems.toString();
|
|
||||||
document.querySelector(".text-info.text-3xl")!.textContent =
|
const eventCountElement = document.querySelector(
|
||||||
stats[4].totalItems.toString();
|
".text-accent.text-3xl"
|
||||||
document.querySelector(".text-warning.text-3xl")!.textContent =
|
);
|
||||||
stats[5].totalItems.toString();
|
if (eventCountElement)
|
||||||
|
eventCountElement.textContent =
|
||||||
|
stats[2].totalItems.toString();
|
||||||
|
|
||||||
|
const upcomingEventsElement = document.querySelector(
|
||||||
|
".text-success.text-3xl"
|
||||||
|
);
|
||||||
|
if (upcomingEventsElement)
|
||||||
|
upcomingEventsElement.textContent =
|
||||||
|
stats[3].totalItems.toString();
|
||||||
|
|
||||||
|
const reimbursementCountElement = document.querySelector(
|
||||||
|
".text-info.text-3xl"
|
||||||
|
);
|
||||||
|
if (reimbursementCountElement)
|
||||||
|
reimbursementCountElement.textContent =
|
||||||
|
stats[4].totalItems.toString();
|
||||||
|
|
||||||
|
const pendingReimbursementsElement = document.querySelector(
|
||||||
|
".text-warning.text-3xl"
|
||||||
|
);
|
||||||
|
if (pendingReimbursementsElement)
|
||||||
|
pendingReimbursementsElement.textContent =
|
||||||
|
stats[5].totalItems.toString();
|
||||||
|
|
||||||
|
// Check if we need to refresh the tables
|
||||||
|
const shouldRefreshTables = document.querySelector(
|
||||||
|
'[data-refresh-needed="true"]'
|
||||||
|
);
|
||||||
|
if (shouldRefreshTables) {
|
||||||
|
location.reload();
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error refreshing dashboard data:", error);
|
console.error("Error refreshing dashboard data:", error);
|
||||||
|
throw error; // Re-throw to allow proper error handling by callers
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Initial data refresh
|
||||||
|
refreshData().catch((error: unknown) => {
|
||||||
|
console.error("Error during initial data refresh:", error);
|
||||||
|
});
|
||||||
|
|
||||||
// Refresh every 5 minutes
|
// Refresh every 5 minutes
|
||||||
setInterval(refreshData, 5 * 60 * 1000);
|
setInterval(refreshData, 5 * 60 * 1000);
|
||||||
});
|
});
|
||||||
|
@ -688,4 +1414,16 @@ const getUserName = (reimbursement: ExpandedReimbursement) => {
|
||||||
.tab-content.hidden {
|
.tab-content.hidden {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.fade-out {
|
||||||
|
opacity: 0;
|
||||||
|
transition: opacity 0.3s ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
#toast-container {
|
||||||
|
position: fixed;
|
||||||
|
top: 1rem;
|
||||||
|
right: 1rem;
|
||||||
|
z-index: 9999;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
Loading…
Reference in a new issue