2336 lines
98 KiB
Text
2336 lines
98 KiB
Text
---
|
||
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>
|