added event revisions based on erik

This commit is contained in:
chark1es 2025-03-13 02:35:57 -07:00
parent f28a076cfb
commit afc5708e21
11 changed files with 624 additions and 258 deletions

View file

@ -217,26 +217,14 @@ const ASFundingSection: React.FC<ASFundingSectionProps> = ({ formData, onDataCha
} }
}; };
// Handle invoice data change // Handle invoice data change from the invoice builder
const handleInvoiceDataChange = (data: InvoiceData) => { const handleInvoiceDataChange = (data: InvoiceData) => {
// Create itemized invoice string for Pocketbase // Calculate if budget exceeds maximum allowed
const itemizedInvoice = JSON.stringify({ const maxBudget = Math.min(formData.expected_attendance * 10, 5000);
vendor: data.vendor,
items: data.items.map((item: InvoiceItem) => ({
item: item.description,
quantity: item.quantity,
unit_price: item.unitPrice,
amount: item.amount
})),
subtotal: data.subtotal,
tax: data.taxAmount,
tip: data.tipAmount,
total: data.total
}, null, 2);
onDataChange({ onDataChange({
invoiceData: data, invoiceData: data,
itemized_invoice: itemizedInvoice itemized_invoice: JSON.stringify(data)
}); });
}; };
@ -251,18 +239,15 @@ const ASFundingSection: React.FC<ASFundingSectionProps> = ({ formData, onDataCha
<h2 className="text-3xl font-bold mb-4 text-primary bg-gradient-to-r from-primary to-primary-focus bg-clip-text text-transparent"> <h2 className="text-3xl font-bold mb-4 text-primary bg-gradient-to-r from-primary to-primary-focus bg-clip-text text-transparent">
AS Funding Details AS Funding Details
</h2> </h2>
<p className="text-lg text-base-content/80 mb-6">
Please provide the necessary information for your Associated Students funding request.
</p>
</motion.div> </motion.div>
<motion.div variants={itemVariants}> <motion.div variants={itemVariants}>
<CustomAlert <CustomAlert
type="warning" type="info"
title="Important Deadline" title="AS Funding Information"
message="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." message="AS funding can cover food and other expenses for your event. Please itemize all expenses in the invoice builder below."
className="mb-4" className="mb-6"
icon="heroicons:clock" icon="heroicons:information-circle"
/> />
</motion.div> </motion.div>
@ -407,18 +392,15 @@ const ASFundingSection: React.FC<ASFundingSectionProps> = ({ formData, onDataCha
</motion.div> </motion.div>
) : ( ) : (
<motion.div <motion.div
initial={{ opacity: 0, y: 10 }} variants={itemVariants}
animate={{ opacity: 1, y: 0 }} className="form-control space-y-6"
transition={{ duration: 0.3 }}
> >
<InvoiceBuilder <InvoiceBuilder
invoiceData={formData.invoiceData || { invoiceData={formData.invoiceData || {
vendor: '', vendor: '',
items: [], items: [],
subtotal: 0, subtotal: 0,
taxRate: 0,
taxAmount: 0, taxAmount: 0,
tipPercentage: 0,
tipAmount: 0, tipAmount: 0,
total: 0 total: 0
}} }}

View file

@ -114,7 +114,7 @@ const EventDetailsSection: React.FC<EventDetailsSectionProps> = ({ formData, onD
{/* Date and Time Section */} {/* Date and Time Section */}
<motion.div <motion.div
variants={itemVariants} variants={itemVariants}
className="grid grid-cols-1 md:grid-cols-2 gap-6" className="grid grid-cols-1 gap-6"
> >
{/* Event Start Date */} {/* Event Start Date */}
<motion.div <motion.div
@ -122,7 +122,7 @@ const EventDetailsSection: React.FC<EventDetailsSectionProps> = ({ formData, onD
whileHover={{ y: -2 }} whileHover={{ y: -2 }}
> >
<label className="label"> <label className="label">
<span className="label-text font-medium text-lg">Event Start</span> <span className="label-text font-medium text-lg">Event Start Date & Time</span>
<span className="label-text-alt text-error">*</span> <span className="label-text-alt text-error">*</span>
</label> </label>
<motion.input <motion.input
@ -134,26 +134,48 @@ const EventDetailsSection: React.FC<EventDetailsSectionProps> = ({ formData, onD
whileHover="hover" whileHover="hover"
variants={inputHoverVariants} variants={inputHoverVariants}
/> />
<p className="text-sm text-base-content/70 mt-2">
Note: For multi-day events, please submit a separate request for each day.
</p>
<p className="text-sm text-base-content/70 mt-1">
The event time should not include setup time.
</p>
</motion.div> </motion.div>
{/* Event End Date */} {/* Event End Time */}
<motion.div <motion.div
className="form-control bg-base-200/50 p-6 rounded-xl shadow-sm hover:shadow-md transition-all duration-300" className="form-control bg-base-200/50 p-6 rounded-xl shadow-sm hover:shadow-md transition-all duration-300"
whileHover={{ y: -2 }} whileHover={{ y: -2 }}
> >
<label className="label"> <label className="label">
<span className="label-text font-medium text-lg">Event End</span> <span className="label-text font-medium text-lg">Event End Time</span>
<span className="label-text-alt text-error">*</span> <span className="label-text-alt text-error">*</span>
</label> </label>
<div className="flex flex-col gap-2">
<motion.input <motion.input
type="datetime-local" type="time"
className="input input-bordered focus:input-primary transition-all duration-300 mt-2" className="input input-bordered focus:input-primary transition-all duration-300"
value={formData.end_date_time} value={formData.end_date_time ? new Date(formData.end_date_time).toTimeString().substring(0, 5) : ''}
onChange={(e) => onDataChange({ end_date_time: e.target.value })} onChange={(e) => {
if (formData.start_date_time) {
// Create a new date object from start_date_time
const startDate = new Date(formData.start_date_time);
// Parse the time value
const [hours, minutes] = e.target.value.split(':').map(Number);
// Set the hours and minutes on the date
startDate.setHours(hours, minutes);
// Update end_date_time with the new time but same date as start
onDataChange({ end_date_time: startDate.toISOString() });
}
}}
required required
whileHover="hover" whileHover="hover"
variants={inputHoverVariants} variants={inputHoverVariants}
/> />
<p className="text-xs text-base-content/60">
The end time will use the same date as the start date.
</p>
</div>
</motion.div> </motion.div>
</motion.div> </motion.div>
@ -202,7 +224,7 @@ const EventDetailsSection: React.FC<EventDetailsSectionProps> = ({ formData, onD
onChange={() => onDataChange({ will_or_have_room_booking: true })} onChange={() => onDataChange({ will_or_have_room_booking: true })}
required required
/> />
<span className="font-medium">Yes, I have/will have a booking</span> <span className="font-medium">Yes, I have a room booking</span>
</motion.label> </motion.label>
<motion.label <motion.label
className="flex items-center gap-3 cursor-pointer bg-base-100 p-3 rounded-lg hover:bg-primary/10 transition-colors" className="flex items-center gap-3 cursor-pointer bg-base-100 p-3 rounded-lg hover:bg-primary/10 transition-colors"
@ -213,12 +235,29 @@ const EventDetailsSection: React.FC<EventDetailsSectionProps> = ({ formData, onD
type="radio" type="radio"
className="radio radio-primary" className="radio radio-primary"
checked={formData.will_or_have_room_booking === false} checked={formData.will_or_have_room_booking === false}
onChange={() => onDataChange({ will_or_have_room_booking: false })} onChange={() => {
onDataChange({ will_or_have_room_booking: false });
}}
required required
/> />
<span className="font-medium">No, I don't need a booking</span> <span className="font-medium">No, I don't need a booking</span>
</motion.label> </motion.label>
</div> </div>
{formData.will_or_have_room_booking === false && (
<motion.div
initial={{ opacity: 0, height: 0 }}
animate={{ opacity: 1, height: 'auto' }}
exit={{ opacity: 0, height: 0 }}
className="mt-4"
>
<CustomAlert
type="warning"
title="IMPORTANT: Event Will Be Cancelled"
message="If you need a booking and submit without one, your event WILL BE CANCELLED. This is non-negotiable. Contact the event coordinator immediately if you have any booking concerns."
/>
</motion.div>
)}
</motion.div> </motion.div>
</motion.div> </motion.div>
); );

View file

@ -13,7 +13,7 @@ import PRSection from './PRSection';
import EventDetailsSection from './EventDetailsSection'; import EventDetailsSection from './EventDetailsSection';
import TAPFormSection from './TAPFormSection'; import TAPFormSection from './TAPFormSection';
import ASFundingSection from './ASFundingSection'; import ASFundingSection from './ASFundingSection';
import EventRequestFormPreview from './EventRequestFormPreview'; import { EventRequestFormPreview } from './EventRequestFormPreview';
import type { InvoiceData, InvoiceItem } from './InvoiceBuilder'; import type { InvoiceData, InvoiceItem } from './InvoiceBuilder';
// Animation variants // Animation variants
@ -119,9 +119,7 @@ const EventRequestForm: React.FC = () => {
invoiceData: { invoiceData: {
items: [], items: [],
subtotal: 0, subtotal: 0,
taxRate: 7.75, // Default tax rate for San Diego
taxAmount: 0, taxAmount: 0,
tipPercentage: 15, // Default tip percentage
tipAmount: 0, tipAmount: 0,
total: 0, total: 0,
vendor: '' vendor: ''
@ -204,7 +202,7 @@ const EventRequestForm: React.FC = () => {
advertising_format: '', advertising_format: '',
will_or_have_room_booking: false, will_or_have_room_booking: false,
expected_attendance: 0, expected_attendance: 0,
room_booking: null, room_booking: null, // No room booking by default
as_funding_required: false, as_funding_required: false,
food_drinks_being_served: false, food_drinks_being_served: false,
itemized_invoice: '', itemized_invoice: '',
@ -215,9 +213,7 @@ const EventRequestForm: React.FC = () => {
invoiceData: { invoiceData: {
items: [], items: [],
subtotal: 0, subtotal: 0,
taxRate: 7.75, // Default tax rate for San Diego
taxAmount: 0, taxAmount: 0,
tipPercentage: 15, // Default tip percentage
tipAmount: 0, tipAmount: 0,
total: 0, total: 0,
vendor: '' vendor: ''
@ -272,13 +268,12 @@ const EventRequestForm: React.FC = () => {
name: formData.name, name: formData.name,
location: formData.location, location: formData.location,
start_date_time: new Date(formData.start_date_time).toISOString(), start_date_time: new Date(formData.start_date_time).toISOString(),
end_date_time: new Date(formData.end_date_time).toISOString(), end_date_time: formData.end_date_time ? new Date(formData.end_date_time).toISOString() : new Date(formData.start_date_time).toISOString(),
event_description: formData.event_description, event_description: formData.event_description,
flyers_needed: formData.flyers_needed, flyers_needed: formData.flyers_needed,
photography_needed: formData.photography_needed, photography_needed: formData.photography_needed,
as_funding_required: formData.needs_as_funding, as_funding_required: formData.needs_as_funding,
food_drinks_being_served: formData.food_drinks_being_served, food_drinks_being_served: formData.food_drinks_being_served,
// Store the itemized_invoice as a string for backward compatibility
itemized_invoice: formData.itemized_invoice, itemized_invoice: formData.itemized_invoice,
flyer_type: formData.flyer_type, flyer_type: formData.flyer_type,
other_flyer_type: formData.other_flyer_type, other_flyer_type: formData.other_flyer_type,
@ -288,23 +283,19 @@ const EventRequestForm: React.FC = () => {
advertising_format: formData.advertising_format, advertising_format: formData.advertising_format,
will_or_have_room_booking: formData.will_or_have_room_booking, will_or_have_room_booking: formData.will_or_have_room_booking,
expected_attendance: formData.expected_attendance, expected_attendance: formData.expected_attendance,
// Add these fields explicitly to match the schema
needs_graphics: formData.needs_graphics, needs_graphics: formData.needs_graphics,
needs_as_funding: formData.needs_as_funding, needs_as_funding: formData.needs_as_funding,
// Store the invoice data as a properly formatted JSON object
invoice_data: { invoice_data: {
items: formData.invoiceData.items.map(item => ({ items: formData.invoiceData.items.map(item => ({
item: item.description, item: item.description,
quantity: item.quantity, quantity: item.quantity,
unit_price: item.unitPrice unit_price: item.unitPrice
})), })),
tax: formData.invoiceData.taxAmount, taxAmount: formData.invoiceData.taxAmount,
tip: formData.invoiceData.tipAmount, tipAmount: formData.invoiceData.tipAmount,
total: formData.invoiceData.total, total: formData.invoiceData.total,
vendor: formData.invoiceData.vendor vendor: formData.invoiceData.vendor
}, },
// Set the initial status to "submitted"
status: EventRequestStatus.SUBMITTED,
}; };
// Create the record using the Update service // Create the record using the Update service
@ -400,37 +391,45 @@ const EventRequestForm: React.FC = () => {
// Validate Event Details Section // Validate Event Details Section
const validateEventDetailsSection = () => { const validateEventDetailsSection = () => {
if (!formData.name) { let valid = true;
toast.error('Please enter an event name'); const errors: string[] = [];
return false;
if (!formData.name || formData.name.trim() === '') {
errors.push('Event name is required');
valid = false;
} }
if (!formData.event_description) { if (!formData.event_description || formData.event_description.trim() === '') {
toast.error('Please enter an event description'); errors.push('Event description is required');
return false; valid = false;
} }
if (!formData.start_date_time) { if (!formData.start_date_time || formData.start_date_time.trim() === '') {
toast.error('Please enter a start date and time'); errors.push('Event start date and time is required');
return false; valid = false;
} }
if (!formData.end_date_time) { if (!formData.end_date_time) {
toast.error('Please enter an end date and time'); errors.push('Event end time is required');
valid = false;
}
if (!formData.location || formData.location.trim() === '') {
errors.push('Event location is required');
valid = false;
}
if (formData.will_or_have_room_booking === undefined) {
errors.push('Room booking status is required');
valid = false;
}
if (errors.length > 0) {
setError(errors[0]);
return false; return false;
} }
if (!formData.location) { return valid;
toast.error('Please enter an event location');
return false;
}
if (formData.will_or_have_room_booking === null || formData.will_or_have_room_booking === undefined) {
toast.error('Please specify if you have a room booking');
return false;
}
return true;
}; };
// Validate TAP Form Section // Validate TAP Form Section
@ -446,6 +445,7 @@ const EventRequestForm: React.FC = () => {
return false; return false;
} }
// Only require room booking file if will_or_have_room_booking is true
if (formData.will_or_have_room_booking && !formData.room_booking) { if (formData.will_or_have_room_booking && !formData.room_booking) {
toast.error('Please upload your room booking confirmation'); toast.error('Please upload your room booking confirmation');
return false; return false;
@ -467,17 +467,22 @@ const EventRequestForm: React.FC = () => {
// Validate AS Funding Section // Validate AS Funding Section
const validateASFundingSection = () => { const validateASFundingSection = () => {
if (formData.needs_as_funding) { if (formData.as_funding_required) {
// Check if vendor is provided // Check if invoice data is present and has items
if (!formData.invoiceData.vendor) { if (!formData.invoiceData || !formData.invoiceData.items || formData.invoiceData.items.length === 0) {
toast.error('Please enter the vendor/restaurant name'); setError('Please add at least one item to your invoice');
return false; return false;
} }
// No longer require items in the invoice // Calculate the total budget from invoice items
// Check if at least one invoice file is uploaded const totalBudget = formData.invoiceData.items.reduce(
if (!formData.invoice && (!formData.invoice_files || formData.invoice_files.length === 0)) { (sum, item) => sum + (item.unitPrice * item.quantity), 0
toast.error('Please upload at least one invoice file'); );
// Check if the budget exceeds the maximum allowed ($5000 cap regardless of attendance)
const maxBudget = Math.min(formData.expected_attendance * 10, 5000);
if (totalBudget > maxBudget) {
setError(`Your budget (${totalBudget.toFixed(2)} dollars) exceeds the maximum allowed (${maxBudget} dollars). The absolute maximum is $5,000.`);
return false; return false;
} }
} }
@ -487,8 +492,11 @@ const EventRequestForm: React.FC = () => {
// Validate all sections before submission // Validate all sections before submission
const validateAllSections = () => { const validateAllSections = () => {
// Validate Event Details // We no longer forcibly set end_date_time to match start_date_time
// The end time is now configured separately with the same date
if (!validateEventDetailsSection()) { if (!validateEventDetailsSection()) {
setCurrentStep(1);
return false; return false;
} }
@ -579,6 +587,14 @@ const EventRequestForm: React.FC = () => {
variants={containerVariants} variants={containerVariants}
className="space-y-6" className="space-y-6"
> >
<CustomAlert
type="warning"
title="Multiple Events Notice"
message="If you have multiple events, you must submit a separate TAP form for each one. Multiple-day events require individual submissions for each day."
icon="heroicons:exclamation-triangle"
className="mb-4"
/>
<h2 className="text-2xl font-bold mb-4 text-primary">Event Request Form</h2> <h2 className="text-2xl font-bold mb-4 text-primary">Event Request Form</h2>
<div className="bg-base-300/50 p-4 rounded-lg mb-6"> <div className="bg-base-300/50 p-4 rounded-lg mb-6">

View file

@ -65,6 +65,20 @@ const modalVariants = {
} }
}; };
// Extended version of InvoiceItem to handle multiple property names
interface ExtendedInvoiceItem {
id?: string;
description?: string;
item?: string;
name?: string;
quantity: number;
unitPrice?: number;
unit_price?: number;
price?: number;
amount?: number;
[key: string]: any; // Allow any additional properties
}
// Helper function to normalize EventRequest to match EventRequestFormData structure // Helper function to normalize EventRequest to match EventRequestFormData structure
const normalizeFormData = (data: EventRequestFormData | (EventRequest & { const normalizeFormData = (data: EventRequestFormData | (EventRequest & {
invoiceData?: any; invoiceData?: any;
@ -108,7 +122,26 @@ const normalizeFormData = (data: EventRequestFormData | (EventRequest & {
...invoiceData, ...invoiceData,
...(parsed as any), ...(parsed as any),
items: Array.isArray((parsed as any).items) ? (parsed as any).items : [], items: Array.isArray((parsed as any).items) ? (parsed as any).items : [],
// Normalize tax/tip fields
taxAmount: parseFloat(parsed.taxAmount || parsed.tax || 0),
tipAmount: parseFloat(parsed.tipAmount || parsed.tip || 0)
}; };
// Normalize item property names
invoiceData.items = invoiceData.items.map(item => {
// Create a normalized item with all possible property names
return {
id: item.id || `item-${Math.random().toString(36).substr(2, 9)}`,
description: item.description || item.item || item.name || 'Item',
quantity: parseFloat(item.quantity) || 1,
unitPrice: parseFloat(item.unitPrice || item.unit_price || item.price || 0),
unit_price: parseFloat(item.unitPrice || item.unit_price || item.price || 0),
price: parseFloat(item.unitPrice || item.unit_price || item.price || 0),
amount: parseFloat(item.amount) ||
(parseFloat(item.quantity) || 1) *
parseFloat(item.unitPrice || item.unit_price || item.price || 0)
};
});
} }
} catch (e) { } catch (e) {
console.error('Error parsing itemized_invoice:', e); console.error('Error parsing itemized_invoice:', e);
@ -119,7 +152,25 @@ const normalizeFormData = (data: EventRequestFormData | (EventRequest & {
...invoiceData, ...invoiceData,
...parsed, ...parsed,
items: Array.isArray(parsed.items) ? parsed.items : [], items: Array.isArray(parsed.items) ? parsed.items : [],
// Normalize tax/tip fields
taxAmount: parseFloat(parsed.taxAmount || parsed.tax || 0),
tipAmount: parseFloat(parsed.tipAmount || parsed.tip || 0)
}; };
// Normalize item property names
invoiceData.items = invoiceData.items.map(item => {
return {
id: item.id || `item-${Math.random().toString(36).substr(2, 9)}`,
description: item.description || item.item || item.name || 'Item',
quantity: parseFloat(item.quantity) || 1,
unitPrice: parseFloat(item.unitPrice || item.unit_price || item.price || 0),
unit_price: parseFloat(item.unitPrice || item.unit_price || item.price || 0),
price: parseFloat(item.unitPrice || item.unit_price || item.price || 0),
amount: parseFloat(item.amount) ||
(parseFloat(item.quantity) || 1) *
parseFloat(item.unitPrice || item.unit_price || item.price || 0)
};
});
} }
} else if (eventRequest.invoiceData) { } else if (eventRequest.invoiceData) {
const parsed = eventRequest.invoiceData as any; const parsed = eventRequest.invoiceData as any;
@ -128,7 +179,25 @@ const normalizeFormData = (data: EventRequestFormData | (EventRequest & {
...invoiceData, ...invoiceData,
...parsed, ...parsed,
items: Array.isArray(parsed.items) ? parsed.items : [], items: Array.isArray(parsed.items) ? parsed.items : [],
// Normalize tax/tip fields
taxAmount: parseFloat(parsed.taxAmount || parsed.tax || 0),
tipAmount: parseFloat(parsed.tipAmount || parsed.tip || 0)
}; };
// Normalize item property names
invoiceData.items = invoiceData.items.map(item => {
return {
id: item.id || `item-${Math.random().toString(36).substr(2, 9)}`,
description: item.description || item.item || item.name || 'Item',
quantity: parseFloat(item.quantity) || 1,
unitPrice: parseFloat(item.unitPrice || item.unit_price || item.price || 0),
unit_price: parseFloat(item.unitPrice || item.unit_price || item.price || 0),
price: parseFloat(item.unitPrice || item.unit_price || item.price || 0),
amount: parseFloat(item.amount) ||
(parseFloat(item.quantity) || 1) *
parseFloat(item.unitPrice || item.unit_price || item.price || 0)
};
});
} }
} }
@ -288,7 +357,7 @@ interface EventRequestFormPreviewProps {
}); // Accept both form data and event request types }); // Accept both form data and event request types
isOpen?: boolean; // Control whether the modal is open isOpen?: boolean; // Control whether the modal is open
onClose?: () => void; // Callback when modal is closed onClose?: () => void; // Callback when modal is closed
isModal?: boolean; // Whether to render as a modal or inline component isModal: boolean; // Whether to render as a modal or inline component
} }
// Define the main EventRequestFormPreview component // Define the main EventRequestFormPreview component
@ -495,22 +564,22 @@ const EventRequestFormPreview: React.FC<EventRequestFormPreviewProps> = ({
<span className="text-sm">Expected Attendance</span> <span className="text-sm">Expected Attendance</span>
</div> </div>
<p className="font-medium text-base-content">{formData.expected_attendance || 'Not specified'}</p> <p className="font-medium text-base-content">{formData.expected_attendance || 'Not specified'}</p>
{formData.expected_attendance > 0 && (
<p className="text-xs text-primary">
Budget limit: ${Math.min(formData.expected_attendance * 10, 5000)} (max $5,000)
</p>
)}
</div> </div>
<div className="space-y-1"> <div className="space-y-1">
<div className="flex items-center text-base-content/60"> <div className="flex items-center text-base-content/60">
<Icon icon="heroicons:calendar" className="w-4 h-4 mr-1" /> <Icon icon="heroicons:calendar" className="w-4 h-4 mr-1" />
<span className="text-sm">Start Date & Time</span> <span className="text-sm">Date & Time</span>
</div> </div>
<p className="font-medium text-base-content">{formatDateTime(formData.start_date_time)}</p> <p className="font-medium text-base-content">{formatDateTime(formData.start_date_time)}</p>
</div> <p className="text-xs text-base-content/60">
Note: Multi-day events require separate submissions for each day.
<div className="space-y-1"> </p>
<div className="flex items-center text-base-content/60">
<Icon icon="heroicons:calendar" className="w-4 h-4 mr-1" />
<span className="text-sm">End Date & Time</span>
</div>
<p className="font-medium text-base-content">{formatDateTime(formData.end_date_time)}</p>
</div> </div>
<div className="space-y-1"> <div className="space-y-1">
@ -637,7 +706,7 @@ const EventRequestFormPreview: React.FC<EventRequestFormPreviewProps> = ({
</h4> </h4>
<div className="flex items-center"> <div className="flex items-center">
<span className={`badge ${formData.will_or_have_room_booking ? 'badge-success' : 'badge-neutral'}`}> <span className={`badge ${formData.will_or_have_room_booking ? 'badge-success' : 'badge-neutral'}`}>
{formData.will_or_have_room_booking ? 'Has/Will Have Booking' : 'No Booking Needed'} {formData.will_or_have_room_booking ? 'Room Booking Confirmed' : 'No Booking Needed'}
</span> </span>
{formData.will_or_have_room_booking && formData.room_booking && ( {formData.will_or_have_room_booking && formData.room_booking && (
@ -690,12 +759,12 @@ const EventRequestFormPreview: React.FC<EventRequestFormPreviewProps> = ({
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{formData.invoiceData.items.map((item: InvoiceItem, index: number) => ( {formData.invoiceData.items.map((item: ExtendedInvoiceItem, index: number) => (
<tr key={item.id || index} className="border-t border-base-300"> <tr key={item.id || index} className="border-t border-base-300">
<td className="py-2 font-medium text-base-content">{item.description || 'Item'}</td> <td className="py-2 font-medium text-base-content">{item.description || item.item || item.name || 'Item'}</td>
<td className="py-2 text-right text-base-content">{item.quantity || 1}</td> <td className="py-2 text-right text-base-content">{item.quantity || 1}</td>
<td className="py-2 text-right text-base-content">${(item.unitPrice || 0).toFixed(2)}</td> <td className="py-2 text-right text-base-content">${(item.unitPrice || item.unit_price || item.price || 0).toFixed(2)}</td>
<td className="py-2 text-right font-medium text-base-content">${(item.amount || 0).toFixed(2)}</td> <td className="py-2 text-right font-medium text-base-content">${(item.amount || (item.quantity * (item.unitPrice || item.unit_price || item.price || 0)) || 0).toFixed(2)}</td>
</tr> </tr>
))} ))}
</tbody> </tbody>
@ -704,18 +773,14 @@ const EventRequestFormPreview: React.FC<EventRequestFormPreviewProps> = ({
<td colSpan={3} className="py-2 text-right font-medium text-base-content">Subtotal:</td> <td colSpan={3} className="py-2 text-right font-medium text-base-content">Subtotal:</td>
<td className="py-2 text-right font-medium text-base-content">${(formData.invoiceData.subtotal || 0).toFixed(2)}</td> <td className="py-2 text-right font-medium text-base-content">${(formData.invoiceData.subtotal || 0).toFixed(2)}</td>
</tr> </tr>
{formData.invoiceData.taxAmount && formData.invoiceData.taxAmount > 0 && (
<tr className="border-t border-base-300"> <tr className="border-t border-base-300">
<td colSpan={3} className="py-2 text-right font-medium text-base-content">Tax:</td> <td colSpan={3} className="py-2 text-right font-medium text-base-content">Tax:</td>
<td className="py-2 text-right font-medium text-base-content">${(formData.invoiceData.taxAmount || 0).toFixed(2)}</td> <td className="py-2 text-right font-medium text-base-content">${(typeof formData.invoiceData.taxAmount === 'number' ? formData.invoiceData.taxAmount : 0).toFixed(2)}</td>
</tr> </tr>
)}
{formData.invoiceData.tipAmount && formData.invoiceData.tipAmount > 0 && (
<tr className="border-t border-base-300"> <tr className="border-t border-base-300">
<td colSpan={3} className="py-2 text-right font-medium text-base-content">Tip:</td> <td colSpan={3} className="py-2 text-right font-medium text-base-content">Tip:</td>
<td className="py-2 text-right font-medium text-base-content">${(formData.invoiceData.tipAmount || 0).toFixed(2)}</td> <td className="py-2 text-right font-medium text-base-content">${(typeof formData.invoiceData.tipAmount === 'number' ? formData.invoiceData.tipAmount : 0).toFixed(2)}</td>
</tr> </tr>
)}
<tr className="bg-primary/5"> <tr className="bg-primary/5">
<td colSpan={3} className="py-2 text-right font-bold text-primary">Total:</td> <td colSpan={3} className="py-2 text-right font-bold text-primary">Total:</td>
<td className="py-2 text-right font-bold text-primary">${(formData.invoiceData.total || 0).toFixed(2)}</td> <td className="py-2 text-right font-bold text-primary">${(formData.invoiceData.total || 0).toFixed(2)}</td>
@ -730,28 +795,7 @@ const EventRequestFormPreview: React.FC<EventRequestFormPreviewProps> = ({
</motion.div> </motion.div>
)} )}
{/* Submission Information */}
<motion.div
variants={sectionVariants}
className="bg-base-100 rounded-xl border border-base-300/50 shadow-sm overflow-hidden mt-8"
>
<div className="p-4 flex justify-between items-center border-b border-base-300/30">
<h4 className="font-medium flex items-center">
<Icon icon="heroicons:check-circle" className="w-5 h-5 mr-2 text-success" />
Ready to Submit
</h4>
{formData.formReviewed && (
<span className="badge badge-success">Reviewed</span>
)}
</div>
<div className="p-4 bg-base-100/50">
<p className="text-sm text-base-content/70">
Once submitted, you'll need to notify PR and/or Coordinators in the #-events Slack channel.
</p>
</div>
</motion.div>
</motion.div> </motion.div>
); );
}; };

View file

@ -713,6 +713,17 @@ const InvoiceBuilder: React.FC<InvoiceBuilderProps> = ({ invoiceData, onChange }
</div> </div>
</motion.div> </motion.div>
{/* Budget Warning */}
{total > 0 && (
<CustomAlert
type="warning"
title="BUDGET RESTRICTION"
message="Your total cannot exceed $10 per expected attendee, with an absolute maximum of $5,000. Your form WILL BE REJECTED if it exceeds this limit."
className="mt-4"
icon="heroicons:exclamation-triangle"
/>
)}
{/* Validation notice */} {/* Validation notice */}
{invoiceData.items.length === 0 && ( {invoiceData.items.length === 0 && (
<CustomAlert <CustomAlert
@ -732,6 +743,42 @@ const InvoiceBuilder: React.FC<InvoiceBuilderProps> = ({ invoiceData, onChange }
className="mt-4" className="mt-4"
icon="heroicons:exclamation-triangle" icon="heroicons:exclamation-triangle"
/> />
{/* Summary Section */}
<motion.div
variants={itemVariants}
className="mt-6 bg-base-200/40 rounded-lg p-4"
>
<div className=" md:grid-cols-2 gap-6">
{/* Right Column: Summary */}
<div>
<h3 className="font-medium text-lg mb-3">Invoice Summary</h3>
<div className="bg-base-100/50 p-4 rounded-lg">
<div className="flex justify-between items-center mb-1">
<span className="text-sm">Subtotal:</span>
<span className="font-medium">${invoiceData.subtotal.toFixed(2)}</span>
</div>
<div className="flex justify-between items-center mb-1">
<span className="text-sm">Tax:</span>
<span className="font-medium">${invoiceData.taxAmount.toFixed(2)}</span>
</div>
<div className="flex justify-between items-center mb-1">
<span className="text-sm">Tip:</span>
<span className="font-medium">${invoiceData.tipAmount.toFixed(2)}</span>
</div>
<div className="divider my-1"></div>
<div className="flex justify-between items-center font-bold text-primary">
<span>Total:</span>
<span>${invoiceData.total.toFixed(2)}</span>
</div>
</div>
</div>
</div>
</motion.div>
</motion.div> </motion.div>
); );
}; };

View file

@ -1,8 +1,9 @@
import React, { useState } from 'react'; import React, { useState, useEffect } from 'react';
import { motion } from 'framer-motion'; import { motion } from 'framer-motion';
import type { EventRequestFormData } from './EventRequestForm'; import type { EventRequestFormData } from './EventRequestForm';
import type { EventRequest } from '../../../schemas/pocketbase'; import type { EventRequest } from '../../../schemas/pocketbase';
import CustomAlert from '../universal/CustomAlert'; import CustomAlert from '../universal/CustomAlert';
import FilePreview from '../universal/FilePreview';
// Enhanced animation variants // Enhanced animation variants
const containerVariants = { const containerVariants = {
@ -38,6 +39,19 @@ const inputHoverVariants = {
} }
}; };
// Add a new CSS class to hide the number input arrows
const hiddenNumberInputArrows = `
/* Hide number input spinners */
input[type=number]::-webkit-inner-spin-button,
input[type=number]::-webkit-outer-spin-button {
-webkit-appearance: none;
margin: 0;
}
input[type=number] {
-moz-appearance: textfield;
}
`;
// File upload animation // File upload animation
const fileUploadVariants = { const fileUploadVariants = {
initial: { scale: 1 }, initial: { scale: 1 },
@ -57,13 +71,42 @@ interface TAPFormSectionProps {
const TAPFormSection: React.FC<TAPFormSectionProps> = ({ formData, onDataChange }) => { const TAPFormSection: React.FC<TAPFormSectionProps> = ({ formData, onDataChange }) => {
const [roomBookingFile, setRoomBookingFile] = useState<File | null>(formData.room_booking); const [roomBookingFile, setRoomBookingFile] = useState<File | null>(formData.room_booking);
const [isDragging, setIsDragging] = useState(false); const [isDragging, setIsDragging] = useState(false);
const [fileError, setFileError] = useState<string | null>(null);
const [showFilePreview, setShowFilePreview] = useState(false);
const [filePreviewUrl, setFilePreviewUrl] = useState<string | null>(null);
// Handle room booking file upload // Add style tag for hidden arrows
useEffect(() => {
const style = document.createElement('style');
style.textContent = hiddenNumberInputArrows;
document.head.appendChild(style);
return () => {
document.head.removeChild(style);
};
}, []);
// Handle room booking file upload with size limit
const handleRoomBookingFileChange = (e: React.ChangeEvent<HTMLInputElement>) => { const handleRoomBookingFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
if (e.target.files && e.target.files.length > 0) { if (e.target.files && e.target.files.length > 0) {
const file = e.target.files[0]; const file = e.target.files[0];
// Check file size - 1MB limit
if (file.size > 1024 * 1024) {
setFileError("Room booking file size must be under 1MB");
return;
}
setFileError(null);
setRoomBookingFile(file); setRoomBookingFile(file);
onDataChange({ room_booking: file }); onDataChange({ room_booking: file });
// Create preview URL
if (filePreviewUrl) {
URL.revokeObjectURL(filePreviewUrl);
}
const url = URL.createObjectURL(file);
setFilePreviewUrl(url);
} }
}; };
@ -84,11 +127,40 @@ const TAPFormSection: React.FC<TAPFormSectionProps> = ({ formData, onDataChange
if (e.dataTransfer.files && e.dataTransfer.files.length > 0) { if (e.dataTransfer.files && e.dataTransfer.files.length > 0) {
const file = e.dataTransfer.files[0]; const file = e.dataTransfer.files[0];
// Check file size - 1MB limit
if (file.size > 1024 * 1024) {
setFileError("Room booking file size must be under 1MB");
return;
}
setFileError(null);
setRoomBookingFile(file); setRoomBookingFile(file);
onDataChange({ room_booking: file }); onDataChange({ room_booking: file });
// Create preview URL
if (filePreviewUrl) {
URL.revokeObjectURL(filePreviewUrl);
}
const url = URL.createObjectURL(file);
setFilePreviewUrl(url);
} }
}; };
// Function to toggle file preview
const toggleFilePreview = () => {
setShowFilePreview(!showFilePreview);
};
// Clean up object URL when component unmounts
useEffect(() => {
return () => {
if (filePreviewUrl) {
URL.revokeObjectURL(filePreviewUrl);
}
};
}, [filePreviewUrl]);
return ( return (
<motion.div <motion.div
className="space-y-8" className="space-y-8"
@ -105,10 +177,10 @@ const TAPFormSection: React.FC<TAPFormSectionProps> = ({ formData, onDataChange
<motion.div variants={itemVariants}> <motion.div variants={itemVariants}>
<CustomAlert <CustomAlert
type="info" type="info"
title="Important Information" title="CRITICAL INFORMATION"
message="Please ensure you have ALL sections completed. If something is not available, let the coordinators know and be advised on how to proceed." message="Failure to complete ALL sections with accurate information WILL result in event cancellation. This is non-negotiable. If information is not available, contact the event coordinator BEFORE submitting."
className="mb-6" className="mb-6"
icon="heroicons:information-circle" icon="heroicons:exclamation-triangle"
/> />
</motion.div> </motion.div>
@ -127,7 +199,11 @@ const TAPFormSection: React.FC<TAPFormSectionProps> = ({ formData, onDataChange
type="number" type="number"
className="input input-bordered focus:input-primary transition-all duration-300 w-full pr-20" className="input input-bordered focus:input-primary transition-all duration-300 w-full pr-20"
value={formData.expected_attendance || ''} value={formData.expected_attendance || ''}
onChange={(e) => onDataChange({ expected_attendance: parseInt(e.target.value) || 0 })} onChange={(e) => {
// Allow any attendance number, no longer limiting to 500
const attendance = parseInt(e.target.value) || 0;
onDataChange({ expected_attendance: attendance });
}}
min="0" min="0"
placeholder="Enter expected attendance" placeholder="Enter expected attendance"
required required
@ -138,6 +214,30 @@ const TAPFormSection: React.FC<TAPFormSectionProps> = ({ formData, onDataChange
people people
</div> </div>
</div> </div>
{formData.expected_attendance > 0 && (
<motion.div
initial={{ opacity: 0, height: 0 }}
animate={{ opacity: 1, height: 'auto' }}
exit={{ opacity: 0, height: 0 }}
className="mt-4 bg-success/20 p-3 rounded-lg"
>
<p className="text-sm font-medium">
Budget Calculator: $10 per person × {formData.expected_attendance} people
</p>
<p className="text-base font-bold mt-1">
{formData.expected_attendance * 10 <= 5000 ? (
`You cannot exceed spending past $${formData.expected_attendance * 10} dollars.`
) : (
`You cannot exceed spending past $5,000 dollars.`
)}
</p>
{formData.expected_attendance * 10 > 5000 && (
<p className="text-xs mt-1 text-warning">
Budget cap reached. Maximum budget is $5,000 regardless of attendance.
</p>
)}
</motion.div>
)}
<motion.div <motion.div
className="mt-4 p-4 bg-base-300/30 rounded-lg text-xs text-gray-400" className="mt-4 p-4 bg-base-300/30 rounded-lg text-xs text-gray-400"
@ -152,7 +252,7 @@ const TAPFormSection: React.FC<TAPFormSectionProps> = ({ formData, onDataChange
</motion.div> </motion.div>
</motion.div> </motion.div>
{/* Room booking confirmation */} {/* Room booking confirmation - Show file error if present */}
<motion.div <motion.div
variants={itemVariants} variants={itemVariants}
className="form-control bg-base-200/50 p-6 rounded-xl shadow-sm hover:shadow-md transition-all duration-300" className="form-control bg-base-200/50 p-6 rounded-xl shadow-sm hover:shadow-md transition-all duration-300"
@ -160,9 +260,20 @@ const TAPFormSection: React.FC<TAPFormSectionProps> = ({ formData, onDataChange
> >
<label className="label"> <label className="label">
<span className="label-text font-medium text-lg">Room Booking Confirmation</span> <span className="label-text font-medium text-lg">Room Booking Confirmation</span>
<span className="label-text-alt text-error">*</span> {formData.will_or_have_room_booking && <span className="label-text-alt text-error">*</span>}
</label> </label>
{fileError && (
<div className="mt-2 mb-2">
<CustomAlert
type="error"
title="File Error"
message={fileError}
/>
</div>
)}
{formData.will_or_have_room_booking ? (
<motion.div <motion.div
className={`mt-2 border-2 border-dashed rounded-lg p-8 text-center cursor-pointer transition-colors ${isDragging ? 'border-primary bg-primary/5' : 'border-base-300 hover:border-primary/50' className={`mt-2 border-2 border-dashed rounded-lg p-8 text-center cursor-pointer transition-colors ${isDragging ? 'border-primary bg-primary/5' : 'border-base-300 hover:border-primary/50'
}`} }`}
@ -197,16 +308,62 @@ const TAPFormSection: React.FC<TAPFormSectionProps> = ({ formData, onDataChange
<> <>
<p className="font-medium text-primary">File selected:</p> <p className="font-medium text-primary">File selected:</p>
<p className="text-sm">{roomBookingFile.name}</p> <p className="text-sm">{roomBookingFile.name}</p>
<p className="text-xs text-gray-500">Click or drag to replace</p> <p className="text-xs text-gray-500">Click or drag to replace (Max size: 1MB)</p>
</> </>
) : ( ) : (
<> <>
<p className="font-medium">Drop your file here or click to browse</p> <p className="font-medium">Drop your file here or click to browse</p>
<p className="text-xs text-gray-500">Accepted formats: PDF, PNG, JPG</p> <p className="text-xs text-gray-500">Accepted formats: PDF, PNG, JPG (Max size: 1MB)</p>
</> </>
)} )}
</div> </div>
</motion.div> </motion.div>
) : (
<motion.div
className="mt-2 bg-base-300/30 rounded-lg p-4 text-center"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
>
<p className="text-sm text-base-content/70">Room booking upload not required when no booking is needed.</p>
</motion.div>
)}
{/* Preview File Button - Outside the upload area */}
{formData.will_or_have_room_booking && roomBookingFile && (
<div className="mt-3 flex justify-end">
<button
type="button"
className="btn btn-primary btn-sm"
onClick={toggleFilePreview}
>
{showFilePreview ? 'Hide Preview' : 'Preview File'}
</button>
</div>
)}
{/* File Preview Component */}
{showFilePreview && filePreviewUrl && roomBookingFile && (
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -20 }}
className="mt-4 p-4 bg-base-200 rounded-lg"
>
<div className="flex justify-between items-center mb-4">
<h3 className="font-medium">File Preview</h3>
<button
type="button"
className="btn btn-sm btn-circle"
onClick={toggleFilePreview}
>
<svg xmlns="http://www.w3.org/2000/svg" className="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
<FilePreview url={filePreviewUrl} filename={roomBookingFile.name} />
</motion.div>
)}
</motion.div> </motion.div>
{/* Food/Drinks */} {/* Food/Drinks */}

View file

@ -256,7 +256,7 @@ try {
<EventRequestModal client:load eventRequests={allEventRequests} /> <EventRequestModal client:load eventRequests={allEventRequests} />
</div> </div>
<script define:vars={{ ANIMATION_DELAYS }}> <script type="module" define:vars={{ ANIMATION_DELAYS }}>
// Import the DataSyncService for client-side use // Import the DataSyncService for client-side use
import { DataSyncService } from "../../scripts/database/DataSyncService"; import { DataSyncService } from "../../scripts/database/DataSyncService";
import { Collections } from "../../schemas/pocketbase/schema"; import { Collections } from "../../schemas/pocketbase/schema";
@ -269,6 +269,20 @@ try {
} }
}); });
// Also force refresh when this tab is clicked in dashboard
document.addEventListener("DOMContentLoaded", () => {
// Find dashboard tab for event request management
const eventRequestManagementTab = document.querySelector(
'[data-section="eventRequestManagement"]'
);
if (eventRequestManagementTab) {
eventRequestManagementTab.addEventListener("click", () => {
// Dispatch custom event to refresh data when this tab is clicked
document.dispatchEvent(new CustomEvent("dashboardTabVisible"));
});
}
});
// Handle authentication errors and initial data loading // Handle authentication errors and initial data loading
document.addEventListener("DOMContentLoaded", async () => { document.addEventListener("DOMContentLoaded", async () => {
// Initialize DataSyncService for client-side // Initialize DataSyncService for client-side

View file

@ -708,7 +708,7 @@ const ASFundingTab: React.FC<{ request: ExtendedEventRequest }> = ({ request })
</div> </div>
<div className="mt-4"> <div className="mt-4">
<InvoiceTable invoiceData={invoiceData} /> <InvoiceTable invoiceData={invoiceData} expectedAttendance={request.expected_attendance} />
</div> </div>
</motion.div> </motion.div>
@ -726,7 +726,7 @@ const ASFundingTab: React.FC<{ request: ExtendedEventRequest }> = ({ request })
}; };
// Separate component for invoice table // Separate component for invoice table
const InvoiceTable: React.FC<{ invoiceData: any }> = ({ invoiceData }) => { const InvoiceTable: React.FC<{ invoiceData: any, expectedAttendance?: number }> = ({ invoiceData, expectedAttendance }) => {
// If no invoice data is provided, show a message // If no invoice data is provided, show a message
if (!invoiceData) { if (!invoiceData) {
return ( return (
@ -811,7 +811,7 @@ const InvoiceTable: React.FC<{ invoiceData: any }> = ({ invoiceData }) => {
// Calculate subtotal from items // Calculate subtotal from items
const subtotal = items.reduce((sum: number, item: any) => { const subtotal = items.reduce((sum: number, item: any) => {
const quantity = parseFloat(item?.quantity || 1); const quantity = parseFloat(item?.quantity || 1);
const price = parseFloat(item?.unit_price || item?.price || 0); const price = parseFloat(item?.unit_price || item?.unitPrice || item?.price || 0);
return sum + (quantity * price); return sum + (quantity * price);
}, 0); }, 0);
@ -820,9 +820,26 @@ const InvoiceTable: React.FC<{ invoiceData: any }> = ({ invoiceData }) => {
const tip = parseFloat(parsedInvoice.tip || parsedInvoice.tipAmount || 0); const tip = parseFloat(parsedInvoice.tip || parsedInvoice.tipAmount || 0);
const total = parseFloat(parsedInvoice.total || 0) || (subtotal + tax + tip); const total = parseFloat(parsedInvoice.total || 0) || (subtotal + tax + tip);
// Calculate budget limit if expected attendance is provided
const budgetLimit = expectedAttendance ? Math.min(expectedAttendance * 10, 5000) : null;
const isOverBudget = budgetLimit !== null && total > budgetLimit;
// Render the invoice table // Render the invoice table
return ( return (
<div className="overflow-x-auto"> <div className="overflow-x-auto">
{budgetLimit !== null && (
<div className={`mb-4 p-3 rounded-lg ${isOverBudget ? 'bg-error/20' : 'bg-success/20'}`}>
<p className="text-sm font-medium">
Budget Limit: ${budgetLimit.toFixed(2)} (based on {expectedAttendance} attendees)
</p>
{isOverBudget && (
<p className="text-sm font-bold text-error mt-1">
WARNING: This invoice exceeds the budget limit by ${(total - budgetLimit).toFixed(2)}
</p>
)}
</div>
)}
<table className="table table-zebra w-full"> <table className="table table-zebra w-full">
<thead> <thead>
<tr> <tr>
@ -840,7 +857,7 @@ const InvoiceTable: React.FC<{ invoiceData: any }> = ({ invoiceData }) => {
: (item?.item || item?.description || item?.name || 'N/A'); : (item?.item || item?.description || item?.name || 'N/A');
const quantity = parseFloat(item?.quantity || 1); const quantity = parseFloat(item?.quantity || 1);
const unitPrice = parseFloat(item?.unit_price || item?.price || 0); const unitPrice = parseFloat(item?.unit_price || item?.unitPrice || item?.price || 0);
const itemTotal = quantity * unitPrice; const itemTotal = quantity * unitPrice;
return ( return (
@ -858,21 +875,17 @@ const InvoiceTable: React.FC<{ invoiceData: any }> = ({ invoiceData }) => {
<td colSpan={3} className="text-right font-medium">Subtotal:</td> <td colSpan={3} className="text-right font-medium">Subtotal:</td>
<td>${subtotal.toFixed(2)}</td> <td>${subtotal.toFixed(2)}</td>
</tr> </tr>
{tax > 0 && (
<tr> <tr>
<td colSpan={3} className="text-right font-medium">Tax:</td> <td colSpan={3} className="text-right font-medium">Tax:</td>
<td>${tax.toFixed(2)}</td> <td>${tax.toFixed(2)}</td>
</tr> </tr>
)}
{tip > 0 && (
<tr> <tr>
<td colSpan={3} className="text-right font-medium">Tip:</td> <td colSpan={3} className="text-right font-medium">Tip:</td>
<td>${tip.toFixed(2)}</td> <td>${tip.toFixed(2)}</td>
</tr> </tr>
)}
<tr> <tr>
<td colSpan={3} className="text-right font-bold">Total:</td> <td colSpan={3} className="text-right font-bold">Total:</td>
<td className="font-bold">${total.toFixed(2)}</td> <td className={`font-bold ${isOverBudget ? 'text-error' : ''}`}>${total.toFixed(2)}</td>
</tr> </tr>
</tfoot> </tfoot>
</table> </table>
@ -888,9 +901,9 @@ const InvoiceTable: React.FC<{ invoiceData: any }> = ({ invoiceData }) => {
return ( return (
<CustomAlert <CustomAlert
type="error" type="error"
title="Processing Error" title="Error"
message="An unexpected error occurred while processing the invoice." message="An error occurred while processing the invoice data."
icon="heroicons:exclamation-triangle" icon="heroicons:exclamation-circle"
/> />
); );
} }
@ -1216,7 +1229,8 @@ const PRMaterialsTab: React.FC<{ request: ExtendedEventRequest }> = ({ request }
)} )}
{/* No files message */} {/* No files message */}
{!hasFiles && ( {
!hasFiles && (
<motion.div <motion.div
className="mt-4" className="mt-4"
initial={{ opacity: 0, y: 10 }} initial={{ opacity: 0, y: 10 }}
@ -1230,7 +1244,8 @@ const PRMaterialsTab: React.FC<{ request: ExtendedEventRequest }> = ({ request }
icon="heroicons:information-circle" icon="heroicons:information-circle"
/> />
</motion.div> </motion.div>
)} )
}
{/* File Preview Modal */} {/* File Preview Modal */}
<FilePreviewModal <FilePreviewModal
@ -1464,11 +1479,6 @@ const EventRequestDetails = ({
<label className="text-xs text-gray-400">Start Date & Time</label> <label className="text-xs text-gray-400">Start Date & Time</label>
<p className="text-white">{formatDate(request.start_date_time)}</p> <p className="text-white">{formatDate(request.start_date_time)}</p>
</div> </div>
<div>
<label className="text-xs text-gray-400">End Date & Time</label>
<p className="text-white">{formatDate(request.end_date_time)}</p>
</div>
</div> </div>
</div> </div>

View file

@ -33,12 +33,14 @@ interface EventRequestManagementTableProps {
eventRequests: ExtendedEventRequest[]; eventRequests: ExtendedEventRequest[];
onRequestSelect: (request: ExtendedEventRequest) => void; onRequestSelect: (request: ExtendedEventRequest) => void;
onStatusChange: (id: string, status: "submitted" | "pending" | "completed" | "declined") => Promise<void>; onStatusChange: (id: string, status: "submitted" | "pending" | "completed" | "declined") => Promise<void>;
isLoadingUserData?: boolean;
} }
const EventRequestManagementTable = ({ const EventRequestManagementTable = ({
eventRequests: initialEventRequests, eventRequests: initialEventRequests,
onRequestSelect, onRequestSelect,
onStatusChange onStatusChange,
isLoadingUserData = false
}: EventRequestManagementTableProps) => { }: EventRequestManagementTableProps) => {
const [eventRequests, setEventRequests] = useState<ExtendedEventRequest[]>(initialEventRequests); const [eventRequests, setEventRequests] = useState<ExtendedEventRequest[]>(initialEventRequests);
const [filteredRequests, setFilteredRequests] = useState<ExtendedEventRequest[]>(initialEventRequests); const [filteredRequests, setFilteredRequests] = useState<ExtendedEventRequest[]>(initialEventRequests);
@ -69,13 +71,54 @@ const EventRequestManagementTable = ({
true, // Force sync true, // Force sync
'', // No filter '', // No filter
'-created', '-created',
'requested_user' 'requested_user' // This is correct but we need to ensure it's working in the DataSyncService
); );
// If we still have "Unknown" users, try to fetch them directly
const requestsWithUsers = await Promise.all(
updatedRequests.map(async (request) => {
// If user data is missing, try to fetch it directly
if (!request.expand?.requested_user && request.requested_user) {
try {
const userData = await dataSync.getItem(
Collections.USERS,
request.requested_user,
true // Force sync the user data
);
if (userData) {
// TypeScript cast to access the properties
const typedUserData = userData as unknown as {
id: string;
name?: string;
email?: string;
};
// Update the expand object with the user data
return {
...request,
expand: {
...(request.expand || {}),
requested_user: {
id: typedUserData.id,
name: typedUserData.name || 'Unknown',
email: typedUserData.email || 'Unknown'
}
}
} as ExtendedEventRequest;
}
} catch (error) {
console.error(`Error fetching user data for request ${request.id}:`, error);
}
}
return request;
})
) as ExtendedEventRequest[];
// console.log(`Fetched ${updatedRequests.length} event requests`); // console.log(`Fetched ${updatedRequests.length} event requests`);
setEventRequests(updatedRequests); setEventRequests(requestsWithUsers);
applyFilters(updatedRequests); applyFilters(requestsWithUsers);
} catch (error) { } catch (error) {
// console.error('Error refreshing event requests:', error); // console.error('Error refreshing event requests:', error);
toast.error('Failed to refresh event requests'); toast.error('Failed to refresh event requests');
@ -217,25 +260,32 @@ const EventRequestManagementTable = ({
// Helper to get user display info - always show email address // Helper to get user display info - always show email address
const getUserDisplayInfo = (request: ExtendedEventRequest) => { const getUserDisplayInfo = (request: ExtendedEventRequest) => {
// If we're still loading user data, show loading indicator
if (isLoadingUserData) {
return {
name: request.expand?.requested_user?.name || 'Loading...',
email: request.expand?.requested_user?.email || 'Loading...'
};
}
// First try to get from the expand object // First try to get from the expand object
if (request.expand?.requested_user) { if (request.expand?.requested_user) {
const user = request.expand.requested_user; const user = request.expand.requested_user;
// Show "Loading..." instead of "Unknown" while data is being fetched const name = user.name || 'Unknown';
const name = user.name || 'Loading...';
// Always show email regardless of emailVisibility // Always show email regardless of emailVisibility
const email = user.email || 'Loading...'; const email = user.email || 'Unknown';
return { name, email }; return { name, email };
} }
// Then try the requested_user_expand // Then try the requested_user_expand
if (request.requested_user_expand) { if (request.requested_user_expand) {
const name = request.requested_user_expand.name || 'Loading...'; const name = request.requested_user_expand.name || 'Unknown';
const email = request.requested_user_expand.email || 'Loading...'; const email = request.requested_user_expand.email || 'Unknown';
return { name, email }; return { name, email };
} }
// Last fallback - don't use "Unknown" to avoid confusing users // Last fallback
return { name: 'Loading...', email: 'Loading...' }; return { name: 'Unknown', email: 'Unknown' };
}; };
// Update openDetailModal to call the prop function // Update openDetailModal to call the prop function

View file

@ -113,12 +113,14 @@ const TableWrapper: React.FC<{
eventRequests: ExtendedEventRequest[]; eventRequests: ExtendedEventRequest[];
handleSelectRequest: (request: ExtendedEventRequest) => void; handleSelectRequest: (request: ExtendedEventRequest) => void;
handleStatusChange: (id: string, status: "submitted" | "pending" | "completed" | "declined") => Promise<void>; handleStatusChange: (id: string, status: "submitted" | "pending" | "completed" | "declined") => Promise<void>;
}> = ({ eventRequests, handleSelectRequest, handleStatusChange }) => { isLoadingUserData?: boolean;
}> = ({ eventRequests, handleSelectRequest, handleStatusChange, isLoadingUserData = false }) => {
return ( return (
<EventRequestManagementTable <EventRequestManagementTable
eventRequests={eventRequests} eventRequests={eventRequests}
onRequestSelect={handleSelectRequest} onRequestSelect={handleSelectRequest}
onStatusChange={handleStatusChange} onStatusChange={handleStatusChange}
isLoadingUserData={isLoadingUserData}
/> />
); );
}; };
@ -189,29 +191,32 @@ const EventRequestModal: React.FC<EventRequestModalProps> = ({ eventRequests })
} }
}; };
// Immediately load user data on mount // Immediately load user data on mount and when eventRequests change
useEffect(() => { useEffect(() => {
refreshUserDataAndUpdate(eventRequests); if (eventRequests && eventRequests.length > 0) {
}, []); // First update with existing data from props
setLocalEventRequests(eventRequests);
// Effect to update local state when props change
useEffect(() => {
// Only update if we have new eventRequests from props
if (eventRequests.length > 0) {
// First update with what we have from props
setLocalEventRequests(prevRequests => {
// Only replace if we have different data
if (eventRequests.length !== prevRequests.length) {
return eventRequests;
}
return prevRequests;
});
// Then refresh user data // Then refresh user data
refreshUserDataAndUpdate(eventRequests); refreshUserDataAndUpdate(eventRequests);
} }
}, [eventRequests]); }, [eventRequests]);
// Ensure user data is loaded immediately when component mounts
useEffect(() => {
// Refresh user data immediately on mount
refreshUserDataAndUpdate();
// Set up auto-refresh every 30 seconds
const refreshInterval = setInterval(() => {
refreshUserDataAndUpdate();
}, 30000); // 30 seconds
// Clear interval on unmount
return () => {
clearInterval(refreshInterval);
};
}, []);
// Set up event listeners for communication with the table component // Set up event listeners for communication with the table component
useEffect(() => { useEffect(() => {
const handleSelectRequest = (event: CustomEvent) => { const handleSelectRequest = (event: CustomEvent) => {
@ -252,7 +257,7 @@ const EventRequestModal: React.FC<EventRequestModalProps> = ({ eventRequests })
return () => { return () => {
document.removeEventListener('dashboardTabVisible', handleTabVisible); document.removeEventListener('dashboardTabVisible', handleTabVisible);
}; };
}, [localEventRequests]); }, []);
const closeModal = () => { const closeModal = () => {
setIsModalOpen(false); setIsModalOpen(false);
@ -338,6 +343,7 @@ const EventRequestModal: React.FC<EventRequestModalProps> = ({ eventRequests })
eventRequests={localEventRequests} eventRequests={localEventRequests}
handleSelectRequest={handleSelectRequest} handleSelectRequest={handleSelectRequest}
handleStatusChange={handleStatusChange} handleStatusChange={handleStatusChange}
isLoadingUserData={isLoadingUserData}
/> />
</div> </div>
</div> </div>

View file

@ -37,6 +37,7 @@ export interface User extends BaseRecord {
display_preferences?: string; // JSON string of display settings (theme, font size, etc.) display_preferences?: string; // JSON string of display settings (theme, font size, etc.)
accessibility_settings?: string; // JSON string of accessibility settings (color blind mode, reduced motion) accessibility_settings?: string; // JSON string of accessibility settings (color blind mode, reduced motion)
resume?: string; resume?: string;
signed_up?: boolean; // Whether the user has filled out the registration form.
requested_email?: boolean; // Whether the user has requested an IEEE email address requested_email?: boolean; // Whether the user has requested an IEEE email address
} }