fix multiple files not showing

This commit is contained in:
chark1es 2025-02-11 02:41:59 -08:00
parent ac36431107
commit b0cb20d0f4
2 changed files with 1448 additions and 1284 deletions

View file

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

View file

@ -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
* @param collectionName The name of the collection