more reliable updates to the modal

This commit is contained in:
chark1es 2025-03-12 04:02:03 -07:00
parent e2933b1c5f
commit fd68c47e9a
7 changed files with 1273 additions and 525 deletions

View file

@ -8,6 +8,7 @@
"@astrojs/react": "^4.2.0",
"@astrojs/tailwind": "5.1.4",
"@iconify-json/heroicons": "^1.2.2",
"@iconify-json/mdi": "^1.2.3",
"@iconify/react": "^5.2.0",
"@types/highlight.js": "^10.1.0",
"@types/js-yaml": "^4.0.9",
@ -173,6 +174,8 @@
"@iconify-json/heroicons": ["@iconify-json/heroicons@1.2.2", "", { "dependencies": { "@iconify/types": "*" } }, "sha512-qoW4pXr5kTTL6juEjgTs83OJIwpePu7q1tdtKVEdj+i0zyyVHgg/dd9grsXJQnpTpBt6/VwNjrXBvFjRsKPENg=="],
"@iconify-json/mdi": ["@iconify-json/mdi@1.2.3", "", { "dependencies": { "@iconify/types": "*" } }, "sha512-O3cLwbDOK7NNDf2ihaQOH5F9JglnulNDFV7WprU2dSoZu3h3cWH//h74uQAB87brHmvFVxIOkuBX2sZSzYhScg=="],
"@iconify/react": ["@iconify/react@5.2.0", "", { "dependencies": { "@iconify/types": "^2.0.0" }, "peerDependencies": { "react": ">=16" } }, "sha512-7Sdjrqq3fkkQNks9SY3adGC37NQTHsBJL2PRKlQd455PoDi9s+Es9AUTY+vGLFOYs5yO9w9yCE42pmxCwG26WA=="],
"@iconify/tools": ["@iconify/tools@4.0.7", "", { "dependencies": { "@iconify/types": "^2.0.0", "@iconify/utils": "^2.1.32", "@types/tar": "^6.1.13", "axios": "^1.7.7", "cheerio": "1.0.0", "domhandler": "^5.0.3", "extract-zip": "^2.0.1", "local-pkg": "^0.5.0", "pathe": "^1.1.2", "svgo": "^3.3.2", "tar": "^6.2.1" } }, "sha512-zOJxKIfZn96ZRGGvIWzDRLD9vb2CsxjcLuM+QIdvwWbv6SWhm49gECzUnd4d2P0sq9sfodT7yCNobWK8nvavxQ=="],

View file

@ -15,6 +15,7 @@
"@astrojs/react": "^4.2.0",
"@astrojs/tailwind": "5.1.4",
"@iconify-json/heroicons": "^1.2.2",
"@iconify-json/mdi": "^1.2.3",
"@iconify/react": "^5.2.0",
"@types/highlight.js": "^10.1.0",
"@types/js-yaml": "^4.0.9",

View file

@ -3,9 +3,11 @@ import { Authentication } from "../../scripts/pocketbase/Authentication";
import { Get } from "../../scripts/pocketbase/Get";
import { toast } from "react-hot-toast";
import EventRequestManagementTable from "./Officer_EventRequestManagement/EventRequestManagementTable";
import type { EventRequest } from "../../schemas/pocketbase";
import EventRequestModal from "./Officer_EventRequestManagement/EventRequestModal";
import type { EventRequest } from "../../schemas/pocketbase/schema";
import { Collections } from "../../schemas/pocketbase/schema";
import { Icon } from "astro-icon/components";
import CustomAlert from "./universal/CustomAlert";
// Get instances
const get = Get.getInstance();
@ -13,196 +15,301 @@ const auth = Authentication.getInstance();
// Extended EventRequest interface with additional properties needed for this component
interface ExtendedEventRequest extends EventRequest {
requested_user_expand?: {
name: string;
email: string;
};
expand?: {
requested_user?: {
id: string;
name: string;
email: string;
[key: string]: any;
requested_user_expand?: {
name: string;
email: string;
};
[key: string]: any;
};
[key: string]: any; // For other optional properties
expand?: {
requested_user?: {
id: string;
name: string;
email: string;
emailVisibility?: boolean; // Add this field to the interface
[key: string]: any;
};
[key: string]: any;
};
[key: string]: any; // For other optional properties
}
// Animation delay constants to ensure consistency with React components
const ANIMATION_DELAYS = {
heading: "0.1s",
info: "0.2s",
content: "0.3s",
};
// Initialize variables for all event requests
let allEventRequests: ExtendedEventRequest[] = [];
let error = null;
try {
// Don't check authentication here - let the client component handle it
// The server-side check is causing issues when the token is valid client-side but not server-side
// Don't check authentication here - let the client component handle it
// The server-side check is causing issues when the token is valid client-side but not server-side
// console.log("Fetching event requests in Astro component...");
// Expand the requested_user field to get user details
allEventRequests = await get
.getAll<ExtendedEventRequest>(Collections.EVENT_REQUESTS, "", "-created", {
expand: ["requested_user"],
})
.catch((err) => {
console.error("Error in get.getAll:", err);
// Return empty array instead of throwing
return [];
allEventRequests = await get
.getAll<ExtendedEventRequest>(
Collections.EVENT_REQUESTS,
"",
"-created",
{
expand: ["requested_user"],
}
)
.catch((err) => {
console.error("Error in get.getAll:", err);
// Return empty array instead of throwing
return [];
});
// Process the event requests to add the requested_user_expand property
allEventRequests = allEventRequests.map((request) => {
const requestWithExpand = { ...request };
// Add the requested_user_expand property if the expand data is available
if (
request.expand?.requested_user &&
request.expand.requested_user.name
) {
// Always include email regardless of emailVisibility setting
requestWithExpand.requested_user_expand = {
name: request.expand.requested_user.name,
email:
request.expand.requested_user.email ||
"(No email available)",
};
// Force emailVisibility to true in the expand data
if (requestWithExpand.expand?.requested_user) {
requestWithExpand.expand.requested_user.emailVisibility = true;
}
}
return requestWithExpand;
});
// console.log(
// `Fetched ${allEventRequests.length} event requests in Astro component`,
// );
// Process the event requests to add the requested_user_expand property
allEventRequests = allEventRequests.map((request) => {
const requestWithExpand = { ...request };
// Add the requested_user_expand property if the expand data is available
if (
request.expand &&
request.expand.requested_user &&
request.expand.requested_user.name &&
request.expand.requested_user.email
) {
requestWithExpand.requested_user_expand = {
name: request.expand.requested_user.name,
email: request.expand.requested_user.email,
};
}
return requestWithExpand;
});
} catch (err) {
console.error("Error fetching event requests:", err);
error = err;
console.error("Error fetching event requests:", err);
error = err;
}
---
<div class="w-full max-w-7xl mx-auto py-8 px-4">
<style>
.event-table-container {
min-height: 600px;
height: auto !important;
max-height: none !important;
}
.event-table-container table {
height: auto !important;
}
.event-table-container .overflow-x-auto {
max-height: none !important;
}
</style>
<div class="w-full max-w-7xl mx-auto py-8 px-4 sm:px-6">
<style>
.event-table-container {
min-height: 600px;
height: auto !important;
max-height: none !important;
}
.event-table-container table {
height: auto !important;
}
.event-table-container .overflow-x-auto {
max-height: none !important;
}
<div class="mb-8">
<h1 class="text-3xl font-bold text-white mb-2">Event Request Management</h1>
<p class="text-gray-300 mb-4">
Review and manage event requests submitted by officers. Update status and
coordinate with the team.
</p>
<div class="bg-base-300/50 p-4 rounded-lg text-sm text-gray-300">
<p class="font-medium mb-2">As an executive officer, you can:</p>
<ul class="list-disc list-inside space-y-1 ml-2">
<li>View all submitted event requests</li>
<li>Update the status of requests (Pending, Completed, Declined)</li>
<li>Filter and sort requests by various criteria</li>
</ul>
</div>
</div>
/* Modern table styles */
.modern-table thead th {
position: sticky;
top: 0;
z-index: 10;
font-weight: 600;
letter-spacing: 0.02em;
}
{
error && (
<div class="alert alert-error mb-6">
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-6 w-6 stroke-current shrink-0"
fill="none"
viewBox="0 0 24 24"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z"
/>
</svg>
<span>{error}</span>
</div>
)
}
.modern-table tbody tr {
transition: all 0.2s ease;
}
{
!error && (
<div class="bg-base-200 rounded-lg shadow-xl min-h-[600px] event-table-container">
<div class="p-4 md:p-6 h-auto">
<EventRequestManagementTable
client:load
eventRequests={allEventRequests}
/>
/* Badge styles */
.animated-badge {
opacity: 0;
animation: fadeIn 0.3s ease forwards;
animation-delay: calc(var(--badge-index) * 0.05s);
}
@keyframes fadeIn {
from {
opacity: 0;
transform: translateY(5px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
/* Card styles */
.dashboard-card {
border-radius: 1rem;
transition:
transform 0.2s,
box-shadow 0.2s;
}
.dashboard-card:hover {
transform: translateY(-2px);
box-shadow: 0 10px 25px -5px rgba(0, 0, 0, 0.1);
}
/* Animation for card entrance */
.card-enter {
animation: cardEnter 0.5s ease forwards;
opacity: 0;
}
@keyframes cardEnter {
from {
opacity: 0;
transform: translateY(20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
</style>
<div
class="mb-8 space-y-4 card-enter"
style={`animation-delay: ${ANIMATION_DELAYS.heading};`}
>
<div class="flex items-center gap-3">
<div class="rounded-full bg-primary/10 p-2">
<Icon name="mdi:calendar-clock" class="w-6 h-6 text-primary" />
</div>
<h1 class="text-3xl font-bold text-white">
Event Request Management
</h1>
</div>
</div>
)
}
<p class="text-gray-300 text-lg max-w-3xl">
Review and manage event requests submitted by officers. Update
status and coordinate with the team.
</p>
<div
class="bg-gradient-to-br from-base-300/50 to-base-300/30 p-5 rounded-xl border border-base-300/50 shadow-inner text-sm text-gray-300 card-enter"
style={`animation-delay: ${ANIMATION_DELAYS.info};`}
>
<div class="flex items-start gap-3">
<Icon
name="mdi:lightbulb-on"
class="w-5 h-5 text-primary mt-1 flex-shrink-0"
/>
<div>
<p class="font-medium mb-2 text-white">
As an executive officer, you can:
</p>
<ul class="grid grid-cols-1 sm:grid-cols-2 gap-2 ml-1">
<li class="flex items-center gap-2">
<Icon
name="mdi:check-circle"
class="h-4 w-4 text-success"
/>
<span>View all submitted event requests</span>
</li>
<li class="flex items-center gap-2">
<Icon
name="mdi:check-circle"
class="h-4 w-4 text-success"
/>
<span>Update request statuses</span>
</li>
<li class="flex items-center gap-2">
<Icon
name="mdi:check-circle"
class="h-4 w-4 text-success"
/>
<span>Filter requests by criteria</span>
</li>
<li class="flex items-center gap-2">
<Icon
name="mdi:check-circle"
class="h-4 w-4 text-success"
/>
<span>Sort requests by various fields</span>
</li>
</ul>
</div>
</div>
</div>
</div>
{
error && (
<div
class="mb-6 card-enter"
style={`animation-delay: ${ANIMATION_DELAYS.content};`}
>
<CustomAlert
client:load
type="error"
title="Error fetching event requests"
message={error.toString()}
icon="heroicons:exclamation-triangle"
/>
</div>
)
}
<!-- Main page content including table and modal -->
<EventRequestModal client:load eventRequests={allEventRequests} />
</div>
<script>
// Import the DataSyncService for client-side use
import { DataSyncService } from "../../scripts/database/DataSyncService";
import { Collections } from "../../schemas/pocketbase/schema";
<script define:vars={{ ANIMATION_DELAYS }}>
// Import the DataSyncService for client-side use
import { DataSyncService } from "../../scripts/database/DataSyncService";
import { Collections } from "../../schemas/pocketbase/schema";
// Remove the visibilitychange event listener that causes full page refresh
// Instead, we'll use a more efficient approach to refresh data only when needed
document.addEventListener("visibilitychange", () => {
if (document.visibilityState === "visible") {
// Instead of reloading the entire page, dispatch a custom event
// that components can listen for to refresh their data
document.dispatchEvent(new CustomEvent("dashboardTabVisible"));
}
});
// Use a more efficient approach to refresh data only when needed
document.addEventListener("visibilitychange", () => {
if (document.visibilityState === "visible") {
// Dispatch a custom event that components can listen for
document.dispatchEvent(new CustomEvent("dashboardTabVisible"));
}
});
// Handle authentication errors
document.addEventListener("DOMContentLoaded", async () => {
// Initialize DataSyncService for client-side
const dataSync = DataSyncService.getInstance();
// Handle authentication errors and initial data loading
document.addEventListener("DOMContentLoaded", async () => {
// Initialize DataSyncService for client-side
const dataSync = DataSyncService.getInstance();
// Prefetch data into IndexedDB
try {
await dataSync.syncCollection(
Collections.EVENT_REQUESTS,
"",
"-created",
{ expand: "requested_user" },
);
// console.log("Initial data sync complete");
} catch (err) {
// console.error("Error during initial data sync:", err);
}
// Add subtle entrance animations to cards
const cards = document.querySelectorAll(".card-enter");
cards.forEach((card, index) => {
// Use the same animation delay calculation logic consistently
const delay =
index === 0
? ANIMATION_DELAYS.heading
: index === 1
? ANIMATION_DELAYS.info
: ANIMATION_DELAYS.content;
// Check for error message in the UI
const errorElement = document.querySelector(".alert-error span");
if (
errorElement &&
errorElement.textContent?.includes("Authentication error")
) {
// console.log(
// "Authentication error detected in UI, redirecting to login...",
// );
// Redirect to login page after a short delay
setTimeout(() => {
window.location.href = "/login";
}, 3000);
return;
}
card.setAttribute("style", `animation-delay: ${delay}`);
});
// Also check if we have any event requests
const tableContainer = document.querySelector(".event-table-container");
if (tableContainer) {
// console.log(
// "Event table container found, component should load normally",
// );
} else {
// console.log(
// "Event table container not found, might be an issue with rendering",
// );
}
});
// Prefetch data into IndexedDB
try {
await dataSync.syncCollection(
Collections.EVENT_REQUESTS,
"",
"-created",
{ expand: "requested_user" }
);
} catch (err) {
console.error("Error during initial data sync:", err);
}
// Check for error message in the UI
const errorElement = document.querySelector(".alert-error span");
if (
errorElement &&
errorElement.textContent?.includes("Authentication error")
) {
// Redirect to login page after a short delay
setTimeout(() => {
window.location.href = "/login";
}, 3000);
}
});
</script>

View file

@ -31,13 +31,17 @@ interface ExtendedEventRequest extends SchemaEventRequest {
interface EventRequestManagementTableProps {
eventRequests: ExtendedEventRequest[];
onRequestSelect: (request: ExtendedEventRequest) => void;
onStatusChange: (id: string, status: "submitted" | "pending" | "completed" | "declined") => Promise<void>;
}
const EventRequestManagementTable = ({ eventRequests: initialEventRequests }: EventRequestManagementTableProps) => {
const EventRequestManagementTable = ({
eventRequests: initialEventRequests,
onRequestSelect,
onStatusChange
}: EventRequestManagementTableProps) => {
const [eventRequests, setEventRequests] = useState<ExtendedEventRequest[]>(initialEventRequests);
const [filteredRequests, setFilteredRequests] = useState<ExtendedEventRequest[]>(initialEventRequests);
const [selectedRequest, setSelectedRequest] = useState<ExtendedEventRequest | null>(null);
const [isModalOpen, setIsModalOpen] = useState<boolean>(false);
const [isRefreshing, setIsRefreshing] = useState<boolean>(false);
const [statusFilter, setStatusFilter] = useState<string>('all');
const [searchTerm, setSearchTerm] = useState<string>('');
@ -135,8 +139,7 @@ const EventRequestManagementTable = ({ eventRequests: initialEventRequests }: Ev
// Update event request status
const updateEventRequestStatus = async (id: string, status: "submitted" | "pending" | "completed" | "declined"): Promise<void> => {
try {
const update = Update.getInstance();
const result = await update.updateField('event_request', id, 'status', status);
await onStatusChange(id, status);
// Find the event request to get its name
const eventRequest = eventRequests.find(req => req.id === id);
@ -155,11 +158,6 @@ const EventRequestManagementTable = ({ eventRequests: initialEventRequests }: Ev
)
);
// Update selected request if open
if (selectedRequest && selectedRequest.id === id) {
setSelectedRequest({ ...selectedRequest, status });
}
// Force sync to update IndexedDB
await dataSync.syncCollection<ExtendedEventRequest>(Collections.EVENT_REQUESTS);
@ -211,43 +209,38 @@ const EventRequestManagementTable = ({ eventRequests: initialEventRequests }: Ev
}
};
// Open modal with event request details
const openDetailModal = (request: ExtendedEventRequest) => {
setSelectedRequest(request);
setIsModalOpen(true);
// Helper function to truncate text
const truncateText = (text: string, maxLength: number) => {
if (!text) return '';
return text.length > maxLength ? `${text.substring(0, maxLength)}...` : text;
};
// Close modal
const closeModal = () => {
setIsModalOpen(false);
setSelectedRequest(null);
};
// Open update modal
const openUpdateModal = (request: ExtendedEventRequest) => {
setRequestToUpdate(request);
setIsUpdateModalOpen(true);
};
// Close update modal
const closeUpdateModal = () => {
setIsUpdateModalOpen(false);
setRequestToUpdate(null);
};
// Update status and close modal
const handleUpdateStatus = async (status: "submitted" | "pending" | "completed" | "declined") => {
if (requestToUpdate) {
try {
await updateEventRequestStatus(requestToUpdate.id, status);
// Toast is now shown in updateEventRequestStatus
closeUpdateModal();
} catch (error) {
// console.error('Error in handleUpdateStatus:', error);
// Toast is now shown in updateEventRequestStatus
// Keep modal open so user can try again
}
// Helper to get user display info - always show email address
const getUserDisplayInfo = (request: ExtendedEventRequest) => {
// First try to get from the expand object
if (request.expand?.requested_user) {
const user = request.expand.requested_user;
// Show "Loading..." instead of "Unknown" while data is being fetched
const name = user.name || 'Unknown';
// Always show email regardless of emailVisibility
const email = user.email || 'Unknown';
return { name, email };
}
// Then try the requested_user_expand
if (request.requested_user_expand) {
const name = request.requested_user_expand.name || 'Unknown';
const email = request.requested_user_expand.email || 'Unknown';
return { name, email };
}
// Last fallback - don't use "Unknown" to avoid confusing users
return { name: 'Unknown', email: '(Unknown)' };
};
// Update openDetailModal to call the prop function
const openDetailModal = (request: ExtendedEventRequest) => {
onRequestSelect(request);
};
// Handle sort change
@ -524,13 +517,22 @@ const EventRequestManagementTable = ({ eventRequests: initialEventRequests }: Ev
<tbody>
{filteredRequests.map((request) => (
<tr key={request.id} className="hover">
<td className="font-medium">{request.name}</td>
<td className="font-medium">
<div className="truncate max-w-[180px] md:max-w-[250px]" title={request.name}>
{truncateText(request.name, 30)}
</div>
</td>
<td className="hidden md:table-cell">{formatDate(request.start_date_time)}</td>
<td>
<div className="flex flex-col">
<span>{request.expand?.requested_user?.name || 'Unknown'}</span>
<span className="text-xs text-gray-400">{request.expand?.requested_user?.email}</span>
</div>
{(() => {
const { name, email } = getUserDisplayInfo(request);
return (
<div className="flex flex-col">
<span>{name}</span>
<span className="text-xs text-gray-400">{email}</span>
</div>
);
})()}
</td>
<td className="hidden lg:table-cell">
{request.flyers_needed ? (
@ -554,12 +556,6 @@ const EventRequestManagementTable = ({ eventRequests: initialEventRequests }: Ev
</td>
<td>
<div className="flex items-center gap-2">
<button
className="btn btn-sm btn-outline"
onClick={() => openUpdateModal(request)}
>
Update
</button>
<button
className="btn btn-sm btn-ghost"
onClick={() => openDetailModal(request)}
@ -568,6 +564,7 @@ const EventRequestManagementTable = ({ eventRequests: initialEventRequests }: Ev
<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>
@ -577,85 +574,6 @@ const EventRequestManagementTable = ({ eventRequests: initialEventRequests }: Ev
</table>
</div>
</motion.div>
{/* Event request details modal - Now outside the main component div */}
{isModalOpen && selectedRequest && (
<AnimatePresence>
<EventRequestDetails
request={selectedRequest}
onClose={closeModal}
onStatusChange={updateEventRequestStatus}
/>
</AnimatePresence>
)}
{/* Update status modal */}
{isUpdateModalOpen && requestToUpdate && (
<AnimatePresence>
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
<motion.div
initial={{ opacity: 0, scale: 0.95 }}
animate={{ opacity: 1, scale: 1 }}
exit={{ opacity: 0, scale: 0.95 }}
transition={{ duration: 0.2 }}
className="bg-base-200 rounded-lg shadow-xl w-full max-w-md overflow-hidden flex flex-col"
>
{/* Header */}
<div className="bg-base-300 p-4 flex justify-between items-center">
<h3 className="text-xl font-bold">Update Status</h3>
<button
className="btn btn-sm btn-circle"
onClick={closeUpdateModal}
>
<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>
{/* Content */}
<div className="p-6">
<p className="mb-4">
Update status for event: <span className="font-semibold">{requestToUpdate.name}</span>
</p>
<div className="flex flex-col gap-3">
<button
className="btn btn-outline w-full justify-start"
onClick={() => handleUpdateStatus("pending")}
>
<span className="badge badge-warning mr-2">Pending</span>
Mark as Pending
</button>
<button
className="btn btn-outline w-full justify-start"
onClick={() => handleUpdateStatus("completed")}
>
<span className="badge badge-success mr-2">Completed</span>
Mark as Completed
</button>
<button
className="btn btn-outline w-full justify-start"
onClick={() => handleUpdateStatus("declined")}
>
<span className="badge badge-error mr-2">Declined</span>
Mark as Declined
</button>
</div>
</div>
{/* Footer */}
<div className="p-4 border-t border-base-300 flex justify-end">
<button
className="btn btn-ghost"
onClick={closeUpdateModal}
>
Cancel
</button>
</div>
</motion.div>
</div>
</AnimatePresence>
)}
</>
);
};

View file

@ -0,0 +1,354 @@
import React, { useState, useEffect } from 'react';
import { motion, AnimatePresence } from 'framer-motion';
import EventRequestDetails from './EventRequestDetails';
import EventRequestManagementTable from './EventRequestManagementTable';
import { Update } from '../../../scripts/pocketbase/Update';
import { Collections, EventRequestStatus } from '../../../schemas/pocketbase/schema';
import { DataSyncService } from '../../../scripts/database/DataSyncService';
import { Authentication } from '../../../scripts/pocketbase/Authentication';
import { Get } from '../../../scripts/pocketbase/Get';
import { toast } from 'react-hot-toast';
import type { EventRequest } from '../../../schemas/pocketbase/schema';
// Extended EventRequest interface to include expanded fields that might come from the API
interface ExtendedEventRequest extends Omit<EventRequest, 'status'> {
status: "submitted" | "pending" | "completed" | "declined";
requested_user_expand?: {
name: string;
email: string;
};
expand?: {
requested_user?: {
id: string;
name: string;
email: string;
emailVisibility?: boolean;
[key: string]: any;
};
[key: string]: any;
};
}
interface EventRequestModalProps {
eventRequests: ExtendedEventRequest[];
}
// Helper to refresh user data in request objects
const refreshUserData = async (requests: ExtendedEventRequest[]): Promise<ExtendedEventRequest[]> => {
try {
const get = Get.getInstance();
const updatedRequests = [...requests];
const userCache: Record<string, any> = {}; // Cache to avoid fetching the same user multiple times
for (let i = 0; i < updatedRequests.length; i++) {
const request = updatedRequests[i];
if (request.requested_user) {
try {
// Check if we've already fetched this user
let typedUserData;
if (userCache[request.requested_user]) {
typedUserData = userCache[request.requested_user];
} else {
// Fetch full user details for each request with expanded options
const userData = await get.getOne('users', request.requested_user);
// Type assertion to ensure we have the correct user data properties
typedUserData = userData as {
id: string;
name: string;
email: string;
[key: string]: any;
};
// Store in cache
userCache[request.requested_user] = typedUserData;
}
// Update expand object with user data
if (!request.expand) request.expand = {};
request.expand.requested_user = {
...typedUserData,
emailVisibility: true // Force this to be true for UI purposes
};
// Update the requested_user_expand property
request.requested_user_expand = {
name: typedUserData.name || 'Unknown',
email: typedUserData.email || '(No email available)'
};
} catch (err) {
console.error(`Error fetching user data for request ${request.id}:`, err);
// Ensure we have fallback values even if the API call fails
if (!request.expand) request.expand = {};
if (!request.expand.requested_user) {
request.expand.requested_user = {
id: request.requested_user,
name: 'Unknown',
email: 'Unknown',
emailVisibility: true
};
}
if (!request.requested_user_expand) {
request.requested_user_expand = {
name: 'Unknown',
email: 'Unknown'
};
}
}
}
}
return updatedRequests;
} catch (err) {
console.error('Error refreshing user data:', err);
return requests;
}
};
// Wrapper component for EventRequestManagementTable that handles string to function conversion
const TableWrapper: React.FC<{
eventRequests: ExtendedEventRequest[];
handleSelectRequest: (request: ExtendedEventRequest) => void;
handleStatusChange: (id: string, status: "submitted" | "pending" | "completed" | "declined") => Promise<void>;
}> = ({ eventRequests, handleSelectRequest, handleStatusChange }) => {
return (
<EventRequestManagementTable
eventRequests={eventRequests}
onRequestSelect={handleSelectRequest}
onStatusChange={handleStatusChange}
/>
);
};
const EventRequestModal: React.FC<EventRequestModalProps> = ({ eventRequests }) => {
// Define animation delay as a constant to keep it consistent
const ANIMATION_DELAY = "0.3s";
const [selectedRequest, setSelectedRequest] = useState<ExtendedEventRequest | null>(null);
const [isModalOpen, setIsModalOpen] = useState(false);
const [localEventRequests, setLocalEventRequests] = useState<ExtendedEventRequest[]>(eventRequests);
const [isLoadingUserData, setIsLoadingUserData] = useState(true); // Start as true to show loading immediately
// Function to refresh user data
const refreshUserDataAndUpdate = async (requests: ExtendedEventRequest[] = localEventRequests) => {
setIsLoadingUserData(true);
try {
const updatedRequests = await refreshUserData(requests);
setLocalEventRequests(updatedRequests);
} catch (err) {
console.error('Error refreshing event request data:', err);
} finally {
setIsLoadingUserData(false);
}
};
// Immediately load user data on mount
useEffect(() => {
refreshUserDataAndUpdate(eventRequests);
}, []);
// Effect to update local state when props change
useEffect(() => {
// Only update if we have new eventRequests from props
if (eventRequests.length > 0) {
// First update with what we have from props
setLocalEventRequests(prevRequests => {
// Only replace if we have different data
if (eventRequests.length !== prevRequests.length) {
return eventRequests;
}
return prevRequests;
});
// Then refresh user data
refreshUserDataAndUpdate(eventRequests);
}
}, [eventRequests]);
// Set up event listeners for communication with the table component
useEffect(() => {
const handleSelectRequest = (event: CustomEvent) => {
setSelectedRequest(event.detail.request);
setIsModalOpen(true);
};
const handleStatusUpdated = (event: CustomEvent) => {
const { id, status } = event.detail;
// Update local state
setLocalEventRequests(prevRequests =>
prevRequests.map(req =>
req.id === id ? { ...req, status } : req
)
);
};
// Add event listeners
document.addEventListener('event-request-select', handleSelectRequest as EventListener);
document.addEventListener('status-updated', handleStatusUpdated as EventListener);
// Clean up
return () => {
document.removeEventListener('event-request-select', handleSelectRequest as EventListener);
document.removeEventListener('status-updated', handleStatusUpdated as EventListener);
};
}, []);
// Listen for dashboardTabVisible event to refresh user data
useEffect(() => {
const handleTabVisible = async () => {
refreshUserDataAndUpdate();
};
document.addEventListener('dashboardTabVisible', handleTabVisible);
return () => {
document.removeEventListener('dashboardTabVisible', handleTabVisible);
};
}, [localEventRequests]);
const closeModal = () => {
setIsModalOpen(false);
setSelectedRequest(null);
};
const handleStatusChange = async (id: string, status: "submitted" | "pending" | "completed" | "declined"): Promise<void> => {
try {
const update = Update.getInstance();
await update.updateField("event_request", id, "status", status);
// Force sync to update IndexedDB
const dataSync = DataSyncService.getInstance();
await dataSync.syncCollection(Collections.EVENT_REQUESTS);
// Update local state
setLocalEventRequests(prevRequests =>
prevRequests.map(req =>
req.id === id ? { ...req, status } : req
)
);
// Find the request to get its name
const request = localEventRequests.find((req) => req.id === id);
const eventName = request?.name || "Event";
// Notify success
toast.success(`"${eventName}" status updated to ${status}`);
// Dispatch event for other components
document.dispatchEvent(
new CustomEvent("status-updated", {
detail: { id, status },
})
);
} catch (err) {
console.error("Error updating status:", err);
toast.error(`Failed to update status`);
throw err;
}
};
// Function to handle request selection
const handleSelectRequest = (request: ExtendedEventRequest) => {
document.dispatchEvent(
new CustomEvent("event-request-select", {
detail: { request },
})
);
};
// Expose the functions globally for table component to use
useEffect(() => {
// @ts-ignore - Adding to window object
window.handleSelectRequest = handleSelectRequest;
// @ts-ignore - Adding to window object
window.handleStatusChange = handleStatusChange;
// @ts-ignore - Adding to window object
window.refreshUserData = refreshUserDataAndUpdate;
return () => {
// @ts-ignore - Cleanup
delete window.handleSelectRequest;
// @ts-ignore - Cleanup
delete window.handleStatusChange;
// @ts-ignore - Cleanup
delete window.refreshUserData;
};
}, [localEventRequests]);
return (
<>
{/* Table component placed here */}
<div
className="bg-base-200 rounded-xl shadow-xl overflow-hidden dashboard-card card-enter event-table-container"
style={{ animationDelay: ANIMATION_DELAY }}
>
<div className="p-4 md:p-6 h-auto">
<div className="flex justify-between items-center mb-4">
{isLoadingUserData ? (
<div className="flex items-center">
<div className="loading loading-spinner loading-sm mr-2"></div>
<span className="text-sm text-gray-400">Loading user data...</span>
</div>
) : (
<div></div>
)}
<button
className="btn btn-sm btn-ghost"
onClick={() => refreshUserDataAndUpdate()}
disabled={isLoadingUserData}
>
<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 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
</svg>
Refresh User Data
</button>
</div>
<div id="event-request-table-container">
<TableWrapper
eventRequests={localEventRequests}
handleSelectRequest={handleSelectRequest}
handleStatusChange={handleStatusChange}
/>
</div>
</div>
</div>
{/* Modal */}
<AnimatePresence>
{isModalOpen && selectedRequest && (
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
className="fixed inset-0 bg-black/70 backdrop-blur-sm z-[200] flex items-center justify-center p-4 overflow-y-auto"
>
<div className="w-full max-w-5xl max-h-[90vh] overflow-y-auto relative">
<div className="absolute top-4 right-4 z-[201]">
<button
onClick={closeModal}
className="btn btn-circle btn-sm bg-base-300/90 hover:bg-base-300"
>
<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>
<EventRequestDetails
request={selectedRequest}
onClose={closeModal}
onStatusChange={handleStatusChange}
/>
</div>
</motion.div>
)}
</AnimatePresence>
</>
);
};
export default EventRequestModal;

View file

@ -95,16 +95,16 @@ export interface EventRequest extends BaseRecord {
event_description: string;
flyers_needed: boolean;
flyer_type?: string[]; // digital_with_social, digital_no_social, physical_with_advertising, physical_no_advertising, newsletter, other
other_flyer_type?: string;
other_flyer_type?: string;
flyer_advertising_start_date?: string;
flyer_additional_requests?: string;
photography_needed: boolean;
required_logos?: string[]; // IEEE, AS, HKN, TESC, PIB, TNT, SWE, OTHER
other_logos?: string[];
other_logos?: string[]; // Array of logo IDs
advertising_format?: string;
will_or_have_room_booking?: boolean;
expected_attendance?: number;
room_booking?: string;
room_booking?: string; // signle file
as_funding_required: boolean;
food_drinks_being_served: boolean;
itemized_invoice?: string; // JSON string