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) => {
// Create itemized invoice string for Pocketbase
const itemizedInvoice = JSON.stringify({
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);
// Calculate if budget exceeds maximum allowed
const maxBudget = Math.min(formData.expected_attendance * 10, 5000);
onDataChange({
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">
AS Funding Details
</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 variants={itemVariants}>
<CustomAlert
type="warning"
title="Important Deadline"
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."
className="mb-4"
icon="heroicons:clock"
type="info"
title="AS Funding Information"
message="AS funding can cover food and other expenses for your event. Please itemize all expenses in the invoice builder below."
className="mb-6"
icon="heroicons:information-circle"
/>
</motion.div>
@ -407,18 +392,15 @@ const ASFundingSection: React.FC<ASFundingSectionProps> = ({ formData, onDataCha
</motion.div>
) : (
<motion.div
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.3 }}
variants={itemVariants}
className="form-control space-y-6"
>
<InvoiceBuilder
invoiceData={formData.invoiceData || {
vendor: '',
items: [],
subtotal: 0,
taxRate: 0,
taxAmount: 0,
tipPercentage: 0,
tipAmount: 0,
total: 0
}}

View file

@ -114,7 +114,7 @@ const EventDetailsSection: React.FC<EventDetailsSectionProps> = ({ formData, onD
{/* Date and Time Section */}
<motion.div
variants={itemVariants}
className="grid grid-cols-1 md:grid-cols-2 gap-6"
className="grid grid-cols-1 gap-6"
>
{/* Event Start Date */}
<motion.div
@ -122,7 +122,7 @@ const EventDetailsSection: React.FC<EventDetailsSectionProps> = ({ formData, onD
whileHover={{ y: -2 }}
>
<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>
</label>
<motion.input
@ -134,26 +134,48 @@ const EventDetailsSection: React.FC<EventDetailsSectionProps> = ({ formData, onD
whileHover="hover"
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>
{/* Event End Date */}
{/* Event End Time */}
<motion.div
className="form-control bg-base-200/50 p-6 rounded-xl shadow-sm hover:shadow-md transition-all duration-300"
whileHover={{ y: -2 }}
>
<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>
</label>
<motion.input
type="datetime-local"
className="input input-bordered focus:input-primary transition-all duration-300 mt-2"
value={formData.end_date_time}
onChange={(e) => onDataChange({ end_date_time: e.target.value })}
required
whileHover="hover"
variants={inputHoverVariants}
/>
<div className="flex flex-col gap-2">
<motion.input
type="time"
className="input input-bordered focus:input-primary transition-all duration-300"
value={formData.end_date_time ? new Date(formData.end_date_time).toTimeString().substring(0, 5) : ''}
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
whileHover="hover"
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>
@ -202,7 +224,7 @@ const EventDetailsSection: React.FC<EventDetailsSectionProps> = ({ formData, onD
onChange={() => onDataChange({ will_or_have_room_booking: true })}
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
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"
className="radio radio-primary"
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
/>
<span className="font-medium">No, I don't need a booking</span>
</motion.label>
</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>
);

View file

@ -13,7 +13,7 @@ import PRSection from './PRSection';
import EventDetailsSection from './EventDetailsSection';
import TAPFormSection from './TAPFormSection';
import ASFundingSection from './ASFundingSection';
import EventRequestFormPreview from './EventRequestFormPreview';
import { EventRequestFormPreview } from './EventRequestFormPreview';
import type { InvoiceData, InvoiceItem } from './InvoiceBuilder';
// Animation variants
@ -119,9 +119,7 @@ const EventRequestForm: React.FC = () => {
invoiceData: {
items: [],
subtotal: 0,
taxRate: 7.75, // Default tax rate for San Diego
taxAmount: 0,
tipPercentage: 15, // Default tip percentage
tipAmount: 0,
total: 0,
vendor: ''
@ -204,7 +202,7 @@ const EventRequestForm: React.FC = () => {
advertising_format: '',
will_or_have_room_booking: false,
expected_attendance: 0,
room_booking: null,
room_booking: null, // No room booking by default
as_funding_required: false,
food_drinks_being_served: false,
itemized_invoice: '',
@ -215,9 +213,7 @@ const EventRequestForm: React.FC = () => {
invoiceData: {
items: [],
subtotal: 0,
taxRate: 7.75, // Default tax rate for San Diego
taxAmount: 0,
tipPercentage: 15, // Default tip percentage
tipAmount: 0,
total: 0,
vendor: ''
@ -272,13 +268,12 @@ const EventRequestForm: React.FC = () => {
name: formData.name,
location: formData.location,
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,
flyers_needed: formData.flyers_needed,
photography_needed: formData.photography_needed,
as_funding_required: formData.needs_as_funding,
food_drinks_being_served: formData.food_drinks_being_served,
// Store the itemized_invoice as a string for backward compatibility
itemized_invoice: formData.itemized_invoice,
flyer_type: formData.flyer_type,
other_flyer_type: formData.other_flyer_type,
@ -288,23 +283,19 @@ const EventRequestForm: React.FC = () => {
advertising_format: formData.advertising_format,
will_or_have_room_booking: formData.will_or_have_room_booking,
expected_attendance: formData.expected_attendance,
// Add these fields explicitly to match the schema
needs_graphics: formData.needs_graphics,
needs_as_funding: formData.needs_as_funding,
// Store the invoice data as a properly formatted JSON object
invoice_data: {
items: formData.invoiceData.items.map(item => ({
item: item.description,
quantity: item.quantity,
unit_price: item.unitPrice
})),
tax: formData.invoiceData.taxAmount,
tip: formData.invoiceData.tipAmount,
taxAmount: formData.invoiceData.taxAmount,
tipAmount: formData.invoiceData.tipAmount,
total: formData.invoiceData.total,
vendor: formData.invoiceData.vendor
},
// Set the initial status to "submitted"
status: EventRequestStatus.SUBMITTED,
};
// Create the record using the Update service
@ -400,37 +391,45 @@ const EventRequestForm: React.FC = () => {
// Validate Event Details Section
const validateEventDetailsSection = () => {
if (!formData.name) {
toast.error('Please enter an event name');
return false;
let valid = true;
const errors: string[] = [];
if (!formData.name || formData.name.trim() === '') {
errors.push('Event name is required');
valid = false;
}
if (!formData.event_description) {
toast.error('Please enter an event description');
return false;
if (!formData.event_description || formData.event_description.trim() === '') {
errors.push('Event description is required');
valid = false;
}
if (!formData.start_date_time) {
toast.error('Please enter a start date and time');
return false;
if (!formData.start_date_time || formData.start_date_time.trim() === '') {
errors.push('Event start date and time is required');
valid = false;
}
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;
}
if (!formData.location) {
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;
return valid;
};
// Validate TAP Form Section
@ -446,6 +445,7 @@ const EventRequestForm: React.FC = () => {
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) {
toast.error('Please upload your room booking confirmation');
return false;
@ -467,17 +467,22 @@ const EventRequestForm: React.FC = () => {
// Validate AS Funding Section
const validateASFundingSection = () => {
if (formData.needs_as_funding) {
// Check if vendor is provided
if (!formData.invoiceData.vendor) {
toast.error('Please enter the vendor/restaurant name');
if (formData.as_funding_required) {
// Check if invoice data is present and has items
if (!formData.invoiceData || !formData.invoiceData.items || formData.invoiceData.items.length === 0) {
setError('Please add at least one item to your invoice');
return false;
}
// No longer require items in the invoice
// Check if at least one invoice file is uploaded
if (!formData.invoice && (!formData.invoice_files || formData.invoice_files.length === 0)) {
toast.error('Please upload at least one invoice file');
// Calculate the total budget from invoice items
const totalBudget = formData.invoiceData.items.reduce(
(sum, item) => sum + (item.unitPrice * item.quantity), 0
);
// 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;
}
}
@ -487,8 +492,11 @@ const EventRequestForm: React.FC = () => {
// Validate all sections before submission
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()) {
setCurrentStep(1);
return false;
}
@ -579,6 +587,14 @@ const EventRequestForm: React.FC = () => {
variants={containerVariants}
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>
<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
const normalizeFormData = (data: EventRequestFormData | (EventRequest & {
invoiceData?: any;
@ -108,7 +122,26 @@ const normalizeFormData = (data: EventRequestFormData | (EventRequest & {
...invoiceData,
...(parsed as any),
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) {
console.error('Error parsing itemized_invoice:', e);
@ -119,7 +152,25 @@ const normalizeFormData = (data: EventRequestFormData | (EventRequest & {
...invoiceData,
...parsed,
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) {
const parsed = eventRequest.invoiceData as any;
@ -128,7 +179,25 @@ const normalizeFormData = (data: EventRequestFormData | (EventRequest & {
...invoiceData,
...parsed,
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
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
isModal: boolean; // Whether to render as a modal or inline component
}
// Define the main EventRequestFormPreview component
@ -495,22 +564,22 @@ const EventRequestFormPreview: React.FC<EventRequestFormPreviewProps> = ({
<span className="text-sm">Expected Attendance</span>
</div>
<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 className="space-y-1">
<div className="flex items-center text-base-content/60">
<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>
<p className="font-medium text-base-content">{formatDateTime(formData.start_date_time)}</p>
</div>
<div className="space-y-1">
<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>
<p className="text-xs text-base-content/60">
Note: Multi-day events require separate submissions for each day.
</p>
</div>
<div className="space-y-1">
@ -637,7 +706,7 @@ const EventRequestFormPreview: React.FC<EventRequestFormPreviewProps> = ({
</h4>
<div className="flex items-center">
<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>
{formData.will_or_have_room_booking && formData.room_booking && (
@ -690,12 +759,12 @@ const EventRequestFormPreview: React.FC<EventRequestFormPreviewProps> = ({
</tr>
</thead>
<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">
<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.unitPrice || 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 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 || (item.quantity * (item.unitPrice || item.unit_price || item.price || 0)) || 0).toFixed(2)}</td>
</tr>
))}
</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 className="py-2 text-right font-medium text-base-content">${(formData.invoiceData.subtotal || 0).toFixed(2)}</td>
</tr>
{formData.invoiceData.taxAmount && formData.invoiceData.taxAmount > 0 && (
<tr className="border-t border-base-300">
<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>
</tr>
)}
{formData.invoiceData.tipAmount && formData.invoiceData.tipAmount > 0 && (
<tr className="border-t border-base-300">
<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>
</tr>
)}
<tr className="border-t border-base-300">
<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">${(typeof formData.invoiceData.taxAmount === 'number' ? formData.invoiceData.taxAmount : 0).toFixed(2)}</td>
</tr>
<tr className="border-t border-base-300">
<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">${(typeof formData.invoiceData.tipAmount === 'number' ? formData.invoiceData.tipAmount : 0).toFixed(2)}</td>
</tr>
<tr className="bg-primary/5">
<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>
@ -730,28 +795,7 @@ const EventRequestFormPreview: React.FC<EventRequestFormPreviewProps> = ({
</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>
);
};

View file

@ -713,6 +713,17 @@ const InvoiceBuilder: React.FC<InvoiceBuilderProps> = ({ invoiceData, onChange }
</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 */}
{invoiceData.items.length === 0 && (
<CustomAlert
@ -732,6 +743,42 @@ const InvoiceBuilder: React.FC<InvoiceBuilderProps> = ({ invoiceData, onChange }
className="mt-4"
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>
);
};

View file

@ -1,8 +1,9 @@
import React, { useState } from 'react';
import React, { useState, useEffect } from 'react';
import { motion } from 'framer-motion';
import type { EventRequestFormData } from './EventRequestForm';
import type { EventRequest } from '../../../schemas/pocketbase';
import CustomAlert from '../universal/CustomAlert';
import FilePreview from '../universal/FilePreview';
// Enhanced animation variants
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
const fileUploadVariants = {
initial: { scale: 1 },
@ -57,13 +71,42 @@ interface TAPFormSectionProps {
const TAPFormSection: React.FC<TAPFormSectionProps> = ({ formData, onDataChange }) => {
const [roomBookingFile, setRoomBookingFile] = useState<File | null>(formData.room_booking);
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>) => {
if (e.target.files && e.target.files.length > 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);
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) {
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);
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 (
<motion.div
className="space-y-8"
@ -105,10 +177,10 @@ const TAPFormSection: React.FC<TAPFormSectionProps> = ({ formData, onDataChange
<motion.div variants={itemVariants}>
<CustomAlert
type="info"
title="Important 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."
title="CRITICAL INFORMATION"
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"
icon="heroicons:information-circle"
icon="heroicons:exclamation-triangle"
/>
</motion.div>
@ -127,7 +199,11 @@ const TAPFormSection: React.FC<TAPFormSectionProps> = ({ formData, onDataChange
type="number"
className="input input-bordered focus:input-primary transition-all duration-300 w-full pr-20"
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"
placeholder="Enter expected attendance"
required
@ -138,6 +214,30 @@ const TAPFormSection: React.FC<TAPFormSectionProps> = ({ formData, onDataChange
people
</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
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>
{/* Room booking confirmation */}
{/* Room booking confirmation - Show file error if present */}
<motion.div
variants={itemVariants}
className="form-control bg-base-200/50 p-6 rounded-xl shadow-sm hover:shadow-md transition-all duration-300"
@ -160,53 +260,110 @@ const TAPFormSection: React.FC<TAPFormSectionProps> = ({ formData, onDataChange
>
<label className="label">
<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>
<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'
}`}
variants={fileUploadVariants}
initial="initial"
whileHover="hover"
whileTap="tap"
onDragOver={handleDragOver}
onDragLeave={handleDragLeave}
onDrop={handleDrop}
onClick={() => document.getElementById('room-booking-file')?.click()}
>
<input
id="room-booking-file"
type="file"
className="hidden"
onChange={handleRoomBookingFileChange}
accept=".pdf,.png,.jpg,.jpeg"
/>
<div className="flex flex-col items-center justify-center gap-3">
<motion.div
className="w-16 h-16 bg-primary/10 rounded-full flex items-center justify-center text-primary"
whileHover={{ rotate: 15, scale: 1.1 }}
>
<svg xmlns="http://www.w3.org/2000/svg" className="h-8 w-8" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M7 16a4 4 0 01-.88-7.903A5 5 0 1115.9 6L16 6a5 5 0 011 9.9M15 13l-3-3m0 0l-3 3m3-3v12" />
</svg>
</motion.div>
{roomBookingFile ? (
<>
<p className="font-medium text-primary">File selected:</p>
<p className="text-sm">{roomBookingFile.name}</p>
<p className="text-xs text-gray-500">Click or drag to replace</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>
</>
)}
{fileError && (
<div className="mt-2 mb-2">
<CustomAlert
type="error"
title="File Error"
message={fileError}
/>
</div>
</motion.div>
)}
{formData.will_or_have_room_booking ? (
<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'
}`}
variants={fileUploadVariants}
initial="initial"
whileHover="hover"
whileTap="tap"
onDragOver={handleDragOver}
onDragLeave={handleDragLeave}
onDrop={handleDrop}
onClick={() => document.getElementById('room-booking-file')?.click()}
>
<input
id="room-booking-file"
type="file"
className="hidden"
onChange={handleRoomBookingFileChange}
accept=".pdf,.png,.jpg,.jpeg"
/>
<div className="flex flex-col items-center justify-center gap-3">
<motion.div
className="w-16 h-16 bg-primary/10 rounded-full flex items-center justify-center text-primary"
whileHover={{ rotate: 15, scale: 1.1 }}
>
<svg xmlns="http://www.w3.org/2000/svg" className="h-8 w-8" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M7 16a4 4 0 01-.88-7.903A5 5 0 1115.9 6L16 6a5 5 0 011 9.9M15 13l-3-3m0 0l-3 3m3-3v12" />
</svg>
</motion.div>
{roomBookingFile ? (
<>
<p className="font-medium text-primary">File selected:</p>
<p className="text-sm">{roomBookingFile.name}</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="text-xs text-gray-500">Accepted formats: PDF, PNG, JPG (Max size: 1MB)</p>
</>
)}
</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>
{/* Food/Drinks */}

View file

@ -256,7 +256,7 @@ try {
<EventRequestModal client:load eventRequests={allEventRequests} />
</div>
<script define:vars={{ ANIMATION_DELAYS }}>
<script type="module" define:vars={{ ANIMATION_DELAYS }}>
// Import the DataSyncService for client-side use
import { DataSyncService } from "../../scripts/database/DataSyncService";
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
document.addEventListener("DOMContentLoaded", async () => {
// Initialize DataSyncService for client-side

View file

@ -708,7 +708,7 @@ const ASFundingTab: React.FC<{ request: ExtendedEventRequest }> = ({ request })
</div>
<div className="mt-4">
<InvoiceTable invoiceData={invoiceData} />
<InvoiceTable invoiceData={invoiceData} expectedAttendance={request.expected_attendance} />
</div>
</motion.div>
@ -726,7 +726,7 @@ const ASFundingTab: React.FC<{ request: ExtendedEventRequest }> = ({ request })
};
// 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 (!invoiceData) {
return (
@ -811,7 +811,7 @@ const InvoiceTable: React.FC<{ invoiceData: any }> = ({ invoiceData }) => {
// Calculate subtotal from items
const subtotal = items.reduce((sum: number, item: any) => {
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);
}, 0);
@ -820,9 +820,26 @@ const InvoiceTable: React.FC<{ invoiceData: any }> = ({ invoiceData }) => {
const tip = parseFloat(parsedInvoice.tip || parsedInvoice.tipAmount || 0);
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
return (
<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">
<thead>
<tr>
@ -840,7 +857,7 @@ const InvoiceTable: React.FC<{ invoiceData: any }> = ({ invoiceData }) => {
: (item?.item || item?.description || item?.name || 'N/A');
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;
return (
@ -858,21 +875,17 @@ const InvoiceTable: React.FC<{ invoiceData: any }> = ({ invoiceData }) => {
<td colSpan={3} className="text-right font-medium">Subtotal:</td>
<td>${subtotal.toFixed(2)}</td>
</tr>
{tax > 0 && (
<tr>
<td colSpan={3} className="text-right font-medium">Tax:</td>
<td>${tax.toFixed(2)}</td>
</tr>
)}
{tip > 0 && (
<tr>
<td colSpan={3} className="text-right font-medium">Tip:</td>
<td>${tip.toFixed(2)}</td>
</tr>
)}
<tr>
<td colSpan={3} className="text-right font-medium">Tax:</td>
<td>${tax.toFixed(2)}</td>
</tr>
<tr>
<td colSpan={3} className="text-right font-medium">Tip:</td>
<td>${tip.toFixed(2)}</td>
</tr>
<tr>
<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>
</tfoot>
</table>
@ -888,9 +901,9 @@ const InvoiceTable: React.FC<{ invoiceData: any }> = ({ invoiceData }) => {
return (
<CustomAlert
type="error"
title="Processing Error"
message="An unexpected error occurred while processing the invoice."
icon="heroicons:exclamation-triangle"
title="Error"
message="An error occurred while processing the invoice data."
icon="heroicons:exclamation-circle"
/>
);
}
@ -1216,21 +1229,23 @@ const PRMaterialsTab: React.FC<{ request: ExtendedEventRequest }> = ({ request }
)}
{/* No files message */}
{!hasFiles && (
<motion.div
className="mt-4"
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.2 }}
>
<CustomAlert
type="info"
title="No PR Files"
message="No PR-related files have been uploaded."
icon="heroicons:information-circle"
/>
</motion.div>
)}
{
!hasFiles && (
<motion.div
className="mt-4"
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.2 }}
>
<CustomAlert
type="info"
title="No PR Files"
message="No PR-related files have been uploaded."
icon="heroicons:information-circle"
/>
</motion.div>
)
}
{/* File Preview Modal */}
<FilePreviewModal
@ -1464,11 +1479,6 @@ const EventRequestDetails = ({
<label className="text-xs text-gray-400">Start Date & Time</label>
<p className="text-white">{formatDate(request.start_date_time)}</p>
</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>

View file

@ -33,12 +33,14 @@ interface EventRequestManagementTableProps {
eventRequests: ExtendedEventRequest[];
onRequestSelect: (request: ExtendedEventRequest) => void;
onStatusChange: (id: string, status: "submitted" | "pending" | "completed" | "declined") => Promise<void>;
isLoadingUserData?: boolean;
}
const EventRequestManagementTable = ({
eventRequests: initialEventRequests,
onRequestSelect,
onStatusChange
onStatusChange,
isLoadingUserData = false
}: EventRequestManagementTableProps) => {
const [eventRequests, setEventRequests] = useState<ExtendedEventRequest[]>(initialEventRequests);
const [filteredRequests, setFilteredRequests] = useState<ExtendedEventRequest[]>(initialEventRequests);
@ -69,13 +71,54 @@ const EventRequestManagementTable = ({
true, // Force sync
'', // No filter
'-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`);
setEventRequests(updatedRequests);
applyFilters(updatedRequests);
setEventRequests(requestsWithUsers);
applyFilters(requestsWithUsers);
} catch (error) {
// console.error('Error refreshing event requests:', error);
toast.error('Failed to refresh event requests');
@ -217,25 +260,32 @@ const EventRequestManagementTable = ({
// Helper to get user display info - always show email address
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
if (request.expand?.requested_user) {
const user = request.expand.requested_user;
// Show "Loading..." instead of "Unknown" while data is being fetched
const name = user.name || 'Loading...';
const name = user.name || 'Unknown';
// Always show email regardless of emailVisibility
const email = user.email || 'Loading...';
const email = user.email || 'Unknown';
return { name, email };
}
// Then try the requested_user_expand
if (request.requested_user_expand) {
const name = request.requested_user_expand.name || 'Loading...';
const email = request.requested_user_expand.email || 'Loading...';
const name = request.requested_user_expand.name || 'Unknown';
const email = request.requested_user_expand.email || 'Unknown';
return { name, email };
}
// Last fallback - don't use "Unknown" to avoid confusing users
return { name: 'Loading...', email: 'Loading...' };
// Last fallback
return { name: 'Unknown', email: 'Unknown' };
};
// Update openDetailModal to call the prop function

View file

@ -113,12 +113,14 @@ const TableWrapper: React.FC<{
eventRequests: ExtendedEventRequest[];
handleSelectRequest: (request: ExtendedEventRequest) => void;
handleStatusChange: (id: string, status: "submitted" | "pending" | "completed" | "declined") => Promise<void>;
}> = ({ eventRequests, handleSelectRequest, handleStatusChange }) => {
isLoadingUserData?: boolean;
}> = ({ eventRequests, handleSelectRequest, handleStatusChange, isLoadingUserData = false }) => {
return (
<EventRequestManagementTable
eventRequests={eventRequests}
onRequestSelect={handleSelectRequest}
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(() => {
refreshUserDataAndUpdate(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;
});
if (eventRequests && eventRequests.length > 0) {
// First update with existing data from props
setLocalEventRequests(eventRequests);
// Then refresh user data
refreshUserDataAndUpdate(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
useEffect(() => {
const handleSelectRequest = (event: CustomEvent) => {
@ -252,7 +257,7 @@ const EventRequestModal: React.FC<EventRequestModalProps> = ({ eventRequests })
return () => {
document.removeEventListener('dashboardTabVisible', handleTabVisible);
};
}, [localEventRequests]);
}, []);
const closeModal = () => {
setIsModalOpen(false);
@ -338,6 +343,7 @@ const EventRequestModal: React.FC<EventRequestModalProps> = ({ eventRequests })
eventRequests={localEventRequests}
handleSelectRequest={handleSelectRequest}
handleStatusChange={handleStatusChange}
isLoadingUserData={isLoadingUserData}
/>
</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.)
accessibility_settings?: string; // JSON string of accessibility settings (color blind mode, reduced motion)
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
}