Add authentication #17
2 changed files with 1448 additions and 1284 deletions
|
@ -56,7 +56,13 @@ let upcomingEvents: Event[] = [];
|
||||||
// Fetch events
|
// Fetch events
|
||||||
try {
|
try {
|
||||||
if (auth.isAuthenticated()) {
|
if (auth.isAuthenticated()) {
|
||||||
eventResponse = await get.getList<Event>("events", 1, 5, "", "-start_date");
|
eventResponse = await get.getList<Event>(
|
||||||
|
"events",
|
||||||
|
1,
|
||||||
|
5,
|
||||||
|
"",
|
||||||
|
"-start_date"
|
||||||
|
);
|
||||||
upcomingEvents = eventResponse.items;
|
upcomingEvents = eventResponse.items;
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
@ -108,7 +114,9 @@ declare global {
|
||||||
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"
|
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">
|
||||||
<div class="stat-title font-medium opacity-80">Total Events</div>
|
<div class="stat-title font-medium opacity-80">
|
||||||
|
Total Events
|
||||||
|
</div>
|
||||||
<div class="stat-value text-primary" id="totalEvents">-</div>
|
<div class="stat-value text-primary" id="totalEvents">-</div>
|
||||||
<div class="stat-desc flex items-center gap-2 mt-1">
|
<div class="stat-desc flex items-center gap-2 mt-1">
|
||||||
<div class="badge badge-primary badge-sm">All Time</div>
|
<div class="badge badge-primary badge-sm">All Time</div>
|
||||||
|
@ -120,9 +128,14 @@ declare global {
|
||||||
>
|
>
|
||||||
<div class="stat">
|
<div class="stat">
|
||||||
<div class="stat-title font-medium opacity-80">Showing</div>
|
<div class="stat-title font-medium opacity-80">Showing</div>
|
||||||
<div class="stat-value text-secondary" id="showingEvents">-</div>
|
<div class="stat-value text-secondary" id="showingEvents">
|
||||||
|
-
|
||||||
|
</div>
|
||||||
<div class="stat-desc flex items-center gap-2 mt-1">
|
<div class="stat-desc flex items-center gap-2 mt-1">
|
||||||
<div class="badge badge-secondary badge-sm" id="totalEventsLabel">
|
<div
|
||||||
|
class="badge badge-secondary badge-sm"
|
||||||
|
id="totalEventsLabel"
|
||||||
|
>
|
||||||
of - Events
|
of - Events
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -135,7 +148,10 @@ declare global {
|
||||||
<div class="stat-title font-medium opacity-80">Pages</div>
|
<div class="stat-title font-medium opacity-80">Pages</div>
|
||||||
<div class="stat-value text-accent" id="currentPage">-</div>
|
<div class="stat-value text-accent" id="currentPage">-</div>
|
||||||
<div class="stat-desc flex items-center gap-2 mt-1">
|
<div class="stat-desc flex items-center gap-2 mt-1">
|
||||||
<div class="badge badge-accent badge-sm" id="totalPagesLabel">
|
<div
|
||||||
|
class="badge badge-accent badge-sm"
|
||||||
|
id="totalPagesLabel"
|
||||||
|
>
|
||||||
of -
|
of -
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -160,7 +176,9 @@ declare global {
|
||||||
<div class="flex flex-col md:flex-row gap-4 mb-4">
|
<div class="flex flex-col md:flex-row gap-4 mb-4">
|
||||||
<div class="form-control flex-1">
|
<div class="form-control flex-1">
|
||||||
<div class="join w-full">
|
<div class="join w-full">
|
||||||
<div class="join-item bg-base-200 flex items-center px-3">
|
<div
|
||||||
|
class="join-item bg-base-200 flex items-center px-3"
|
||||||
|
>
|
||||||
<svg
|
<svg
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
class="h-5 w-5 opacity-70"
|
class="h-5 w-5 opacity-70"
|
||||||
|
@ -183,10 +201,15 @@ declare global {
|
||||||
</div>
|
</div>
|
||||||
<div class="form-control w-full md:w-auto">
|
<div class="form-control w-full md:w-auto">
|
||||||
<div class="join">
|
<div class="join">
|
||||||
<div class="join-item bg-base-200 flex items-center px-3">
|
<div
|
||||||
|
class="join-item bg-base-200 flex items-center px-3"
|
||||||
|
>
|
||||||
Per Page
|
Per Page
|
||||||
</div>
|
</div>
|
||||||
<select id="perPageSelect" class="select select-bordered join-item">
|
<select
|
||||||
|
id="perPageSelect"
|
||||||
|
class="select select-bordered join-item"
|
||||||
|
>
|
||||||
<option value="5">5</option>
|
<option value="5">5</option>
|
||||||
<option value="10">10</option>
|
<option value="10">10</option>
|
||||||
<option value="25">25</option>
|
<option value="25">25</option>
|
||||||
|
@ -210,13 +233,21 @@ declare global {
|
||||||
<!-- Pagination -->
|
<!-- Pagination -->
|
||||||
<div class="flex justify-center mt-6" id="paginationContainer">
|
<div class="flex justify-center mt-6" id="paginationContainer">
|
||||||
<div class="join">
|
<div class="join">
|
||||||
<button class="join-item btn btn-sm" id="firstPageBtn">«</button>
|
<button class="join-item btn btn-sm" id="firstPageBtn"
|
||||||
<button class="join-item btn btn-sm" id="prevPageBtn">‹</button>
|
>«</button
|
||||||
|
>
|
||||||
|
<button class="join-item btn btn-sm" id="prevPageBtn"
|
||||||
|
>‹</button
|
||||||
|
>
|
||||||
<button class="join-item btn btn-sm"
|
<button class="join-item btn btn-sm"
|
||||||
>Page <span id="currentPageNumber">1</span></button
|
>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="nextPageBtn"
|
||||||
<button class="join-item btn btn-sm" id="lastPageBtn">»</button>
|
>›</button
|
||||||
|
>
|
||||||
|
<button class="join-item btn btn-sm" id="lastPageBtn"
|
||||||
|
>»</button
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -372,8 +403,8 @@ declare global {
|
||||||
</label>
|
</label>
|
||||||
<label class="label">
|
<label class="label">
|
||||||
<span class="label-text-alt text-info"
|
<span class="label-text-alt text-info"
|
||||||
>This has to be clicked if you want to make this event available to
|
>This has to be clicked if you want to make this event
|
||||||
the public</span
|
available to the public</span
|
||||||
>
|
>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
@ -397,9 +428,13 @@ declare global {
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="modal-action">
|
<div class="modal-action">
|
||||||
<button type="submit" class="btn btn-primary">Save Changes</button>
|
<button type="submit" class="btn btn-primary"
|
||||||
<button type="button" class="btn" onclick="editEventModal.close()"
|
>Save Changes</button
|
||||||
>Cancel</button
|
>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="btn"
|
||||||
|
onclick="editEventModal.close()">Cancel</button
|
||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
@ -448,7 +483,10 @@ declare global {
|
||||||
<div id="attendeesContent" class="space-y-4 hidden">
|
<div id="attendeesContent" class="space-y-4 hidden">
|
||||||
<div class="flex justify-between items-center mb-4">
|
<div class="flex justify-between items-center mb-4">
|
||||||
<h3 class="text-lg font-semibold">Event Attendees</h3>
|
<h3 class="text-lg font-semibold">Event Attendees</h3>
|
||||||
<button id="downloadAttendeesCSV" class="btn btn-primary btn-sm gap-2">
|
<button
|
||||||
|
id="downloadAttendeesCSV"
|
||||||
|
class="btn btn-primary btn-sm gap-2"
|
||||||
|
>
|
||||||
<svg
|
<svg
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
class="h-4 w-4"
|
class="h-4 w-4"
|
||||||
|
@ -470,10 +508,14 @@ declare global {
|
||||||
<div id="filePreviewSection" class="hidden">
|
<div id="filePreviewSection" class="hidden">
|
||||||
<div class="flex justify-between items-center mb-4">
|
<div class="flex justify-between items-center mb-4">
|
||||||
<div class="flex items-center gap-3">
|
<div class="flex items-center gap-3">
|
||||||
<button class="btn btn-ghost btn-sm" onclick="backToFileList()">
|
<button
|
||||||
|
class="btn btn-ghost btn-sm"
|
||||||
|
onclick="backToFileList()"
|
||||||
|
>
|
||||||
← Back
|
← Back
|
||||||
</button>
|
</button>
|
||||||
<h3 class="font-bold text-lg truncate" id="previewFileName"></h3>
|
<h3 class="font-bold text-lg truncate" id="previewFileName">
|
||||||
|
</h3>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="relative" id="previewContainer">
|
<div class="relative" id="previewContainer">
|
||||||
|
@ -538,10 +580,11 @@ declare global {
|
||||||
function resetModalState() {
|
function resetModalState() {
|
||||||
const filesContent = document.getElementById("filesContent");
|
const filesContent = document.getElementById("filesContent");
|
||||||
const attendeesContent = document.getElementById("attendeesContent");
|
const attendeesContent = document.getElementById("attendeesContent");
|
||||||
const filePreviewSection = document.getElementById("filePreviewSection");
|
const filePreviewSection =
|
||||||
|
document.getElementById("filePreviewSection");
|
||||||
const modalTitle = document.getElementById("modalTitle");
|
const modalTitle = document.getElementById("modalTitle");
|
||||||
const tabs = document.querySelectorAll(
|
const tabs = document.querySelectorAll(
|
||||||
".tab",
|
".tab"
|
||||||
) as NodeListOf<HTMLButtonElement>;
|
) as NodeListOf<HTMLButtonElement>;
|
||||||
|
|
||||||
// Reset tab states
|
// Reset tab states
|
||||||
|
@ -568,7 +611,7 @@ declare global {
|
||||||
// Add event listener for modal close when the DOM is loaded
|
// Add event listener for modal close when the DOM is loaded
|
||||||
document.addEventListener("DOMContentLoaded", () => {
|
document.addEventListener("DOMContentLoaded", () => {
|
||||||
const eventDetailsModal = document.getElementById(
|
const eventDetailsModal = document.getElementById(
|
||||||
"eventDetailsModal",
|
"eventDetailsModal"
|
||||||
) as HTMLDialogElement;
|
) as HTMLDialogElement;
|
||||||
if (eventDetailsModal) {
|
if (eventDetailsModal) {
|
||||||
eventDetailsModal.addEventListener("close", resetModalState);
|
eventDetailsModal.addEventListener("close", resetModalState);
|
||||||
|
@ -577,10 +620,10 @@ declare global {
|
||||||
|
|
||||||
// Add event listeners for filters
|
// Add event listeners for filters
|
||||||
const searchInput = document.getElementById(
|
const searchInput = document.getElementById(
|
||||||
"searchInput",
|
"searchInput"
|
||||||
) as HTMLInputElement;
|
) as HTMLInputElement;
|
||||||
const perPageSelect = document.getElementById(
|
const perPageSelect = document.getElementById(
|
||||||
"perPageSelect",
|
"perPageSelect"
|
||||||
) as HTMLSelectElement;
|
) as HTMLSelectElement;
|
||||||
|
|
||||||
if (searchInput) {
|
if (searchInput) {
|
||||||
|
@ -609,42 +652,48 @@ declare global {
|
||||||
const localEvent = event ? Get.convertUTCToLocal(event) : null;
|
const localEvent = event ? Get.convertUTCToLocal(event) : null;
|
||||||
|
|
||||||
const modal = document.getElementById(
|
const modal = document.getElementById(
|
||||||
"editEventModal",
|
"editEventModal"
|
||||||
) as HTMLDialogElement;
|
) as HTMLDialogElement;
|
||||||
const modalTitle = document.getElementById("editModalTitle");
|
const modalTitle = document.getElementById("editModalTitle");
|
||||||
const form = document.getElementById("editEventForm") as HTMLFormElement;
|
const form = document.getElementById(
|
||||||
const idInput = document.getElementById("editEventId") as HTMLInputElement;
|
"editEventForm"
|
||||||
|
) as HTMLFormElement;
|
||||||
|
const idInput = document.getElementById(
|
||||||
|
"editEventId"
|
||||||
|
) as HTMLInputElement;
|
||||||
const nameInput = document.getElementById(
|
const nameInput = document.getElementById(
|
||||||
"editEventName",
|
"editEventName"
|
||||||
) as HTMLInputElement;
|
) as HTMLInputElement;
|
||||||
const descInput = document.getElementById(
|
const descInput = document.getElementById(
|
||||||
"editEventDescription",
|
"editEventDescription"
|
||||||
) as HTMLTextAreaElement;
|
) as HTMLTextAreaElement;
|
||||||
const codeInput = document.getElementById(
|
const codeInput = document.getElementById(
|
||||||
"editEventCode",
|
"editEventCode"
|
||||||
) as HTMLInputElement;
|
) as HTMLInputElement;
|
||||||
const locationInput = document.getElementById(
|
const locationInput = document.getElementById(
|
||||||
"editEventLocation",
|
"editEventLocation"
|
||||||
) as HTMLInputElement;
|
) as HTMLInputElement;
|
||||||
const pointsInput = document.getElementById(
|
const pointsInput = document.getElementById(
|
||||||
"editEventPoints",
|
"editEventPoints"
|
||||||
) as HTMLInputElement;
|
) as HTMLInputElement;
|
||||||
const startDateInput = document.getElementById(
|
const startDateInput = document.getElementById(
|
||||||
"editEventStartDate",
|
"editEventStartDate"
|
||||||
) as HTMLInputElement;
|
) as HTMLInputElement;
|
||||||
const endDateInput = document.getElementById(
|
const endDateInput = document.getElementById(
|
||||||
"editEventEndDate",
|
"editEventEndDate"
|
||||||
) as HTMLInputElement;
|
) as HTMLInputElement;
|
||||||
const publishedInput = document.getElementById(
|
const publishedInput = document.getElementById(
|
||||||
"editEventPublished",
|
"editEventPublished"
|
||||||
) as HTMLInputElement;
|
) as HTMLInputElement;
|
||||||
const currentFilesDiv = document.getElementById(
|
const currentFilesDiv = document.getElementById(
|
||||||
"currentFiles",
|
"currentFiles"
|
||||||
) as HTMLDivElement;
|
) as HTMLDivElement;
|
||||||
|
|
||||||
// Update modal title based on whether we're editing or creating
|
// Update modal title based on whether we're editing or creating
|
||||||
if (modalTitle) {
|
if (modalTitle) {
|
||||||
modalTitle.textContent = localEvent ? "Edit Event" : "Create New Event";
|
modalTitle.textContent = localEvent
|
||||||
|
? "Edit Event"
|
||||||
|
: "Create New Event";
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set values
|
// Set values
|
||||||
|
@ -656,21 +705,23 @@ declare global {
|
||||||
pointsInput.value = localEvent?.points_to_reward?.toString() || "0";
|
pointsInput.value = localEvent?.points_to_reward?.toString() || "0";
|
||||||
publishedInput.checked = localEvent?.published || false;
|
publishedInput.checked = localEvent?.published || false;
|
||||||
const hasFoodInput = document.getElementById(
|
const hasFoodInput = document.getElementById(
|
||||||
"editEventHasFood",
|
"editEventHasFood"
|
||||||
) as HTMLInputElement;
|
) as HTMLInputElement;
|
||||||
hasFoodInput.checked = localEvent?.has_food || false;
|
hasFoodInput.checked = localEvent?.has_food || false;
|
||||||
|
|
||||||
// Format dates properly for datetime-local input
|
// Format dates properly for datetime-local input
|
||||||
try {
|
try {
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
const startDate = localEvent ? new Date(localEvent.start_date) : now;
|
const startDate = localEvent
|
||||||
|
? new Date(localEvent.start_date)
|
||||||
|
: now;
|
||||||
const endDate = localEvent
|
const endDate = localEvent
|
||||||
? new Date(localEvent.end_date)
|
? new Date(localEvent.end_date)
|
||||||
: new Date(now.getTime() + 60 * 60 * 1000); // Default to 1 hour duration
|
: new Date(now.getTime() + 60 * 60 * 1000); // Default to 1 hour duration
|
||||||
|
|
||||||
if (!isNaN(startDate.getTime())) {
|
if (!isNaN(startDate.getTime())) {
|
||||||
startDateInput.value = new Date(
|
startDateInput.value = new Date(
|
||||||
startDate.getTime() - startDate.getTimezoneOffset() * 60000,
|
startDate.getTime() - startDate.getTimezoneOffset() * 60000
|
||||||
)
|
)
|
||||||
.toISOString()
|
.toISOString()
|
||||||
.slice(0, 16);
|
.slice(0, 16);
|
||||||
|
@ -678,7 +729,7 @@ declare global {
|
||||||
|
|
||||||
if (!isNaN(endDate.getTime())) {
|
if (!isNaN(endDate.getTime())) {
|
||||||
endDateInput.value = new Date(
|
endDateInput.value = new Date(
|
||||||
endDate.getTime() - endDate.getTimezoneOffset() * 60000,
|
endDate.getTime() - endDate.getTimezoneOffset() * 60000
|
||||||
)
|
)
|
||||||
.toISOString()
|
.toISOString()
|
||||||
.slice(0, 16);
|
.slice(0, 16);
|
||||||
|
@ -689,7 +740,9 @@ declare global {
|
||||||
|
|
||||||
// Reset temp files
|
// Reset temp files
|
||||||
tempFiles = [];
|
tempFiles = [];
|
||||||
const newFilesDiv = document.getElementById("newFiles") as HTMLDivElement;
|
const newFilesDiv = document.getElementById(
|
||||||
|
"newFiles"
|
||||||
|
) as HTMLDivElement;
|
||||||
newFilesDiv.innerHTML = "";
|
newFilesDiv.innerHTML = "";
|
||||||
|
|
||||||
// Display current files if any
|
// Display current files if any
|
||||||
|
@ -711,7 +764,7 @@ declare global {
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
`,
|
`
|
||||||
)
|
)
|
||||||
.join("");
|
.join("");
|
||||||
} else {
|
} else {
|
||||||
|
@ -767,7 +820,7 @@ declare global {
|
||||||
await sendLog.send(
|
await sendLog.send(
|
||||||
"delete",
|
"delete",
|
||||||
"event_files",
|
"event_files",
|
||||||
`Deleted file ${filename} from event ${eventId}`,
|
`Deleted file ${filename} from event ${eventId}`
|
||||||
);
|
);
|
||||||
await fetchEvents(); // Refresh the list
|
await fetchEvents(); // Refresh the list
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
@ -777,13 +830,13 @@ declare global {
|
||||||
|
|
||||||
// Handle file input change
|
// Handle file input change
|
||||||
const fileInput = document.getElementById(
|
const fileInput = document.getElementById(
|
||||||
"editEventFiles",
|
"editEventFiles"
|
||||||
) as HTMLInputElement;
|
) as HTMLInputElement;
|
||||||
if (fileInput) {
|
if (fileInput) {
|
||||||
fileInput.addEventListener("change", function () {
|
fileInput.addEventListener("change", function () {
|
||||||
if (this.files && this.files.length > 0) {
|
if (this.files && this.files.length > 0) {
|
||||||
const newFilesDiv = document.getElementById(
|
const newFilesDiv = document.getElementById(
|
||||||
"newFiles",
|
"newFiles"
|
||||||
) as HTMLDivElement;
|
) as HTMLDivElement;
|
||||||
|
|
||||||
// Add new files to temp storage
|
// Add new files to temp storage
|
||||||
|
@ -805,7 +858,7 @@ declare global {
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
`,
|
`
|
||||||
)
|
)
|
||||||
.join("");
|
.join("");
|
||||||
|
|
||||||
|
@ -818,7 +871,9 @@ declare global {
|
||||||
// Remove temp file
|
// Remove temp file
|
||||||
window.removeTempFile = function (index: number) {
|
window.removeTempFile = function (index: number) {
|
||||||
tempFiles.splice(index, 1);
|
tempFiles.splice(index, 1);
|
||||||
const newFilesDiv = document.getElementById("newFiles") as HTMLDivElement;
|
const newFilesDiv = document.getElementById(
|
||||||
|
"newFiles"
|
||||||
|
) as HTMLDivElement;
|
||||||
|
|
||||||
if (tempFiles.length === 0) {
|
if (tempFiles.length === 0) {
|
||||||
newFilesDiv.innerHTML = "";
|
newFilesDiv.innerHTML = "";
|
||||||
|
@ -839,18 +894,20 @@ declare global {
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
`,
|
`
|
||||||
)
|
)
|
||||||
.join("");
|
.join("");
|
||||||
};
|
};
|
||||||
|
|
||||||
// Update form submission to handle both create and edit
|
// Update form submission to handle both create and edit
|
||||||
const editForm = document.getElementById("editEventForm") as HTMLFormElement;
|
const editForm = document.getElementById(
|
||||||
|
"editEventForm"
|
||||||
|
) as HTMLFormElement;
|
||||||
if (editForm) {
|
if (editForm) {
|
||||||
editForm.addEventListener("submit", async (e) => {
|
editForm.addEventListener("submit", async (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
const modal = document.getElementById(
|
const modal = document.getElementById(
|
||||||
"editEventModal",
|
"editEventModal"
|
||||||
) as HTMLDialogElement;
|
) as HTMLDialogElement;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
@ -860,7 +917,9 @@ declare global {
|
||||||
).value;
|
).value;
|
||||||
|
|
||||||
// Get the date strings from the form
|
// Get the date strings from the form
|
||||||
const startDateStr = formData.get("editEventStartDate") as string;
|
const startDateStr = formData.get(
|
||||||
|
"editEventStartDate"
|
||||||
|
) as string;
|
||||||
const endDateStr = formData.get("editEventEndDate") as string;
|
const endDateStr = formData.get("editEventEndDate") as string;
|
||||||
|
|
||||||
// Convert local dates to UTC ISO strings
|
// Convert local dates to UTC ISO strings
|
||||||
|
@ -868,7 +927,9 @@ declare global {
|
||||||
const endDate = new Date(endDateStr);
|
const endDate = new Date(endDateStr);
|
||||||
|
|
||||||
// Get points value and ensure it's a number
|
// Get points value and ensure it's a number
|
||||||
const pointsValue = parseInt(formData.get("editEventPoints") as string);
|
const pointsValue = parseInt(
|
||||||
|
formData.get("editEventPoints") as string
|
||||||
|
);
|
||||||
const points = isNaN(pointsValue) ? 0 : pointsValue;
|
const points = isNaN(pointsValue) ? 0 : pointsValue;
|
||||||
|
|
||||||
// Prepare base event data without attendees
|
// Prepare base event data without attendees
|
||||||
|
@ -896,28 +957,44 @@ declare global {
|
||||||
|
|
||||||
if (eventId) {
|
if (eventId) {
|
||||||
// Update existing event using Update class
|
// Update existing event using Update class
|
||||||
result = await update.updateFields("events", eventId, eventData);
|
result = await update.updateFields(
|
||||||
|
"events",
|
||||||
|
eventId,
|
||||||
|
eventData
|
||||||
|
);
|
||||||
|
|
||||||
|
// Append new files if any, using appendFiles to preserve existing ones
|
||||||
|
if (tempFiles.length > 0) {
|
||||||
|
await fileManager.appendFiles(
|
||||||
|
"events",
|
||||||
|
result.id,
|
||||||
|
"files",
|
||||||
|
tempFiles
|
||||||
|
);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
// Create new event using PocketBase
|
// Create new event using PocketBase
|
||||||
const pb = auth.getPocketBase();
|
const pb = auth.getPocketBase();
|
||||||
result = await pb.collection("events").create(eventData);
|
result = await pb
|
||||||
}
|
.collection("events")
|
||||||
|
.create(eventData);
|
||||||
|
|
||||||
// Upload temp files if any
|
// For new events, use uploadFiles since there are no existing files
|
||||||
if (tempFiles.length > 0) {
|
if (tempFiles.length > 0) {
|
||||||
await fileManager.uploadFiles(
|
await fileManager.uploadFiles(
|
||||||
"events",
|
"events",
|
||||||
result.id,
|
result.id,
|
||||||
"files",
|
"files",
|
||||||
tempFiles,
|
tempFiles
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Log the action
|
// Log the action
|
||||||
await sendLog.send(
|
await sendLog.send(
|
||||||
eventId ? "update" : "create",
|
eventId ? "update" : "create",
|
||||||
"events",
|
"events",
|
||||||
`${eventId ? "Updated" : "Created"} event ${eventData.event_name}`,
|
`${eventId ? "Updated" : "Created"} event ${eventData.event_name}`
|
||||||
);
|
);
|
||||||
|
|
||||||
modal.close();
|
modal.close();
|
||||||
|
@ -958,7 +1035,8 @@ declare global {
|
||||||
}
|
}
|
||||||
|
|
||||||
function backToFileList() {
|
function backToFileList() {
|
||||||
const filePreviewSection = document.getElementById("filePreviewSection");
|
const filePreviewSection =
|
||||||
|
document.getElementById("filePreviewSection");
|
||||||
const filesContent = document.getElementById("filesContent");
|
const filesContent = document.getElementById("filesContent");
|
||||||
const modalTitle = document.getElementById("modalTitle");
|
const modalTitle = document.getElementById("modalTitle");
|
||||||
|
|
||||||
|
@ -967,8 +1045,13 @@ declare global {
|
||||||
if (modalTitle) modalTitle.textContent = "Event Details";
|
if (modalTitle) modalTitle.textContent = "Event Details";
|
||||||
}
|
}
|
||||||
|
|
||||||
function showFilePreview(file: { url: string; type: string; name: string }) {
|
function showFilePreview(file: {
|
||||||
const filePreviewSection = document.getElementById("filePreviewSection");
|
url: string;
|
||||||
|
type: string;
|
||||||
|
name: string;
|
||||||
|
}) {
|
||||||
|
const filePreviewSection =
|
||||||
|
document.getElementById("filePreviewSection");
|
||||||
const filesContent = document.getElementById("filesContent");
|
const filesContent = document.getElementById("filesContent");
|
||||||
const previewContent = document.getElementById("previewContent");
|
const previewContent = document.getElementById("previewContent");
|
||||||
const previewFileName = document.getElementById("previewFileName");
|
const previewFileName = document.getElementById("previewFileName");
|
||||||
|
@ -1081,7 +1164,7 @@ declare global {
|
||||||
|
|
||||||
// Fetch all user details for the attendees
|
// Fetch all user details for the attendees
|
||||||
const attendeePromises = event.attendees.map(function (
|
const attendeePromises = event.attendees.map(function (
|
||||||
attendee: AttendeeEntry,
|
attendee: AttendeeEntry
|
||||||
) {
|
) {
|
||||||
return pb.collection("users").getOne(attendee.user_id);
|
return pb.collection("users").getOne(attendee.user_id);
|
||||||
});
|
});
|
||||||
|
@ -1104,7 +1187,7 @@ declare global {
|
||||||
(attendee: AttendeeEntry, index: number) => {
|
(attendee: AttendeeEntry, index: number) => {
|
||||||
const user = users[index];
|
const user = users[index];
|
||||||
const checkInTime = new Date(
|
const checkInTime = new Date(
|
||||||
attendee.time_checked_in,
|
attendee.time_checked_in
|
||||||
).toLocaleString();
|
).toLocaleString();
|
||||||
|
|
||||||
// Escape and format fields to handle commas and quotes
|
// Escape and format fields to handle commas and quotes
|
||||||
|
@ -1123,14 +1206,16 @@ declare global {
|
||||||
formatField(checkInTime),
|
formatField(checkInTime),
|
||||||
formatField(attendee.food),
|
formatField(attendee.food),
|
||||||
].join(",");
|
].join(",");
|
||||||
},
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
// Combine header and rows
|
// Combine header and rows
|
||||||
const csvContent = [csvHeader, ...csvRows].join("\n");
|
const csvContent = [csvHeader, ...csvRows].join("\n");
|
||||||
|
|
||||||
// Create and trigger download
|
// Create and trigger download
|
||||||
const blob = new Blob([csvContent], { type: "text/csv;charset=utf-8;" });
|
const blob = new Blob([csvContent], {
|
||||||
|
type: "text/csv;charset=utf-8;",
|
||||||
|
});
|
||||||
const link = document.createElement("a");
|
const link = document.createElement("a");
|
||||||
const url = URL.createObjectURL(blob);
|
const url = URL.createObjectURL(blob);
|
||||||
link.setAttribute("href", url);
|
link.setAttribute("href", url);
|
||||||
|
@ -1190,19 +1275,19 @@ declare global {
|
||||||
const localEvent = Get.convertUTCToLocal(event);
|
const localEvent = Get.convertUTCToLocal(event);
|
||||||
|
|
||||||
const modal = document.getElementById(
|
const modal = document.getElementById(
|
||||||
"eventDetailsModal",
|
"eventDetailsModal"
|
||||||
) as HTMLDialogElement;
|
) as HTMLDialogElement;
|
||||||
const filesContent = document.getElementById(
|
const filesContent = document.getElementById(
|
||||||
"filesContent",
|
"filesContent"
|
||||||
) as HTMLDivElement;
|
) as HTMLDivElement;
|
||||||
const attendeesContent = document.getElementById(
|
const attendeesContent = document.getElementById(
|
||||||
"attendeesContent",
|
"attendeesContent"
|
||||||
) as HTMLDivElement;
|
) as HTMLDivElement;
|
||||||
const filePreviewSection = document.getElementById(
|
const filePreviewSection = document.getElementById(
|
||||||
"filePreviewSection",
|
"filePreviewSection"
|
||||||
) as HTMLDivElement;
|
) as HTMLDivElement;
|
||||||
const tabs = modal.querySelectorAll(
|
const tabs = modal.querySelectorAll(
|
||||||
".tab",
|
".tab"
|
||||||
) as NodeListOf<HTMLButtonElement>;
|
) as NodeListOf<HTMLButtonElement>;
|
||||||
|
|
||||||
// Reset state
|
// Reset state
|
||||||
|
@ -1237,7 +1322,10 @@ declare global {
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Check if event has attendees
|
// Check if event has attendees
|
||||||
if (!localEvent.attendees || localEvent.attendees.length === 0) {
|
if (
|
||||||
|
!localEvent.attendees ||
|
||||||
|
localEvent.attendees.length === 0
|
||||||
|
) {
|
||||||
attendeesContent.innerHTML = `
|
attendeesContent.innerHTML = `
|
||||||
<div class="text-center py-8 text-base-content/70">
|
<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">
|
<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">
|
||||||
|
@ -1251,11 +1339,13 @@ declare global {
|
||||||
|
|
||||||
// Fetch user details for each attendee
|
// Fetch user details for each attendee
|
||||||
const pb = auth.getPocketBase();
|
const pb = auth.getPocketBase();
|
||||||
const attendeePromises = localEvent.attendees.map(function (
|
const attendeePromises = localEvent.attendees.map(
|
||||||
attendee: AttendeeEntry,
|
function (attendee: AttendeeEntry) {
|
||||||
) {
|
return pb
|
||||||
return pb.collection("users").getOne(attendee.user_id);
|
.collection("users")
|
||||||
});
|
.getOne(attendee.user_id);
|
||||||
|
}
|
||||||
|
);
|
||||||
const users = await Promise.all(attendeePromises);
|
const users = await Promise.all(attendeePromises);
|
||||||
|
|
||||||
// Display attendees in a table
|
// Display attendees in a table
|
||||||
|
@ -1285,11 +1375,11 @@ declare global {
|
||||||
${localEvent.attendees
|
${localEvent.attendees
|
||||||
.map(function (
|
.map(function (
|
||||||
attendee: AttendeeEntry,
|
attendee: AttendeeEntry,
|
||||||
index: number,
|
index: number
|
||||||
): string {
|
): string {
|
||||||
const user = users[index];
|
const user = users[index];
|
||||||
const checkInTime = new Date(
|
const checkInTime = new Date(
|
||||||
attendee.time_checked_in,
|
attendee.time_checked_in
|
||||||
).toLocaleString();
|
).toLocaleString();
|
||||||
return `
|
return `
|
||||||
<tr>
|
<tr>
|
||||||
|
@ -1313,10 +1403,11 @@ declare global {
|
||||||
|
|
||||||
// Add click handler for CSV download button
|
// Add click handler for CSV download button
|
||||||
const downloadButton = document.getElementById(
|
const downloadButton = document.getElementById(
|
||||||
"downloadAttendeesCSV",
|
"downloadAttendeesCSV"
|
||||||
);
|
);
|
||||||
if (downloadButton) {
|
if (downloadButton) {
|
||||||
downloadButton.onclick = () => exportAttendeesToCSV(localEvent);
|
downloadButton.onclick = () =>
|
||||||
|
exportAttendeesToCSV(localEvent);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Failed to fetch attendees:", error);
|
console.error("Failed to fetch attendees:", error);
|
||||||
|
@ -1366,7 +1457,7 @@ declare global {
|
||||||
url: fileUrl,
|
url: fileUrl,
|
||||||
type: fileType,
|
type: fileType,
|
||||||
name: file,
|
name: file,
|
||||||
},
|
}
|
||||||
)})'>
|
)})'>
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" viewBox="0 0 20 20" fill="currentColor">
|
<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 d="M10 12a2 2 0 100-4 2 2 0 000 4z" />
|
||||||
|
@ -1412,7 +1503,7 @@ declare global {
|
||||||
window.deleteEvent = async function (eventId: string, eventName: string) {
|
window.deleteEvent = async function (eventId: string, eventName: string) {
|
||||||
if (
|
if (
|
||||||
!confirm(
|
!confirm(
|
||||||
`Are you sure you want to delete "${eventName}"? This action cannot be undone.`,
|
`Are you sure you want to delete "${eventName}"? This action cannot be undone.`
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
return;
|
return;
|
||||||
|
@ -1422,7 +1513,11 @@ declare global {
|
||||||
auth.setUpdating(true);
|
auth.setUpdating(true);
|
||||||
const pb = auth.getPocketBase();
|
const pb = auth.getPocketBase();
|
||||||
await pb.collection("events").delete(eventId);
|
await pb.collection("events").delete(eventId);
|
||||||
await sendLog.send("delete", "events", `Deleted event ${eventName}`);
|
await sendLog.send(
|
||||||
|
"delete",
|
||||||
|
"events",
|
||||||
|
`Deleted event ${eventName}`
|
||||||
|
);
|
||||||
await fetchEvents(); // Refresh the list
|
await fetchEvents(); // Refresh the list
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Failed to delete event:", error);
|
console.error("Failed to delete event:", error);
|
||||||
|
@ -1434,7 +1529,9 @@ declare global {
|
||||||
|
|
||||||
async function fetchEvents() {
|
async function fetchEvents() {
|
||||||
const eventsList = document.getElementById("eventsList");
|
const eventsList = document.getElementById("eventsList");
|
||||||
const paginationContainer = document.getElementById("paginationContainer");
|
const paginationContainer = document.getElementById(
|
||||||
|
"paginationContainer"
|
||||||
|
);
|
||||||
if (!eventsList || !paginationContainer) return;
|
if (!eventsList || !paginationContainer) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
@ -1467,21 +1564,25 @@ declare global {
|
||||||
perPage,
|
perPage,
|
||||||
filter,
|
filter,
|
||||||
"-start_date",
|
"-start_date",
|
||||||
{ disableAutoCancellation: true },
|
{ disableAutoCancellation: true }
|
||||||
);
|
);
|
||||||
|
|
||||||
// Convert response items to local time
|
// Convert response items to local time
|
||||||
const localEvents = response.items.map((event) =>
|
const localEvents = response.items.map((event) =>
|
||||||
Get.convertUTCToLocal(event),
|
Get.convertUTCToLocal(event)
|
||||||
);
|
);
|
||||||
|
|
||||||
// Update stats
|
// Update stats
|
||||||
const totalEventsEl = document.getElementById("totalEvents");
|
const totalEventsEl = document.getElementById("totalEvents");
|
||||||
const showingEventsEl = document.getElementById("showingEvents");
|
const showingEventsEl =
|
||||||
const totalEventsLabelEl = document.getElementById("totalEventsLabel");
|
document.getElementById("showingEvents");
|
||||||
|
const totalEventsLabelEl =
|
||||||
|
document.getElementById("totalEventsLabel");
|
||||||
const currentPageEl = document.getElementById("currentPage");
|
const currentPageEl = document.getElementById("currentPage");
|
||||||
const totalPagesLabelEl = document.getElementById("totalPagesLabel");
|
const totalPagesLabelEl =
|
||||||
const currentPageNumber = document.getElementById("currentPageNumber");
|
document.getElementById("totalPagesLabel");
|
||||||
|
const currentPageNumber =
|
||||||
|
document.getElementById("currentPageNumber");
|
||||||
|
|
||||||
if (totalEventsEl)
|
if (totalEventsEl)
|
||||||
totalEventsEl.textContent = response.totalItems.toString();
|
totalEventsEl.textContent = response.totalItems.toString();
|
||||||
|
@ -1489,7 +1590,8 @@ declare global {
|
||||||
showingEventsEl.textContent = localEvents.length.toString();
|
showingEventsEl.textContent = localEvents.length.toString();
|
||||||
if (totalEventsLabelEl)
|
if (totalEventsLabelEl)
|
||||||
totalEventsLabelEl.textContent = `of ${response.totalItems} Events`;
|
totalEventsLabelEl.textContent = `of ${response.totalItems} Events`;
|
||||||
if (currentPageEl) currentPageEl.textContent = response.page.toString();
|
if (currentPageEl)
|
||||||
|
currentPageEl.textContent = response.page.toString();
|
||||||
if (totalPagesLabelEl)
|
if (totalPagesLabelEl)
|
||||||
totalPagesLabelEl.textContent = `of ${response.totalPages}`;
|
totalPagesLabelEl.textContent = `of ${response.totalPages}`;
|
||||||
if (currentPageNumber)
|
if (currentPageNumber)
|
||||||
|
@ -1497,16 +1599,16 @@ declare global {
|
||||||
|
|
||||||
// Update pagination buttons state
|
// Update pagination buttons state
|
||||||
const firstPageBtn = document.getElementById(
|
const firstPageBtn = document.getElementById(
|
||||||
"firstPageBtn",
|
"firstPageBtn"
|
||||||
) as HTMLButtonElement;
|
) as HTMLButtonElement;
|
||||||
const prevPageBtn = document.getElementById(
|
const prevPageBtn = document.getElementById(
|
||||||
"prevPageBtn",
|
"prevPageBtn"
|
||||||
) as HTMLButtonElement;
|
) as HTMLButtonElement;
|
||||||
const nextPageBtn = document.getElementById(
|
const nextPageBtn = document.getElementById(
|
||||||
"nextPageBtn",
|
"nextPageBtn"
|
||||||
) as HTMLButtonElement;
|
) as HTMLButtonElement;
|
||||||
const lastPageBtn = document.getElementById(
|
const lastPageBtn = document.getElementById(
|
||||||
"lastPageBtn",
|
"lastPageBtn"
|
||||||
) as HTMLButtonElement;
|
) as HTMLButtonElement;
|
||||||
|
|
||||||
if (firstPageBtn) firstPageBtn.disabled = response.page <= 1;
|
if (firstPageBtn) firstPageBtn.disabled = response.page <= 1;
|
||||||
|
@ -1555,8 +1657,12 @@ declare global {
|
||||||
console.error("Error formatting date:", e);
|
console.error("Error formatting date:", e);
|
||||||
}
|
}
|
||||||
|
|
||||||
const locationStr = event.location ? `${event.location}` : "";
|
const locationStr = event.location
|
||||||
const codeStr = event.event_code ? `${event.event_code}` : "";
|
? `${event.location}`
|
||||||
|
: "";
|
||||||
|
const codeStr = event.event_code
|
||||||
|
? `${event.event_code}`
|
||||||
|
: "";
|
||||||
const detailsStr = [locationStr, codeStr]
|
const detailsStr = [locationStr, codeStr]
|
||||||
.filter(Boolean)
|
.filter(Boolean)
|
||||||
.join(" | code: ");
|
.join(" | code: ");
|
||||||
|
|
|
@ -89,6 +89,64 @@ export class FileManager {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Append multiple files to a record without overriding existing ones
|
||||||
|
* @param collectionName The name of the collection
|
||||||
|
* @param recordId The ID of the record to attach the files to
|
||||||
|
* @param field The field name for the files
|
||||||
|
* @param files Array of files to upload
|
||||||
|
* @returns The updated record
|
||||||
|
*/
|
||||||
|
public async appendFiles<T = any>(
|
||||||
|
collectionName: string,
|
||||||
|
recordId: string,
|
||||||
|
field: string,
|
||||||
|
files: File[]
|
||||||
|
): Promise<T> {
|
||||||
|
if (!this.auth.isAuthenticated()) {
|
||||||
|
throw new Error("User must be authenticated to upload files");
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
this.auth.setUpdating(true);
|
||||||
|
const pb = this.auth.getPocketBase();
|
||||||
|
|
||||||
|
// First, get the current record to check existing files
|
||||||
|
const record = await pb.collection(collectionName).getOne<T>(recordId);
|
||||||
|
|
||||||
|
// Create FormData with existing files
|
||||||
|
const formData = new FormData();
|
||||||
|
|
||||||
|
// Get existing files from the record
|
||||||
|
const existingFiles = (record as any)[field] || [];
|
||||||
|
|
||||||
|
// For each existing file, we need to fetch it and add it to the FormData
|
||||||
|
for (const existingFile of existingFiles) {
|
||||||
|
try {
|
||||||
|
const response = await fetch(this.getFileUrl(collectionName, recordId, existingFile));
|
||||||
|
const blob = await response.blob();
|
||||||
|
const file = new File([blob], existingFile, { type: blob.type });
|
||||||
|
formData.append(field, file);
|
||||||
|
} catch (error) {
|
||||||
|
console.warn(`Failed to fetch existing file ${existingFile}:`, error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Append new files
|
||||||
|
files.forEach(file => {
|
||||||
|
formData.append(field, file);
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await pb.collection(collectionName).update<T>(recordId, formData);
|
||||||
|
return result;
|
||||||
|
} catch (err) {
|
||||||
|
console.error(`Failed to append files to ${collectionName}:`, err);
|
||||||
|
throw err;
|
||||||
|
} finally {
|
||||||
|
this.auth.setUpdating(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the URL for a file
|
* Get the URL for a file
|
||||||
* @param collectionName The name of the collection
|
* @param collectionName The name of the collection
|
||||||
|
|
Loading…
Reference in a new issue