From 4065105baba59e222215ded60a546767e7c8d940 Mon Sep 17 00:00:00 2001 From: chark1es Date: Mon, 24 Feb 2025 16:16:55 -0800 Subject: [PATCH] better event form --- .../dashboard/Officer_EventRequestForm.astro | 585 ++----------- .../ASFundingSection.tsx | 623 ++++---------- .../EventDetailsSection.tsx | 269 +++--- .../EventRequestForm.tsx | 813 ++++++++++++++++++ .../EventRequestFormPreview.tsx | 324 +++++++ .../Officer_EventRequestForm/InfoCard.tsx | 78 -- .../InvoiceBuilder.tsx | 406 +++++++++ .../Officer_EventRequestForm/PRSection.tsx | 470 +++++----- .../TAPFormSection.tsx | 150 ++++ .../Officer_EventRequestForm/TAPSection.tsx | 395 --------- .../Officer_EventRequestForm/Tooltip.tsx | 157 ---- .../Officer_EventRequestForm/tooltips.ts | 99 --- src/scripts/pocketbase/Update.ts | 25 +- 13 files changed, 2311 insertions(+), 2083 deletions(-) create mode 100644 src/components/dashboard/Officer_EventRequestForm/EventRequestForm.tsx create mode 100644 src/components/dashboard/Officer_EventRequestForm/EventRequestFormPreview.tsx delete mode 100644 src/components/dashboard/Officer_EventRequestForm/InfoCard.tsx create mode 100644 src/components/dashboard/Officer_EventRequestForm/InvoiceBuilder.tsx create mode 100644 src/components/dashboard/Officer_EventRequestForm/TAPFormSection.tsx delete mode 100644 src/components/dashboard/Officer_EventRequestForm/TAPSection.tsx delete mode 100644 src/components/dashboard/Officer_EventRequestForm/Tooltip.tsx delete mode 100644 src/components/dashboard/Officer_EventRequestForm/tooltips.ts diff --git a/src/components/dashboard/Officer_EventRequestForm.astro b/src/components/dashboard/Officer_EventRequestForm.astro index 433f2c3..7f459db 100644 --- a/src/components/dashboard/Officer_EventRequestForm.astro +++ b/src/components/dashboard/Officer_EventRequestForm.astro @@ -4,551 +4,78 @@ import { Update } from "../../scripts/pocketbase/Update"; import { FileManager } from "../../scripts/pocketbase/FileManager"; import { Get } from "../../scripts/pocketbase/Get"; import { toast, Toaster } from "react-hot-toast"; - - -// Form sections -import PRSection from "./Officer_EventRequestForm/PRSection"; -import EventDetailsSection from "./Officer_EventRequestForm/EventDetailsSection"; -import TAPSection from "./Officer_EventRequestForm/TAPSection"; -import ASFundingSection from "./Officer_EventRequestForm/ASFundingSection"; - -interface EventRequest { - id: string; - created: string; - event_name: string; - event_description: string; - location: string; - start_date: string; - end_date: string; - status: 'draft' | 'pending' | 'approved' | 'rejected'; - has_food: boolean; - [key: string]: any; // For other fields we might need -} - -interface ListResponse { - page: number; - perPage: number; - totalItems: number; - totalPages: number; - items: T[]; -} - -const auth = Authentication.getInstance(); -const update = Update.getInstance(); -const fileManager = FileManager.getInstance(); -const get = Get.getInstance(); - -// Get user's submitted event requests -let userEventRequests: ListResponse = { - page: 1, - perPage: 50, - totalItems: 0, - totalPages: 0, - items: [] -}; - -if (auth.isAuthenticated()) { - try { - userEventRequests = await get.getList("event_request", 1, 50, `requested_user = "${auth.getUserId()}"`, "-created"); - } catch (error) { - console.error("Failed to fetch event requests:", error); - } -} +import EventRequestFormPreview from "./Officer_EventRequestForm/EventRequestFormPreview"; +import EventRequestForm from "./Officer_EventRequestForm/EventRequestForm"; --- -
- -

- Event Request Form -

+
+
+

Event Request Form

+

+ Submit your event request at least 6 weeks before your event. After + submitting, please notify PR and/or Coordinators in the #-events Slack + channel. +

+
+

This form includes sections for:

+
    +
  • PR Materials (if needed)
  • +
  • Event Details
  • +
  • TAP Form Information
  • +
  • AS Funding (if needed)
  • +
+

+ You can switch between the form and preview tabs at any time. Your + progress is automatically saved. +

+
+
-
- -
- - - -
- - -
+
+
+ + +
- -
-
-
-
-
-

- Do you need graphics from our design team? -

-
- - -
-
-
+
+ +
- - -
- -
- -
- - - -
- -
- - -
-
-
- - -
+
- - diff --git a/src/components/dashboard/Officer_EventRequestForm/ASFundingSection.tsx b/src/components/dashboard/Officer_EventRequestForm/ASFundingSection.tsx index a9af928..c110e06 100644 --- a/src/components/dashboard/Officer_EventRequestForm/ASFundingSection.tsx +++ b/src/components/dashboard/Officer_EventRequestForm/ASFundingSection.tsx @@ -1,475 +1,190 @@ import React, { useState } from 'react'; -import { motion, AnimatePresence } from 'framer-motion'; -import { toast } from 'react-hot-toast'; -import InfoCard from './InfoCard'; -import Tooltip from './Tooltip'; -import { tooltips, infoNotes } from './tooltips'; -import { Icon } from '@iconify/react'; +import { motion } from 'framer-motion'; +import type { EventRequestFormData } from './EventRequestForm'; +import InvoiceBuilder from './InvoiceBuilder'; +import type { InvoiceData } from './InvoiceBuilder'; -export interface InvoiceItem { - quantity: number; - item_name: string; - unit_cost: number; +// Animation variants +const itemVariants = { + hidden: { opacity: 0, y: 20 }, + visible: { + opacity: 1, + y: 0, + transition: { + type: "spring", + stiffness: 300, + damping: 24 + } + } +}; + +interface ASFundingSectionProps { + formData: EventRequestFormData; + onDataChange: (data: Partial) => void; } -interface InvoiceData { - items: InvoiceItem[]; - tax: number; - tip: number; - total: number; - vendor: string; -} +const ASFundingSection: React.FC = ({ formData, onDataChange }) => { + const [invoiceFile, setInvoiceFile] = useState(formData.invoice); + const [invoiceFiles, setInvoiceFiles] = useState(formData.invoice_files || []); -export interface ASFundingSectionProps { - onDataChange?: (data: any) => void; - onItemizedItemsUpdate?: (items: InvoiceItem[]) => void; -} - -const ASFundingSection: React.FC = ({ onDataChange, onItemizedItemsUpdate }) => { - const [invoiceData, setInvoiceData] = useState({ - items: [{ quantity: 1, item_name: '', unit_cost: 0 }], - tax: 0, - tip: 0, - total: 0, - vendor: '' - }); - - const handleItemChange = (index: number, field: keyof InvoiceItem, value: string | number) => { - const newItems = [...invoiceData.items]; - newItems[index] = { ...newItems[index], [field]: value }; - - // Calculate new total - 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); - // Send the entire invoice data object - onDataChange?.({ - 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 = () => { - 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); - 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('Item removed'); + // Handle single invoice file upload (for backward compatibility) + const handleInvoiceFileChange = (e: React.ChangeEvent) => { + if (e.target.files && e.target.files.length > 0) { + const file = e.target.files[0]; + setInvoiceFile(file); + onDataChange({ invoice: file }); } }; - const handleExtraChange = (field: 'tax' | 'tip' | 'vendor', value: string | number) => { - const numValue = field !== 'vendor' ? Number(value) : value; - const itemsTotal = invoiceData.items.reduce((sum, item) => sum + (item.quantity * item.unit_cost), 0); + // Handle multiple invoice files upload + const handleMultipleInvoiceFilesChange = (e: React.ChangeEvent) => { + if (e.target.files && e.target.files.length > 0) { + const files = Array.from(e.target.files); + setInvoiceFiles(prevFiles => [...prevFiles, ...files]); + onDataChange({ invoice_files: [...formData.invoice_files, ...files] }); - const newTotal = field === 'tax' ? - itemsTotal + Number(value) + (invoiceData.tip || 0) : - field === 'tip' ? - itemsTotal + (invoiceData.tax || 0) + Number(value) : - itemsTotal + (invoiceData.tax || 0) + (invoiceData.tip || 0); - - const newInvoiceData = { - ...invoiceData, - [field]: numValue, - total: field !== 'vendor' ? newTotal : invoiceData.total - }; - - setInvoiceData(newInvoiceData); - onDataChange?.({ - itemized_invoice: newInvoiceData, - total_amount: newTotal - }); - document.querySelector('input[name="itemized_invoice"]')?.setAttribute('value', JSON.stringify(newInvoiceData)); + // Also set the first file as the main invoice file for backward compatibility + if (files.length > 0 && !formData.invoice) { + setInvoiceFile(files[0]); + onDataChange({ invoice: files[0] }); + } + } }; - const handleFileUpload = (e: React.ChangeEvent) => { - const file = e.target.files?.[0]; - if (file) { - onDataChange?.({ invoice: file }); - toast('Invoice file uploaded'); + // Remove an invoice file + const handleRemoveInvoiceFile = (index: number) => { + const updatedFiles = [...invoiceFiles]; + updatedFiles.splice(index, 1); + setInvoiceFiles(updatedFiles); + onDataChange({ invoice_files: updatedFiles }); + + // Update the main invoice file if needed + if (index === 0 && updatedFiles.length > 0) { + setInvoiceFile(updatedFiles[0]); + onDataChange({ invoice: updatedFiles[0] }); + } else if (updatedFiles.length === 0) { + setInvoiceFile(null); + onDataChange({ invoice: null }); } }; + // Handle invoice data change + const handleInvoiceDataChange = (invoiceData: InvoiceData) => { + // Update the invoiceData in the form + onDataChange({ invoiceData }); + + // For backward compatibility, create a properly formatted JSON string + const jsonFormat = { + items: invoiceData.items.map(item => ({ + item: item.description, + quantity: item.quantity, + unit_price: item.unitPrice + })), + tax: invoiceData.taxAmount, + tip: invoiceData.tipAmount, + total: invoiceData.total, + vendor: invoiceData.vendor + }; + + // For backward compatibility, still update the itemized_invoice field + // but with a more structured format that's easier to parse if needed + const itemizedText = JSON.stringify(jsonFormat, null, 2); + + // Update the itemized_invoice field for backward compatibility + onDataChange({ itemized_invoice: itemizedText }); + }; + return ( - -
- - - AS Funding Details - +
+

AS Funding Information

-
- - - -
- - -
- -
-
-
- -
- handleExtraChange('vendor', e.target.value)} - required - /> -
- - - -
-
-
- - -
- - -
- -
-
-
- - -
- {invoiceData.items.map((item, index) => ( - -
- - handleItemChange(index, 'quantity', Number(e.target.value))} - required - /> -
-
- - handleItemChange(index, 'item_name', e.target.value)} - required - /> -
-
- -
- handleItemChange(index, 'unit_cost', Number(e.target.value))} - required - /> -
- $ -
-
-
- removeItem(index)} - disabled={invoiceData.items.length === 1} - > - - - - -
- ))} -
-
- - - - Add Item - -
- - -
-
- - -
- -
-
-
-
- handleExtraChange('tax', Number(e.target.value))} - required - /> -
- $ -
-
-
-
-
- - -
- -
-
-
-
- handleExtraChange('tip', Number(e.target.value))} - required - /> -
- $ -
-
-
-
- - -
- - -
- -
-
-
-
- -
- $ -
-
-
- - -
- - -
- -
-
-
- - - -
- -
- - - -
-
-
+
+
+ + + +

+ Please make sure the restaurant is a valid AS Funding food vendor! An invoice can be an unofficial receipt. Just make sure that the restaurant name and location, desired pickup or delivery date and time, all the items ordered plus their prices, discount/fees/tax/tip, and total are on the invoice! We don't recommend paying out of pocket because reimbursements can be a hassle when you're not a Principal Member. +

- + + {/* Invoice Builder Instructions */} + +

How to Use the Invoice Builder

+
    +
  1. Enter the vendor/restaurant name in the field provided.
  2. +
  3. Add each item from your invoice by filling in the description, quantity, and unit price, then click "Add Item".
  4. +
  5. The subtotal, tax, and tip will be calculated automatically based on the tax rate and tip percentage.
  6. +
  7. You can adjust the tax rate (default is 7.75% for San Diego) and tip percentage as needed.
  8. +
  9. Remove items by clicking the "X" button next to each item.
  10. +
  11. Upload your actual invoice file (receipt, screenshot, etc.) using the file upload below.
  12. +
+

Note: The invoice builder helps you itemize your expenses for AS funding. You must still upload the actual invoice file.

+
+ + {/* Invoice Builder */} + + + {/* Invoice file upload */} + + + + + {invoiceFiles.length > 0 && ( +
+

Uploaded files:

+
+ {invoiceFiles.map((file, index) => ( +
+ {file.name} + +
+ ))} +
+
+ )} + +

+ Official food invoices will be required 2 weeks before the start of your event. Please use the following naming format: EventName_OrderLocation_DateOfEvent (i.e. QPWorkathon#1_PapaJohns_01/06/2025) +

+
+ + +
+

Important Note

+
+ AS Funding requests must be submitted at least 6 weeks before your event. Please check the Funding Guide or the Google Calendar for the funding request deadlines. +
+
+
+
); }; diff --git a/src/components/dashboard/Officer_EventRequestForm/EventDetailsSection.tsx b/src/components/dashboard/Officer_EventRequestForm/EventDetailsSection.tsx index 642259f..2c0d68f 100644 --- a/src/components/dashboard/Officer_EventRequestForm/EventDetailsSection.tsx +++ b/src/components/dashboard/Officer_EventRequestForm/EventDetailsSection.tsx @@ -1,147 +1,144 @@ import React from 'react'; +import { motion } from 'framer-motion'; +import type { EventRequestFormData } from './EventRequestForm'; + +// Animation variants +const itemVariants = { + hidden: { opacity: 0, y: 20 }, + visible: { + opacity: 1, + y: 0, + transition: { + type: "spring", + stiffness: 300, + damping: 24 + } + } +}; interface EventDetailsSectionProps { - onDataChange?: (data: any) => void; + formData: EventRequestFormData; + onDataChange: (data: Partial) => void; } -const EventDetailsSection: React.FC = ({ onDataChange }) => { +const EventDetailsSection: React.FC = ({ formData, onDataChange }) => { return ( -
-
-

- - - - Event Details -

+
+

Event Details

-
-
- - onDataChange?.({ name: e.target.value })} - required - /> -
- -
- -