From 2501fe4ed893bcd6fb362977d4405ca4b74d7720 Mon Sep 17 00:00:00 2001 From: chark1es Date: Mon, 24 Feb 2025 12:39:27 -0800 Subject: [PATCH] fixed itemized selection and added toasts --- .../dashboard/Officer_EventRequestForm.astro | 197 +++++++++--------- .../ASFundingSection.tsx | 120 ++++++----- .../EventDetailsSection.tsx | 4 +- .../Officer_EventRequestForm/PRSection.tsx | 32 ++- .../Officer_EventRequestForm/TAPSection.tsx | 144 +++++++++++-- .../ToastNotifications.tsx | 37 ++++ src/components/dashboard/universal/Toast.tsx | 44 ++++ src/scripts/pocketbase/Update.ts | 34 +++ 8 files changed, 439 insertions(+), 173 deletions(-) create mode 100644 src/components/dashboard/Officer_EventRequestForm/ToastNotifications.tsx create mode 100644 src/components/dashboard/universal/Toast.tsx 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 />
diff --git a/src/components/dashboard/Officer_EventRequestForm/EventDetailsSection.tsx b/src/components/dashboard/Officer_EventRequestForm/EventDetailsSection.tsx index 25e44f4..642259f 100644 --- a/src/components/dashboard/Officer_EventRequestForm/EventDetailsSection.tsx +++ b/src/components/dashboard/Officer_EventRequestForm/EventDetailsSection.tsx @@ -27,7 +27,7 @@ const EventDetailsSection: React.FC = ({ onDataChange onDataChange?.({ name: e.target.value })} required @@ -44,7 +44,7 @@ const EventDetailsSection: React.FC = ({ onDataChange