ieeeucsd-org/src/components/dashboard/Officer_EventManagement.astro
2025-02-15 03:01:43 -08:00

2336 lines
98 KiB
Text
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

---
import { Icon } from "astro-icon/components";
import { Get } from "../pocketbase/Get";
import { Authentication } from "../pocketbase/Authentication";
import EventEditor from "./Officer_EventManagement/EventEditor";
import FilePreview from "../modals/FilePreview";
// Get instances
const get = Get.getInstance();
const auth = Authentication.getInstance();
interface Event {
id: string;
event_name: string;
event_description: string;
event_code: string;
location: string;
files: string[];
points_to_reward: number;
start_date: string;
end_date: string;
published: boolean;
has_food: boolean;
attendees: AttendeeEntry[];
}
interface AttendeeEntry {
user_id: string;
time_checked_in: string;
food: string;
}
interface ListResponse<T> {
page: number;
perPage: number;
totalItems: number;
totalPages: number;
items: T[];
}
// Initialize variables
let eventResponse: ListResponse<Event> = {
page: 1,
perPage: 5,
totalItems: 0,
totalPages: 0,
items: [],
};
let upcomingEvents: Event[] = [];
// Fetch events
try {
if (auth.isAuthenticated()) {
eventResponse = await get.getList<Event>(
"events",
1,
5,
"",
"-start_date"
);
upcomingEvents = eventResponse.items;
}
} catch (error) {
console.error("Failed to fetch events:", error);
}
const totalEvents = eventResponse.totalItems;
const totalPages = eventResponse.totalPages;
const currentPage = eventResponse.page;
---
<div id="eventManagementSection" class="dashboard-section hidden">
<div class="mb-6 flex justify-between items-center">
<div>
<h2 class="text-2xl font-bold">Event Management</h2>
<p class="opacity-70">Manage and create IEEE UCSD events</p>
</div>
<button class="btn btn-primary gap-2" onclick="window.openEditModal()">
<Icon name="heroicons:plus" class="h-5 w-5" />
Add New Event
</button>
</div>
<!-- Stats Cards -->
<div class="grid grid-cols-1 md:grid-cols-3 gap-6 mb-8">
<div
class="stats shadow-lg bg-base-100 rounded-2xl border border-base-200 hover:border-primary transition-all duration-300 hover:-translate-y-1 transform"
>
<div class="stat">
<div class="stat-title font-medium opacity-80">
Total Events
</div>
<div class="stat-value text-primary" id="totalEvents">-</div>
<div class="stat-desc flex items-center gap-2 mt-1">
<div class="badge badge-primary badge-sm" id="quarterLabel">
Current Academic Term
</div>
</div>
</div>
</div>
<div
class="stats shadow-lg bg-base-100 rounded-2xl border border-base-200 hover:border-secondary transition-all duration-300 hover:-translate-y-1 transform"
>
<div class="stat">
<div class="stat-title font-medium opacity-80">
Unique Attendees
</div>
<div class="stat-value text-secondary" id="uniqueAttendees">
-
</div>
<div class="stat-desc flex items-center gap-2 mt-1">
<div class="badge badge-secondary badge-sm">
Current Academic Term
</div>
</div>
</div>
</div>
<div
class="stats shadow-lg bg-base-100 rounded-2xl border border-base-200 hover:border-accent transition-all duration-300 hover:-translate-y-1 transform"
>
<div class="stat">
<div class="stat-title font-medium opacity-80">
Recurring Attendees
</div>
<div class="stat-value text-accent" id="recurringAttendees">
-
</div>
<div class="stat-desc flex items-center gap-2 mt-1">
<div class="badge badge-accent badge-sm">
Current Quarter (<span id="quarterName">-</span>)
</div>
</div>
</div>
</div>
</div>
<!-- Events List -->
<div
class="card bg-base-100 shadow-lg border border-base-200 hover:border-primary transition-all duration-300"
>
<div class="card-body">
<h3 class="card-title text-xl font-bold flex items-center gap-3">
<div class="badge badge-primary p-3">
<Icon name="heroicons:calendar" class="h-5 w-5" />
</div>
Events List
</h3>
<div class="divider"></div>
<!-- Filter Controls -->
<div class="mb-4">
<!-- All Filters in One Line -->
<div class="flex flex-wrap items-end gap-4">
<div class="form-control">
<label class="label">
<span class="label-text font-medium"
>Time Filter</span
>
</label>
<div class="join m-1">
<input
type="radio"
name="timeFilter"
value="all"
class="join-item btn btn-sm"
checked
aria-label="All Events"
/>
<input
type="radio"
name="timeFilter"
value="ongoing"
class="join-item btn btn-sm"
aria-label="Ongoing"
/>
<input
type="radio"
name="timeFilter"
value="upcoming"
class="join-item btn btn-sm"
aria-label="Upcoming"
/>
<input
type="radio"
name="timeFilter"
value="past"
class="join-item btn btn-sm"
aria-label="Past"
/>
</div>
</div>
<div class="form-control">
<label class="label">
<span class="label-text font-medium">Year</span>
</label>
<div class="dropdown">
<label
tabindex="0"
class="btn btn-sm m-1 w-[180px] justify-between items-center"
>
<div class="flex-1 text-left truncate">
<span id="yearFilterLabel">All Years</span>
</div>
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-4 w-4 flex-shrink-0 ml-2"
viewBox="0 0 20 20"
fill="currentColor"
>
<path
fill-rule="evenodd"
d="M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z"
clip-rule="evenodd"></path>
</svg>
</label>
<div
tabindex="0"
class="dropdown-content z-[1] menu p-2 shadow bg-base-100 rounded-box w-52"
>
<div class="form-control">
<label class="label cursor-pointer">
<span class="label-text">All Years</span
>
<input
type="checkbox"
class="checkbox"
value="all"
checked
/>
</label>
</div>
<div id="yearCheckboxes" class="space-y-1">
<!-- Year checkboxes will be populated dynamically -->
</div>
</div>
</div>
</div>
<div class="form-control">
<label class="label">
<span class="label-text font-medium">Quarter</span>
</label>
<div class="dropdown">
<label
tabindex="0"
class="btn btn-sm m-1 w-[180px] justify-between items-center"
>
<div class="flex-1 text-left truncate">
<span id="quarterFilterLabel"
>All Quarters</span
>
</div>
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-4 w-4 flex-shrink-0 ml-2"
viewBox="0 0 20 20"
fill="currentColor"
>
<path
fill-rule="evenodd"
d="M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z"
clip-rule="evenodd"></path>
</svg>
</label>
<div
tabindex="0"
id="quarterDropdownContent"
class="dropdown-content z-[1] menu p-2 shadow bg-base-100 rounded-box w-52"
>
<div class="form-control">
<label class="label cursor-pointer">
<span class="label-text"
>All Quarters</span
>
<input
type="checkbox"
class="checkbox"
value="all"
checked
/>
</label>
</div>
<div class="form-control">
<label class="label cursor-pointer">
<span class="label-text">Fall</span>
<input
type="checkbox"
class="checkbox"
value="fall"
/>
</label>
</div>
<div class="form-control">
<label class="label cursor-pointer">
<span class="label-text">Winter</span>
<input
type="checkbox"
class="checkbox"
value="winter"
/>
</label>
</div>
<div class="form-control">
<label class="label cursor-pointer">
<span class="label-text">Spring</span>
<input
type="checkbox"
class="checkbox"
value="spring"
/>
</label>
</div>
<div class="form-control">
<label class="label cursor-pointer">
<span class="label-text">Summer</span>
<input
type="checkbox"
class="checkbox"
value="summer"
/>
</label>
</div>
</div>
</div>
</div>
<div class="form-control">
<label class="label">
<span class="label-text font-medium">Published</span
>
</label>
<div class="dropdown">
<label
tabindex="0"
class="btn btn-sm m-1 w-[140px] justify-between"
>
<span id="publishedFilterLabel">All</span>
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-4 w-4"
viewBox="0 0 20 20"
fill="currentColor"
>
<path
fill-rule="evenodd"
d="M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z"
clip-rule="evenodd"></path>
</svg>
</label>
<div
tabindex="0"
class="dropdown-content z-[1] menu p-2 shadow bg-base-100 rounded-box w-52"
>
<div class="form-control">
<label class="label cursor-pointer">
<span class="label-text">All</span>
<input
type="radio"
name="publishedFilter"
class="radio"
value="all"
checked
/>
</label>
</div>
<div class="form-control">
<label class="label cursor-pointer">
<span class="label-text">Yes</span>
<input
type="radio"
name="publishedFilter"
class="radio"
value="yes"
/>
</label>
</div>
<div class="form-control">
<label class="label cursor-pointer">
<span class="label-text">No</span>
<input
type="radio"
name="publishedFilter"
class="radio"
value="no"
/>
</label>
</div>
</div>
</div>
</div>
<div class="form-control">
<label class="label">
<span class="label-text font-medium">Has Files</span
>
</label>
<div class="dropdown">
<label
tabindex="0"
class="btn btn-sm m-1 w-[140px] justify-between"
>
<span id="hasFilesFilterLabel">All</span>
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-4 w-4"
viewBox="0 0 20 20"
fill="currentColor"
>
<path
fill-rule="evenodd"
d="M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z"
clip-rule="evenodd"></path>
</svg>
</label>
<div
tabindex="0"
class="dropdown-content z-[1] menu p-2 shadow bg-base-100 rounded-box w-52"
>
<div class="form-control">
<label class="label cursor-pointer">
<span class="label-text">All</span>
<input
type="radio"
name="hasFilesFilter"
class="radio"
value="all"
checked
/>
</label>
</div>
<div class="form-control">
<label class="label cursor-pointer">
<span class="label-text">Yes</span>
<input
type="radio"
name="hasFilesFilter"
class="radio"
value="yes"
/>
</label>
</div>
<div class="form-control">
<label class="label cursor-pointer">
<span class="label-text">No</span>
<input
type="radio"
name="hasFilesFilter"
class="radio"
value="no"
/>
</label>
</div>
</div>
</div>
</div>
<div class="form-control">
<label class="label">
<span class="label-text font-medium">Has Food</span>
</label>
<div class="dropdown">
<label
tabindex="0"
class="btn btn-sm m-1 w-[140px] justify-between"
>
<span id="hasFoodFilterLabel">All</span>
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-4 w-4"
viewBox="0 0 20 20"
fill="currentColor"
>
<path
fill-rule="evenodd"
d="M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z"
clip-rule="evenodd"></path>
</svg>
</label>
<div
tabindex="0"
class="dropdown-content z-[1] menu p-2 shadow bg-base-100 rounded-box w-52"
>
<div class="form-control">
<label class="label cursor-pointer">
<span class="label-text">All</span>
<input
type="radio"
name="hasFoodFilter"
class="radio"
value="all"
checked
/>
</label>
</div>
<div class="form-control">
<label class="label cursor-pointer">
<span class="label-text">Yes</span>
<input
type="radio"
name="hasFoodFilter"
class="radio"
value="yes"
/>
</label>
</div>
<div class="form-control">
<label class="label cursor-pointer">
<span class="label-text">No</span>
<input
type="radio"
name="hasFoodFilter"
class="radio"
value="no"
/>
</label>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Search and Per Page Controls -->
<div class="flex flex-col md:flex-row gap-4 mb-4">
<div class="form-control flex-1">
<div class="join w-full">
<div
class="join-item bg-base-200 flex items-center px-3"
>
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-5 w-5 opacity-70"
viewBox="0 0 20 20"
fill="currentColor"
>
<path
fill-rule="evenodd"
d="M8 4a4 4 0 100 8 4 4 0 000-8zM2 8a6 6 0 1110.89 3.476l4.817 4.817a1 1 0 01-1.414 1.414l-4.816-4.816A6 6 0 012 8z"
clip-rule="evenodd"></path>
</svg>
</div>
<input
type="text"
id="searchInput"
placeholder="Search events..."
class="input input-bordered join-item w-full"
/>
</div>
</div>
<div class="form-control w-full md:w-auto">
<div class="join">
<div
class="join-item bg-base-200 flex items-center px-3"
>
Per Page
</div>
<select
id="perPageSelect"
class="select select-bordered join-item"
>
<option value="5">5</option>
<option value="10">10</option>
<option value="25">25</option>
<option value="50">50</option>
</select>
</div>
</div>
</div>
<!-- Event Items -->
<div class="space-y-4" id="eventsList">
<div class="text-center py-8 text-base-content/70">
<Icon
name="heroicons:calendar"
class="h-12 w-12 mx-auto mb-4 opacity-50"
/>
<p>Loading events...</p>
</div>
</div>
<!-- Pagination -->
<div class="flex justify-center mt-6" id="paginationContainer">
<div class="join">
<button class="join-item btn btn-sm" id="firstPageBtn"
>«</button
>
<button class="join-item btn btn-sm" id="prevPageBtn"
></button
>
<button class="join-item btn btn-sm"
>Page <span id="currentPageNumber">1</span></button
>
<button class="join-item btn btn-sm" id="nextPageBtn"
></button
>
<button class="join-item btn btn-sm" id="lastPageBtn"
>»</button
>
</div>
</div>
</div>
</div>
</div>
<EventEditor
client:load
onEventSaved={() => {
// Reset cache timestamp to force refresh
window.lastCacheUpdate = 0;
// Refresh events list
window.fetchEvents();
}}
/>
<!-- Event Details Modal -->
<dialog id="eventDetailsModal" class="modal">
<div class="modal-box max-w-4xl">
<div class="flex justify-between items-center mb-4">
<h3 class="font-bold text-lg">Event Details</h3>
<button
class="btn btn-circle btn-ghost"
onclick="eventDetailsModal.close()"
>
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-6 w-6"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M6 18L18 6M6 6l12 12"></path>
</svg>
</button>
</div>
<div class="tabs tabs-boxed mb-4">
<button class="tab tab-active" data-tab="files">Files</button>
<button class="tab" data-tab="attendees">Attendees</button>
</div>
<div id="filesContent" class="space-y-4">
<!-- Files list will be populated here -->
</div>
<div id="attendeesContent" class="space-y-4 hidden">
<div class="flex justify-between items-center mb-4">
<h3 class="text-lg font-semibold">Event Attendees</h3>
<button
id="downloadAttendeesCSV"
class="btn btn-primary btn-sm gap-2"
>
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-4 w-4"
viewBox="0 0 20 20"
fill="currentColor"
>
<path
fill-rule="evenodd"
d="M3 17a1 1 0 011-1h12a1 1 0 110 2H4a1 1 0 01-1-1zm3.293-7.707a1 1 0 011.414 0L9 10.586V3a1 1 0 112 0v7.586l1.293-1.293a1 1 0 111.414 1.414l-3 3a1 1 0 01-1.414 0l-3-3a1 1 0 010-1.414z"
clip-rule="evenodd"></path>
</svg>
Download CSV
</button>
</div>
<!-- Attendees list will be populated here -->
</div>
</div>
<form method="dialog" class="modal-backdrop">
<button onclick="window.closeEventDetailsModal()">close</button>
</form>
</dialog>
<!-- Universal File Preview Modal -->
<dialog id="filePreviewModal" class="modal">
<div class="modal-box max-w-4xl">
<div class="flex justify-between items-center mb-4">
<div class="flex items-center gap-3">
<button
class="btn btn-ghost btn-sm"
onclick="window.closeFilePreview()"
>
← Back
</button>
<h3 class="font-bold text-lg truncate" id="previewFileName">
</h3>
</div>
</div>
<div class="relative" id="previewContainer">
<div
id="previewLoadingSpinner"
class="absolute inset-0 flex items-center justify-center bg-base-200 bg-opacity-50 hidden"
>
<span class="loading loading-spinner loading-lg"></span>
</div>
<div id="previewContent" class="w-full">
<FilePreview client:load url="" filename="" id="universalFilePreview" />
</div>
</div>
</div>
<form method="dialog" class="modal-backdrop">
<button onclick="window.closeFilePreview()">close</button>
</form>
</dialog>
<script>
import { Get } from "../pocketbase/Get";
import { Authentication } from "../pocketbase/Authentication";
import { Update } from "../pocketbase/Update";
import { FileManager } from "../pocketbase/FileManager";
import { SendLog } from "../pocketbase/SendLog";
import FilePreview from "../modals/FilePreview";
// Add file storage
const selectedFileStorage = new Map<string, File>();
const filesToDelete = new Set<string>(); // Add storage for files to delete
interface AttendeeEntry {
user_id: string;
time_checked_in: string;
food: string;
}
interface Event {
id: string;
event_name: string;
event_description: string;
event_code: string;
location: string;
files: string[];
points_to_reward: number;
start_date: string;
end_date: string;
published: boolean;
has_food: boolean;
attendees: AttendeeEntry[];
}
const get = Get.getInstance();
const auth = Authentication.getInstance();
const update = Update.getInstance();
const fileManager = FileManager.getInstance();
const sendLog = SendLog.getInstance();
let currentPage = 1;
let totalPages = 0;
let searchQuery = "";
let perPage = 5;
let quarterlyStats = {
totalEvents: 0,
uniqueAttendees: 0,
recurringAttendees: 0,
};
// Add filter state
let filterState = {
time: "all",
year: ["all"],
quarter: ["all"],
published: "all",
hasFiles: "all",
hasFood: "all",
};
// Add cache for events
let cachedEvents: Event[] = [];
let lastCacheUpdate = 0; // Will force a refresh on page load
const CACHE_DURATION = 5 * 60 * 1000; // 5 minutes in milliseconds
// Function to refresh cache
async function refreshCache() {
try {
const now = Date.now();
if (
lastCacheUpdate === 0 ||
now - lastCacheUpdate >= CACHE_DURATION ||
cachedEvents.length === 0
) {
auth.setUpdating(true);
const response = await get.getAll<Event>("events");
cachedEvents = response.map((event) =>
Get.convertUTCToLocal(event)
);
lastCacheUpdate = now;
// Initialize year filter options from cache
const years = new Set<number>();
cachedEvents.forEach((event) => {
const year = new Date(event.start_date).getFullYear();
years.add(year);
});
const yearCheckboxes =
document.getElementById("yearCheckboxes");
if (yearCheckboxes) {
const sortedYears = Array.from(years).sort((a, b) => b - a);
yearCheckboxes.innerHTML = sortedYears
.map(
(year) => `
<label class="label cursor-pointer">
<span class="label-text">${year}</span>
<input type="checkbox" class="checkbox" value="${year}" />
</label>
`
)
.join("");
// Add event listeners to checkboxes
const allYearsCheckbox = document.querySelector(
'input[type="checkbox"][value="all"]'
) as HTMLInputElement;
const yearInputs = Array.from(
yearCheckboxes.querySelectorAll(
'input[type="checkbox"]'
)
) as HTMLInputElement[];
if (allYearsCheckbox) {
allYearsCheckbox.addEventListener("change", (e) => {
const target = e.target as HTMLInputElement;
if (target.checked) {
yearInputs.forEach((input) => {
input.checked = false;
});
filterState.year = ["all"];
document.getElementById(
"yearFilterLabel"
)!.textContent = "All Years";
}
currentPage = 1;
fetchEvents();
});
}
yearInputs.forEach((input) => {
input.addEventListener("change", () => {
const checkedYears = yearInputs
.filter((inp) => inp.checked)
.map((inp) => inp.value);
if (checkedYears.length === 0) {
allYearsCheckbox.checked = true;
filterState.year = ["all"];
document.getElementById(
"yearFilterLabel"
)!.textContent = "All Years";
} else {
allYearsCheckbox.checked = false;
filterState.year = checkedYears;
document.getElementById(
"yearFilterLabel"
)!.textContent =
checkedYears.length === 1
? checkedYears[0]
: `${checkedYears.length} Years Selected`;
}
currentPage = 1;
fetchEvents();
});
});
}
}
} catch (error) {
console.error("Failed to refresh cache:", error);
throw error;
} finally {
auth.setUpdating(false);
}
}
// Helper function to determine current academic term
function getCurrentTerm(): { start: Date; end: Date } {
const now = new Date();
const year = now.getFullYear();
let start: Date, end: Date;
// If we're before September, term started last year
if (now.getMonth() < 8) {
// Before September
start = new Date(year - 1, 8, 1); // Sept 1 of last year
end = new Date(year, 8, 0); // Aug 31 of current year
} else {
start = new Date(year, 8, 1); // Sept 1 of current year
end = new Date(year + 1, 8, 0); // Aug 31 of next year
}
return { start, end };
}
// Helper function to determine current quarter
function getCurrentQuarter(): { start: Date; end: Date } {
const now = new Date();
const year = now.getFullYear();
const month = now.getMonth();
let start: Date, end: Date;
// Determine quarter (0-based months: 0-11)
// Q1: Sept-Dec (8-11)
// Q2: Jan-Mar (0-2)
// Q3: Mar-Jun (2-5)
// Q4: Jun-Sept (5-8)
if (month >= 8) {
// Q1: Sept-Dec
start = new Date(year, 8, 1);
end = new Date(year, 11, 31);
} else if (month < 2) {
// Q2: Jan-Mar
start = new Date(year, 0, 1);
end = new Date(year, 2, 31);
} else if (month < 5) {
// Q3: Mar-Jun
start = new Date(year, 2, 1);
end = new Date(year, 5, 30);
} else {
// Q4: Jun-Sept
start = new Date(year, 5, 1);
end = new Date(year, 8, 0); // End on Aug 31
}
return { start, end };
}
// Helper function to get quarter name
function getQuarterName(): string {
const month = new Date().getMonth();
if (month >= 8) {
// Sept-Dec
return "Fall";
} else if (month < 2) {
// Jan-Mar
return "Winter";
} else if (month < 5) {
// Mar-Jun
return "Spring";
} else {
// Jun-Sept
return "Summer";
}
}
// Function to apply filters to cached data
function filterEvents(events: Event[]): Event[] {
return events.filter((event) => {
const now = new Date().toISOString();
const eventStart = new Date(event.start_date).toISOString();
const eventEnd = new Date(event.end_date).toISOString();
// Time filter
if (filterState.time === "upcoming" && eventStart <= now)
return false;
if (filterState.time === "past" && eventEnd >= now) return false;
if (
filterState.time === "ongoing" &&
(eventStart > now || eventEnd < now)
)
return false;
// Year filter
if (!filterState.year.includes("all")) {
const eventStartDate = new Date(event.start_date);
const eventEndDate = new Date(event.end_date);
const eventStartYear = eventStartDate.getFullYear().toString();
const eventEndYear = eventEndDate.getFullYear().toString();
// Check if either the start year or end year matches any selected year
const yearMatches = filterState.year.some(
(year) => year === eventStartYear || year === eventEndYear
);
if (!yearMatches) return false;
}
// Quarter filter
if (!filterState.quarter.includes("all")) {
const eventDate = new Date(event.start_date);
const month = eventDate.getMonth();
let isInSelectedQuarter = false;
for (const quarter of filterState.quarter) {
let isInQuarter = false;
switch (quarter) {
case "fall":
isInQuarter = month >= 8 && month <= 11; // Sept-Dec
break;
case "winter":
isInQuarter = month >= 0 && month <= 2; // Jan-Mar
break;
case "spring":
isInQuarter = month >= 2 && month <= 5; // Mar-Jun
break;
case "summer":
isInQuarter = month >= 5 && month <= 8; // Jun-Sept
break;
}
if (isInQuarter) {
isInSelectedQuarter = true;
break;
}
}
if (!isInSelectedQuarter) return false;
}
// Published filter
if (filterState.published !== "all") {
if ((filterState.published === "yes") !== event.published)
return false;
}
// Has Files filter
if (filterState.hasFiles !== "all") {
const hasFiles = event.files && event.files.length > 0;
if ((filterState.hasFiles === "yes") !== hasFiles) return false;
}
// Has Food filter
if (filterState.hasFood !== "all") {
if ((filterState.hasFood === "yes") !== event.has_food)
return false;
}
// Search query
if (searchQuery) {
const searchLower = searchQuery.toLowerCase();
const searchTerms = searchLower.split(/\s+/).filter(Boolean);
return (
searchTerms.length === 0 || // If no search terms, return true
searchTerms.every(
(term) =>
event.event_name.toLowerCase().includes(term) ||
event.event_code.toLowerCase().includes(term) ||
event.location.toLowerCase().includes(term) ||
event.event_description.toLowerCase().includes(term)
)
);
}
return true;
});
}
// Fetch and display events using cached data
async function fetchEvents() {
const eventsList = document.getElementById("eventsList");
const paginationContainer = document.getElementById(
"paginationContainer"
);
if (!eventsList || !paginationContainer) return;
try {
if (!auth.isAuthenticated()) {
eventsList.innerHTML = `
<div class="text-center py-8 text-base-content/70">
<svg xmlns="http://www.w3.org/2000/svg" class="h-12 w-12 mx-auto mb-4 opacity-50" viewBox="0 0 20 20" fill="currentColor">
<path d="M10 2a6 6 0 00-6 6v3.586l-.707.707A1 1 0 004 14h12a1 1 0 00.707-1.707L16 11.586V8a6 6 0 00-6-6zM10 18a3 3 0 01-3-3h6a3 3 0 01-3 3z" />
</svg>
<p>Please log in to view events</p>
</div>
`;
paginationContainer.classList.add("hidden");
return;
}
// Refresh cache if needed
await refreshCache();
// Apply filters to cached data
const filteredEvents = filterEvents(cachedEvents);
// Sort events by start date (newest first)
filteredEvents.sort(
(a, b) =>
new Date(b.start_date).getTime() -
new Date(a.start_date).getTime()
);
// Calculate pagination
const totalItems = filteredEvents.length;
totalPages = Math.ceil(totalItems / perPage);
const startIndex = (currentPage - 1) * perPage;
const endIndex = startIndex + perPage;
const paginatedEvents = filteredEvents.slice(startIndex, endIndex);
// Update pagination UI
const firstPageBtn = document.getElementById(
"firstPageBtn"
) as HTMLButtonElement;
const prevPageBtn = document.getElementById(
"prevPageBtn"
) as HTMLButtonElement;
const nextPageBtn = document.getElementById(
"nextPageBtn"
) as HTMLButtonElement;
const lastPageBtn = document.getElementById(
"lastPageBtn"
) as HTMLButtonElement;
const currentPageNumber =
document.getElementById("currentPageNumber");
if (firstPageBtn) firstPageBtn.disabled = currentPage <= 1;
if (prevPageBtn) prevPageBtn.disabled = currentPage <= 1;
if (nextPageBtn) nextPageBtn.disabled = currentPage >= totalPages;
if (lastPageBtn) lastPageBtn.disabled = currentPage >= totalPages;
if (currentPageNumber)
currentPageNumber.textContent = currentPage.toString();
// Show/hide pagination based on total pages
if (totalPages <= 1) {
paginationContainer.classList.add("hidden");
} else {
paginationContainer.classList.remove("hidden");
}
// Update events list
if (paginatedEvents.length === 0) {
eventsList.innerHTML = `
<div class="text-center py-8 text-base-content/70">
<svg xmlns="http://www.w3.org/2000/svg" class="h-12 w-12 mx-auto mb-4 opacity-50" viewBox="0 0 20 20" fill="currentColor">
<path d="M10 2a6 6 0 00-6 6v3.586l-.707.707A1 1 0 004 14h12a1 1 0 00.707-1.707L16 11.586V8a6 6 0 00-6-6zM10 18a3 3 0 01-3-3h6a3 3 0 01-3 3z" />
</svg>
<p>No events found</p>
</div>
`;
return;
}
eventsList.innerHTML = paginatedEvents
.map((event) => {
// Safely parse and format the date
let dateStr = "Invalid date";
try {
const date = new Date(event.start_date);
if (!isNaN(date.getTime())) {
dateStr = date.toLocaleDateString("en-US", {
month: "long",
day: "numeric",
year: "numeric",
hour: "2-digit",
minute: "2-digit",
});
}
} catch (e) {
console.error("Error formatting date:", e);
}
const locationStr = event.location
? `${event.location}`
: "";
const codeStr = event.event_code
? `${event.event_code}`
: "";
const detailsStr = [locationStr, codeStr]
.filter(Boolean)
.join(" | code: ");
// Store event data in a global map using the event ID as key
const eventDataId = `event_${event.id}`;
window[eventDataId] = event;
return `
<div class="flex items-center justify-between p-4 bg-base-200 rounded-xl hover:bg-base-300 transition-all duration-300">
<div class="flex items-center gap-4">
<div class="badge badge-lg p-3 badge-primary">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
<path d="M10 2a6 6 0 00-6 6v3.586l-.707.707A1 1 0 004 14h12a1 1 0 00.707-1.707L16 11.586V8a6 6 0 00-6-6zM10 18a3 3 0 01-3-3h6a3 3 0 01-3 3z" />
</svg>
</div>
<div>
<h4 class="font-semibold">${event.event_name}</h4>
<p class="text-sm opacity-70">${dateStr}${detailsStr ? ` • ${detailsStr}` : ""}</p>
</div>
</div>
<div class="flex gap-2">
<button class="btn btn-ghost btn-sm" onclick="window.openDetailsModal(window['${eventDataId}'])">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
<path d="M10 12a2 2 0 100-4 2 2 0 000 4z" />
<path fill-rule="evenodd" d="M.458 10C1.732 5.943 5.522 3 10 3s8.268 2.943 9.542 7c-1.274 4.057-5.064 7-9.542 7S1.732 14.057.458 10zM14 10a4 4 0 11-8 0 4 4 0 018 0z" clip-rule="evenodd" />
</svg>
</button>
<button class="btn btn-ghost btn-sm" onclick="window.openEditModal(window['${eventDataId}'])">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
<path d="M13.586 3.586a2 2 0 112.828 2.828l-.793.793-2.828-2.828.793-.793zM11.379 5.793L3 14.172V17h2.828l8.38-8.379-2.83-2.828z" />
</svg>
</button>
<button class="btn btn-ghost btn-sm text-error" onclick="window.deleteEvent('${event.id}', '${event.event_name.replace(/'/g, "\\'")}')">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
<path fill-rule="evenodd" d="M9 2a1 1 0 00-.894.553L7.382 4H4a1 1 0 000 2v10a2 2 0 002 2h8a2 2 0 002-2V6a1 1 0 100-2h-3.382l-.724-1.447A1 1 0 0011 2H9zM7 8a1 1 0 012 0v6a1 1 0 11-2 0V8zm5-1a1 1 0 00-1 1v6a1 1 0 102 0V8a1 1 0 00-1-1z" clip-rule="evenodd" />
</svg>
</button>
</div>
</div>
`;
})
.join("");
// Calculate and update stats using cached data
await calculateQuarterlyStats();
} catch (error) {
console.error("Failed to fetch events:", error);
eventsList.innerHTML = `
<div class="alert alert-error">
<svg xmlns="http://www.w3.org/2000/svg" class="stroke-current 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>
<span>Failed to load events. Please try refreshing the page.</span>
</div>
`;
paginationContainer.classList.add("hidden");
}
}
// Calculate quarterly stats using cached data
async function calculateQuarterlyStats() {
try {
const { start: termStart, end: termEnd } = getCurrentTerm();
const { start: quarterStart, end: quarterEnd } =
getCurrentQuarter();
// Update quarter name in UI
const quarterNameEl = document.getElementById("quarterName");
if (quarterNameEl) {
quarterNameEl.textContent = getQuarterName();
}
// Filter events for current term and quarter
const termEvents = cachedEvents.filter((event) => {
const eventDate = new Date(event.start_date);
return eventDate >= termStart && eventDate <= termEnd;
});
const quarterEvents = cachedEvents.filter((event) => {
const eventDate = new Date(event.start_date);
return eventDate >= quarterStart && eventDate <= quarterEnd;
});
// Calculate stats
quarterlyStats.totalEvents = termEvents.length;
const termAttendees = new Map<string, number>();
termEvents.forEach((event) => {
event.attendees?.forEach((attendee) => {
const count = termAttendees.get(attendee.user_id) || 0;
termAttendees.set(attendee.user_id, count + 1);
});
});
quarterlyStats.uniqueAttendees = termAttendees.size;
const quarterAttendees = new Map<string, number>();
quarterEvents.forEach((event) => {
event.attendees?.forEach((attendee) => {
const count = quarterAttendees.get(attendee.user_id) || 0;
quarterAttendees.set(attendee.user_id, count + 1);
});
});
quarterlyStats.recurringAttendees = Array.from(
quarterAttendees.values()
).filter((count) => count > 1).length;
// Update the UI
const totalEventsEl = document.getElementById("totalEvents");
const uniqueAttendeesEl =
document.getElementById("uniqueAttendees");
const recurringAttendeesEl =
document.getElementById("recurringAttendees");
if (totalEventsEl)
totalEventsEl.textContent =
quarterlyStats.totalEvents.toString();
if (uniqueAttendeesEl)
uniqueAttendeesEl.textContent =
quarterlyStats.uniqueAttendees.toString();
if (recurringAttendeesEl)
recurringAttendeesEl.textContent =
quarterlyStats.recurringAttendees.toString();
} catch (error) {
console.error("Failed to calculate quarterly stats:", error);
}
}
// Add pagination event listeners
document.getElementById("firstPageBtn")?.addEventListener("click", () => {
if (currentPage > 1) {
currentPage = 1;
fetchEvents();
}
});
document.getElementById("prevPageBtn")?.addEventListener("click", () => {
if (currentPage > 1) {
currentPage--;
fetchEvents();
}
});
document.getElementById("nextPageBtn")?.addEventListener("click", () => {
if (currentPage < totalPages) {
currentPage++;
fetchEvents();
}
});
document.getElementById("lastPageBtn")?.addEventListener("click", () => {
if (currentPage < totalPages) {
currentPage = totalPages;
fetchEvents();
}
});
// Add filter event listeners
// Time filter
document.querySelectorAll('input[name="timeFilter"]').forEach((radio) => {
radio.addEventListener("change", (e) => {
const target = e.target as HTMLInputElement;
filterState.time = target.value;
currentPage = 1;
fetchEvents();
});
});
// Year filter
document
.getElementById("yearCheckboxes")
?.addEventListener("change", (e) => {
const target = e.target as HTMLInputElement;
if (!target.matches('input[type="checkbox"]')) return;
const allYearsCheckbox = target
.closest(".dropdown-content")
?.querySelector(
'input[type="checkbox"][value="all"]'
) as HTMLInputElement;
const yearInputs = Array.from(
target
.closest(".dropdown-content")
?.querySelectorAll(
'#yearCheckboxes input[type="checkbox"]'
) || []
) as HTMLInputElement[];
if (target.value === "all" && target.checked) {
yearInputs.forEach((input) => {
input.checked = false;
});
filterState.year = ["all"];
document.getElementById("yearFilterLabel")!.textContent =
"All Years";
} else {
const checkedYears = yearInputs
.filter((inp) => inp.checked)
.map((inp) => inp.value);
if (checkedYears.length === 0) {
allYearsCheckbox.checked = true;
filterState.year = ["all"];
document.getElementById("yearFilterLabel")!.textContent =
"All Years";
} else {
allYearsCheckbox.checked = false;
filterState.year = checkedYears;
document.getElementById("yearFilterLabel")!.textContent =
checkedYears.length === 1
? checkedYears[0]
: `${checkedYears.length} Years Selected`;
}
}
currentPage = 1;
fetchEvents();
});
// Add event listener for "All Years" checkbox
document
.querySelector('input[type="checkbox"][value="all"]')
?.addEventListener("change", (e) => {
const target = e.target as HTMLInputElement;
const yearInputs = Array.from(
document.querySelectorAll(
'#yearCheckboxes input[type="checkbox"]'
)
) as HTMLInputElement[];
if (target.checked) {
yearInputs.forEach((input) => {
input.checked = false;
});
filterState.year = ["all"];
document.getElementById("yearFilterLabel")!.textContent =
"All Years";
currentPage = 1;
fetchEvents();
}
});
// Quarter filter
document
.getElementById("quarterDropdownContent")
?.addEventListener("change", (e) => {
const target = e.target as HTMLInputElement;
if (!target.matches('input[type="checkbox"]')) return;
const allQuartersCheckbox = target
.closest("#quarterDropdownContent")
?.querySelector(
'input[type="checkbox"][value="all"]'
) as HTMLInputElement;
const quarterInputs = Array.from(
target
.closest("#quarterDropdownContent")
?.querySelectorAll('input[type="checkbox"]') || []
).filter(
(inp) => (inp as HTMLInputElement).value !== "all"
) as HTMLInputElement[];
if (target.value === "all" && target.checked) {
quarterInputs.forEach((input) => {
input.checked = false;
});
filterState.quarter = ["all"];
document.getElementById("quarterFilterLabel")!.textContent =
"All Quarters";
} else {
const checkedQuarters = quarterInputs
.filter((inp) => inp.checked)
.map((inp) => inp.value);
if (checkedQuarters.length === 0) {
allQuartersCheckbox.checked = true;
filterState.quarter = ["all"];
document.getElementById("quarterFilterLabel")!.textContent =
"All Quarters";
} else {
allQuartersCheckbox.checked = false;
filterState.quarter = checkedQuarters;
document.getElementById("quarterFilterLabel")!.textContent =
checkedQuarters.length === 1
? checkedQuarters[0].charAt(0).toUpperCase() +
checkedQuarters[0].slice(1)
: `${checkedQuarters.length} Quarters Selected`;
}
}
currentPage = 1;
fetchEvents();
});
// Published filter
document
.querySelectorAll('input[name="publishedFilter"]')
.forEach((radio: Element) => {
(radio as HTMLInputElement).addEventListener("change", (e) => {
const target = e.target as HTMLInputElement;
filterState.published = target.value;
document.getElementById("publishedFilterLabel")!.textContent =
target.parentElement?.querySelector(".label-text")
?.textContent || "All";
currentPage = 1;
fetchEvents();
});
});
// Has Files filter
document
.querySelectorAll('input[name="hasFilesFilter"]')
.forEach((radio: Element) => {
(radio as HTMLInputElement).addEventListener("change", (e) => {
const target = e.target as HTMLInputElement;
filterState.hasFiles = target.value;
document.getElementById("hasFilesFilterLabel")!.textContent =
target.parentElement?.querySelector(".label-text")
?.textContent || "All";
currentPage = 1;
fetchEvents();
});
});
// Has Food filter
document
.querySelectorAll('input[name="hasFoodFilter"]')
.forEach((radio: Element) => {
(radio as HTMLInputElement).addEventListener("change", (e) => {
const target = e.target as HTMLInputElement;
filterState.hasFood = target.value;
document.getElementById("hasFoodFilterLabel")!.textContent =
target.parentElement?.querySelector(".label-text")
?.textContent || "All";
currentPage = 1;
fetchEvents();
});
});
// Search input with instant search (no debounce needed for cached data)
const searchInput = document.getElementById("searchInput");
if (searchInput) {
// Add input event for instant search
searchInput.addEventListener("input", (e) => {
const target = e.target as HTMLInputElement;
searchQuery = target.value.trim();
currentPage = 1;
fetchEvents();
});
// Clear search on Escape key
searchInput.addEventListener("keydown", (e) => {
if (e.key === "Escape") {
(e.target as HTMLInputElement).value = "";
searchQuery = "";
currentPage = 1;
fetchEvents();
}
});
}
// Per page select
document
.getElementById("perPageSelect")
?.addEventListener("change", (e) => {
const target = e.target as HTMLSelectElement;
perPage = parseInt(target.value);
currentPage = 1;
fetchEvents();
});
// Initial setup
if (typeof window !== "undefined") {
window.addEventListener("DOMContentLoaded", () => {
// Reset all filters to defaults
const timeFilterAll = document.querySelector(
'input[name="timeFilter"][value="all"]'
) as HTMLInputElement;
if (timeFilterAll) {
// Ensure the radio button is properly checked
timeFilterAll.checked = true;
filterState.time = "all";
}
// Reset year filter
const yearAllCheckbox = document.querySelector(
'input[type="checkbox"][value="all"]'
) as HTMLInputElement;
const yearCheckboxes = document.querySelectorAll(
'#yearCheckboxes input[type="checkbox"]'
);
if (yearAllCheckbox && yearCheckboxes) {
yearAllCheckbox.checked = true;
yearCheckboxes.forEach((checkbox: Element) => {
(checkbox as HTMLInputElement).checked = false;
});
filterState.year = ["all"];
document.getElementById("yearFilterLabel")!.textContent =
"All Years";
}
// Reset quarter filter
const quarterAllCheckbox = document.querySelector(
'#quarterDropdownContent input[type="checkbox"][value="all"]'
) as HTMLInputElement;
const quarterCheckboxes = document.querySelectorAll(
'#quarterDropdownContent input[type="checkbox"]:not([value="all"])'
);
if (quarterAllCheckbox && quarterCheckboxes) {
quarterAllCheckbox.checked = true;
quarterCheckboxes.forEach((checkbox: Element) => {
(checkbox as HTMLInputElement).checked = false;
});
filterState.quarter = ["all"];
document.getElementById("quarterFilterLabel")!.textContent =
"All Quarters";
}
const publishedFilter = document.getElementById(
"publishedFilter"
) as HTMLSelectElement;
if (publishedFilter) publishedFilter.value = "all";
const hasFilesFilter = document.getElementById(
"hasFilesFilter"
) as HTMLSelectElement;
if (hasFilesFilter) hasFilesFilter.value = "all";
const hasFoodFilter = document.getElementById(
"hasFoodFilter"
) as HTMLSelectElement;
if (hasFoodFilter) hasFoodFilter.value = "all";
const searchInput = document.getElementById(
"searchInput"
) as HTMLInputElement;
if (searchInput) searchInput.value = "";
const perPageSelect = document.getElementById(
"perPageSelect"
) as HTMLSelectElement;
if (perPageSelect) perPageSelect.value = "5";
// Reset variables
searchQuery = "";
perPage = 5;
currentPage = 1;
// Reset filter state
filterState = {
time: "all",
year: ["all"],
quarter: ["all"],
published: "all",
hasFiles: "all",
hasFood: "all",
};
// Fetch events with reset filters
fetchEvents();
});
}
// Initial fetch
fetchEvents();
// Add openEditModal function
window.openEditModal = async function (event?: any) {
const modal = document.getElementById(
"editEventModal"
) as HTMLDialogElement;
const form = document.getElementById(
"editEventForm"
) as HTMLFormElement;
const modalTitle = document.getElementById("editModalTitle");
const currentFiles = document.getElementById("currentFiles");
const newFiles = document.getElementById("newFiles");
if (!modal || !form) return;
try {
// Clear previous form data
form.reset();
if (currentFiles) currentFiles.innerHTML = "";
if (newFiles) newFiles.innerHTML = "";
// Set modal title based on whether we're editing or creating
if (modalTitle) {
modalTitle.textContent = event ? "Edit Event" : "Add New Event";
}
if (event) {
// Fetch fresh data from PocketBase for the event
const freshEventData = await get.getOne<Event>(
"events",
event.id
);
// Populate form with fresh data
const idInput = document.getElementById(
"editEventId"
) as HTMLInputElement;
const nameInput = document.getElementById(
"editEventName"
) as HTMLInputElement;
const codeInput = document.getElementById(
"editEventCode"
) as HTMLInputElement;
const locationInput = document.getElementById(
"editEventLocation"
) as HTMLInputElement;
const pointsInput = document.getElementById(
"editEventPoints"
) as HTMLInputElement;
const startDateInput = document.getElementById(
"editEventStartDate"
) as HTMLInputElement;
const endDateInput = document.getElementById(
"editEventEndDate"
) as HTMLInputElement;
const descriptionInput = document.getElementById(
"editEventDescription"
) as HTMLTextAreaElement;
const publishedInput = document.getElementById(
"editEventPublished"
) as HTMLInputElement;
const hasFoodInput = document.getElementById(
"editEventHasFood"
) as HTMLInputElement;
if (idInput) idInput.value = freshEventData.id;
if (nameInput) nameInput.value = freshEventData.event_name;
if (codeInput) codeInput.value = freshEventData.event_code;
if (locationInput)
locationInput.value = freshEventData.location;
if (pointsInput)
pointsInput.value =
freshEventData.points_to_reward.toString();
if (startDateInput)
startDateInput.value = new Date(freshEventData.start_date)
.toISOString()
.slice(0, 16);
if (endDateInput)
endDateInput.value = new Date(freshEventData.end_date)
.toISOString()
.slice(0, 16);
if (descriptionInput)
descriptionInput.value = freshEventData.event_description;
if (publishedInput)
publishedInput.checked = freshEventData.published;
if (hasFoodInput)
hasFoodInput.checked = freshEventData.has_food;
// Display current files if any
if (
currentFiles &&
freshEventData.files &&
freshEventData.files.length > 0
) {
currentFiles.innerHTML = updateFilePreviewButtons(
freshEventData.files,
freshEventData.id
);
}
}
// Show the modal
modal.showModal();
} catch (error) {
console.error("Failed to open edit modal:", error);
// Show error toast or alert
}
};
// Update the previewFileInEditModal function
window.previewFileInEditModal = async function (
url: string,
filename: string
) {
const editFormSection = document.getElementById("editFormSection");
const previewSection = document.getElementById(
"editModalPreviewSection"
);
const editFilePreview = document.getElementById("editFilePreview");
const previewFileName = document.getElementById("editPreviewFileName");
const loadingSpinner = document.getElementById("editLoadingSpinner");
if (
!editFormSection ||
!previewSection ||
!editFilePreview ||
!previewFileName ||
!loadingSpinner
)
return;
// Hide form and show preview section
editFormSection.classList.add("hidden");
previewSection.classList.remove("hidden");
previewFileName.textContent = filename;
// Show loading spinner
loadingSpinner.classList.remove("hidden");
try {
// Dispatch a custom event to update the FilePreview
const event = new CustomEvent("updateFilePreview", {
detail: { url, filename },
});
editFilePreview.dispatchEvent(event);
} finally {
// Hide loading spinner
loadingSpinner.classList.add("hidden");
}
};
// Update the showFilePreview function
window.showFilePreview = function (file: { url: string; name: string }) {
const fileListSection = document.getElementById("filesContent");
const previewSection = document.getElementById("filePreviewSection");
const mainFilePreview = document.getElementById("mainFilePreview");
const previewFileName = document.getElementById("previewFileName");
if (
!fileListSection ||
!previewSection ||
!mainFilePreview ||
!previewFileName
)
return;
// Hide file list and show preview section
fileListSection.classList.add("hidden");
previewSection.classList.remove("hidden");
previewFileName.textContent = file.name;
// Dispatch a custom event to update the FilePreview
const event = new CustomEvent("updateFilePreview", {
detail: { url: file.url, filename: file.name },
});
mainFilePreview.dispatchEvent(event);
};
// Add backToEditForm function
window.backToEditForm = function () {
const editFormSection = document.getElementById("editFormSection");
const previewSection = document.getElementById(
"editModalPreviewSection"
);
const editFilePreview = document.getElementById("editFilePreview");
const previewFileName = document.getElementById("editPreviewFileName");
if (
editFormSection &&
previewSection &&
editFilePreview &&
previewFileName
) {
editFormSection.classList.remove("hidden");
previewSection.classList.add("hidden");
// Reset the preview
const event = new CustomEvent("updateFilePreview", {
detail: { url: "", filename: "" },
});
editFilePreview.dispatchEvent(event);
previewFileName.textContent = "";
}
};
// Universal file preview function
window.previewFile = function (url: string, filename: string) {
const modal = document.getElementById("filePreviewModal") as HTMLDialogElement;
const filePreview = document.getElementById("universalFilePreview") as any;
const previewFileName = document.getElementById("previewFileName");
if (filePreview && modal && previewFileName) {
// Update the preview component
const event = new CustomEvent("updateFilePreview", {
detail: { url, filename },
});
filePreview.dispatchEvent(event);
// Update the filename display
previewFileName.textContent = filename;
// Show the modal
modal.showModal();
}
};
// Close file preview
window.closeFilePreview = function () {
const modal = document.getElementById("filePreviewModal") as HTMLDialogElement;
const filePreview = document.getElementById("universalFilePreview");
const previewFileName = document.getElementById("previewFileName");
const filesContent = document.getElementById("filesContent");
if (modal && filePreview && previewFileName) {
// Reset the preview
const event = new CustomEvent("updateFilePreview", {
detail: { url: "", filename: "" },
});
filePreview.dispatchEvent(event);
previewFileName.textContent = "";
modal.close();
// Show the files list if we're in the event details modal
if (filesContent) {
filesContent.classList.remove('hidden');
}
}
};
// Close event details modal
window.closeEventDetailsModal = function () {
const modal = document.getElementById("eventDetailsModal") as HTMLDialogElement;
const filesContent = document.getElementById("filesContent");
const attendeesContent = document.getElementById("attendeesContent");
if (modal) {
// Reset tab states
if (filesContent && attendeesContent) {
filesContent.classList.remove('hidden');
attendeesContent.classList.add('hidden');
}
// Close the modal
modal.close();
}
};
// Update all file preview buttons to use the universal preview
function updateFilePreviewButtons(files: string[], eventId: string) {
return files
.map(
(filename) => {
const fileUrl = fileManager.getFileUrl("events", eventId, filename);
return `
<div class="flex items-center justify-between p-2 bg-base-200 rounded-lg${filesToDelete.has(filename) ? " opacity-50" : ""}" data-filename="${filename}">
<div class="flex items-center gap-2 flex-1 min-w-0">
<button type="button" class="btn btn-ghost btn-xs" onclick="window.previewFile('${fileUrl}', '${filename}')">
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" viewBox="0 0 20 20" fill="currentColor">
<path d="M10 12a2 2 0 100-4 2 2 0 000 4z"/>
<path fill-rule="evenodd" d="M.458 10C1.732 5.943 5.522 3 10 3s8.268 2.943 9.542 7c-1.274 4.057-5.064 7-9.542 7S1.732 14.057.458 10zM14 10a4 4 0 11-8 0 4 4 0 018 0z" clip-rule="evenodd"/>
</svg>
</button>
<span class="truncate">${filename}</span>
</div>
<div class="flex gap-2 flex-shrink-0">
<a
href="${fileUrl}"
download
class="btn btn-ghost btn-xs"
target="_blank"
rel="noopener noreferrer"
>
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" viewBox="0 0 20 20" fill="currentColor">
<path fill-rule="evenodd" d="M3 17a1 1 0 011-1h12a1 1 0 110 2H4a1 1 0 01-1-1zm3.293-7.707a1 1 0 011.414 0L9 10.586V3a1 1 0 112 0v7.586l1.293-1.293a1 1 0 111.414 1.414l-3 3a1 1 0 01-1.414 0l-3-3a1 1 0 010-1.414z" clip-rule="evenodd"/>
</svg>
</a>
<div class="text-error">
${
filesToDelete.has(filename)
? `<button type="button" class="btn btn-ghost btn-xs" onclick="window.undoDeleteFile('${eventId}', '${filename}')">Undo</button>`
: `<button type="button" class="btn btn-ghost btn-xs text-error" onclick="window.deleteFile('${eventId}', '${filename}')">
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" viewBox="0 0 20 20" fill="currentColor">
<path fill-rule="evenodd" d="M9 2a1 1 0 00-.894.553L7.382 4H4a1 1 0 000 2v10a2 2 0 002 2h8a2 2 0 002-2V6a1 1 0 100-2h-3.382l-.724-1.447A1 1 0 0011 2H9zM7 8a1 1 0 012 0v6a1 1 0 11-2 0V8zm5-1a1 1 0 00-1 1v6a1 1 0 102 0V8a1 1 0 00-1-1z" clip-rule="evenodd"/>
</svg>
</button>`
}
</div>
</div>
</div>
`;
}
)
.join("");
}
// Update the openDetailsModal function to use the universal preview
window.openDetailsModal = function (event: Event) {
const modal = document.getElementById(
"eventDetailsModal"
) as HTMLDialogElement;
const filesContent = document.getElementById("filesContent");
const attendeesContent = document.getElementById("attendeesContent");
const tabs = document.querySelectorAll('.tabs .tab');
if (!modal || !filesContent || !attendeesContent || !tabs) return;
// Show modal
modal.showModal();
// Add tab functionality
tabs.forEach(tab => {
tab.addEventListener('click', () => {
const tabName = (tab as HTMLElement).dataset.tab;
tabs.forEach(t => t.classList.remove('tab-active'));
tab.classList.add('tab-active');
if (tabName === 'files') {
filesContent.classList.remove('hidden');
attendeesContent.classList.add('hidden');
} else {
filesContent.classList.add('hidden');
attendeesContent.classList.remove('hidden');
}
});
});
// Update files list
if (event.files && event.files.length > 0) {
filesContent.innerHTML = `
<div class="space-y-2">
${updateFilePreviewButtons(event.files, event.id)}
</div>
`;
} else {
filesContent.innerHTML = `
<div class="text-center py-8 text-base-content/70">
<p>No files attached to this event</p>
</div>
`;
}
// Show files tab by default
const filesTab = Array.from(tabs).find(tab => (tab as HTMLElement).dataset.tab === 'files');
if (filesTab) {
filesTab.classList.add('tab-active');
filesContent.classList.remove('hidden');
attendeesContent.classList.add('hidden');
}
};
// Add file input change handler to show selected files
document
.getElementById("editEventFiles")
?.addEventListener("change", function (e) {
const fileInput = e.target as HTMLInputElement;
const newFiles = document.getElementById("newFiles");
if (newFiles && fileInput.files) {
// Get existing files if any
const existingFiles = newFiles.querySelectorAll(".file-item");
const existingFilesArray = Array.from(existingFiles).map(
(item) => {
const nameSpan = item.querySelector(".file-name");
return nameSpan ? nameSpan.textContent : "";
}
);
// Store new files in the storage and update UI
Array.from(fileInput.files)
.filter((file) => !existingFilesArray.includes(file.name))
.forEach((file) => {
selectedFileStorage.set(file.name, file);
const fileDiv = document.createElement("div");
fileDiv.className =
"flex items-center justify-between p-2 bg-base-200 rounded-lg file-item";
fileDiv.innerHTML = `
<span class="truncate file-name">${file.name}</span>
<div class="flex gap-2">
<div class="badge badge-primary">New</div>
<button type="button" class="btn btn-ghost btn-xs text-error" onclick="this.closest('.file-item').remove(); selectedFileStorage.delete('${file.name}');">
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" viewBox="0 0 20 20" fill="currentColor">
<path fill-rule="evenodd" d="M9 2a1 1 0 00-.894.553L7.382 4H4a1 1 0 000 2v10a2 2 0 002 2h8a2 2 0 002-2V6a1 1 0 100-2h-3.382l-.724-1.447A1 1 0 0011 2H9zM7 8a1 1 0 012 0v6a1 1 0 11-2 0V8zm5-1a1 1 0 00-1 1v6a1 1 0 102 0V8a1 1 0 00-1-1z" clip-rule="evenodd"/>
</svg>
</button>
</div>
`;
newFiles.appendChild(fileDiv);
});
}
// Clear the file input
fileInput.value = "";
});
// Modify form submission handler to use selectedFileStorage
document
.getElementById("editEventForm")
?.addEventListener("submit", async function (e) {
e.preventDefault();
const form = e.target as HTMLFormElement;
const modalAction = document.querySelector(".modal-action");
const submitButton = modalAction?.querySelector(
".btn-primary"
) as HTMLButtonElement;
const cancelButton = modalAction?.querySelector(
".btn:not(.btn-primary)"
) as HTMLButtonElement;
if (!submitButton || !cancelButton) {
console.error("Could not find submit or cancel buttons");
return;
}
// Store original button content
const originalText = submitButton.innerHTML;
// Immediately disable buttons and show loading state
submitButton.disabled = true;
cancelButton.disabled = true;
submitButton.classList.add("btn-disabled");
submitButton.innerHTML = `<span class="loading loading-spinner"></span>`;
try {
const formData = new FormData(form);
const eventId = (
document.getElementById("editEventId") as HTMLInputElement
)?.value;
// Get files from storage
const selectedFiles = Array.from(selectedFileStorage.values());
window.showLoading?.();
// Prepare event data
const eventData = {
event_name: formData.get("editEventName"),
event_code: formData.get("editEventCode"),
event_description: formData.get("editEventDescription"),
location: formData.get("editEventLocation"),
points_to_reward: Number(formData.get("editEventPoints")),
start_date: new Date(
formData.get("editEventStartDate") as string
).toISOString(),
end_date: new Date(
formData.get("editEventEndDate") as string
).toISOString(),
published: formData.get("editEventPublished") === "on",
has_food: formData.get("editEventHasFood") === "on",
};
const pb = auth.getPocketBase();
try {
if (eventId) {
// Update existing event
submitButton.innerHTML = `<span class="loading loading-spinner"></span>`;
// Get current event data
const currentEvent = await pb
.collection("events")
.getOne(eventId);
const currentFiles = currentEvent.files || [];
// Filter out files marked for deletion
const remainingFiles = currentFiles.filter(
(filename: string) => !filesToDelete.has(filename)
);
// Create a single FormData instance for the entire update
const updateFormData = new FormData();
// Add all event data fields
Object.entries(eventData).forEach(([key, value]) => {
updateFormData.append(key, String(value));
});
// Handle files
// First, fetch all remaining files as blobs
const filePromises = remainingFiles.map(
async (filename: string) => {
try {
const response = await fetch(
fileManager.getFileUrl(
"events",
eventId,
filename
)
);
const blob = await response.blob();
return new File([blob], filename, {
type: blob.type,
});
} catch (error) {
console.error(
`Failed to fetch file ${filename}:`,
error
);
return null;
}
}
);
try {
const existingFiles = (
await Promise.all(filePromises)
).filter((file): file is File => file !== null);
// Add all files (both existing and new) to FormData
[...existingFiles, ...selectedFiles].forEach(
(file: File) => {
updateFormData.append("files", file);
}
);
// Perform single update operation
const updatedEvent = await pb
.collection("events")
.update(eventId, updateFormData);
// Log the update
await sendLog.send(
"update",
"event",
`Updated event: ${eventData.event_name}`
);
// Log file deletions
for (const filename of filesToDelete) {
await sendLog.send(
"delete",
"event_file",
`Deleted file ${filename} from event ${eventData.event_name}`
);
}
} catch (error) {
console.error("Failed to process files:", error);
throw error;
}
} else {
// Create new event with files in a single operation
const createFormData = new FormData();
// Add all event data fields
Object.entries(eventData).forEach(([key, value]) => {
createFormData.append(key, String(value));
});
// Initialize attendees as empty array
createFormData.append("attendees", JSON.stringify([]));
// Add new files
selectedFiles.forEach((file: File) => {
createFormData.append("files", file);
});
// Create event with files in a single operation
const newEvent = await pb
.collection("events")
.create(createFormData);
await sendLog.send(
"create",
"event",
`Created event: ${eventData.event_name}`
);
}
// Show success state briefly
submitButton.classList.remove("btn-disabled");
submitButton.classList.add("btn-success");
submitButton.innerHTML = `
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
<path fill-rule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clip-rule="evenodd"/>
</svg>
`;
await new Promise((resolve) => setTimeout(resolve, 1000));
// Close modal and refresh events list
const modal = document.getElementById(
"editEventModal"
) as HTMLDialogElement;
modal?.close();
// Force cache refresh and update events list
lastCacheUpdate = 0; // Reset cache timestamp to force refresh
await refreshCache(); // Refresh the cache
await fetchEvents(); // Update the UI
// Clear form inputs and storage
const formFileInput = document.getElementById(
"editEventFiles"
) as HTMLInputElement;
const newFiles = document.getElementById("newFiles");
if (formFileInput) formFileInput.value = "";
if (newFiles) newFiles.innerHTML = "";
// Clear storages after successful save
selectedFileStorage.clear();
filesToDelete.clear();
} catch (error) {
throw error;
}
} catch (error) {
console.error("Failed to save event:", error);
submitButton.classList.remove("btn-disabled");
submitButton.classList.add("btn-error");
submitButton.innerHTML = `
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z" clip-rule="evenodd"/>
</svg>
`;
await new Promise((resolve) => setTimeout(resolve, 2000));
alert("Failed to save event. Please try again.");
} finally {
submitButton.disabled = false;
cancelButton.disabled = false;
submitButton.classList.remove(
"btn-disabled",
"btn-success",
"btn-error"
);
submitButton.innerHTML = originalText;
window.hideLoading?.();
}
});
// Clear both storages when modal is closed
document.getElementById("editEventModal")?.addEventListener("close", () => {
selectedFileStorage.clear();
filesToDelete.clear();
});
// Add delete file handler
window.deleteFile = async function (eventId: string, filename: string) {
if (!confirm("Are you sure you want to remove this file?")) return;
try {
// Add file to deletion set
filesToDelete.add(filename);
// Update the UI to show file as pending deletion
const currentFiles = document.getElementById("currentFiles");
if (currentFiles) {
const fileElement = currentFiles.querySelector(
`[data-filename="${filename}"]`
);
if (fileElement) {
fileElement.classList.add("opacity-50");
const deleteButton =
fileElement.querySelector(".text-error");
if (deleteButton) {
deleteButton.innerHTML = `
<button type="button" class="btn btn-ghost btn-xs" onclick="window.undoDeleteFile('${eventId}', '${filename}')">
Undo
</button>
`;
}
}
}
} catch (error) {
console.error("Failed to stage file deletion:", error);
alert("Failed to stage file deletion. Please try again.");
}
};
// Add undo delete function
window.undoDeleteFile = function (eventId: string, filename: string) {
filesToDelete.delete(filename);
// Update the UI to show file as restored
const currentFiles = document.getElementById("currentFiles");
if (currentFiles) {
const fileElement = currentFiles.querySelector(
`[data-filename="${filename}"]`
);
if (fileElement) {
fileElement.classList.remove("opacity-50");
const undoButton = fileElement.querySelector(".text-error");
if (undoButton) {
undoButton.innerHTML = `
<button type="button" class="btn btn-ghost btn-xs text-error" onclick="window.deleteFile('${eventId}', '${filename}')">
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" viewBox="0 0 20 20" fill="currentColor">
<path fill-rule="evenodd" d="M9 2a1 1 0 00-.894.553L7.382 4H4a1 1 0 000 2v10a2 2 0 002 2h8a2 2 0 002-2V6a1 1 0 100-2h-3.382l-.724-1.447A1 1 0 0011 2H9zM7 8a1 1 0 012 0v6a1 1 0 11-2 0V8zm5-1a1 1 0 00-1 1v6a1 1 0 102 0V8a1 1 0 00-1-1z" clip-rule="evenodd"/>
</svg>
</button>
`;
}
}
}
};
</script>