better view of the event submissions

does not improve view for event management
This commit is contained in:
chark1es 2025-03-11 06:46:40 -07:00
parent 97ce397281
commit e829eef69f
7 changed files with 968 additions and 872 deletions

View file

@ -17,7 +17,6 @@
"astro": "5.1.1",
"astro-expressive-code": "^0.40.2",
"astro-icon": "^1.1.5",
"axios": "^1.8.2",
"chart.js": "^4.4.7",
"dexie": "^4.0.11",
"framer-motion": "^12.4.4",
@ -438,7 +437,7 @@
"autoprefixer": ["autoprefixer@10.4.20", "", { "dependencies": { "browserslist": "^4.23.3", "caniuse-lite": "^1.0.30001646", "fraction.js": "^4.3.7", "normalize-range": "^0.1.2", "picocolors": "^1.0.1", "postcss-value-parser": "^4.2.0" }, "peerDependencies": { "postcss": "^8.1.0" }, "bin": { "autoprefixer": "bin/autoprefixer" } }, "sha512-XY25y5xSv/wEoqzDyXXME4AFfkZI0P23z6Fs3YgymDnKJkCGOnkL0iTxCa85UTqaSgfcqyf3UA6+c7wUvx/16g=="],
"axios": ["axios@1.8.2", "", { "dependencies": { "follow-redirects": "^1.15.6", "form-data": "^4.0.0", "proxy-from-env": "^1.1.0" } }, "sha512-ls4GYBm5aig9vWx8AWDSGLpnpDQRtWAfrjU+EuytuODrFBkqesN2RkOQCBzrA1RQNHw1SmRMSDDDSwzNAYQ6Rg=="],
"axios": ["axios@1.7.9", "", { "dependencies": { "follow-redirects": "^1.15.6", "form-data": "^4.0.0", "proxy-from-env": "^1.1.0" } }, "sha512-LhLcE7Hbiryz8oMDdDptSrWowmB4Bl6RCt6sIJKpRB4XtVf0iEgewX3au/pJqm+Py1kCASkb/FFKjxQaLtxJvw=="],
"axobject-query": ["axobject-query@4.1.0", "", {}, "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ=="],
@ -1428,8 +1427,6 @@
"@expressive-code/plugin-shiki/shiki": ["shiki@1.29.2", "", { "dependencies": { "@shikijs/core": "1.29.2", "@shikijs/engine-javascript": "1.29.2", "@shikijs/engine-oniguruma": "1.29.2", "@shikijs/langs": "1.29.2", "@shikijs/themes": "1.29.2", "@shikijs/types": "1.29.2", "@shikijs/vscode-textmate": "^10.0.1", "@types/hast": "^3.0.4" } }, "sha512-njXuliz/cP+67jU2hukkxCNuH1yUi4QfdZZY+sMr5PPrIyXSu5iTb/qYC4BiWWB0vZ+7TbdvYUCeL23zpwCfbg=="],
"@iconify/tools/axios": ["axios@1.7.9", "", { "dependencies": { "follow-redirects": "^1.15.6", "form-data": "^4.0.0", "proxy-from-env": "^1.1.0" } }, "sha512-LhLcE7Hbiryz8oMDdDptSrWowmB4Bl6RCt6sIJKpRB4XtVf0iEgewX3au/pJqm+Py1kCASkb/FFKjxQaLtxJvw=="],
"@isaacs/cliui/string-width": ["string-width@5.1.2", "", { "dependencies": { "eastasianwidth": "^0.2.0", "emoji-regex": "^9.2.2", "strip-ansi": "^7.0.1" } }, "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA=="],
"@isaacs/cliui/wrap-ansi": ["wrap-ansi@8.1.0", "", { "dependencies": { "ansi-styles": "^6.1.0", "string-width": "^5.0.1", "strip-ansi": "^7.0.1" } }, "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ=="],

View file

@ -24,7 +24,6 @@
"astro": "5.1.1",
"astro-expressive-code": "^0.40.2",
"astro-icon": "^1.1.5",
"axios": "^1.8.2",
"chart.js": "^4.4.7",
"dexie": "^4.0.11",
"framer-motion": "^12.4.4",

View file

@ -5,7 +5,7 @@ import EventRequestForm from "./Officer_EventRequestForm/EventRequestForm";
import UserEventRequests from "./Officer_EventRequestForm/UserEventRequests";
import { Collections } from "../../schemas/pocketbase/schema";
import { DataSyncService } from "../../scripts/database/DataSyncService";
import { EventRequestFormPreviewModal } from "./Officer_EventRequestForm/EventRequestFormPreview";
import { EventRequestFormPreviewModalWrapper } from "./Officer_EventRequestForm/EventRequestFormPreview";
// Import the EventRequest type from UserEventRequests to ensure consistency
import type { EventRequest as UserEventRequest } from "./Officer_EventRequestForm/UserEventRequests";
@ -116,123 +116,72 @@ if (auth.isAuthenticated()) {
</div>
</div>
<!-- The modal will be rendered through the global function and event system -->
<EventRequestFormPreviewModalWrapper client:load />
<style is:global>
/* Ensure the modal container is always visible */
#event-request-preview-modal-container {
#event-request-preview-modal-overlay {
position: fixed !important;
top: 0 !important;
left: 0 !important;
right: 0 !important;
bottom: 0 !important;
width: 100vw !important;
height: 100vh !important;
z-index: 99999 !important;
max-width: 100vw !important;
max-height: 100vh !important;
z-index: 999999 !important;
overflow: auto !important;
margin: 0 !important;
padding: 0 !important;
}
/* Style for the modal backdrop */
#event-request-preview-modal-container > div > div:first-child {
position: fixed !important;
top: 0 !important;
left: 0 !important;
width: 100vw !important;
height: 100vh !important;
z-index: 99999 !important;
background-color: rgba(0, 0, 0, 0.8) !important;
backdrop-filter: blur(8px) !important;
display: flex !important;
align-items: center !important;
justify-content: center !important;
overflow: auto !important;
background-color: rgba(0, 0, 0, 0.6) !important;
backdrop-filter: blur(4px) !important;
}
/* Style for the modal content */
#event-request-preview-modal-container > div > div > div {
z-index: 100000 !important;
#event-request-preview-modal-overlay > div {
z-index: 1000000 !important;
position: relative !important;
max-width: 90vw !important;
max-width: min(90vw, 1024px) !important;
width: 100% !important;
max-height: 90vh !important;
overflow: auto !important;
margin: 2rem !important;
margin: 0 !important;
background-color: var(--color-base-100) !important;
border-radius: 1rem !important;
box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25) !important;
}
</style>
<!-- Add the modal component -->
<EventRequestFormPreviewModal client:load />
<div class="dashboard-section hidden" id="eventRequestFormSection">
<!-- ... existing code ... -->
</div>
<script is:inline>
// Define the global function immediately to ensure it's available
window.showEventRequestFormPreview = function (formData) {
// console.log(
// "Global showEventRequestFormPreview called with data",
// formData
// );
// Remove any elements that might be obstructing the view
const removeObstructions = () => {
// Find any elements with high z-index that might be obstructing
document.querySelectorAll('[style*="z-index"]').forEach((el) => {
if (
el.id !== "event-request-preview-modal-container" &&
!el.closest("#event-request-preview-modal-container")
) {
// Store original z-index to restore later
if (!el.dataset.originalZIndex) {
el.dataset.originalZIndex = el.style.zIndex;
}
// Temporarily lower z-index
el.style.zIndex = "0";
}
});
};
console.log("showEventRequestFormPreview called with formData:", formData);
// Create a custom event to trigger the preview
const event = new CustomEvent("showEventRequestPreviewModal", {
detail: { formData },
});
// Remove obstructions before showing modal
removeObstructions();
console.log("Dispatching event with detail:", event.detail);
// Dispatch event to show modal
document.dispatchEvent(event);
// console.log("showEventRequestPreviewModal event dispatched");
// Ensure modal container is visible
setTimeout(() => {
const modalContainer = document.getElementById(
"event-request-preview-modal-container",
);
if (modalContainer) {
modalContainer.style.zIndex = "99999";
modalContainer.style.position = "fixed";
modalContainer.style.top = "0";
modalContainer.style.left = "0";
modalContainer.style.width = "100vw";
modalContainer.style.height = "100vh";
modalContainer.style.overflow = "auto";
modalContainer.style.margin = "0";
modalContainer.style.padding = "0";
// Prevent body scrolling when modal is open
document.body.style.overflow = "hidden";
// Force body to allow scrolling
document.body.style.overflow = "auto";
// Add event listener to restore scrolling when modal is closed
const handleModalClose = () => {
document.body.style.overflow = "";
document.removeEventListener("modalClosed", handleModalClose);
};
// Ensure the modal content is properly sized
const modalContent = modalContainer.querySelector("div > div > div");
if (modalContent) {
modalContent.style.maxWidth = "90vw";
modalContent.style.width = "100%";
modalContent.style.maxHeight = "90vh";
modalContent.style.overflow = "auto";
modalContent.style.margin = "2rem";
}
}
}, 100);
document.addEventListener("modalClosed", handleModalClose);
};
</script>

View file

@ -333,30 +333,24 @@ const ASFundingSection: React.FC<ASFundingSectionProps> = ({ formData, onDataCha
<div className="flex justify-between items-center mb-4">
<h3 className="text-xl font-bold text-primary">Invoice Details</h3>
<motion.div
className="bg-base-300/50 p-1 rounded-lg flex items-center"
whileHover="hover"
variants={toggleVariants}
>
<motion.button
className={`px-4 py-2 rounded-md text-sm font-medium transition-colors ${!showJsonInput ? 'bg-primary text-primary-content' : 'hover:bg-base-200'
}`}
<div className="flex mb-4 border rounded-lg overflow-hidden">
<button
type="button"
onClick={() => setShowJsonInput(false)}
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }}
>
Invoice Builder
</motion.button>
<motion.button
className={`px-4 py-2 rounded-md text-sm font-medium transition-colors ${showJsonInput ? 'bg-primary text-primary-content' : 'hover:bg-base-200'
className={`px-4 py-2 rounded-md text-sm font-medium transition-colors ${!showJsonInput ? 'bg-primary text-white' : 'hover:bg-base-200'
}`}
onClick={() => setShowJsonInput(true)}
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }}
>
Paste JSON
</motion.button>
</motion.div>
Visual Editor
</button>
<button
type="button"
onClick={() => setShowJsonInput(true)}
className={`px-4 py-2 rounded-md text-sm font-medium transition-colors ${showJsonInput ? 'bg-primary text-white' : 'hover:bg-base-200'
}`}
>
JSON Editor
</button>
</div>
</div>
{showJsonInput ? (

View file

@ -4,6 +4,8 @@ import { Authentication } from '../../../scripts/pocketbase/Authentication';
import { DataSyncService } from '../../../scripts/database/DataSyncService';
import { Collections } from '../../../schemas/pocketbase/schema';
import type { EventRequest as SchemaEventRequest } from '../../../schemas/pocketbase';
import { EventRequestFormPreview } from './EventRequestFormPreview';
import type { EventRequestFormData } from './EventRequestForm';
// Declare the global window interface to include our custom function
declare global {
@ -17,10 +19,146 @@ export interface EventRequest extends SchemaEventRequest {
invoice_data?: any;
}
// Helper function to convert EventRequest to EventRequestFormData
const convertToFormData = (request: EventRequest): EventRequestFormData => {
try {
// Parse itemized_invoice if it's a string
let invoiceData = {};
try {
if (request.itemized_invoice) {
if (typeof request.itemized_invoice === 'string') {
invoiceData = JSON.parse(request.itemized_invoice);
} else {
invoiceData = request.itemized_invoice;
}
} else if (request.invoice_data) {
invoiceData = request.invoice_data;
}
} catch (e) {
console.error('Error parsing invoice data:', e);
}
// Cast to unknown first, then to EventRequestFormData to avoid type checking
return {
name: request.name,
location: request.location,
start_date_time: request.start_date_time,
end_date_time: request.end_date_time,
event_description: request.event_description || '',
flyers_needed: request.flyers_needed || false,
photography_needed: request.photography_needed || false,
flyer_type: request.flyer_type || [],
other_flyer_type: request.other_flyer_type || '',
flyer_advertising_start_date: request.flyer_advertising_start_date || '',
advertising_format: request.advertising_format || '',
required_logos: request.required_logos || [],
other_logos: [] as File[], // EventRequest doesn't have this as files
flyer_additional_requests: request.flyer_additional_requests || '',
will_or_have_room_booking: request.will_or_have_room_booking || false,
room_booking: null,
room_booking_confirmation: [] as File[], // EventRequest doesn't have this as files
expected_attendance: request.expected_attendance || 0,
food_drinks_being_served: request.food_drinks_being_served || false,
needs_as_funding: request.as_funding_required || false,
as_funding_required: request.as_funding_required || false,
invoice: null,
invoice_files: [],
invoiceData: invoiceData,
needs_graphics: request.flyers_needed || false,
status: request.status || '',
created_by: request.requested_user || '',
id: request.id || '',
created: request.created || '',
updated: request.updated || '',
itemized_invoice: request.itemized_invoice || '',
} as unknown as EventRequestFormData;
} catch (error) {
console.error("Error converting EventRequest to EventRequestFormData:", error);
// Return a minimal valid object to prevent rendering errors
return {
name: request?.name || "Unknown Event",
location: request?.location || "",
start_date_time: request?.start_date_time || new Date().toISOString(),
end_date_time: request?.end_date_time || new Date().toISOString(),
event_description: request?.event_description || "",
flyers_needed: false,
photography_needed: false,
flyer_type: [],
other_flyer_type: "",
flyer_advertising_start_date: "",
advertising_format: "",
required_logos: [],
other_logos: [] as File[],
flyer_additional_requests: "",
will_or_have_room_booking: false,
room_booking: null,
room_booking_confirmation: [] as File[],
expected_attendance: 0,
food_drinks_being_served: false,
needs_as_funding: false,
as_funding_required: false,
invoice: null,
invoice_files: [],
invoiceData: {},
needs_graphics: false,
status: request?.status || "",
created_by: "",
id: request?.id || "",
created: request?.created || "",
updated: request?.updated || "",
itemized_invoice: ""
} as unknown as EventRequestFormData;
}
};
interface UserEventRequestsProps {
eventRequests: EventRequest[];
}
// Create a portal component for the modal to ensure it's rendered at the root level
const EventRequestModal: React.FC<{ isOpen: boolean, onClose: () => void, children: React.ReactNode }> = ({
isOpen,
onClose,
children
}) => {
if (!isOpen) return null;
return (
<div
className="fixed inset-0 bg-black/60 backdrop-blur-sm z-[99999]"
style={{
position: 'fixed',
top: 0,
left: 0,
right: 0,
bottom: 0,
width: '100vw',
height: '100vh',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
padding: '1rem',
margin: 0,
overflow: 'auto'
}}
onClick={onClose}
>
<div
className="bg-base-100 rounded-xl shadow-2xl max-w-4xl w-full max-h-[90vh] overflow-y-auto"
style={{
position: 'relative',
margin: 'auto',
zIndex: 100000
}}
onClick={(e) => e.stopPropagation()}
>
{children}
</div>
</div>
);
};
const UserEventRequests: React.FC<UserEventRequestsProps> = ({ eventRequests: initialEventRequests }) => {
const [eventRequests, setEventRequests] = useState<EventRequest[]>(initialEventRequests);
const [selectedRequest, setSelectedRequest] = useState<EventRequest | null>(null);
@ -191,8 +329,8 @@ const UserEventRequests: React.FC<UserEventRequestsProps> = ({ eventRequests: in
return (
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
className="space-y-6"
>
<div className="flex flex-col sm:flex-row justify-between items-start sm:items-center gap-4 mb-4">
@ -280,15 +418,17 @@ const UserEventRequests: React.FC<UserEventRequestsProps> = ({ eventRequests: in
</span>
</td>
<td>
<div className="flex gap-2">
<button
className="btn btn-ghost btn-sm rounded-full"
onClick={() => openDetailModal(request)}
className="btn btn-sm btn-outline"
onClick={(e) => {
e.stopPropagation();
openDetailModal(request);
}}
>
<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="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z" />
</svg>
View Details
</button>
</div>
</td>
</tr>
))}
@ -339,14 +479,19 @@ const UserEventRequests: React.FC<UserEventRequestsProps> = ({ eventRequests: in
)}
</div>
<div className="card-actions justify-end mt-4">
<div className="flex gap-2">
<button
className="btn btn-primary btn-sm"
onClick={() => openDetailModal(request)}
className="btn btn-sm btn-outline"
onClick={(e) => {
e.stopPropagation();
openDetailModal(request);
}}
>
View Details
</button>
</div>
</div>
</div>
</motion.div>
))}
</div>
@ -381,382 +526,45 @@ const UserEventRequests: React.FC<UserEventRequestsProps> = ({ eventRequests: in
</ul>
</div>
{/* Event Request Detail Modal */}
<AnimatePresence>
{/* Use the new portal component for the modal */}
{isModalOpen && selectedRequest && (
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
className="fixed inset-0 z-50 flex items-center justify-center bg-black bg-opacity-60 backdrop-blur-sm"
onClick={closeModal}
>
<motion.div
initial={{ opacity: 0, scale: 0.95, y: 20 }}
animate={{ opacity: 1, scale: 1, y: 0 }}
exit={{ opacity: 0, scale: 0.95, y: 20 }}
transition={{ type: "spring", damping: 25, stiffness: 300 }}
className="bg-base-100 rounded-xl shadow-2xl max-w-4xl w-full max-h-[90vh] overflow-y-auto"
onClick={(e) => e.stopPropagation()}
<EventRequestModal
isOpen={isModalOpen}
onClose={closeModal}
>
<div className="sticky top-0 z-10 bg-base-100 px-6 py-4 border-b border-base-300 flex justify-between items-center">
<div className="flex items-center gap-2">
<h2 className="text-xl font-bold">{selectedRequest.name}</h2>
<h2 className="text-xl font-bold text-base-content">{selectedRequest.name}</h2>
<span className={`badge ${getStatusBadge(selectedRequest.status)}`}>
{selectedRequest.status || 'Pending'}
</span>
</div>
<div className="flex items-center gap-2">
<button
className="btn btn-sm btn-primary"
onClick={(e) => {
e.stopPropagation();
// console.log('Full Preview button clicked', selectedRequest);
try {
// Direct call to the global function
if (typeof window.showEventRequestFormPreview === 'function') {
window.showEventRequestFormPreview(selectedRequest);
} else {
// console.log('Fallback: showEventRequestPreviewModal event dispatched');
// Fallback to event dispatch if function is not available
const event = new CustomEvent("showEventRequestPreviewModal", {
detail: { formData: selectedRequest }
});
document.dispatchEvent(event);
// console.log('Fallback: showEventRequestPreviewModal event dispatched');
}
} catch (error) {
console.error('Error showing full preview:', error);
}
}}
>
Full Preview
</button>
<button
className="btn btn-sm btn-circle btn-ghost"
className="btn btn-sm btn-circle"
onClick={closeModal}
>
<svg xmlns="http://www.w3.org/2000/svg" className="h-5 w-5" 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>
</div>
<div className="p-6">
<div className="grid grid-cols-1 md:grid-cols-2 gap-8 mb-8">
<div>
<h3 className="text-lg font-semibold mb-4 flex items-center gap-2 text-primary">
<svg xmlns="http://www.w3.org/2000/svg" className="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z" />
</svg>
Event Details
</h3>
<div className="space-y-4 bg-base-200/50 p-4 rounded-lg">
<div>
<p className="text-sm text-base-content/60">Event Name</p>
<p className="font-medium">{selectedRequest.name}</p>
</div>
<div>
<p className="text-sm text-base-content/60">Location</p>
<p className="font-medium">{selectedRequest.location}</p>
</div>
<div>
<p className="text-sm text-base-content/60">Start Date & Time</p>
<p className="font-medium">{formatDate(selectedRequest.start_date_time)}</p>
</div>
<div>
<p className="text-sm text-base-content/60">End Date & Time</p>
<p className="font-medium">{formatDate(selectedRequest.end_date_time)}</p>
</div>
<div>
<p className="text-sm text-base-content/60">Room Booking</p>
<p className="font-medium">{selectedRequest.will_or_have_room_booking ? 'Yes' : 'No'}</p>
</div>
<div>
<p className="text-sm text-base-content/60">Expected Attendance</p>
<p className="font-medium">{selectedRequest.expected_attendance || 'Not specified'}</p>
</div>
</div>
</div>
<div>
<h3 className="text-lg font-semibold mb-4 flex items-center gap-2 text-primary">
<svg xmlns="http://www.w3.org/2000/svg" className="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
</svg>
Event Description
</h3>
<div className="bg-base-200/50 p-4 rounded-lg h-full">
<p className="whitespace-pre-line">{selectedRequest.event_description}</p>
</div>
</div>
</div>
{selectedRequest.flyers_needed && (
<div className="mb-8">
<h3 className="text-lg font-semibold mb-4 flex items-center gap-2 text-primary">
<svg xmlns="http://www.w3.org/2000/svg" className="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z" />
</svg>
PR Materials
</h3>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 bg-base-200/50 p-4 rounded-lg">
<div>
<p className="text-sm text-base-content/60">Flyer Types</p>
<p className="font-medium">
{selectedRequest.flyer_type?.join(', ') || 'Not specified'}
{selectedRequest.other_flyer_type && ` (${selectedRequest.other_flyer_type})`}
</p>
</div>
<div>
<p className="text-sm text-base-content/60">Advertising Start Date</p>
<p className="font-medium">
{selectedRequest.flyer_advertising_start_date
? formatDate(selectedRequest.flyer_advertising_start_date)
: 'Not specified'}
</p>
</div>
<div>
<p className="text-sm text-base-content/60">Required Logos</p>
<p className="font-medium">
{selectedRequest.required_logos?.join(', ') || 'None'}
</p>
</div>
<div>
<p className="text-sm text-base-content/60">Advertising Format</p>
<p className="font-medium">{selectedRequest.advertising_format || 'Not specified'}</p>
</div>
<div className="md:col-span-2">
<p className="text-sm text-base-content/60">Additional Requests</p>
<p className="font-medium whitespace-pre-line">
{selectedRequest.flyer_additional_requests || 'None'}
</p>
</div>
</div>
</div>
)}
{selectedRequest.as_funding_required && (
<div className="mb-8">
<h3 className="text-lg font-semibold mb-4 flex items-center gap-2 text-primary">
<svg xmlns="http://www.w3.org/2000/svg" className="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 8c-1.657 0-3 .895-3 2s1.343 2 3 2 3 .895 3 2-1.343 2-3 2m0-8c1.11 0 2.08.402 2.599 1M12 8V7m0 1v8m0 0v1m0-1c-1.11 0-2.08-.402-2.599-1M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
AS Funding Details
</h3>
<div className="space-y-4 bg-base-200/50 p-4 rounded-lg">
<div>
<p className="text-sm text-base-content/60">Food/Drinks Being Served</p>
<p className="font-medium">
{selectedRequest.food_drinks_being_served ? 'Yes' : 'No'}
</p>
</div>
{selectedRequest.invoice_data && (
<div>
<p className="text-sm text-base-content/60">Vendor</p>
<p className="font-medium">
{selectedRequest.invoice_data.vendor || 'Not specified'}
</p>
</div>
)}
<div>
<p className="text-sm text-base-content/60">Itemized Invoice</p>
{(() => {
try {
let invoiceData: any = null;
// Parse the invoice data if it's a string, or use it directly if it's an object
if (typeof selectedRequest.itemized_invoice === 'string') {
try {
invoiceData = JSON.parse(selectedRequest.itemized_invoice);
} catch (e) {
console.error('Failed to parse invoice JSON:', e);
return (
<pre className="bg-base-300 p-3 rounded-lg text-xs overflow-x-auto mt-2">
{selectedRequest.itemized_invoice || 'Not provided'}
</pre>
);
}
} else if (typeof selectedRequest.itemized_invoice === 'object') {
invoiceData = selectedRequest.itemized_invoice;
}
// If we have valid invoice data with items
if (invoiceData && Array.isArray(invoiceData.items) && invoiceData.items.length > 0) {
// Calculate total from items if not provided or if NaN
let calculatedTotal = 0;
// Try to use the provided total first
if (invoiceData.total !== undefined) {
const parsedTotal = typeof invoiceData.total === 'string'
? parseFloat(invoiceData.total)
: invoiceData.total;
if (!isNaN(parsedTotal)) {
calculatedTotal = parsedTotal;
}
}
// If total is NaN or not provided, calculate from items
if (calculatedTotal === 0 || isNaN(calculatedTotal)) {
calculatedTotal = invoiceData.items.reduce((sum: number, item: any) => {
const quantity = typeof item.quantity === 'string'
? parseFloat(item.quantity)
: (item.quantity || 1);
const unitPrice = typeof item.unit_price === 'string'
? parseFloat(item.unit_price)
: (item.unit_price || 0);
const itemTotal = !isNaN(quantity) && !isNaN(unitPrice)
? quantity * unitPrice
: 0;
return sum + itemTotal;
}, 0);
// Add tax and tip if available
if (invoiceData.tax && !isNaN(parseFloat(invoiceData.tax))) {
calculatedTotal += parseFloat(invoiceData.tax);
}
if (invoiceData.tip && !isNaN(parseFloat(invoiceData.tip))) {
calculatedTotal += parseFloat(invoiceData.tip);
}
}
return (
<div className="bg-base-300 p-3 rounded-lg overflow-x-auto mt-2">
<table className="table w-full">
<thead>
<tr>
<th>Item</th>
<th className="text-right">Qty</th>
<th className="text-right">Price</th>
<th className="text-right">Total</th>
</tr>
</thead>
<tbody>
{invoiceData.items.map((item: any, index: number) => {
const quantity = typeof item.quantity === 'string'
? parseFloat(item.quantity)
: (item.quantity || 1);
const unitPrice = typeof item.unit_price === 'string'
? parseFloat(item.unit_price)
: (item.unit_price || 0);
const itemTotal = !isNaN(quantity) && !isNaN(unitPrice)
? quantity * unitPrice
: 0;
return (
<tr key={index}>
<td>{item.item || 'Unnamed item'}</td>
<td className="text-right">{!isNaN(quantity) ? quantity : 1}</td>
<td className="text-right">${!isNaN(unitPrice) ? unitPrice.toFixed(2) : '0.00'}</td>
<td className="text-right">${!isNaN(itemTotal) ? itemTotal.toFixed(2) : '0.00'}</td>
</tr>
);
})}
</tbody>
<tfoot>
{invoiceData.tax !== undefined && (
<tr>
<td colSpan={3} className="text-right font-medium">Tax:</td>
<td className="text-right">
${typeof invoiceData.tax === 'string'
? (parseFloat(invoiceData.tax) || 0).toFixed(2)
: (invoiceData.tax || 0).toFixed(2)}
</td>
</tr>
)}
{invoiceData.tip !== undefined && (
<tr>
<td colSpan={3} className="text-right font-medium">Tip:</td>
<td className="text-right">
${typeof invoiceData.tip === 'string'
? (parseFloat(invoiceData.tip) || 0).toFixed(2)
: (invoiceData.tip || 0).toFixed(2)}
</td>
</tr>
)}
<tr>
<td colSpan={3} className="text-right font-bold">Total:</td>
<td className="text-right font-bold">
${!isNaN(calculatedTotal) ? calculatedTotal.toFixed(2) : '0.00'}
</td>
</tr>
</tfoot>
</table>
{invoiceData.vendor && (
<div className="mt-3 text-sm">
<span className="font-medium">Vendor:</span> {invoiceData.vendor}
{selectedRequest ? (
<EventRequestFormPreview
formData={convertToFormData(selectedRequest)}
isModal={true}
/>
) : (
<div className="flex items-center justify-center h-64">
<div className="loading loading-spinner loading-lg text-primary"></div>
</div>
)}
</div>
);
} else if (invoiceData && typeof invoiceData.total !== 'undefined') {
// If we have a total but no items, show a simplified view
const total = typeof invoiceData.total === 'string'
? parseFloat(invoiceData.total)
: invoiceData.total;
return (
<div className="bg-base-300 p-3 rounded-lg mt-2">
<div className="flex justify-between items-center">
<span className="font-medium">Total Amount:</span>
<span className="font-bold">${!isNaN(total) ? total.toFixed(2) : '0.00'}</span>
</div>
{invoiceData.vendor && (
<div className="mt-2">
<span className="font-medium">Vendor:</span> {invoiceData.vendor}
</div>
</EventRequestModal>
)}
</div>
);
} else {
// Fallback to display the JSON in a readable format
return (
<pre className="bg-base-300 p-3 rounded-lg text-xs overflow-x-auto mt-2">
{typeof selectedRequest.itemized_invoice === 'object'
? JSON.stringify(selectedRequest.itemized_invoice, null, 2)
: (selectedRequest.itemized_invoice || 'Not provided')}
</pre>
);
}
} catch (error) {
console.error('Error rendering invoice:', error);
return (
<pre className="bg-base-300 p-3 rounded-lg text-xs overflow-x-auto mt-2">
Error displaying invoice. Please check the console for details.
</pre>
);
}
})()}
</div>
</div>
</div>
)}
<div className="mt-8 pt-4 border-t border-base-300">
<div className="flex flex-col sm:flex-row justify-between items-start sm:items-center gap-4">
<div>
<p className="text-sm text-base-content/60">Submission Date</p>
<p className="font-medium">{formatDate(selectedRequest.created)}</p>
</div>
<div className="flex items-center gap-2">
<p className="text-sm text-base-content/60">Status:</p>
<span className={`badge ${getStatusBadge(selectedRequest.status)} badge-lg`}>
{selectedRequest.status || 'Pending'}
</span>
</div>
</div>
</div>
</div>
</motion.div>
</motion.div>
)}
</AnimatePresence>
</motion.div>
);
};

View file

@ -547,7 +547,7 @@ export default function ReimbursementList() {
? 'bg-success text-success-content ring-2 ring-success/20'
: status === 'in_progress'
? 'bg-warning text-warning-content ring-2 ring-warning/20'
: 'bg-primary text-primary-content ring-2 ring-primary/20'
: 'bg-primary text-white ring-2 ring-primary/20'
: isActive
? status === 'rejected'
? 'bg-error/20 text-error'