Add authentication #17
4 changed files with 718 additions and 419 deletions
|
@ -4,6 +4,8 @@ import { Get } from "../../scripts/pocketbase/Get";
|
||||||
import EventRequestForm from "./Officer_EventRequestForm/EventRequestForm";
|
import EventRequestForm from "./Officer_EventRequestForm/EventRequestForm";
|
||||||
import UserEventRequests from "./Officer_EventRequestForm/UserEventRequests";
|
import UserEventRequests from "./Officer_EventRequestForm/UserEventRequests";
|
||||||
import { Collections } from "../../schemas/pocketbase/schema";
|
import { Collections } from "../../schemas/pocketbase/schema";
|
||||||
|
import { DataSyncService } from "../../scripts/database/DataSyncService";
|
||||||
|
import { EventRequestFormPreviewModal } from "./Officer_EventRequestForm/EventRequestFormPreview";
|
||||||
|
|
||||||
// Import the EventRequest type from UserEventRequests to ensure consistency
|
// Import the EventRequest type from UserEventRequests to ensure consistency
|
||||||
import type { EventRequest as UserEventRequest } from "./Officer_EventRequestForm/UserEventRequests";
|
import type { EventRequest as UserEventRequest } from "./Officer_EventRequestForm/UserEventRequests";
|
||||||
|
@ -23,196 +25,267 @@ let error: string | null = null;
|
||||||
// This provides initial data for server-side rendering
|
// This provides initial data for server-side rendering
|
||||||
// Client-side will use IndexedDB for data management
|
// Client-side will use IndexedDB for data management
|
||||||
if (auth.isAuthenticated()) {
|
if (auth.isAuthenticated()) {
|
||||||
try {
|
try {
|
||||||
const userId = auth.getUserId();
|
const userId = auth.getUserId();
|
||||||
if (userId) {
|
if (userId) {
|
||||||
userEventRequests = await get.getAll<EventRequest>(
|
userEventRequests = await get.getAll<EventRequest>(
|
||||||
Collections.EVENT_REQUESTS,
|
Collections.EVENT_REQUESTS,
|
||||||
`requested_user="${userId}"`,
|
`requested_user="${userId}"`,
|
||||||
"-created"
|
"-created",
|
||||||
);
|
);
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
console.error("Failed to fetch user event requests:", err);
|
|
||||||
error = "Failed to load your event requests. Please try again later.";
|
|
||||||
}
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Failed to fetch user event requests:", err);
|
||||||
|
error = "Failed to load your event requests. Please try again later.";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
---
|
---
|
||||||
|
|
||||||
<div class="w-full max-w-6xl mx-auto py-8 px-4">
|
<div class="w-full max-w-6xl mx-auto py-8 px-4">
|
||||||
<div class="mb-8">
|
<div class="mb-8">
|
||||||
<h1 class="text-3xl font-bold text-white mb-2">Event Request Form</h1>
|
<h1 class="text-3xl font-bold text-white mb-2">Event Request Form</h1>
|
||||||
<p class="text-gray-300 mb-4">
|
<p class="text-gray-300 mb-4">
|
||||||
Submit your event request at least 6 weeks before your event. After
|
Submit your event request at least 6 weeks before your event. After
|
||||||
submitting, please notify PR and/or Coordinators in the #-events
|
submitting, please notify PR and/or Coordinators in the #-events Slack
|
||||||
Slack channel.
|
channel.
|
||||||
</p>
|
</p>
|
||||||
<div class="bg-base-300/50 p-4 rounded-lg text-sm text-gray-300">
|
<div class="bg-base-300/50 p-4 rounded-lg text-sm text-gray-300">
|
||||||
<p class="font-medium mb-2">This form includes sections for:</p>
|
<p class="font-medium mb-2">This form includes sections for:</p>
|
||||||
<ul class="list-disc list-inside space-y-1 ml-2">
|
<ul class="list-disc list-inside space-y-1 ml-2">
|
||||||
<li>PR Materials (if needed)</li>
|
<li>PR Materials (if needed)</li>
|
||||||
<li>Event Details</li>
|
<li>Event Details</li>
|
||||||
<li>TAP Form Information</li>
|
<li>TAP Form Information</li>
|
||||||
<li>AS Funding (if needed)</li>
|
<li>AS Funding (if needed)</li>
|
||||||
</ul>
|
</ul>
|
||||||
<p class="mt-3">
|
<p class="mt-3">
|
||||||
Your progress is automatically saved as you fill out the form.
|
Your progress is automatically saved as you fill out the form.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Tabs -->
|
<!-- Tabs -->
|
||||||
<div class="tabs tabs-boxed mb-6">
|
<div class="tabs tabs-boxed mb-6">
|
||||||
<a class="tab tab-lg tab-active" id="form-tab">Submit Event Request</a>
|
<a class="tab tab-lg tab-active" id="form-tab">Submit Event Request</a>
|
||||||
<a class="tab tab-lg" id="submissions-tab">View Your Submissions</a>
|
<a class="tab tab-lg" id="submissions-tab">View Your Submissions</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Form Tab Content -->
|
||||||
|
<div
|
||||||
|
id="form-content"
|
||||||
|
class="bg-base-200 rounded-lg shadow-xl overflow-hidden"
|
||||||
|
>
|
||||||
|
<div class="p-6">
|
||||||
|
<EventRequestForm client:load />
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Form Tab Content -->
|
<!-- Submissions Tab Content -->
|
||||||
<div
|
<div id="submissions-content" class="hidden">
|
||||||
id="form-content"
|
<div class="bg-base-200 rounded-lg shadow-xl overflow-hidden p-6">
|
||||||
class="bg-base-200 rounded-lg shadow-xl overflow-hidden"
|
<h2 class="text-2xl font-bold text-white mb-4">
|
||||||
>
|
Your Event Request Submissions
|
||||||
<div class="p-6">
|
</h2>
|
||||||
<EventRequestForm client:load />
|
|
||||||
</div>
|
{
|
||||||
</div>
|
error && (
|
||||||
|
<div class="alert alert-error mb-6">
|
||||||
<!-- Submissions Tab Content -->
|
<svg
|
||||||
<div id="submissions-content" class="hidden">
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
<div class="bg-base-200 rounded-lg shadow-xl overflow-hidden p-6">
|
class="h-6 w-6 stroke-current shrink-0"
|
||||||
<h2 class="text-2xl font-bold text-white mb-4">
|
fill="none"
|
||||||
Your Event Request Submissions
|
viewBox="0 0 24 24"
|
||||||
</h2>
|
>
|
||||||
|
<path
|
||||||
{
|
stroke-linecap="round"
|
||||||
error && (
|
stroke-linejoin="round"
|
||||||
<div class="alert alert-error mb-6">
|
stroke-width="2"
|
||||||
<svg
|
d="M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z"
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
/>
|
||||||
class="h-6 w-6 stroke-current shrink-0"
|
</svg>
|
||||||
fill="none"
|
<span>{error}</span>
|
||||||
viewBox="0 0 24 24"
|
</div>
|
||||||
>
|
)
|
||||||
<path
|
}
|
||||||
stroke-linecap="round"
|
|
||||||
stroke-linejoin="round"
|
{
|
||||||
stroke-width="2"
|
!error && (
|
||||||
d="M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z"
|
<UserEventRequests client:load eventRequests={userEventRequests} />
|
||||||
/>
|
)
|
||||||
</svg>
|
}
|
||||||
<span>{error}</span>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
!error && (
|
|
||||||
<UserEventRequests
|
|
||||||
client:load
|
|
||||||
eventRequests={userEventRequests}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<style is:global>
|
||||||
|
/* Ensure the modal container is always visible */
|
||||||
|
#event-request-preview-modal-container {
|
||||||
|
position: fixed !important;
|
||||||
|
top: 0 !important;
|
||||||
|
left: 0 !important;
|
||||||
|
width: 100% !important;
|
||||||
|
height: 100% !important;
|
||||||
|
z-index: 99999 !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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Style for the modal content */
|
||||||
|
#event-request-preview-modal-container > div > div > div {
|
||||||
|
z-index: 100000 !important;
|
||||||
|
position: relative !important;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<!-- Add the modal component -->
|
||||||
|
<EventRequestFormPreviewModal client:load />
|
||||||
|
|
||||||
<div class="dashboard-section hidden" id="eventRequestFormSection">
|
<div class="dashboard-section hidden" id="eventRequestFormSection">
|
||||||
<!-- ... existing code ... -->
|
<!-- ... existing code ... -->
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script>
|
<script is:inline>
|
||||||
// Import the DataSyncService for client-side use
|
// Define the global function immediately to ensure it's available
|
||||||
import { DataSyncService } from "../../scripts/database/DataSyncService";
|
window.showEventRequestFormPreview = function (formData) {
|
||||||
import { Collections } from "../../schemas/pocketbase/schema";
|
console.log(
|
||||||
import { Authentication } from "../../scripts/pocketbase/Authentication";
|
"Global showEventRequestFormPreview called with data",
|
||||||
|
formData,
|
||||||
|
);
|
||||||
|
|
||||||
// Tab switching logic
|
// Remove any elements that might be obstructing the view
|
||||||
document.addEventListener("DOMContentLoaded", async () => {
|
const removeObstructions = () => {
|
||||||
// Initialize DataSyncService for client-side
|
// Find any elements with high z-index that might be obstructing
|
||||||
const dataSync = DataSyncService.getInstance();
|
document.querySelectorAll('[style*="z-index"]').forEach((el) => {
|
||||||
const auth = Authentication.getInstance();
|
if (
|
||||||
|
el.id !== "event-request-preview-modal-container" &&
|
||||||
// Prefetch data into IndexedDB if authenticated
|
!el.closest("#event-request-preview-modal-container")
|
||||||
if (auth.isAuthenticated()) {
|
) {
|
||||||
try {
|
// Store original z-index to restore later
|
||||||
const userId = auth.getUserId();
|
if (!el.dataset.originalZIndex) {
|
||||||
if (userId) {
|
el.dataset.originalZIndex = el.style.zIndex;
|
||||||
// Force sync to ensure we have the latest data
|
}
|
||||||
await dataSync.syncCollection(
|
// Temporarily lower z-index
|
||||||
Collections.EVENT_REQUESTS,
|
el.style.zIndex = "0";
|
||||||
`requested_user="${userId}"`,
|
|
||||||
"-created"
|
|
||||||
);
|
|
||||||
console.log(
|
|
||||||
"Initial data sync complete for user event requests"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
console.error("Error during initial data sync:", err);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
const formTab = document.getElementById("form-tab");
|
// Create a custom event to trigger the preview
|
||||||
const submissionsTab = document.getElementById("submissions-tab");
|
const event = new CustomEvent("showEventRequestPreviewModal", {
|
||||||
const formContent = document.getElementById("form-content");
|
detail: { formData },
|
||||||
const submissionsContent = document.getElementById(
|
|
||||||
"submissions-content"
|
|
||||||
);
|
|
||||||
|
|
||||||
// Function to switch tabs
|
|
||||||
const switchTab = (
|
|
||||||
activeTab: HTMLElement,
|
|
||||||
activeContent: HTMLElement,
|
|
||||||
inactiveTab: HTMLElement,
|
|
||||||
inactiveContent: HTMLElement
|
|
||||||
) => {
|
|
||||||
// Update tab classes
|
|
||||||
activeTab.classList.add("tab-active");
|
|
||||||
inactiveTab.classList.remove("tab-active");
|
|
||||||
|
|
||||||
// Show/hide content
|
|
||||||
activeContent.classList.remove("hidden");
|
|
||||||
inactiveContent.classList.add("hidden");
|
|
||||||
|
|
||||||
// Dispatch event to refresh submissions when switching to submissions tab
|
|
||||||
if (activeTab.id === "submissions-tab") {
|
|
||||||
// Dispatch a custom event that the UserEventRequests component listens for
|
|
||||||
document.dispatchEvent(new CustomEvent("dashboardTabVisible"));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Add click event listeners to tabs
|
|
||||||
formTab?.addEventListener("click", (e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
if (formContent && submissionsContent && submissionsTab) {
|
|
||||||
switchTab(
|
|
||||||
formTab,
|
|
||||||
formContent,
|
|
||||||
submissionsTab,
|
|
||||||
submissionsContent
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
submissionsTab?.addEventListener("click", (e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
if (formContent && submissionsContent && formTab) {
|
|
||||||
switchTab(
|
|
||||||
submissionsTab,
|
|
||||||
submissionsContent,
|
|
||||||
formTab,
|
|
||||||
formContent
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Listen for visibility changes
|
|
||||||
document.addEventListener("visibilitychange", () => {
|
|
||||||
if (document.visibilityState === "visible") {
|
|
||||||
// Dispatch custom event that components can listen for
|
|
||||||
document.dispatchEvent(new CustomEvent("dashboardTabVisible"));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Remove obstructions before showing modal
|
||||||
|
removeObstructions();
|
||||||
|
|
||||||
|
// 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 = "100%";
|
||||||
|
modalContainer.style.height = "100%";
|
||||||
|
}
|
||||||
|
}, 100);
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
// Import the DataSyncService for client-side use
|
||||||
|
import { DataSyncService } from "../../scripts/database/DataSyncService";
|
||||||
|
import { Collections } from "../../schemas/pocketbase/schema";
|
||||||
|
import { Authentication } from "../../scripts/pocketbase/Authentication";
|
||||||
|
|
||||||
|
// Tab switching logic
|
||||||
|
document.addEventListener("DOMContentLoaded", async () => {
|
||||||
|
// Initialize DataSyncService for client-side
|
||||||
|
const dataSync = DataSyncService.getInstance();
|
||||||
|
const auth = Authentication.getInstance();
|
||||||
|
|
||||||
|
// Prefetch data into IndexedDB if authenticated
|
||||||
|
if (auth.isAuthenticated()) {
|
||||||
|
try {
|
||||||
|
const userId = auth.getUserId();
|
||||||
|
if (userId) {
|
||||||
|
// Force sync to ensure we have the latest data
|
||||||
|
await dataSync.syncCollection(
|
||||||
|
Collections.EVENT_REQUESTS,
|
||||||
|
`requested_user="${userId}"`,
|
||||||
|
"-created",
|
||||||
|
);
|
||||||
|
console.log("Initial data sync complete for user event requests");
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Error during initial data sync:", err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const formTab = document.getElementById("form-tab");
|
||||||
|
const submissionsTab = document.getElementById("submissions-tab");
|
||||||
|
const formContent = document.getElementById("form-content");
|
||||||
|
const submissionsContent = document.getElementById("submissions-content");
|
||||||
|
|
||||||
|
// Function to switch tabs
|
||||||
|
const switchTab = (
|
||||||
|
activeTab: HTMLElement,
|
||||||
|
activeContent: HTMLElement,
|
||||||
|
inactiveTab: HTMLElement,
|
||||||
|
inactiveContent: HTMLElement,
|
||||||
|
) => {
|
||||||
|
// Update tab classes
|
||||||
|
activeTab.classList.add("tab-active");
|
||||||
|
inactiveTab.classList.remove("tab-active");
|
||||||
|
|
||||||
|
// Show/hide content
|
||||||
|
activeContent.classList.remove("hidden");
|
||||||
|
inactiveContent.classList.add("hidden");
|
||||||
|
|
||||||
|
// Dispatch event to refresh submissions when switching to submissions tab
|
||||||
|
if (activeTab.id === "submissions-tab") {
|
||||||
|
// Dispatch a custom event that the UserEventRequests component listens for
|
||||||
|
document.dispatchEvent(new CustomEvent("dashboardTabVisible"));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Add click event listeners to tabs
|
||||||
|
formTab?.addEventListener("click", (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
if (formContent && submissionsContent && submissionsTab) {
|
||||||
|
switchTab(formTab, formContent, submissionsTab, submissionsContent);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
submissionsTab?.addEventListener("click", (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
if (formContent && submissionsContent && formTab) {
|
||||||
|
switchTab(submissionsTab, submissionsContent, formTab, formContent);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Listen for visibility changes
|
||||||
|
document.addEventListener("visibilitychange", () => {
|
||||||
|
if (document.visibilityState === "visible") {
|
||||||
|
// Dispatch custom event that components can listen for
|
||||||
|
document.dispatchEvent(new CustomEvent("dashboardTabVisible"));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -720,7 +720,7 @@ const EventRequestForm: React.FC = () => {
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="bg-base-200/50 p-6 rounded-lg">
|
<div className="bg-base-200/50 p-6 rounded-lg">
|
||||||
<EventRequestFormPreview formData={formData} />
|
<EventRequestFormPreview formData={formData} isModal={false} />
|
||||||
|
|
||||||
<div className="divider my-6">Ready to Submit?</div>
|
<div className="divider my-6">Ready to Submit?</div>
|
||||||
|
|
||||||
|
|
|
@ -1,15 +1,104 @@
|
||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
import { motion } from 'framer-motion';
|
import { motion, AnimatePresence } from 'framer-motion';
|
||||||
import type { EventRequestFormData } from './EventRequestForm';
|
import type { EventRequestFormData } from './EventRequestForm';
|
||||||
import type { InvoiceItem } from './InvoiceBuilder';
|
import type { InvoiceItem } from './InvoiceBuilder';
|
||||||
import type { EventRequest } from '../../../schemas/pocketbase';
|
import type { EventRequest } from '../../../schemas/pocketbase';
|
||||||
import { FlyerTypes, LogoOptions, EventRequestStatus } from '../../../schemas/pocketbase';
|
import { FlyerTypes, LogoOptions, EventRequestStatus } from '../../../schemas/pocketbase';
|
||||||
|
|
||||||
|
// Create a standalone component that can be used to show the preview as a modal
|
||||||
|
export const EventRequestFormPreviewModal: React.FC = () => {
|
||||||
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
|
const [formData, setFormData] = useState<EventRequestFormData | null>(null);
|
||||||
|
|
||||||
|
// Function to handle showing the modal
|
||||||
|
const showModal = (data: any) => {
|
||||||
|
console.log('showModal called with data', data);
|
||||||
|
setFormData(data);
|
||||||
|
setIsOpen(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Add the global function to the window object directly from the component
|
||||||
|
useEffect(() => {
|
||||||
|
// Store the original function if it exists
|
||||||
|
const originalFunction = window.showEventRequestFormPreview;
|
||||||
|
|
||||||
|
// Define the global function
|
||||||
|
window.showEventRequestFormPreview = (data: any) => {
|
||||||
|
console.log('Global showEventRequestFormPreview called with data', data);
|
||||||
|
showModal(data);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Listen for the custom event as a fallback
|
||||||
|
const handleShowModal = (event: CustomEvent) => {
|
||||||
|
console.log('Received showEventRequestPreviewModal event', event.detail);
|
||||||
|
if (event.detail && event.detail.formData) {
|
||||||
|
showModal(event.detail.formData);
|
||||||
|
} else {
|
||||||
|
console.error('Event detail or formData is missing', event.detail);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Add event listener
|
||||||
|
document.addEventListener('showEventRequestPreviewModal', handleShowModal as EventListener);
|
||||||
|
console.log('Event listener for showEventRequestPreviewModal added');
|
||||||
|
|
||||||
|
// Clean up
|
||||||
|
return () => {
|
||||||
|
// Restore the original function if it existed
|
||||||
|
if (originalFunction) {
|
||||||
|
window.showEventRequestFormPreview = originalFunction;
|
||||||
|
} else {
|
||||||
|
// Otherwise delete our function
|
||||||
|
delete window.showEventRequestFormPreview;
|
||||||
|
}
|
||||||
|
|
||||||
|
document.removeEventListener('showEventRequestPreviewModal', handleShowModal as EventListener);
|
||||||
|
console.log('Event listener for showEventRequestPreviewModal removed');
|
||||||
|
};
|
||||||
|
}, []); // Empty dependency array - only run once on mount
|
||||||
|
|
||||||
|
const handleClose = () => {
|
||||||
|
console.log('Modal closed');
|
||||||
|
setIsOpen(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Force the modal to be in the document body to avoid nesting issues
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
id="event-request-preview-modal-container"
|
||||||
|
style={{
|
||||||
|
position: 'fixed',
|
||||||
|
top: 0,
|
||||||
|
left: 0,
|
||||||
|
width: '100%',
|
||||||
|
height: '100%',
|
||||||
|
zIndex: 99999,
|
||||||
|
pointerEvents: isOpen ? 'auto' : 'none'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<EventRequestFormPreview
|
||||||
|
formData={formData || undefined}
|
||||||
|
isOpen={isOpen}
|
||||||
|
onClose={handleClose}
|
||||||
|
isModal={true}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
interface EventRequestFormPreviewProps {
|
interface EventRequestFormPreviewProps {
|
||||||
formData?: EventRequestFormData; // Optional prop to directly pass form data
|
formData?: EventRequestFormData; // Optional prop to directly pass form data
|
||||||
|
isOpen?: boolean; // Control whether the modal is open
|
||||||
|
onClose?: () => void; // Callback when modal is closed
|
||||||
|
isModal?: boolean; // Whether to render as a modal or inline component
|
||||||
}
|
}
|
||||||
|
|
||||||
const EventRequestFormPreview: React.FC<EventRequestFormPreviewProps> = ({ formData: propFormData }) => {
|
const EventRequestFormPreview: React.FC<EventRequestFormPreviewProps> = ({
|
||||||
|
formData: propFormData,
|
||||||
|
isOpen = true,
|
||||||
|
onClose = () => { },
|
||||||
|
isModal = false
|
||||||
|
}) => {
|
||||||
const [formData, setFormData] = useState<EventRequestFormData | null>(propFormData || null);
|
const [formData, setFormData] = useState<EventRequestFormData | null>(propFormData || null);
|
||||||
const [loading, setLoading] = useState<boolean>(!propFormData);
|
const [loading, setLoading] = useState<boolean>(!propFormData);
|
||||||
|
|
||||||
|
@ -55,23 +144,6 @@ const EventRequestFormPreview: React.FC<EventRequestFormPreviewProps> = ({ formD
|
||||||
};
|
};
|
||||||
}, [propFormData]);
|
}, [propFormData]);
|
||||||
|
|
||||||
if (loading) {
|
|
||||||
return (
|
|
||||||
<div className="flex justify-center items-center h-64">
|
|
||||||
<div className="loading loading-spinner loading-lg"></div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!formData) {
|
|
||||||
return (
|
|
||||||
<div className="text-center py-12">
|
|
||||||
<h3 className="text-xl font-bold mb-4">No Form Data Available</h3>
|
|
||||||
<p className="text-gray-400">Please fill out the form to see a preview.</p>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Format date and time for display
|
// Format date and time for display
|
||||||
const formatDateTime = (dateTimeString: string) => {
|
const formatDateTime = (dateTimeString: string) => {
|
||||||
if (!dateTimeString) return 'Not specified';
|
if (!dateTimeString) return 'Not specified';
|
||||||
|
@ -84,242 +156,335 @@ const EventRequestFormPreview: React.FC<EventRequestFormPreviewProps> = ({ formD
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
// Handle click on the backdrop to close the modal
|
||||||
<motion.div
|
const handleBackdropClick = (e: React.MouseEvent<HTMLDivElement>) => {
|
||||||
initial={{ opacity: 0 }}
|
if (e.target === e.currentTarget) {
|
||||||
animate={{ opacity: 1 }}
|
onClose();
|
||||||
className="space-y-8"
|
}
|
||||||
>
|
};
|
||||||
<div className="bg-base-300 p-6 rounded-lg">
|
|
||||||
<h2 className="text-2xl font-bold mb-4">Event Request Preview</h2>
|
|
||||||
<p className="text-sm text-gray-400 mb-6">
|
|
||||||
This is a preview of your event request. Please review all information before submitting.
|
|
||||||
</p>
|
|
||||||
|
|
||||||
{/* Event Details Section */}
|
// Render the content of the preview
|
||||||
<div className="mb-8">
|
const renderContent = () => {
|
||||||
<h3 className="text-xl font-semibold mb-4 border-b border-base-content/20 pb-2">
|
if (loading) {
|
||||||
Event Details
|
return (
|
||||||
</h3>
|
<div className="flex justify-center items-center h-64">
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
<div className="loading loading-spinner loading-lg"></div>
|
||||||
<div>
|
|
||||||
<p className="text-sm text-gray-400">Event Name</p>
|
|
||||||
<p className="font-medium">{formData.name || 'Not specified'}</p>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<p className="text-sm text-gray-400">Location</p>
|
|
||||||
<p className="font-medium">{formData.location || 'Not specified'}</p>
|
|
||||||
</div>
|
|
||||||
<div className="md:col-span-2">
|
|
||||||
<p className="text-sm text-gray-400">Event Description</p>
|
|
||||||
<p className="font-medium whitespace-pre-line">{formData.event_description || 'Not specified'}</p>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<p className="text-sm text-gray-400">Start Date & Time</p>
|
|
||||||
<p className="font-medium">{formatDateTime(formData.start_date_time)}</p>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<p className="text-sm text-gray-400">End Date & Time</p>
|
|
||||||
<p className="font-medium">{formatDateTime(formData.end_date_time)}</p>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<p className="text-sm text-gray-400">Room Booking</p>
|
|
||||||
<p className="font-medium">{formData.will_or_have_room_booking ? 'Yes' : 'No'}</p>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<p className="text-sm text-gray-400">Expected Attendance</p>
|
|
||||||
<p className="font-medium">{formData.expected_attendance || 'Not specified'}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
{/* PR Materials Section */}
|
if (!formData) {
|
||||||
{formData.flyers_needed && (
|
return (
|
||||||
|
<div className="text-center py-12">
|
||||||
|
<h3 className="text-xl font-bold mb-4">No Form Data Available</h3>
|
||||||
|
<p className="text-gray-400">Please fill out the form to see a preview.</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-8">
|
||||||
|
<div className={`${isModal ? 'bg-base-300' : ''} p-6 rounded-lg`}>
|
||||||
|
{isModal && (
|
||||||
|
<>
|
||||||
|
<h2 className="text-2xl font-bold mb-4">Event Request Preview</h2>
|
||||||
|
<p className="text-sm text-gray-400 mb-6">
|
||||||
|
This is a preview of your event request. Please review all information before submitting.
|
||||||
|
</p>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Event Details Section */}
|
||||||
<div className="mb-8">
|
<div className="mb-8">
|
||||||
<h3 className="text-xl font-semibold mb-4 border-b border-base-content/20 pb-2">
|
<h3 className="text-xl font-semibold mb-4 border-b border-base-content/20 pb-2">
|
||||||
PR Materials
|
Event Details
|
||||||
</h3>
|
</h3>
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
<div>
|
<div>
|
||||||
<p className="text-sm text-gray-400">Flyer Types</p>
|
<p className="text-sm text-gray-400">Event Name</p>
|
||||||
<ul className="list-disc list-inside">
|
<p className="font-medium">{formData.name || 'Not specified'}</p>
|
||||||
{formData.flyer_type.map((type, index) => (
|
|
||||||
<li key={index} className="font-medium">
|
|
||||||
{type === 'digital_with_social' && 'Digital flyer with social media advertising'}
|
|
||||||
{type === 'digital_no_social' && 'Digital flyer without social media advertising'}
|
|
||||||
{type === 'physical_with_advertising' && 'Physical flyer with advertising'}
|
|
||||||
{type === 'physical_no_advertising' && 'Physical flyer without advertising'}
|
|
||||||
{type === 'newsletter' && 'Newsletter'}
|
|
||||||
{type === 'other' && 'Other: ' + formData.other_flyer_type}
|
|
||||||
</li>
|
|
||||||
))}
|
|
||||||
</ul>
|
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<p className="text-sm text-gray-400">Advertising Start Date</p>
|
<p className="text-sm text-gray-400">Location</p>
|
||||||
<p className="font-medium">{formData.flyer_advertising_start_date || 'Not specified'}</p>
|
<p className="font-medium">{formData.location || 'Not specified'}</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="md:col-span-2">
|
<div className="md:col-span-2">
|
||||||
<p className="text-sm text-gray-400">Required Logos</p>
|
<p className="text-sm text-gray-400">Event Description</p>
|
||||||
<div className="flex flex-wrap gap-2 mt-1">
|
<p className="font-medium whitespace-pre-line">{formData.event_description || 'Not specified'}</p>
|
||||||
{formData.required_logos.map((logo, index) => (
|
|
||||||
<span key={index} className="badge badge-primary">{logo}</span>
|
|
||||||
))}
|
|
||||||
{formData.required_logos.length === 0 && <p className="font-medium">None specified</p>}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<p className="text-sm text-gray-400">Advertising Format</p>
|
<p className="text-sm text-gray-400">Start Date & Time</p>
|
||||||
|
<p className="font-medium">{formatDateTime(formData.start_date_time)}</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p className="text-sm text-gray-400">End Date & Time</p>
|
||||||
|
<p className="font-medium">{formatDateTime(formData.end_date_time)}</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p className="text-sm text-gray-400">Room Booking</p>
|
||||||
|
<p className="font-medium">{formData.will_or_have_room_booking ? 'Yes' : 'No'}</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p className="text-sm text-gray-400">Expected Attendance</p>
|
||||||
|
<p className="font-medium">{formData.expected_attendance || 'Not specified'}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* PR Materials Section */}
|
||||||
|
{formData.flyers_needed && (
|
||||||
|
<div className="mb-8">
|
||||||
|
<h3 className="text-xl font-semibold mb-4 border-b border-base-content/20 pb-2">
|
||||||
|
PR Materials
|
||||||
|
</h3>
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
|
<div>
|
||||||
|
<p className="text-sm text-gray-400">Flyer Types</p>
|
||||||
|
<ul className="list-disc list-inside">
|
||||||
|
{formData.flyer_type.map((type, index) => (
|
||||||
|
<li key={index} className="font-medium">
|
||||||
|
{type === 'digital_with_social' && 'Digital flyer with social media advertising'}
|
||||||
|
{type === 'digital_no_social' && 'Digital flyer without social media advertising'}
|
||||||
|
{type === 'physical_with_advertising' && 'Physical flyer with advertising'}
|
||||||
|
{type === 'physical_no_advertising' && 'Physical flyer without advertising'}
|
||||||
|
{type === 'newsletter' && 'Newsletter'}
|
||||||
|
{type === 'other' && 'Other: ' + formData.other_flyer_type}
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p className="text-sm text-gray-400">Advertising Start Date</p>
|
||||||
|
<p className="font-medium">{formData.flyer_advertising_start_date || 'Not specified'}</p>
|
||||||
|
</div>
|
||||||
|
<div className="md:col-span-2">
|
||||||
|
<p className="text-sm text-gray-400">Required Logos</p>
|
||||||
|
<div className="flex flex-wrap gap-2 mt-1">
|
||||||
|
{formData.required_logos.map((logo, index) => (
|
||||||
|
<span key={index} className="badge badge-primary">{logo}</span>
|
||||||
|
))}
|
||||||
|
{formData.required_logos.length === 0 && <p className="font-medium">None specified</p>}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p className="text-sm text-gray-400">Advertising Format</p>
|
||||||
|
<p className="font-medium">
|
||||||
|
{formData.advertising_format === 'pdf' && 'PDF'}
|
||||||
|
{formData.advertising_format === 'jpeg' && 'JPEG'}
|
||||||
|
{formData.advertising_format === 'png' && 'PNG'}
|
||||||
|
{formData.advertising_format === 'does_not_matter' && 'Does not matter'}
|
||||||
|
{!formData.advertising_format && 'Not specified'}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p className="text-sm text-gray-400">Photography Needed</p>
|
||||||
|
<p className="font-medium">{formData.photography_needed ? 'Yes' : 'No'}</p>
|
||||||
|
</div>
|
||||||
|
{formData.flyer_additional_requests && (
|
||||||
|
<div className="md:col-span-2">
|
||||||
|
<p className="text-sm text-gray-400">Additional Requests</p>
|
||||||
|
<p className="font-medium whitespace-pre-line">{formData.flyer_additional_requests}</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* TAP Form Section */}
|
||||||
|
<div className="mb-8">
|
||||||
|
<h3 className="text-xl font-semibold mb-4 border-b border-base-content/20 pb-2">
|
||||||
|
TAP Form Details
|
||||||
|
</h3>
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
|
<div>
|
||||||
|
<p className="text-sm text-gray-400">Expected Attendance</p>
|
||||||
|
<p className="font-medium">{formData.expected_attendance || 'Not specified'}</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p className="text-sm text-gray-400">Room Booking</p>
|
||||||
<p className="font-medium">
|
<p className="font-medium">
|
||||||
{formData.advertising_format === 'pdf' && 'PDF'}
|
{formData.room_booking ? formData.room_booking.name : 'No file uploaded'}
|
||||||
{formData.advertising_format === 'jpeg' && 'JPEG'}
|
|
||||||
{formData.advertising_format === 'png' && 'PNG'}
|
|
||||||
{formData.advertising_format === 'does_not_matter' && 'Does not matter'}
|
|
||||||
{!formData.advertising_format && 'Not specified'}
|
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<p className="text-sm text-gray-400">Photography Needed</p>
|
<p className="text-sm text-gray-400">AS Funding Required</p>
|
||||||
<p className="font-medium">{formData.photography_needed ? 'Yes' : 'No'}</p>
|
<p className="font-medium">{formData.as_funding_required ? 'Yes' : 'No'}</p>
|
||||||
</div>
|
</div>
|
||||||
{formData.flyer_additional_requests && (
|
|
||||||
<div className="md:col-span-2">
|
|
||||||
<p className="text-sm text-gray-400">Additional Requests</p>
|
|
||||||
<p className="font-medium whitespace-pre-line">{formData.flyer_additional_requests}</p>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* TAP Form Section */}
|
|
||||||
<div className="mb-8">
|
|
||||||
<h3 className="text-xl font-semibold mb-4 border-b border-base-content/20 pb-2">
|
|
||||||
TAP Form Details
|
|
||||||
</h3>
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
||||||
<div>
|
|
||||||
<p className="text-sm text-gray-400">Expected Attendance</p>
|
|
||||||
<p className="font-medium">{formData.expected_attendance || 'Not specified'}</p>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<p className="text-sm text-gray-400">Room Booking</p>
|
|
||||||
<p className="font-medium">
|
|
||||||
{formData.room_booking ? formData.room_booking.name : 'No file uploaded'}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<p className="text-sm text-gray-400">AS Funding Required</p>
|
|
||||||
<p className="font-medium">{formData.as_funding_required ? 'Yes' : 'No'}</p>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<p className="text-sm text-gray-400">Food/Drinks Being Served</p>
|
|
||||||
<p className="font-medium">{formData.food_drinks_being_served ? 'Yes' : 'No'}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* AS Funding Section */}
|
|
||||||
{formData.as_funding_required && (
|
|
||||||
<div className="mb-8">
|
|
||||||
<h3 className="text-xl font-semibold mb-4 border-b border-base-content/20 pb-2">
|
|
||||||
AS Funding Details
|
|
||||||
</h3>
|
|
||||||
|
|
||||||
<div className="grid grid-cols-1 gap-4 mb-4">
|
|
||||||
<div>
|
<div>
|
||||||
<p className="text-sm text-gray-400">Vendor</p>
|
<p className="text-sm text-gray-400">Food/Drinks Being Served</p>
|
||||||
<p className="font-medium">{formData.invoiceData.vendor || 'Not specified'}</p>
|
<p className="font-medium">{formData.food_drinks_being_served ? 'Yes' : 'No'}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{formData.invoiceData.items.length > 0 ? (
|
|
||||||
<div className="overflow-x-auto mb-4">
|
|
||||||
<table className="table w-full">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>Description</th>
|
|
||||||
<th className="text-right">Qty</th>
|
|
||||||
<th className="text-right">Unit Price</th>
|
|
||||||
<th className="text-right">Amount</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
{formData.invoiceData.items.map((item: InvoiceItem) => (
|
|
||||||
<tr key={item.id}>
|
|
||||||
<td>{item.description}</td>
|
|
||||||
<td className="text-right">{item.quantity}</td>
|
|
||||||
<td className="text-right">${item.unitPrice.toFixed(2)}</td>
|
|
||||||
<td className="text-right">${item.amount.toFixed(2)}</td>
|
|
||||||
</tr>
|
|
||||||
))}
|
|
||||||
</tbody>
|
|
||||||
<tfoot>
|
|
||||||
<tr>
|
|
||||||
<td colSpan={3} className="text-right font-medium">Subtotal:</td>
|
|
||||||
<td className="text-right">${formData.invoiceData.subtotal.toFixed(2)}</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td colSpan={3} className="text-right font-medium">Tax ({formData.invoiceData.taxRate}%):</td>
|
|
||||||
<td className="text-right">${formData.invoiceData.taxAmount.toFixed(2)}</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td colSpan={3} className="text-right font-medium">Tip ({formData.invoiceData.tipPercentage}%):</td>
|
|
||||||
<td className="text-right">${formData.invoiceData.tipAmount.toFixed(2)}</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td colSpan={3} className="text-right font-bold">Total:</td>
|
|
||||||
<td className="text-right font-bold">${formData.invoiceData.total.toFixed(2)}</td>
|
|
||||||
</tr>
|
|
||||||
</tfoot>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<div className="alert alert-info mb-4">
|
|
||||||
<div>No invoice items have been added yet.</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<div className="mt-4 mb-4">
|
|
||||||
<p className="text-sm text-gray-400 mb-2">JSON Format (For Submission):</p>
|
|
||||||
<pre className="bg-base-300 p-4 rounded-lg overflow-x-auto text-xs">
|
|
||||||
{JSON.stringify({
|
|
||||||
items: formData.invoiceData.items.map(item => ({
|
|
||||||
item: item.description,
|
|
||||||
quantity: item.quantity,
|
|
||||||
unit_price: item.unitPrice
|
|
||||||
})),
|
|
||||||
tax: formData.invoiceData.taxAmount,
|
|
||||||
tip: formData.invoiceData.tipAmount,
|
|
||||||
total: formData.invoiceData.total,
|
|
||||||
vendor: formData.invoiceData.vendor
|
|
||||||
}, null, 2)}
|
|
||||||
</pre>
|
|
||||||
<p className="text-xs text-gray-400 mt-2">
|
|
||||||
This is the structured format that will be submitted to our database.
|
|
||||||
It ensures that your invoice data is properly organized and can be
|
|
||||||
processed correctly by our system.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<p className="text-sm text-gray-400">Invoice Files</p>
|
|
||||||
{formData.invoice_files && formData.invoice_files.length > 0 ? (
|
|
||||||
<div className="mt-2 space-y-1">
|
|
||||||
{formData.invoice_files.map((file, index) => (
|
|
||||||
<p key={index} className="font-medium">{file.name}</p>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
) : formData.invoice ? (
|
|
||||||
<p className="font-medium">{formData.invoice.name}</p>
|
|
||||||
) : (
|
|
||||||
<p className="font-medium">No files uploaded</p>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
|
||||||
|
{/* AS Funding Section */}
|
||||||
|
{formData.as_funding_required && (
|
||||||
|
<div className="mb-8">
|
||||||
|
<h3 className="text-xl font-semibold mb-4 border-b border-base-content/20 pb-2">
|
||||||
|
AS Funding Details
|
||||||
|
</h3>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-1 gap-4 mb-4">
|
||||||
|
<div>
|
||||||
|
<p className="text-sm text-gray-400">Vendor</p>
|
||||||
|
<p className="font-medium">{formData.invoiceData.vendor || 'Not specified'}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{formData.invoiceData.items.length > 0 ? (
|
||||||
|
<div className="overflow-x-auto mb-4">
|
||||||
|
<table className="table w-full">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Description</th>
|
||||||
|
<th className="text-right">Qty</th>
|
||||||
|
<th className="text-right">Unit Price</th>
|
||||||
|
<th className="text-right">Amount</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{formData.invoiceData.items.map((item: InvoiceItem) => (
|
||||||
|
<tr key={item.id}>
|
||||||
|
<td>{item.description}</td>
|
||||||
|
<td className="text-right">{item.quantity}</td>
|
||||||
|
<td className="text-right">${item.unitPrice.toFixed(2)}</td>
|
||||||
|
<td className="text-right">${item.amount.toFixed(2)}</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
<tfoot>
|
||||||
|
<tr>
|
||||||
|
<td colSpan={3} className="text-right font-medium">Subtotal:</td>
|
||||||
|
<td className="text-right">${formData.invoiceData.subtotal.toFixed(2)}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td colSpan={3} className="text-right font-medium">Tax ({formData.invoiceData.taxRate}%):</td>
|
||||||
|
<td className="text-right">${formData.invoiceData.taxAmount.toFixed(2)}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td colSpan={3} className="text-right font-medium">Tip ({formData.invoiceData.tipPercentage}%):</td>
|
||||||
|
<td className="text-right">${formData.invoiceData.tipAmount.toFixed(2)}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td colSpan={3} className="text-right font-bold">Total:</td>
|
||||||
|
<td className="text-right font-bold">${formData.invoiceData.total.toFixed(2)}</td>
|
||||||
|
</tr>
|
||||||
|
</tfoot>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="alert alert-info mb-4">
|
||||||
|
<div>No invoice items have been added yet.</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="mt-4 mb-4">
|
||||||
|
<p className="text-sm text-gray-400 mb-2">JSON Format (For Submission):</p>
|
||||||
|
<pre className="bg-base-300 p-4 rounded-lg overflow-x-auto text-xs">
|
||||||
|
{JSON.stringify({
|
||||||
|
items: formData.invoiceData.items.map(item => ({
|
||||||
|
item: item.description,
|
||||||
|
quantity: item.quantity,
|
||||||
|
unit_price: item.unitPrice
|
||||||
|
})),
|
||||||
|
tax: formData.invoiceData.taxAmount,
|
||||||
|
tip: formData.invoiceData.tipAmount,
|
||||||
|
total: formData.invoiceData.total,
|
||||||
|
vendor: formData.invoiceData.vendor
|
||||||
|
}, null, 2)}
|
||||||
|
</pre>
|
||||||
|
<p className="text-xs text-gray-400 mt-2">
|
||||||
|
This is the structured format that will be submitted to our database.
|
||||||
|
It ensures that your invoice data is properly organized and can be
|
||||||
|
processed correctly by our system.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<p className="text-sm text-gray-400">Invoice Files</p>
|
||||||
|
{formData.invoice_files && formData.invoice_files.length > 0 ? (
|
||||||
|
<div className="mt-2 space-y-1">
|
||||||
|
{formData.invoice_files.map((file, index) => (
|
||||||
|
<p key={index} className="font-medium">{file.name}</p>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
) : formData.invoice ? (
|
||||||
|
<p className="font-medium">{formData.invoice.name}</p>
|
||||||
|
) : (
|
||||||
|
<p className="font-medium">No files uploaded</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</motion.div>
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
// If not a modal, render the content directly
|
||||||
|
if (!isModal) {
|
||||||
|
return (
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0 }}
|
||||||
|
animate={{ opacity: 1 }}
|
||||||
|
className="w-full"
|
||||||
|
>
|
||||||
|
{renderContent()}
|
||||||
|
</motion.div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// If it's a modal, render with the modal wrapper
|
||||||
|
return (
|
||||||
|
<AnimatePresence>
|
||||||
|
{isOpen && (
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0 }}
|
||||||
|
animate={{ opacity: 1 }}
|
||||||
|
exit={{ opacity: 0 }}
|
||||||
|
className="fixed inset-0 flex items-center justify-center bg-black/80 backdrop-blur-md overflow-hidden"
|
||||||
|
onClick={handleBackdropClick}
|
||||||
|
style={{
|
||||||
|
position: 'fixed',
|
||||||
|
top: 0,
|
||||||
|
left: 0,
|
||||||
|
right: 0,
|
||||||
|
bottom: 0,
|
||||||
|
zIndex: 99999,
|
||||||
|
width: '100vw',
|
||||||
|
height: '100vh',
|
||||||
|
margin: 0,
|
||||||
|
padding: 0
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<motion.div
|
||||||
|
initial={{ scale: 0.95, opacity: 0 }}
|
||||||
|
animate={{ scale: 1, opacity: 1 }}
|
||||||
|
exit={{ scale: 0.95, opacity: 0 }}
|
||||||
|
transition={{ type: "spring", stiffness: 300, damping: 25 }}
|
||||||
|
className="bg-base-100 rounded-xl shadow-2xl w-full max-w-5xl max-h-[90vh] overflow-y-auto m-4"
|
||||||
|
onClick={(e) => e.stopPropagation()}
|
||||||
|
style={{
|
||||||
|
position: 'relative',
|
||||||
|
zIndex: 100000
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div className="sticky top-0 z-10 bg-base-100 px-6 py-4 border-b border-base-300 flex justify-between items-center">
|
||||||
|
<h2 className="text-xl font-bold">Event Request Preview</h2>
|
||||||
|
<button
|
||||||
|
className="btn btn-sm btn-circle btn-ghost"
|
||||||
|
onClick={onClose}
|
||||||
|
>
|
||||||
|
✕
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div className="p-6">
|
||||||
|
{renderContent()}
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
</motion.div>
|
||||||
|
)}
|
||||||
|
</AnimatePresence>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -5,6 +5,13 @@ import { DataSyncService } from '../../../scripts/database/DataSyncService';
|
||||||
import { Collections } from '../../../schemas/pocketbase/schema';
|
import { Collections } from '../../../schemas/pocketbase/schema';
|
||||||
import type { EventRequest as SchemaEventRequest } from '../../../schemas/pocketbase';
|
import type { EventRequest as SchemaEventRequest } from '../../../schemas/pocketbase';
|
||||||
|
|
||||||
|
// Declare the global window interface to include our custom function
|
||||||
|
declare global {
|
||||||
|
interface Window {
|
||||||
|
showEventRequestFormPreview?: (formData: any) => void;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Extended EventRequest interface with additional properties needed for this component
|
// Extended EventRequest interface with additional properties needed for this component
|
||||||
export interface EventRequest extends SchemaEventRequest {
|
export interface EventRequest extends SchemaEventRequest {
|
||||||
invoice_data?: any;
|
invoice_data?: any;
|
||||||
|
@ -98,15 +105,37 @@ const UserEventRequests: React.FC<UserEventRequestsProps> = ({ eventRequests: in
|
||||||
|
|
||||||
switch (status.toLowerCase()) {
|
switch (status.toLowerCase()) {
|
||||||
case 'approved':
|
case 'approved':
|
||||||
return 'badge-success';
|
case 'completed':
|
||||||
|
return 'badge-success text-white';
|
||||||
case 'rejected':
|
case 'rejected':
|
||||||
return 'badge-error';
|
case 'declined':
|
||||||
|
return 'badge-error text-white';
|
||||||
case 'pending':
|
case 'pending':
|
||||||
return 'badge-warning';
|
return 'badge-warning text-black';
|
||||||
case 'submitted':
|
case 'submitted':
|
||||||
return 'badge-info';
|
return 'badge-info text-white';
|
||||||
default:
|
default:
|
||||||
return 'badge-warning';
|
return 'badge-warning text-black';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Get card border class based on status
|
||||||
|
const getCardBorderClass = (status?: string) => {
|
||||||
|
if (!status) return 'border-l-warning';
|
||||||
|
|
||||||
|
switch (status.toLowerCase()) {
|
||||||
|
case 'approved':
|
||||||
|
case 'completed':
|
||||||
|
return 'border-l-success';
|
||||||
|
case 'rejected':
|
||||||
|
case 'declined':
|
||||||
|
return 'border-l-error';
|
||||||
|
case 'pending':
|
||||||
|
return 'border-l-warning';
|
||||||
|
case 'submitted':
|
||||||
|
return 'border-l-info';
|
||||||
|
default:
|
||||||
|
return 'border-l-warning';
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -226,7 +255,7 @@ const UserEventRequests: React.FC<UserEventRequestsProps> = ({ eventRequests: in
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{eventRequests.map((request) => (
|
{eventRequests.map((request) => (
|
||||||
<tr key={request.id} className="hover">
|
<tr key={request.id} className={`hover border-l-4 ${getCardBorderClass(request.status)}`}>
|
||||||
<td className="font-medium">{request.name}</td>
|
<td className="font-medium">{request.name}</td>
|
||||||
<td>{formatDate(request.start_date_time)}</td>
|
<td>{formatDate(request.start_date_time)}</td>
|
||||||
<td>{request.location}</td>
|
<td>{request.location}</td>
|
||||||
|
@ -274,7 +303,7 @@ const UserEventRequests: React.FC<UserEventRequestsProps> = ({ eventRequests: in
|
||||||
initial={{ opacity: 0, y: 20 }}
|
initial={{ opacity: 0, y: 20 }}
|
||||||
animate={{ opacity: 1, y: 0 }}
|
animate={{ opacity: 1, y: 0 }}
|
||||||
transition={{ duration: 0.3 }}
|
transition={{ duration: 0.3 }}
|
||||||
className="card bg-base-200 shadow-sm hover:shadow-md transition-shadow"
|
className={`card bg-base-200 shadow-sm hover:shadow-md transition-shadow border-l-4 ${getCardBorderClass(request.status)}`}
|
||||||
>
|
>
|
||||||
<div className="card-body p-5">
|
<div className="card-body p-5">
|
||||||
<div className="flex justify-between items-start">
|
<div className="flex justify-between items-start">
|
||||||
|
@ -371,13 +400,45 @@ const UserEventRequests: React.FC<UserEventRequestsProps> = ({ eventRequests: in
|
||||||
onClick={(e) => e.stopPropagation()}
|
onClick={(e) => e.stopPropagation()}
|
||||||
>
|
>
|
||||||
<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="sticky top-0 z-10 bg-base-100 px-6 py-4 border-b border-base-300 flex justify-between items-center">
|
||||||
<h2 className="text-xl font-bold">{selectedRequest.name}</h2>
|
<div className="flex items-center gap-2">
|
||||||
<button
|
<h2 className="text-xl font-bold">{selectedRequest.name}</h2>
|
||||||
className="btn btn-sm btn-circle btn-ghost"
|
<span className={`badge ${getStatusBadge(selectedRequest.status)}`}>
|
||||||
onClick={closeModal}
|
{selectedRequest.status || 'Pending'}
|
||||||
>
|
</span>
|
||||||
✕
|
</div>
|
||||||
</button>
|
<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.error('showEventRequestFormPreview is not a function', window.showEventRequestFormPreview);
|
||||||
|
// 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"
|
||||||
|
onClick={closeModal}
|
||||||
|
>
|
||||||
|
✕
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="p-6">
|
<div className="p-6">
|
||||||
|
|
Loading…
Reference in a new issue