diff --git a/src/components/auth/EventAuth.ts b/src/components/auth/EventAuth.ts new file mode 100644 index 0000000..a86d70f --- /dev/null +++ b/src/components/auth/EventAuth.ts @@ -0,0 +1,412 @@ +import PocketBase from "pocketbase"; +import yaml from "js-yaml"; +import configYaml from "../../data/storeConfig.yaml?raw"; + +// Configuration type definitions +interface Config { + api: { + baseUrl: string; + }; + ui: { + messages: { + event: { + saving: string; + success: string; + error: string; + deleting: string; + deleteSuccess: string; + deleteError: string; + messageTimeout: number; + }; + }; + defaults: { + pageSize: number; + sortField: string; + }; + }; +} + +// Parse YAML configuration with type +const config = yaml.load(configYaml) as Config; + +interface Event { + id: string; + event_id: string; + event_name: string; + event_code: string; + registered_users: string; // JSON string + points_to_reward: number; + start_date: string; + end_date: string; + collectionId: string; + collectionName: string; +} + +interface AuthElements { + eventList: HTMLTableSectionElement; + eventSearch: HTMLInputElement; + searchEvents: HTMLButtonElement; + addEvent: HTMLButtonElement; + eventEditor: HTMLDialogElement; + editorEventId: HTMLInputElement; + editorEventName: HTMLInputElement; + editorEventCode: HTMLInputElement; + editorStartDate: HTMLInputElement; + editorEndDate: HTMLInputElement; + editorPointsToReward: HTMLInputElement; + saveEventButton: HTMLButtonElement; +} + +export class EventAuth { + private pb: PocketBase; + private elements: AuthElements; + private cachedEvents: Event[] = []; + + constructor() { + this.pb = new PocketBase(config.api.baseUrl); + this.elements = this.getElements(); + this.init(); + } + + private getElements(): AuthElements { + const eventList = document.getElementById("eventList") as HTMLTableSectionElement; + const eventSearch = document.getElementById("eventSearch") as HTMLInputElement; + const searchEvents = document.getElementById("searchEvents") as HTMLButtonElement; + const addEvent = document.getElementById("addEvent") as HTMLButtonElement; + const eventEditor = document.getElementById("eventEditor") as HTMLDialogElement; + const editorEventId = document.getElementById("editorEventId") as HTMLInputElement; + const editorEventName = document.getElementById("editorEventName") as HTMLInputElement; + const editorEventCode = document.getElementById("editorEventCode") as HTMLInputElement; + const editorStartDate = document.getElementById("editorStartDate") as HTMLInputElement; + const editorEndDate = document.getElementById("editorEndDate") as HTMLInputElement; + const editorPointsToReward = document.getElementById("editorPointsToReward") as HTMLInputElement; + const saveEventButton = document.getElementById("saveEventButton") as HTMLButtonElement; + + if ( + !eventList || + !eventSearch || + !searchEvents || + !addEvent || + !eventEditor || + !editorEventId || + !editorEventName || + !editorEventCode || + !editorStartDate || + !editorEndDate || + !editorPointsToReward || + !saveEventButton + ) { + throw new Error("Required DOM elements not found"); + } + + return { + eventList, + eventSearch, + searchEvents, + addEvent, + eventEditor, + editorEventId, + editorEventName, + editorEventCode, + editorStartDate, + editorEndDate, + editorPointsToReward, + saveEventButton, + }; + } + + private getRegisteredUsersCount(registeredUsers: string): number { + try { + if (!registeredUsers) return 0; + const users = JSON.parse(registeredUsers); + return Array.isArray(users) ? users.length : 0; + } catch (err) { + console.warn("Failed to parse registered_users:", err); + return 0; + } + } + + private async fetchEvents(searchQuery: string = "") { + try { + // Only fetch from API if we don't have cached data + if (this.cachedEvents.length === 0) { + const records = await this.pb.collection("events").getList(1, config.ui.defaults.pageSize, { + sort: config.ui.defaults.sortField, + }); + this.cachedEvents = records.items as Event[]; + } + + // Filter cached data based on search query + let filteredEvents = this.cachedEvents; + if (searchQuery) { + const terms = searchQuery.toLowerCase().split(" ").filter(term => term.length > 0); + if (terms.length > 0) { + filteredEvents = this.cachedEvents.filter(event => { + return terms.every(term => + (event.event_name?.toLowerCase().includes(term) || + event.event_id?.toLowerCase().includes(term) || + event.event_code?.toLowerCase().includes(term)) + ); + }); + } + } + + const { eventList } = this.elements; + const fragment = document.createDocumentFragment(); + + if (filteredEvents.length === 0) { + const row = document.createElement("tr"); + row.innerHTML = ` + + ${searchQuery ? "No events found matching your search." : "No events found."} + + `; + fragment.appendChild(row); + } else { + filteredEvents.forEach((event) => { + const row = document.createElement("tr"); + const startDate = event.start_date ? new Date(event.start_date).toLocaleString() : "N/A"; + const endDate = event.end_date ? new Date(event.end_date).toLocaleString() : "N/A"; + const registeredCount = this.getRegisteredUsersCount(event.registered_users); + + row.innerHTML = ` + + +
+
${event.event_name || "N/A"}
+
Event ID: ${event.event_id || "N/A"}
+
Code: ${event.event_code || "N/A"}
+
Start: ${startDate}
+
End: ${endDate}
+
Points: ${event.points_to_reward || 0}
+
Registered: ${registeredCount}
+
+ + +
+
+ + + + + ${event.event_id || "N/A"} + ${event.event_code || "N/A"} + ${startDate} + ${endDate} + ${event.points_to_reward || 0} + ${registeredCount} + +
+ + +
+ + `; + + fragment.appendChild(row); + }); + } + + eventList.innerHTML = ""; + eventList.appendChild(fragment); + + // Setup event listeners for edit and delete buttons + const editButtons = eventList.querySelectorAll(".edit-event"); + editButtons.forEach((button) => { + button.addEventListener("click", () => { + const eventId = (button as HTMLButtonElement).dataset.eventId; + if (eventId) { + this.handleEventEdit(eventId); + } + }); + }); + + const deleteButtons = eventList.querySelectorAll(".delete-event"); + deleteButtons.forEach((button) => { + button.addEventListener("click", () => { + const eventId = (button as HTMLButtonElement).dataset.eventId; + if (eventId) { + this.handleEventDelete(eventId); + } + }); + }); + } catch (err) { + console.error("Failed to fetch events:", err); + const { eventList } = this.elements; + eventList.innerHTML = ` + + + Failed to fetch events. Please try again. + + + `; + } + } + + private async handleEventEdit(eventId: string) { + try { + const event = await this.pb.collection("events").getOne(eventId); + const { + eventEditor, + editorEventId, + editorEventName, + editorEventCode, + editorStartDate, + editorEndDate, + editorPointsToReward, + saveEventButton, + } = this.elements; + + // Populate the form + editorEventId.value = event.event_id || ""; + editorEventName.value = event.event_name || ""; + editorEventCode.value = event.event_code || ""; + editorStartDate.value = event.start_date ? new Date(event.start_date).toISOString().slice(0, 16) : ""; + editorEndDate.value = event.end_date ? new Date(event.end_date).toISOString().slice(0, 16) : ""; + editorPointsToReward.value = event.points_to_reward?.toString() || "0"; + + // Store the event ID for saving + saveEventButton.dataset.eventId = eventId; + + // Disable event_id field for existing events + editorEventId.disabled = true; + + // Show the dialog + eventEditor.showModal(); + } catch (err) { + console.error("Failed to load event for editing:", err); + } + } + + private async handleEventSave() { + const { + eventEditor, + editorEventId, + editorEventName, + editorEventCode, + editorStartDate, + editorEndDate, + editorPointsToReward, + saveEventButton, + } = this.elements; + + const eventId = saveEventButton.dataset.eventId; + const isNewEvent = !eventId; + + try { + const eventData: Record = { + event_name: editorEventName.value, + event_code: editorEventCode.value, + start_date: editorStartDate.value, + end_date: editorEndDate.value, + points_to_reward: parseInt(editorPointsToReward.value) || 0, + }; + + // Only set registered_users for new events + if (isNewEvent) { + eventData.event_id = editorEventId.value; + eventData.registered_users = "[]"; + } + + if (isNewEvent) { + await this.pb.collection("events").create(eventData); + } else { + await this.pb.collection("events").update(eventId, eventData); + } + + // Close the dialog and refresh the table + eventEditor.close(); + this.cachedEvents = []; // Clear cache to force refresh + this.fetchEvents(); + } catch (err) { + console.error("Failed to save event:", err); + } + } + + private async handleEventDelete(eventId: string) { + if (confirm("Are you sure you want to delete this event?")) { + try { + await this.pb.collection("events").delete(eventId); + this.cachedEvents = []; // Clear cache to force refresh + this.fetchEvents(); + } catch (err) { + console.error("Failed to delete event:", err); + } + } + } + + private init() { + // Initial fetch + this.fetchEvents(); + + // Search functionality + const handleSearch = () => { + const searchQuery = this.elements.eventSearch.value.trim(); + this.fetchEvents(searchQuery); + }; + + // Real-time search + this.elements.eventSearch.addEventListener("input", handleSearch); + + // Search button click handler + this.elements.searchEvents.addEventListener("click", handleSearch); + + // Add event button + this.elements.addEvent.addEventListener("click", () => { + const { eventEditor, editorEventId, saveEventButton } = this.elements; + + // Clear the form + this.elements.editorEventId.value = ""; + this.elements.editorEventName.value = ""; + this.elements.editorEventCode.value = ""; + this.elements.editorStartDate.value = ""; + this.elements.editorEndDate.value = ""; + this.elements.editorPointsToReward.value = "0"; + + // Enable event_id field for new events + editorEventId.disabled = false; + + // Clear the event ID to indicate this is a new event + saveEventButton.dataset.eventId = ""; + + // Show the dialog + eventEditor.showModal(); + }); + + // Event editor dialog + const { eventEditor, saveEventButton } = this.elements; + + // Close dialog when clicking outside + eventEditor.addEventListener("click", (e) => { + if (e.target === eventEditor) { + eventEditor.close(); + } + }); + + // Save event button + saveEventButton.addEventListener("click", (e) => { + e.preventDefault(); + this.handleEventSave(); + }); + } +} \ No newline at end of file diff --git a/src/components/store/EventEditor.astro b/src/components/store/EventEditor.astro new file mode 100644 index 0000000..062574c --- /dev/null +++ b/src/components/store/EventEditor.astro @@ -0,0 +1,96 @@ + + + + + diff --git a/src/components/store/EventManagement.astro b/src/components/store/EventManagement.astro new file mode 100644 index 0000000..11428b6 --- /dev/null +++ b/src/components/store/EventManagement.astro @@ -0,0 +1,82 @@ +--- +import EventEditor from "./EventEditor.astro"; +--- + +
+

Event Management

+
+
+ +
+
+ + +
+
+
+
+ + + + + + + + + + + + + + + + +
+
+ + + + diff --git a/src/components/store/MemberManagement.astro b/src/components/store/MemberManagement.astro new file mode 100644 index 0000000..ca2a9da --- /dev/null +++ b/src/components/store/MemberManagement.astro @@ -0,0 +1,67 @@ +
+

Member Management

+
+
+ +
+
+ + +
+
+
+
+ + + + + + + + + + + + + + +
+
diff --git a/src/data/storeConfig.yaml b/src/data/storeConfig.yaml index 106c9d1..3cf86d0 100644 --- a/src/data/storeConfig.yaml +++ b/src/data/storeConfig.yaml @@ -53,6 +53,15 @@ ui: deleteError: Failed to delete resume. Please try again. messageTimeout: 3000 + event: + saving: Saving event... + success: Event saved successfully! + error: Failed to save event. Please try again. + deleting: Deleting event... + deleteSuccess: Event deleted successfully! + deleteError: Failed to delete event. Please try again. + messageTimeout: 3000 + auth: loginError: Failed to start authentication notSignedIn: Not signed in