added event revisions based on erik
This commit is contained in:
parent
f28a076cfb
commit
afc5708e21
11 changed files with 624 additions and 258 deletions
|
@ -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
|
||||||
}}
|
}}
|
||||||
|
|
|
@ -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>
|
||||||
);
|
);
|
||||||
|
|
|
@ -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">
|
||||||
|
|
|
@ -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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -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 */}
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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>
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue