From e829eef69f150e732473ecdc188edd187c22d483 Mon Sep 17 00:00:00 2001 From: chark1es Date: Tue, 11 Mar 2025 06:46:40 -0700 Subject: [PATCH] better view of the event submissions does not improve view for event management --- bun.lock | 5 +- package.json | 1 - .../dashboard/Officer_EventRequestForm.astro | 109 +- .../ASFundingSection.tsx | 36 +- .../EventRequestFormPreview.tsx | 1095 +++++++++++------ .../UserEventRequests.tsx | 592 +++------ .../reimbursement/ReimbursementList.tsx | 2 +- 7 files changed, 968 insertions(+), 872 deletions(-) diff --git a/bun.lock b/bun.lock index d9b694c..7b10398 100644 --- a/bun.lock +++ b/bun.lock @@ -17,7 +17,6 @@ "astro": "5.1.1", "astro-expressive-code": "^0.40.2", "astro-icon": "^1.1.5", - "axios": "^1.8.2", "chart.js": "^4.4.7", "dexie": "^4.0.11", "framer-motion": "^12.4.4", @@ -438,7 +437,7 @@ "autoprefixer": ["autoprefixer@10.4.20", "", { "dependencies": { "browserslist": "^4.23.3", "caniuse-lite": "^1.0.30001646", "fraction.js": "^4.3.7", "normalize-range": "^0.1.2", "picocolors": "^1.0.1", "postcss-value-parser": "^4.2.0" }, "peerDependencies": { "postcss": "^8.1.0" }, "bin": { "autoprefixer": "bin/autoprefixer" } }, "sha512-XY25y5xSv/wEoqzDyXXME4AFfkZI0P23z6Fs3YgymDnKJkCGOnkL0iTxCa85UTqaSgfcqyf3UA6+c7wUvx/16g=="], - "axios": ["axios@1.8.2", "", { "dependencies": { "follow-redirects": "^1.15.6", "form-data": "^4.0.0", "proxy-from-env": "^1.1.0" } }, "sha512-ls4GYBm5aig9vWx8AWDSGLpnpDQRtWAfrjU+EuytuODrFBkqesN2RkOQCBzrA1RQNHw1SmRMSDDDSwzNAYQ6Rg=="], + "axios": ["axios@1.7.9", "", { "dependencies": { "follow-redirects": "^1.15.6", "form-data": "^4.0.0", "proxy-from-env": "^1.1.0" } }, "sha512-LhLcE7Hbiryz8oMDdDptSrWowmB4Bl6RCt6sIJKpRB4XtVf0iEgewX3au/pJqm+Py1kCASkb/FFKjxQaLtxJvw=="], "axobject-query": ["axobject-query@4.1.0", "", {}, "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ=="], @@ -1428,8 +1427,6 @@ "@expressive-code/plugin-shiki/shiki": ["shiki@1.29.2", "", { "dependencies": { "@shikijs/core": "1.29.2", "@shikijs/engine-javascript": "1.29.2", "@shikijs/engine-oniguruma": "1.29.2", "@shikijs/langs": "1.29.2", "@shikijs/themes": "1.29.2", "@shikijs/types": "1.29.2", "@shikijs/vscode-textmate": "^10.0.1", "@types/hast": "^3.0.4" } }, "sha512-njXuliz/cP+67jU2hukkxCNuH1yUi4QfdZZY+sMr5PPrIyXSu5iTb/qYC4BiWWB0vZ+7TbdvYUCeL23zpwCfbg=="], - "@iconify/tools/axios": ["axios@1.7.9", "", { "dependencies": { "follow-redirects": "^1.15.6", "form-data": "^4.0.0", "proxy-from-env": "^1.1.0" } }, "sha512-LhLcE7Hbiryz8oMDdDptSrWowmB4Bl6RCt6sIJKpRB4XtVf0iEgewX3au/pJqm+Py1kCASkb/FFKjxQaLtxJvw=="], - "@isaacs/cliui/string-width": ["string-width@5.1.2", "", { "dependencies": { "eastasianwidth": "^0.2.0", "emoji-regex": "^9.2.2", "strip-ansi": "^7.0.1" } }, "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA=="], "@isaacs/cliui/wrap-ansi": ["wrap-ansi@8.1.0", "", { "dependencies": { "ansi-styles": "^6.1.0", "string-width": "^5.0.1", "strip-ansi": "^7.0.1" } }, "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ=="], diff --git a/package.json b/package.json index cd61df5..22baf45 100644 --- a/package.json +++ b/package.json @@ -24,7 +24,6 @@ "astro": "5.1.1", "astro-expressive-code": "^0.40.2", "astro-icon": "^1.1.5", - "axios": "^1.8.2", "chart.js": "^4.4.7", "dexie": "^4.0.11", "framer-motion": "^12.4.4", diff --git a/src/components/dashboard/Officer_EventRequestForm.astro b/src/components/dashboard/Officer_EventRequestForm.astro index 399507a..9ed4986 100644 --- a/src/components/dashboard/Officer_EventRequestForm.astro +++ b/src/components/dashboard/Officer_EventRequestForm.astro @@ -5,7 +5,7 @@ import EventRequestForm from "./Officer_EventRequestForm/EventRequestForm"; import UserEventRequests from "./Officer_EventRequestForm/UserEventRequests"; import { Collections } from "../../schemas/pocketbase/schema"; import { DataSyncService } from "../../scripts/database/DataSyncService"; -import { EventRequestFormPreviewModal } from "./Officer_EventRequestForm/EventRequestFormPreview"; +import { EventRequestFormPreviewModalWrapper } from "./Officer_EventRequestForm/EventRequestFormPreview"; // Import the EventRequest type from UserEventRequests to ensure consistency import type { EventRequest as UserEventRequest } from "./Officer_EventRequestForm/UserEventRequests"; @@ -116,123 +116,72 @@ if (auth.isAuthenticated()) { + + + - - - - - diff --git a/src/components/dashboard/Officer_EventRequestForm/ASFundingSection.tsx b/src/components/dashboard/Officer_EventRequestForm/ASFundingSection.tsx index 4069910..e762149 100644 --- a/src/components/dashboard/Officer_EventRequestForm/ASFundingSection.tsx +++ b/src/components/dashboard/Officer_EventRequestForm/ASFundingSection.tsx @@ -333,30 +333,24 @@ const ASFundingSection: React.FC = ({ formData, onDataCha

Invoice Details

- - + + +
{showJsonInput ? ( diff --git a/src/components/dashboard/Officer_EventRequestForm/EventRequestFormPreview.tsx b/src/components/dashboard/Officer_EventRequestForm/EventRequestFormPreview.tsx index 77484fe..ec9e950 100644 --- a/src/components/dashboard/Officer_EventRequestForm/EventRequestFormPreview.tsx +++ b/src/components/dashboard/Officer_EventRequestForm/EventRequestFormPreview.tsx @@ -5,498 +5,847 @@ import type { InvoiceItem } from './InvoiceBuilder'; import type { EventRequest } from '../../../schemas/pocketbase'; import { FlyerTypes, LogoOptions, EventRequestStatus } from '../../../schemas/pocketbase'; import CustomAlert from '../universal/CustomAlert'; +import { Icon } from '@iconify/react'; +import axios from 'axios'; + +// Define modal props interface +interface EventRequestFormPreviewModalProps { + formData: EventRequestFormData; + closeModal: () => void; +} + +// Animation variants +const containerVariants = { + hidden: { opacity: 0 }, + visible: { + opacity: 1, + transition: { + staggerChildren: 0.05, + delayChildren: 0.1, + duration: 0.3 + } + }, + exit: { + opacity: 0, + transition: { duration: 0.2 } + } +}; + +const sectionVariants = { + hidden: { opacity: 0, y: 20 }, + visible: { + opacity: 1, + y: 0, + transition: { + type: "spring", + stiffness: 400, + damping: 30, + mass: 0.8 + } + } +}; + +const modalVariants = { + hidden: { opacity: 0, scale: 0.95 }, + visible: { + opacity: 1, + scale: 1, + transition: { + duration: 0.3, + ease: [0.19, 1.0, 0.22, 1.0] // Ease out expo + } + }, + exit: { + opacity: 0, + scale: 0.95, + transition: { + duration: 0.2, + ease: "easeIn" + } + } +}; + +// Helper function to normalize EventRequest to match EventRequestFormData structure +const normalizeFormData = (data: EventRequestFormData | (EventRequest & { + invoiceData?: any; + needs_as_funding?: boolean; +})): EventRequestFormData => { + // If it's already EventRequestFormData, return it + if ('needs_as_funding' in data && data.needs_as_funding !== undefined && 'invoiceData' in data) { + return data as EventRequestFormData; + } + + // Convert EventRequest to EventRequestFormData format + const eventRequest = data as EventRequest & { + invoiceData?: any; + needs_as_funding?: boolean; + }; + + try { + // Parse invoice data + let invoiceData: { + vendor?: string; + items: any[]; + subtotal: number; + taxAmount: number; + tipAmount: number; + total: number; + } = { + items: [], + subtotal: 0, + taxAmount: 0, + tipAmount: 0, + total: 0 + }; + + // Parse existing invoice data if available + if (eventRequest.itemized_invoice) { + if (typeof eventRequest.itemized_invoice === 'string') { + try { + const parsed = JSON.parse(eventRequest.itemized_invoice || '{}'); + if (parsed && typeof parsed === 'object') { + invoiceData = { + ...invoiceData, + ...(parsed as any), + items: Array.isArray((parsed as any).items) ? (parsed as any).items : [], + }; + } + } catch (e) { + console.error('Error parsing itemized_invoice:', e); + } + } else if (typeof eventRequest.itemized_invoice === 'object' && eventRequest.itemized_invoice !== null) { + const parsed = eventRequest.itemized_invoice as any; + invoiceData = { + ...invoiceData, + ...parsed, + items: Array.isArray(parsed.items) ? parsed.items : [], + }; + } + } else if (eventRequest.invoiceData) { + const parsed = eventRequest.invoiceData as any; + if (parsed && typeof parsed === 'object') { + invoiceData = { + ...invoiceData, + ...parsed, + items: Array.isArray(parsed.items) ? parsed.items : [], + }; + } + } + + // Calculate subtotal if not set + if (typeof invoiceData.subtotal !== 'number' || isNaN(invoiceData.subtotal)) { + invoiceData.subtotal = invoiceData.items.reduce((sum: number, item: any) => { + const amount = typeof item.amount === 'number' ? item.amount : 0; + return sum + amount; + }, 0); + } + + // Ensure tax and tip amounts are numbers + if (typeof invoiceData.taxAmount !== 'number' || isNaN(invoiceData.taxAmount)) { + invoiceData.taxAmount = 0; + } + + if (typeof invoiceData.tipAmount !== 'number' || isNaN(invoiceData.tipAmount)) { + invoiceData.tipAmount = 0; + } + + // Calculate total if not set + if (typeof invoiceData.total !== 'number' || isNaN(invoiceData.total)) { + invoiceData.total = invoiceData.subtotal + invoiceData.taxAmount + invoiceData.tipAmount; + } + + // Create a normalized object that implements the EventRequestFormData interface + const normalized = { + name: eventRequest.name, + location: eventRequest.location, + start_date_time: eventRequest.start_date_time, + end_date_time: eventRequest.end_date_time, + event_description: eventRequest.event_description || '', + flyers_needed: eventRequest.flyers_needed || false, + photography_needed: eventRequest.photography_needed || false, + flyer_type: eventRequest.flyer_type || [], + other_flyer_type: eventRequest.other_flyer_type || '', + flyer_advertising_start_date: eventRequest.flyer_advertising_start_date || '', + advertising_format: eventRequest.advertising_format || '', + required_logos: eventRequest.required_logos || [], + other_logos: [] as File[], // EventRequest has this as strings but we need File[] + flyer_additional_requests: eventRequest.flyer_additional_requests || '', + will_or_have_room_booking: eventRequest.will_or_have_room_booking || false, + room_booking: null, + room_booking_confirmation: [] as File[], + expected_attendance: eventRequest.expected_attendance || 0, + food_drinks_being_served: eventRequest.food_drinks_being_served || false, + needs_as_funding: eventRequest.needs_as_funding ?? eventRequest.as_funding_required ?? false, + as_funding_required: eventRequest.as_funding_required || false, + invoice: null, + invoice_files: [] as File[], + invoiceData: invoiceData, + needs_graphics: eventRequest.needs_graphics ?? eventRequest.flyers_needed ?? false, + status: eventRequest.status || '', + created_by: eventRequest.requested_user || '', + id: eventRequest.id || '', + created: eventRequest.created || '', + updated: eventRequest.updated || '', + itemized_invoice: eventRequest.itemized_invoice || '' + }; + + return normalized as unknown as EventRequestFormData; + } catch (error) { + console.error("Error normalizing form data:", error); + // Return a minimal valid object to prevent rendering errors + return { + name: eventRequest.name || 'Unknown Event', + location: eventRequest.location || '', + start_date_time: eventRequest.start_date_time || new Date().toISOString(), + end_date_time: eventRequest.end_date_time || new Date().toISOString(), + event_description: eventRequest.event_description || '', + flyers_needed: false, + photography_needed: false, + as_funding_required: false, + food_drinks_being_served: false, + flyer_type: [], + other_flyer_type: '', + flyer_advertising_start_date: '', + flyer_additional_requests: '', + required_logos: [], + other_logos: [] as File[], + advertising_format: '', + will_or_have_room_booking: false, + expected_attendance: 0, + room_booking: null, + invoice: null, + invoice_files: [] as File[], + invoiceData: { + items: [], + subtotal: 0, + taxAmount: 0, + tipAmount: 0, + total: 0 + }, + needs_graphics: false + } as unknown as EventRequestFormData; + } +}; // Create a standalone component that can be used to show the preview as a modal -export const EventRequestFormPreviewModal: React.FC = () => { - const [isOpen, setIsOpen] = useState(false); - const [formData, setFormData] = useState(null); +export const EventRequestFormPreviewModal = ({ formData, closeModal }: EventRequestFormPreviewModalProps) => { + console.log("EventRequestFormPreviewModal rendered with formData:", formData); - // Function to handle showing the modal - const showModal = (data: any) => { - // console.log('showModal called with data', data); - setFormData(data); - setIsOpen(true); - }; + // Normalize the form data to ensure it's in the correct format + const normalizedFormData = normalizeFormData(formData); + console.log("Normalized formData:", normalizedFormData); - // Add the global function to the window object directly from the component useEffect(() => { - // Store the original function if it exists - const originalFunction = window.showEventRequestFormPreview; + console.log("Modal opened with styles applied"); + const body = document.body; + body.style.overflow = 'hidden'; // Prevent background scrolling - // Define the global function - window.showEventRequestFormPreview = (data: any) => { - // console.log('Global showEventRequestFormPreview called with data', data); - showModal(data); - }; - - // Listen for the custom event as a fallback - const handleShowModal = (event: CustomEvent) => { - // console.log('Received showEventRequestPreviewModal event', event.detail); - if (event.detail && event.detail.formData) { - showModal(event.detail.formData); - } else { - // console.error('Event detail or formData is missing', event.detail); - } - }; - - // Add event listener - document.addEventListener('showEventRequestPreviewModal', handleShowModal as EventListener); - // console.log('Event listener for showEventRequestPreviewModal added'); - - // Clean up return () => { - // Restore the original function if it existed - if (originalFunction) { - window.showEventRequestFormPreview = originalFunction; - } else { - // Otherwise delete our function - delete window.showEventRequestFormPreview; - } - - document.removeEventListener('showEventRequestPreviewModal', handleShowModal as EventListener); - // console.log('Event listener for showEventRequestPreviewModal removed'); + body.style.overflow = ''; // Restore scrolling when modal closes }; - }, []); // Empty dependency array - only run once on mount + }, []); - const handleClose = () => { - // console.log('Modal closed'); - setIsOpen(false); - }; - - // Force the modal to be in the document body to avoid nesting issues return (
{ + if (e.target === e.currentTarget) { + closeModal(); + } }} > - +
e.stopPropagation()} + > +
+

Event Request Preview

+ +
+
+ +
+
); }; +// Define the interface for the EventRequestFormPreview component interface EventRequestFormPreviewProps { - formData?: EventRequestFormData; // Optional prop to directly pass form data + formData?: EventRequestFormData | (EventRequest & { + invoiceData?: any; + needs_as_funding?: boolean; + }); // Accept both form data and event request types isOpen?: boolean; // Control whether the modal is open onClose?: () => void; // Callback when modal is closed isModal?: boolean; // Whether to render as a modal or inline component } +// Define the main EventRequestFormPreview component const EventRequestFormPreview: React.FC = ({ formData: propFormData, isOpen = true, onClose = () => { }, isModal = false }) => { - const [formData, setFormData] = useState(propFormData || null); - const [loading, setLoading] = useState(!propFormData); + console.log("EventRequestFormPreview received props:", { propFormData, isOpen, isModal }); - // Load form data from localStorage on initial load and when updated + const [formData, setFormData] = useState( + propFormData ? normalizeFormData(propFormData) : null + ); + const [loading, setLoading] = useState(propFormData ? false : true); + + // Log whenever formData changes for debugging + useEffect(() => { + console.log("EventRequestFormPreview formData state:", formData); + }, [formData]); + + // Load form data from local storage if not provided via props useEffect(() => { - // If formData is provided as a prop, use it directly if (propFormData) { - setFormData(propFormData); + setFormData(normalizeFormData(propFormData)); setLoading(false); - return; + } else { + loadFormData(); } - const loadFormData = () => { - setLoading(true); - const savedData = localStorage.getItem('eventRequestFormData'); - if (savedData) { - try { - const parsedData = JSON.parse(savedData); - setFormData(parsedData); - } catch (e) { - // console.error('Error parsing saved form data:', e); - } - } - setLoading(false); - }; - - // Load initial data - loadFormData(); - // Listen for form data updates - const handleFormDataUpdate = (event: CustomEvent) => { - if (event.detail && event.detail.formData) { - setFormData(event.detail.formData); - } - }; - - window.addEventListener('formDataUpdated', handleFormDataUpdate as EventListener); - document.addEventListener('updatePreview', loadFormData); + document.addEventListener('eventRequestFormDataUpdated', handleFormDataUpdate as EventListener); return () => { - window.removeEventListener('formDataUpdated', handleFormDataUpdate as EventListener); - document.removeEventListener('updatePreview', loadFormData); + document.removeEventListener('eventRequestFormDataUpdated', handleFormDataUpdate as EventListener); }; }, [propFormData]); - // Format date and time for display + // Load form data from local storage + const loadFormData = () => { + setLoading(true); + const savedData = localStorage.getItem('eventRequestFormData'); + + if (savedData) { + try { + const parsedData = JSON.parse(savedData); + setFormData(parsedData); + } catch (e) { + console.error('Error parsing saved form data:', e); + } + } + + setLoading(false); + }; + + // Handle form data updates + const handleFormDataUpdate = (event: CustomEvent) => { + if (event.detail && event.detail.formData) { + setFormData(event.detail.formData); + } + }; + + // Format date and time const formatDateTime = (dateTimeString: string) => { if (!dateTimeString) return 'Not specified'; try { const date = new Date(dateTimeString); - return date.toLocaleString(); + const options: Intl.DateTimeFormatOptions = { + weekday: 'long', + year: 'numeric', + month: 'long', + day: 'numeric', + hour: '2-digit', + minute: '2-digit' + }; + return date.toLocaleDateString('en-US', options); } catch (e) { return dateTimeString; } }; - // Handle click on the backdrop to close the modal + // Handle backdrop click for modal const handleBackdropClick = (e: React.MouseEvent) => { - if (e.target === e.currentTarget) { + if (isModal && e.target === e.currentTarget) { onClose(); } }; + // Map badge colors for status + const getStatusBadge = (status?: string) => { + if (!status) return null; + + const statusMap: { [key: string]: string } = { + 'submitted': 'badge-info', + 'pending': 'badge-warning', + 'completed': 'badge-success', + 'declined': 'badge-error' + }; + + return ( + + {status.charAt(0).toUpperCase() + status.slice(1)} + + ); + }; + // Render the content of the preview const renderContent = () => { + console.log("renderContent called, loading:", loading, "formData:", formData); + if (loading) { + console.log("Rendering loading state"); return ( -
-
-
+ +
+
); } if (!formData) { + console.log("Rendering no form data state"); return ( -
-

No Form Data Available

-

Please fill out the form to see a preview.

-
+ + +

No Form Data Available

+

Please fill out the form to see a preview.

+
); } + // Content layout to display the form data return ( -
-
- {isModal && ( - <> -

Event Request Preview

-

- This is a preview of your event request. Please review all information before submitting. -

- - )} + + {/* Only show the review header when in modal view */} + {isModal && ( + +
+ +

+ Review Your Event Request + {getStatusBadge(formData.status)} +

+
+

+ Please review all information carefully before submitting. You can go back to any section to make changes if needed. +

+
+ )} - {/* Event Details Section */} -
-

+ {/* Event Details Section */} + +
+ +

Event Details

-
-
-

Event Name

-

{formData.name || 'Not specified'}

+
+ +
+
+
+

+ {formData.name || 'Untitled Event'} +

+

+ {formData.event_description || 'No description provided.'} +

-
-

Location

-

{formData.location || 'Not specified'}

+ +
+
+ + Location +
+

{formData.location || 'Not specified'}

-
-

Event Description

-

{formData.event_description || 'Not specified'}

+ +
+
+ + Expected Attendance +
+

{formData.expected_attendance || 'Not specified'}

-
-

Start Date & Time

-

{formatDateTime(formData.start_date_time)}

+ +
+
+ + Start Date & Time +
+

{formatDateTime(formData.start_date_time)}

-
-

End Date & Time

-

{formatDateTime(formData.end_date_time)}

+ +
+
+ + End Date & Time +
+

{formatDateTime(formData.end_date_time)}

-
-

Room Booking

-

{formData.will_or_have_room_booking ? 'Yes' : 'No'}

-
-
-

Expected Attendance

-

{formData.expected_attendance || 'Not specified'}

+ +
+
+ + Room Booking +
+

+ {formData.will_or_have_room_booking ? 'Yes' : 'No'} +

+ - {/* PR Materials Section */} - {formData.flyers_needed && ( -
-

+ {/* PR Materials Section - Only show if flyers are needed */} + {formData.flyers_needed && ( + +
+ +

PR Materials

-
-
-

Flyer Types

-
    - {formData.flyer_type.map((type, index) => ( -
  • - {type === 'digital_with_social' && 'Digital flyer with social media advertising'} - {type === 'digital_no_social' && 'Digital flyer without social media advertising'} - {type === 'physical_with_advertising' && 'Physical flyer with advertising'} - {type === 'physical_no_advertising' && 'Physical flyer without advertising'} - {type === 'newsletter' && 'Newsletter'} - {type === 'other' && 'Other: ' + formData.other_flyer_type} -
  • - ))} -
-
-
-

Advertising Start Date

-

{formData.flyer_advertising_start_date || 'Not specified'}

-
-
-

Required Logos

-
- {formData.required_logos.map((logo, index) => ( - {logo} - ))} - {formData.required_logos.length === 0 &&

None specified

} +
+ +
+
+
+
+ + Flyer Types
-
-
-

Advertising Format

-

- {formData.advertising_format === 'pdf' && 'PDF'} - {formData.advertising_format === 'jpeg' && 'JPEG'} - {formData.advertising_format === 'png' && 'PNG'} - {formData.advertising_format === 'does_not_matter' && 'Does not matter'} - {!formData.advertising_format && 'Not specified'} +

+ {formData.flyer_type?.length + ? formData.flyer_type.map(type => { + switch (type) { + case 'digital_with_social': return 'Digital (with social media)'; + case 'digital_no_social': return 'Digital (no social media)'; + case 'physical_with_advertising': return 'Physical (with advertising)'; + case 'physical_no_advertising': return 'Physical (no advertising)'; + case 'newsletter': return 'Newsletter'; + case 'other': return formData.other_flyer_type || 'Other'; + default: return type; + } + }).join(', ') + : 'None specified' + }

-
-

Photography Needed

-

{formData.photography_needed ? 'Yes' : 'No'}

+ +
+
+ + Advertising Start Date +
+

+ {formData.flyer_advertising_start_date + ? formatDateTime(formData.flyer_advertising_start_date) + : 'Not specified' + } +

+ +
+
+ + Required Logos +
+

+ {formData.required_logos?.length + ? formData.required_logos.join(', ') + : 'None required' + } +

+
+ +
+
+ + Advertising Format +
+

+ {formData.advertising_format || 'Not specified'} +

+
+ {formData.flyer_additional_requests && ( -
-

Additional Requests

-

{formData.flyer_additional_requests}

+
+
+ + Additional Requests +
+

+ {formData.flyer_additional_requests} +

)}
- )} + + )} - {/* TAP Form Section */} -
-

- TAP Form Details + {/* TAP Form Section */} + +
+ +

+ TAP Information

-
-
-

Expected Attendance

-

{formData.expected_attendance || 'Not specified'}

-
-
-

Room Booking

-

- {formData.room_booking ? formData.room_booking.name : 'No file uploaded'} -

-
-
-

AS Funding Required

-

{formData.as_funding_required ? 'Yes' : 'No'}

-
-
-

Food/Drinks Being Served

-

{formData.food_drinks_being_served ? 'Yes' : 'No'}

-
-
- {/* AS Funding Section */} - {formData.as_funding_required && ( -
-

- AS Funding Details -

+
+
+
+

+ + Room Booking Status +

+
+ + {formData.will_or_have_room_booking ? 'Has/Will Have Booking' : 'No Booking Needed'} + -
-
-

Vendor

-

{formData.invoiceData.vendor || 'Not specified'}

+ {formData.will_or_have_room_booking && formData.room_booking && ( + File Uploaded + )}
- {formData.invoiceData.items.length > 0 ? ( -
- - +
+

+ + Food and Drinks +

+ + {formData.food_drinks_being_served ? 'Being Served' : 'Not Being Served'} + +
+ + + + + {/* AS Funding Section - Only show if needed */} + {formData.needs_as_funding && ( + +
+ +

+ AS Funding +

+
+ +
+ {formData.invoiceData && formData.invoiceData.items && formData.invoiceData.items.length > 0 ? ( +
+

+ + Invoice from {formData.invoiceData.vendor || 'Unknown Vendor'} +

+ +
+ - - - - + + + + - {formData.invoiceData.items.map((item: InvoiceItem) => ( - - - - - + {formData.invoiceData.items.map((item: InvoiceItem, index: number) => ( + + + + + ))} - - - - + + + + - - - - - - - - - - - + {formData.invoiceData.taxAmount && formData.invoiceData.taxAmount > 0 && ( + + + + + )} + {formData.invoiceData.tipAmount && formData.invoiceData.tipAmount > 0 && ( + + + + + )} + + +
DescriptionQtyUnit PriceAmountItemQtyUnit PriceAmount
{item.description}{item.quantity}${item.unitPrice.toFixed(2)}${item.amount.toFixed(2)}
{item.description || 'Item'}{item.quantity || 1}${(item.unitPrice || 0).toFixed(2)}${(item.amount || 0).toFixed(2)}
Subtotal:${formData.invoiceData.subtotal.toFixed(2)}
Subtotal:${(formData.invoiceData.subtotal || 0).toFixed(2)}
Tax ({formData.invoiceData.taxRate}%):${formData.invoiceData.taxAmount.toFixed(2)}
Tip ({formData.invoiceData.tipPercentage}%):${formData.invoiceData.tipAmount.toFixed(2)}
Total:${formData.invoiceData.total.toFixed(2)}
Tax:${(formData.invoiceData.taxAmount || 0).toFixed(2)}
Tip:${(formData.invoiceData.tipAmount || 0).toFixed(2)}
Total:${(formData.invoiceData.total || 0).toFixed(2)}
) : ( - +

No itemized invoice available.

)} - -
-

JSON Format (For Submission):

-
-                                    {JSON.stringify({
-                                        items: formData.invoiceData.items.map(item => ({
-                                            item: item.description,
-                                            quantity: item.quantity,
-                                            unit_price: item.unitPrice
-                                        })),
-                                        tax: formData.invoiceData.taxAmount,
-                                        tip: formData.invoiceData.tipAmount,
-                                        total: formData.invoiceData.total,
-                                        vendor: formData.invoiceData.vendor
-                                    }, null, 2)}
-                                
-

- This is the structured format that will be submitted to our database. - It ensures that your invoice data is properly organized and can be - processed correctly by our system. -

-
- -
-

Invoice Files

- {formData.invoice_files && formData.invoice_files.length > 0 ? ( -
- {formData.invoice_files.map((file, index) => ( -

{file.name}

- ))} -
- ) : formData.invoice ? ( -

{formData.invoice.name}

- ) : ( -

No files uploaded

- )} -
- )} -
-
+ + )} + + {/* Submission Information */} + +
+

+ + Ready to Submit +

+ + {formData.formReviewed && ( + Reviewed + )} +
+ +
+

+ Once submitted, you'll need to notify PR and/or Coordinators in the #-events Slack channel. +

+
+
+ ); }; - // If not a modal, render the content directly - if (!isModal) { - return ( - - {renderContent()} - - ); - } - - // If it's a modal, render with the modal wrapper + // Render the whole component return ( - - {isOpen && ( - - e.stopPropagation()} - style={{ - position: 'relative', - zIndex: 100000, - maxWidth: '90vw', - width: '100%' - }} - > -
-

Event Request Preview

- -
-
- {renderContent()} -
-
-
- )} -
+
+ {renderContent()} +
); }; -export default EventRequestFormPreview; \ No newline at end of file +// Ensure the modal always appears properly by adding global styles +const modalStyles = ` + /* Global styles for event request preview modal */ + #event-request-preview-modal-overlay, + .fixed[id="event-request-preview-modal-overlay"] { + position: fixed !important; + top: 0 !important; + left: 0 !important; + right: 0 !important; + bottom: 0 !important; + width: 100vw !important; + height: 100vh !important; + max-width: 100vw !important; + max-height: 100vh !important; + z-index: 999999 !important; + margin: 0 !important; + padding: 0 !important; + display: flex !important; + align-items: center !important; + justify-content: center !important; + overflow: auto !important; + background-color: rgba(0, 0, 0, 0.6) !important; + backdrop-filter: blur(4px) !important; + } +`; + +// Add the styles to the document head +if (typeof document !== 'undefined') { + const styleElement = document.createElement('style'); + styleElement.textContent = modalStyles; + document.head.appendChild(styleElement); +} + +// Create a wrapper component that listens for the custom event +export const EventRequestFormPreviewModalWrapper: React.FC = () => { + const [isOpen, setIsOpen] = useState(false); + const [formData, setFormData] = useState(null); + + useEffect(() => { + // Define the event handler + const handleShowModal = (event: CustomEvent) => { + console.log("showEventRequestPreviewModal event received:", event.detail); + if (event.detail && event.detail.formData) { + console.log("Setting formData from event:", event.detail.formData); + setFormData(event.detail.formData); + setIsOpen(true); + } + }; + + // Add event listener + document.addEventListener('showEventRequestPreviewModal', handleShowModal as EventListener); + + // Clean up + return () => { + document.removeEventListener('showEventRequestPreviewModal', handleShowModal as EventListener); + }; + }, []); + + const closeModal = () => { + console.log("Closing modal"); + setIsOpen(false); + // Dispatch custom event to notify modal has closed + document.dispatchEvent(new CustomEvent('modalClosed')); + }; + + console.log("EventRequestFormPreviewModalWrapper state:", { isOpen, hasFormData: !!formData }); + + if (!isOpen || !formData) return null; + + return ( + + ); +}; + +// Export the EventRequestFormPreview component as a named export +export { EventRequestFormPreview }; + +// Export the wrapper component as default +export default EventRequestFormPreviewModalWrapper; \ No newline at end of file diff --git a/src/components/dashboard/Officer_EventRequestForm/UserEventRequests.tsx b/src/components/dashboard/Officer_EventRequestForm/UserEventRequests.tsx index 8269acb..9195718 100644 --- a/src/components/dashboard/Officer_EventRequestForm/UserEventRequests.tsx +++ b/src/components/dashboard/Officer_EventRequestForm/UserEventRequests.tsx @@ -4,6 +4,8 @@ import { Authentication } from '../../../scripts/pocketbase/Authentication'; import { DataSyncService } from '../../../scripts/database/DataSyncService'; import { Collections } from '../../../schemas/pocketbase/schema'; import type { EventRequest as SchemaEventRequest } from '../../../schemas/pocketbase'; +import { EventRequestFormPreview } from './EventRequestFormPreview'; +import type { EventRequestFormData } from './EventRequestForm'; // Declare the global window interface to include our custom function declare global { @@ -17,10 +19,146 @@ export interface EventRequest extends SchemaEventRequest { invoice_data?: any; } +// Helper function to convert EventRequest to EventRequestFormData +const convertToFormData = (request: EventRequest): EventRequestFormData => { + try { + // Parse itemized_invoice if it's a string + let invoiceData = {}; + try { + if (request.itemized_invoice) { + if (typeof request.itemized_invoice === 'string') { + invoiceData = JSON.parse(request.itemized_invoice); + } else { + invoiceData = request.itemized_invoice; + } + } else if (request.invoice_data) { + invoiceData = request.invoice_data; + } + } catch (e) { + console.error('Error parsing invoice data:', e); + } + + // Cast to unknown first, then to EventRequestFormData to avoid type checking + return { + name: request.name, + location: request.location, + start_date_time: request.start_date_time, + end_date_time: request.end_date_time, + event_description: request.event_description || '', + flyers_needed: request.flyers_needed || false, + photography_needed: request.photography_needed || false, + flyer_type: request.flyer_type || [], + other_flyer_type: request.other_flyer_type || '', + flyer_advertising_start_date: request.flyer_advertising_start_date || '', + advertising_format: request.advertising_format || '', + required_logos: request.required_logos || [], + other_logos: [] as File[], // EventRequest doesn't have this as files + flyer_additional_requests: request.flyer_additional_requests || '', + will_or_have_room_booking: request.will_or_have_room_booking || false, + room_booking: null, + room_booking_confirmation: [] as File[], // EventRequest doesn't have this as files + expected_attendance: request.expected_attendance || 0, + food_drinks_being_served: request.food_drinks_being_served || false, + needs_as_funding: request.as_funding_required || false, + as_funding_required: request.as_funding_required || false, + invoice: null, + invoice_files: [], + invoiceData: invoiceData, + needs_graphics: request.flyers_needed || false, + status: request.status || '', + created_by: request.requested_user || '', + id: request.id || '', + created: request.created || '', + updated: request.updated || '', + itemized_invoice: request.itemized_invoice || '', + } as unknown as EventRequestFormData; + } catch (error) { + console.error("Error converting EventRequest to EventRequestFormData:", error); + + // Return a minimal valid object to prevent rendering errors + return { + name: request?.name || "Unknown Event", + location: request?.location || "", + start_date_time: request?.start_date_time || new Date().toISOString(), + end_date_time: request?.end_date_time || new Date().toISOString(), + event_description: request?.event_description || "", + flyers_needed: false, + photography_needed: false, + flyer_type: [], + other_flyer_type: "", + flyer_advertising_start_date: "", + advertising_format: "", + required_logos: [], + other_logos: [] as File[], + flyer_additional_requests: "", + will_or_have_room_booking: false, + room_booking: null, + room_booking_confirmation: [] as File[], + expected_attendance: 0, + food_drinks_being_served: false, + needs_as_funding: false, + as_funding_required: false, + invoice: null, + invoice_files: [], + invoiceData: {}, + needs_graphics: false, + status: request?.status || "", + created_by: "", + id: request?.id || "", + created: request?.created || "", + updated: request?.updated || "", + itemized_invoice: "" + } as unknown as EventRequestFormData; + } +}; + interface UserEventRequestsProps { eventRequests: EventRequest[]; } +// Create a portal component for the modal to ensure it's rendered at the root level +const EventRequestModal: React.FC<{ isOpen: boolean, onClose: () => void, children: React.ReactNode }> = ({ + isOpen, + onClose, + children +}) => { + if (!isOpen) return null; + + return ( +
+
e.stopPropagation()} + > + {children} +
+
+ ); +}; + const UserEventRequests: React.FC = ({ eventRequests: initialEventRequests }) => { const [eventRequests, setEventRequests] = useState(initialEventRequests); const [selectedRequest, setSelectedRequest] = useState(null); @@ -191,8 +329,8 @@ const UserEventRequests: React.FC = ({ eventRequests: in return (
@@ -280,15 +418,17 @@ const UserEventRequests: React.FC = ({ eventRequests: in - +
+ +
))} @@ -339,12 +479,17 @@ const UserEventRequests: React.FC = ({ eventRequests: in )}
- +
+ +
@@ -381,382 +526,45 @@ const UserEventRequests: React.FC = ({ eventRequests: in
- {/* Event Request Detail Modal */} - - {isModalOpen && selectedRequest && ( - - e.stopPropagation()} - > -
-
-

{selectedRequest.name}

- - {selectedRequest.status || 'Pending'} - -
-
- - -
+ {/* Use the new portal component for the modal */} + {isModalOpen && selectedRequest && ( + +
+
+

{selectedRequest.name}

+ + {selectedRequest.status || 'Pending'} + +
+
+ +
+
+ +
+ {selectedRequest ? ( + + ) : ( +
+
- -
-
-
-

- - - - Event Details -

-
-
-

Event Name

-

{selectedRequest.name}

-
-
-

Location

-

{selectedRequest.location}

-
-
-

Start Date & Time

-

{formatDate(selectedRequest.start_date_time)}

-
-
-

End Date & Time

-

{formatDate(selectedRequest.end_date_time)}

-
-
-

Room Booking

-

{selectedRequest.will_or_have_room_booking ? 'Yes' : 'No'}

-
-
-

Expected Attendance

-

{selectedRequest.expected_attendance || 'Not specified'}

-
-
-
- -
-

- - - - Event Description -

-
-

{selectedRequest.event_description}

-
-
-
- - {selectedRequest.flyers_needed && ( -
-

- - - - PR Materials -

-
-
-

Flyer Types

-

- {selectedRequest.flyer_type?.join(', ') || 'Not specified'} - {selectedRequest.other_flyer_type && ` (${selectedRequest.other_flyer_type})`} -

-
-
-

Advertising Start Date

-

- {selectedRequest.flyer_advertising_start_date - ? formatDate(selectedRequest.flyer_advertising_start_date) - : 'Not specified'} -

-
-
-

Required Logos

-

- {selectedRequest.required_logos?.join(', ') || 'None'} -

-
-
-

Advertising Format

-

{selectedRequest.advertising_format || 'Not specified'}

-
-
-

Additional Requests

-

- {selectedRequest.flyer_additional_requests || 'None'} -

-
-
-
- )} - - {selectedRequest.as_funding_required && ( -
-

- - - - AS Funding Details -

-
-
-

Food/Drinks Being Served

-

- {selectedRequest.food_drinks_being_served ? 'Yes' : 'No'} -

-
- {selectedRequest.invoice_data && ( -
-

Vendor

-

- {selectedRequest.invoice_data.vendor || 'Not specified'} -

-
- )} -
-

Itemized Invoice

- {(() => { - try { - let invoiceData: any = null; - - // Parse the invoice data if it's a string, or use it directly if it's an object - if (typeof selectedRequest.itemized_invoice === 'string') { - try { - invoiceData = JSON.parse(selectedRequest.itemized_invoice); - } catch (e) { - console.error('Failed to parse invoice JSON:', e); - return ( -
-                                                                        {selectedRequest.itemized_invoice || 'Not provided'}
-                                                                    
- ); - } - } else if (typeof selectedRequest.itemized_invoice === 'object') { - invoiceData = selectedRequest.itemized_invoice; - } - - // If we have valid invoice data with items - if (invoiceData && Array.isArray(invoiceData.items) && invoiceData.items.length > 0) { - // Calculate total from items if not provided or if NaN - let calculatedTotal = 0; - - // Try to use the provided total first - if (invoiceData.total !== undefined) { - const parsedTotal = typeof invoiceData.total === 'string' - ? parseFloat(invoiceData.total) - : invoiceData.total; - - if (!isNaN(parsedTotal)) { - calculatedTotal = parsedTotal; - } - } - - // If total is NaN or not provided, calculate from items - if (calculatedTotal === 0 || isNaN(calculatedTotal)) { - calculatedTotal = invoiceData.items.reduce((sum: number, item: any) => { - const quantity = typeof item.quantity === 'string' - ? parseFloat(item.quantity) - : (item.quantity || 1); - - const unitPrice = typeof item.unit_price === 'string' - ? parseFloat(item.unit_price) - : (item.unit_price || 0); - - const itemTotal = !isNaN(quantity) && !isNaN(unitPrice) - ? quantity * unitPrice - : 0; - - return sum + itemTotal; - }, 0); - - // Add tax and tip if available - if (invoiceData.tax && !isNaN(parseFloat(invoiceData.tax))) { - calculatedTotal += parseFloat(invoiceData.tax); - } - - if (invoiceData.tip && !isNaN(parseFloat(invoiceData.tip))) { - calculatedTotal += parseFloat(invoiceData.tip); - } - } - - return ( -
- - - - - - - - - - - {invoiceData.items.map((item: any, index: number) => { - const quantity = typeof item.quantity === 'string' - ? parseFloat(item.quantity) - : (item.quantity || 1); - - const unitPrice = typeof item.unit_price === 'string' - ? parseFloat(item.unit_price) - : (item.unit_price || 0); - - const itemTotal = !isNaN(quantity) && !isNaN(unitPrice) - ? quantity * unitPrice - : 0; - - return ( - - - - - - - ); - })} - - - {invoiceData.tax !== undefined && ( - - - - - )} - {invoiceData.tip !== undefined && ( - - - - - )} - - - - - -
ItemQtyPriceTotal
{item.item || 'Unnamed item'}{!isNaN(quantity) ? quantity : 1}${!isNaN(unitPrice) ? unitPrice.toFixed(2) : '0.00'}${!isNaN(itemTotal) ? itemTotal.toFixed(2) : '0.00'}
Tax: - ${typeof invoiceData.tax === 'string' - ? (parseFloat(invoiceData.tax) || 0).toFixed(2) - : (invoiceData.tax || 0).toFixed(2)} -
Tip: - ${typeof invoiceData.tip === 'string' - ? (parseFloat(invoiceData.tip) || 0).toFixed(2) - : (invoiceData.tip || 0).toFixed(2)} -
Total: - ${!isNaN(calculatedTotal) ? calculatedTotal.toFixed(2) : '0.00'} -
- {invoiceData.vendor && ( -
- Vendor: {invoiceData.vendor} -
- )} -
- ); - } else if (invoiceData && typeof invoiceData.total !== 'undefined') { - // If we have a total but no items, show a simplified view - const total = typeof invoiceData.total === 'string' - ? parseFloat(invoiceData.total) - : invoiceData.total; - - return ( -
-
- Total Amount: - ${!isNaN(total) ? total.toFixed(2) : '0.00'} -
- {invoiceData.vendor && ( -
- Vendor: {invoiceData.vendor} -
- )} -
- ); - } else { - // Fallback to display the JSON in a readable format - return ( -
-                                                                    {typeof selectedRequest.itemized_invoice === 'object'
-                                                                        ? JSON.stringify(selectedRequest.itemized_invoice, null, 2)
-                                                                        : (selectedRequest.itemized_invoice || 'Not provided')}
-                                                                
- ); - } - } catch (error) { - console.error('Error rendering invoice:', error); - return ( -
-                                                                Error displaying invoice. Please check the console for details.
-                                                            
- ); - } - })()} -
-
-
- )} - -
-
-
-

Submission Date

-

{formatDate(selectedRequest.created)}

-
-
-

Status:

- - {selectedRequest.status || 'Pending'} - -
-
-
-
- - - )} - + )} +
+
+ )} ); }; diff --git a/src/components/dashboard/reimbursement/ReimbursementList.tsx b/src/components/dashboard/reimbursement/ReimbursementList.tsx index 91c1c43..6f04c28 100644 --- a/src/components/dashboard/reimbursement/ReimbursementList.tsx +++ b/src/components/dashboard/reimbursement/ReimbursementList.tsx @@ -547,7 +547,7 @@ export default function ReimbursementList() { ? 'bg-success text-success-content ring-2 ring-success/20' : status === 'in_progress' ? 'bg-warning text-warning-content ring-2 ring-warning/20' - : 'bg-primary text-primary-content ring-2 ring-primary/20' + : 'bg-primary text-white ring-2 ring-primary/20' : isActive ? status === 'rejected' ? 'bg-error/20 text-error'