diff --git a/src/components/dashboard/Officer_EventRequestForm.astro b/src/components/dashboard/Officer_EventRequestForm.astro
index 45bd1cb..85398ea 100644
--- a/src/components/dashboard/Officer_EventRequestForm.astro
+++ b/src/components/dashboard/Officer_EventRequestForm.astro
@@ -3,6 +3,9 @@ import { Authentication } from "../../scripts/pocketbase/Authentication";
import { Update } from "../../scripts/pocketbase/Update";
import { FileManager } from "../../scripts/pocketbase/FileManager";
import { Get } from "../../scripts/pocketbase/Get";
+import { toast } from "react-hot-toast";
+import Toast from "./universal/Toast";
+import { Icon } from "@iconify/react";
// Form sections
import PRSection from "./Officer_EventRequestForm/PRSection";
@@ -55,6 +58,7 @@ if (auth.isAuthenticated()) {
---
+
@@ -286,6 +290,8 @@ if (auth.isAuthenticated()) {
import { Update } from "../../scripts/pocketbase/Update";
import { FileManager } from "../../scripts/pocketbase/FileManager";
import { Get } from "../../scripts/pocketbase/Get";
+ import { toast } from "react-hot-toast";
+ import { showLoadingToast, showSuccessToast, showErrorToast } from "./Officer_EventRequestForm/ToastNotifications";
// Add TypeScript interfaces
interface EventRequest {
@@ -330,86 +336,104 @@ if (auth.isAuthenticated()) {
form?.addEventListener("submit", async (e) => {
e.preventDefault();
- // Collect form data
- const formData = new FormData(form);
- const data: Record = {};
-
- // Convert FormData to a proper object with correct types
- formData.forEach((value, key) => {
- if (value instanceof File) {
- // Skip file fields as they'll be handled separately
- return;
- }
- data[key] = value;
- });
-
try {
- // Create event request record
const auth = Authentication.getInstance();
const update = Update.getInstance();
-
- // Add user ID to the request
const userId = auth.getUserId();
- if (userId) {
- data.requested_user = userId;
+
+ if (!userId) {
+ throw new Error('User not authenticated');
}
+ // Show loading toast
+ const loadingToastId = toast.custom(showLoadingToast, { duration: 0 });
+
+ // Get graphics need value
+ const needsGraphics = form.querySelector('input[name="needsGraphics"]:checked')?.value === "yes";
+
+ // Collect data from all sections
+ const eventData = {
+ requested_user: userId,
+ name: form.querySelector('[name="event_name"]')?.value,
+ description: form.querySelector('[name="event_description"]')?.value,
+ location: form.querySelector('[name="location"]')?.value,
+ start_date_time: new Date(form.querySelector('[name="start_date_time"]')?.value || '').toISOString(),
+ end_date_time: new Date(form.querySelector('[name="end_date_time"]')?.value || '').toISOString(),
+ flyers_needed: needsGraphics,
+ flyer_type: Array.from(form.querySelectorAll('input[type="checkbox"][name="flyer_type[]"]:checked')).map(cb => cb.value),
+ other_flyer_type: form.querySelector('[name="other_flyer_type"]')?.value,
+ flyer_advertising_start_date: form.querySelector('[name="flyer_advertising_start_date"]')?.value,
+ flyer_additional_requests: form.querySelector('[name="flyer_additional_requests"]')?.value,
+ photography_needed: form.querySelector('input[name="photography_needed"]:checked')?.value === 'true',
+ required_logos: Array.from(form.querySelectorAll('input[type="checkbox"][name="required_logos[]"]:checked')).map(cb => cb.value),
+ advertising_format: form.querySelector('[name="advertising_format"]')?.value,
+ will_or_have_room_booking: form.querySelector('input[name="will_or_have_room_booking"]:checked')?.value === 'true',
+ expected_attendance: parseInt(form.querySelector('[name="expected_attendance"]')?.value || '0'),
+ as_funding_required: form.querySelector('input[name="as_funding"]:checked')?.value === 'true',
+ food_drinks_being_served: form.querySelector('input[name="food_drinks"]:checked')?.value === 'true',
+ status: 'pending',
+ draft: false,
+ // Add itemized items if AS funding is required
+ itemized_items: form.querySelector('input[name="as_funding"]:checked')?.value === 'true'
+ ? JSON.parse(form.querySelector('[name="itemized_items"]')?.value || '[]')
+ : undefined,
+ // Add itemized invoice data
+ itemized_invoice: form.querySelector('input[name="as_funding"]:checked')?.value === 'true'
+ ? JSON.parse(form.querySelector('[name="itemized_invoice"]')?.value || '{}')
+ : undefined
+ };
+
// Create the record
- const record = await update.updateFields(
- "event_request",
- data.id || "",
- data,
- );
+ const record = await update.create("event_request", eventData);
- // Handle file uploads if any
+ // Handle file uploads
const fileManager = FileManager.getInstance();
- const fileFields = ["room_booking", "invoice", "other_logos"];
+ const fileFields = {
+ room_booking: form.querySelector('input[type="file"][name="room_booking"]')?.files?.[0],
+ itemized_invoice: form.querySelector('input[type="file"][name="itemized_invoice"]')?.files?.[0],
+ invoice: form.querySelector('input[type="file"][name="invoice"]')?.files?.[0],
+ other_logos: Array.from(form.querySelector('input[type="file"][name="other_logos"]')?.files || [])
+ };
- for (const field of fileFields) {
- const files = formData
- .getAll(field)
- .filter((f): f is File => f instanceof File);
- if (files.length > 0) {
- await fileManager.uploadFiles(
+ // Upload files one by one
+ for (const [field, files] of Object.entries(fileFields)) {
+ if (!files) continue;
+
+ if (Array.isArray(files)) {
+ // Handle multiple files (other_logos)
+ const validFiles = files.filter((f): f is File => f instanceof File);
+ if (validFiles.length > 0) {
+ await fileManager.uploadFiles(
+ "event_request",
+ record.id,
+ field,
+ validFiles
+ );
+ }
+ } else {
+ // Handle single file
+ await fileManager.uploadFile(
"event_request",
record.id,
field,
- files,
+ files
);
}
}
- // Show success message using a toast
- const toast = document.createElement("div");
- toast.className = "toast toast-end";
- toast.innerHTML = `
-
-
-
-
-
Event request submitted successfully!
-
- `;
- document.body.appendChild(toast);
- setTimeout(() => toast.remove(), 3000);
+ // Dismiss loading toast
+ toast.dismiss(loadingToastId);
- // Redirect to events management page
- window.location.href = "/dashboard/events";
+ // Show success toast
+ toast.custom(() => showSuccessToast('Event request submitted successfully!'));
+
+ // Reset form
+ form.reset();
} catch (error) {
console.error("Error submitting form:", error);
+
// Show error toast
- const toast = document.createElement("div");
- toast.className = "toast toast-end";
- toast.innerHTML = `
-
-
-
-
-
Error submitting form. Please try again.
-
- `;
- document.body.appendChild(toast);
- setTimeout(() => toast.remove(), 3000);
+ toast.custom(() => showErrorToast('Failed to submit event request. Please try again.'));
}
});
@@ -417,22 +441,21 @@ if (auth.isAuthenticated()) {
document
.getElementById("saveAsDraft")
?.addEventListener("click", async () => {
- // Similar to submit but mark as draft
- const formData = new FormData(form);
- const data: Record = {};
-
- // Convert FormData to a proper object with correct types
- formData.forEach((value, key) => {
- if (value instanceof File) {
- // Skip file fields as they'll be handled separately
- return;
- }
- data[key] = value;
- });
-
- data.status = "draft";
-
try {
+ const formData = new FormData(form);
+ const data: Record = {};
+
+ // Convert FormData to a proper object with correct types
+ formData.forEach((value, key) => {
+ if (value instanceof File) {
+ // Skip file fields as they'll be handled separately
+ return;
+ }
+ data[key] = value;
+ });
+
+ data.status = "draft";
+
const auth = Authentication.getInstance();
const update = Update.getInstance();
@@ -444,33 +467,11 @@ if (auth.isAuthenticated()) {
await update.updateFields("event_request", data.id || "", data);
// Show success toast
- const toast = document.createElement("div");
- toast.className = "toast toast-end";
- toast.innerHTML = `
-
-
-
-
-
Draft saved successfully!
-
- `;
- document.body.appendChild(toast);
- setTimeout(() => toast.remove(), 3000);
+ toast.custom(() => showSuccessToast('Draft saved successfully!'));
} catch (error) {
console.error("Error saving draft:", error);
// Show error toast
- const toast = document.createElement("div");
- toast.className = "toast toast-end";
- toast.innerHTML = `
-
-
-
-
-
Error saving draft. Please try again.
-
- `;
- document.body.appendChild(toast);
- setTimeout(() => toast.remove(), 3000);
+ toast.custom(() => showErrorToast('Error saving draft. Please try again.'));
}
});
diff --git a/src/components/dashboard/Officer_EventRequestForm/ASFundingSection.tsx b/src/components/dashboard/Officer_EventRequestForm/ASFundingSection.tsx
index 93b9b33..a9af928 100644
--- a/src/components/dashboard/Officer_EventRequestForm/ASFundingSection.tsx
+++ b/src/components/dashboard/Officer_EventRequestForm/ASFundingSection.tsx
@@ -6,7 +6,7 @@ import Tooltip from './Tooltip';
import { tooltips, infoNotes } from './tooltips';
import { Icon } from '@iconify/react';
-interface InvoiceItem {
+export interface InvoiceItem {
quantity: number;
item_name: string;
unit_cost: number;
@@ -20,13 +20,14 @@ interface InvoiceData {
vendor: string;
}
-interface ASFundingSectionProps {
+export interface ASFundingSectionProps {
onDataChange?: (data: any) => void;
+ onItemizedItemsUpdate?: (items: InvoiceItem[]) => void;
}
-const ASFundingSection: React.FC = ({ onDataChange }) => {
+const ASFundingSection: React.FC = ({ onDataChange, onItemizedItemsUpdate }) => {
const [invoiceData, setInvoiceData] = useState({
- items: [{ quantity: 0, item_name: '', unit_cost: 0 }],
+ items: [{ quantity: 1, item_name: '', unit_cost: 0 }],
tax: 0,
tip: 0,
total: 0,
@@ -39,83 +40,99 @@ const ASFundingSection: React.FC = ({ onDataChange }) =>
// Calculate new total
const itemsTotal = newItems.reduce((sum, item) => sum + (item.quantity * item.unit_cost), 0);
- const newTotal = itemsTotal + invoiceData.tax + invoiceData.tip;
+ const newTotal = itemsTotal + (invoiceData.tax || 0) + (invoiceData.tip || 0);
- setInvoiceData(prev => ({
- ...prev,
+ const newInvoiceData = {
+ ...invoiceData,
items: newItems,
total: newTotal
- }));
+ };
- // Notify parent with JSON string
+ setInvoiceData(newInvoiceData);
+ // Send the entire invoice data object
onDataChange?.({
- itemized_invoice: JSON.stringify({
- ...invoiceData,
- items: newItems,
- total: newTotal
- })
+ itemized_invoice: newInvoiceData,
+ total_amount: newTotal
});
+ // Update parent with itemized items and invoice data
+ onItemizedItemsUpdate?.(newItems);
+ document.querySelector('input[name="itemized_invoice"]')?.setAttribute('value', JSON.stringify(newInvoiceData));
};
const addItem = () => {
- setInvoiceData(prev => ({
- ...prev,
- items: [...prev.items, { quantity: 0, item_name: '', unit_cost: 0 }]
- }));
- toast('New item added', { icon: '➕' });
+ const newItems = [...invoiceData.items, { quantity: 1, item_name: '', unit_cost: 0 }];
+ const itemsTotal = newItems.reduce((sum, item) => sum + (item.quantity * item.unit_cost), 0);
+ const newTotal = itemsTotal + (invoiceData.tax || 0) + (invoiceData.tip || 0);
+
+ const newInvoiceData = {
+ ...invoiceData,
+ items: newItems,
+ total: newTotal
+ };
+
+ setInvoiceData(newInvoiceData);
+ onDataChange?.({
+ itemized_invoice: newInvoiceData,
+ total_amount: newTotal
+ });
+ onItemizedItemsUpdate?.(newItems);
+ document.querySelector('input[name="itemized_invoice"]')?.setAttribute('value', JSON.stringify(newInvoiceData));
+ toast('New item added');
};
const removeItem = (index: number) => {
if (invoiceData.items.length > 1) {
const newItems = invoiceData.items.filter((_, i) => i !== index);
-
- // Recalculate total
const itemsTotal = newItems.reduce((sum, item) => sum + (item.quantity * item.unit_cost), 0);
- const newTotal = itemsTotal + invoiceData.tax + invoiceData.tip;
+ const newTotal = itemsTotal + (invoiceData.tax || 0) + (invoiceData.tip || 0);
- setInvoiceData(prev => ({
- ...prev,
+ const newInvoiceData = {
+ ...invoiceData,
items: newItems,
total: newTotal
- }));
+ };
- // Notify parent with JSON string
+ setInvoiceData(newInvoiceData);
onDataChange?.({
- itemized_invoice: JSON.stringify({
- ...invoiceData,
- items: newItems,
- total: newTotal
- })
+ itemized_invoice: newInvoiceData,
+ total_amount: newTotal
});
- toast('Item removed', { icon: '🗑️' });
+ onItemizedItemsUpdate?.(newItems);
+ document.querySelector('input[name="itemized_invoice"]')?.setAttribute('value', JSON.stringify(newInvoiceData));
+ toast('Item removed');
}
};
const handleExtraChange = (field: 'tax' | 'tip' | 'vendor', value: string | number) => {
const numValue = field !== 'vendor' ? Number(value) : value;
-
- // Calculate new total for tax/tip changes
const itemsTotal = invoiceData.items.reduce((sum, item) => sum + (item.quantity * item.unit_cost), 0);
+
const newTotal = field === 'tax' ?
- itemsTotal + Number(value) + invoiceData.tip :
+ itemsTotal + Number(value) + (invoiceData.tip || 0) :
field === 'tip' ?
- itemsTotal + invoiceData.tax + Number(value) :
- itemsTotal + invoiceData.tax + invoiceData.tip;
+ itemsTotal + (invoiceData.tax || 0) + Number(value) :
+ itemsTotal + (invoiceData.tax || 0) + (invoiceData.tip || 0);
- setInvoiceData(prev => ({
- ...prev,
+ const newInvoiceData = {
+ ...invoiceData,
[field]: numValue,
- total: field !== 'vendor' ? newTotal : prev.total
- }));
+ total: field !== 'vendor' ? newTotal : invoiceData.total
+ };
- // Notify parent with JSON string
+ setInvoiceData(newInvoiceData);
onDataChange?.({
- itemized_invoice: JSON.stringify({
- ...invoiceData,
- [field]: numValue,
- total: field !== 'vendor' ? newTotal : invoiceData.total
- })
+ itemized_invoice: newInvoiceData,
+ total_amount: newTotal
});
+ document.querySelector('input[name="itemized_invoice"]')?.setAttribute('value', JSON.stringify(newInvoiceData));
+ };
+
+ const handleFileUpload = (e: React.ChangeEvent) => {
+ const file = e.target.files?.[0];
+ if (file) {
+ onDataChange?.({ invoice: file });
+ toast('Invoice file uploaded');
+ }
};
return (
@@ -438,14 +455,9 @@ const ASFundingSection: React.FC = ({ onDataChange }) =>
{
- onDataChange?.({ invoice: e.target.files?.[0] });
- if (e.target.files?.[0]) {
- toast('Invoice file uploaded', { icon: '📄' });
- }
- }}
required
/>
+
handleAttendanceChange(Number(e.target.value))}
+ value={expectedAttendance}
+ onChange={(e) => handleAttendanceChange(parseInt(e.target.value) || 0)}
required
/>
@@ -151,6 +220,27 @@ const TAPSection: React.FC = ({ onDataChange, onASFundingChange
required
/>
+
+
+
+
+
+ Room Booking File Upload
+
+
+
+
+
+ Max file size: 50MB
+
+
+
= ({ onDataChange, onASFundingChange
className="radio radio-primary"
checked={needsASFunding}
onChange={() => handleASFundingChange(true)}
+ value="true"
/>
Yes, I need AS Funding
@@ -195,6 +286,7 @@ const TAPSection: React.FC = ({ onDataChange, onASFundingChange
className="radio radio-primary"
checked={!needsASFunding}
onChange={() => handleASFundingChange(false)}
+ value="false"
/>
No, I don't need AS Funding
@@ -236,6 +328,7 @@ const TAPSection: React.FC = ({ onDataChange, onASFundingChange
className="radio radio-primary"
checked={needsFoodDrinks}
onChange={() => handleFoodDrinksChange(true)}
+ value="true"
/>
Yes, I need food/drinks
@@ -246,6 +339,7 @@ const TAPSection: React.FC = ({ onDataChange, onASFundingChange
className="radio radio-primary"
checked={!needsFoodDrinks}
onChange={() => handleFoodDrinksChange(false)}
+ value="false"
/>
No, I don't need food/drinks
@@ -257,6 +351,24 @@ const TAPSection: React.FC = ({ onDataChange, onASFundingChange
+
+
+ sum + (item.quantity * item.unit_cost), 0),
+ vendor: ''
+ })}
+ />
+
{needsASFunding && (
= ({ onDataChange, onASFundingChange
}}
className="mt-8"
>
- {children}
+ {children && React.cloneElement(children, {
+ onItemizedItemsUpdate: handleItemizedItemsUpdate
+ })}
)}
diff --git a/src/components/dashboard/Officer_EventRequestForm/ToastNotifications.tsx b/src/components/dashboard/Officer_EventRequestForm/ToastNotifications.tsx
new file mode 100644
index 0000000..8ec7580
--- /dev/null
+++ b/src/components/dashboard/Officer_EventRequestForm/ToastNotifications.tsx
@@ -0,0 +1,37 @@
+import React from 'react';
+import { Icon } from '@iconify/react';
+
+export const showLoadingToast = () => (
+
+
+ Submitting event request...
+
+);
+
+export const showSuccessToast = (message: string) => (
+
+
+ {message}
+
+);
+
+export const showErrorToast = (message: string) => (
+
+
+ {message}
+
+);
+
+export const showWarningToast = (message: string) => (
+
+
+ {message}
+
+);
+
+export const showInfoToast = (message: string) => (
+
+
+ {message}
+
+);
\ No newline at end of file
diff --git a/src/components/dashboard/universal/Toast.tsx b/src/components/dashboard/universal/Toast.tsx
new file mode 100644
index 0000000..79dc2e2
--- /dev/null
+++ b/src/components/dashboard/universal/Toast.tsx
@@ -0,0 +1,44 @@
+import React from 'react';
+import { Toaster } from 'react-hot-toast';
+
+// This is just a wrapper component to make react-hot-toast work with Astro
+const Toast: React.FC = () => {
+ return (
+
+ );
+};
+
+export default Toast;
\ No newline at end of file
diff --git a/src/scripts/pocketbase/Update.ts b/src/scripts/pocketbase/Update.ts
index b5a77b5..ac3afb4 100644
--- a/src/scripts/pocketbase/Update.ts
+++ b/src/scripts/pocketbase/Update.ts
@@ -42,6 +42,34 @@ export class Update {
return Update.instance;
}
+ /**
+ * Create a new record
+ * @param collectionName The name of the collection
+ * @param data The data for the new record
+ * @returns The created record
+ */
+ public async create(
+ collectionName: string,
+ data: Record,
+ ): Promise {
+ if (!this.auth.isAuthenticated()) {
+ throw new Error("User must be authenticated to create records");
+ }
+
+ try {
+ this.auth.setUpdating(true);
+ const pb = this.auth.getPocketBase();
+ const convertedData = convertLocalToUTC(data);
+ const result = await pb.collection(collectionName).create(convertedData);
+ return result;
+ } catch (err) {
+ console.error(`Failed to create record in ${collectionName}:`, err);
+ throw err;
+ } finally {
+ this.auth.setUpdating(false);
+ }
+ }
+
/**
* Update a single field in a record
* @param collectionName The name of the collection
@@ -97,6 +125,12 @@ export class Update {
this.auth.setUpdating(true);
const pb = this.auth.getPocketBase();
const convertedUpdates = convertLocalToUTC(updates);
+
+ // If recordId is empty, create a new record instead of updating
+ if (!recordId) {
+ return this.create(collectionName, convertedUpdates);
+ }
+
const result = await pb
.collection(collectionName)
.update(recordId, convertedUpdates);