diff --git a/bun.lock b/bun.lock index 7b10398..b47d031 100644 --- a/bun.lock +++ b/bun.lock @@ -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=="], diff --git a/package.json b/package.json index 22baf45..6ac270d 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/components/dashboard/Officer_EventRequestManagement.astro b/src/components/dashboard/Officer_EventRequestManagement.astro index 72f6c9d..d16bde6 100644 --- a/src/components/dashboard/Officer_EventRequestManagement.astro +++ b/src/components/dashboard/Officer_EventRequestManagement.astro @@ -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(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( + 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; } --- -
- +
+ + +
+
+
+ +
+

+ Event Request Management +

-
- ) - } + +

+ Review and manage event requests submitted by officers. Update + status and coordinate with the team. +

+ +
+
+ +
+

+ As an executive officer, you can: +

+
    +
  • + + View all submitted event requests +
  • +
  • + + Update request statuses +
  • +
  • + + Filter requests by criteria +
  • +
  • + + Sort requests by various fields +
  • +
+
+
+
+
+ + { + error && ( +
+ +
+ ) + } + + +
- diff --git a/src/components/dashboard/Officer_EventRequestManagement/EventRequestDetails.tsx b/src/components/dashboard/Officer_EventRequestManagement/EventRequestDetails.tsx index 26c95b8..fdbec64 100644 --- a/src/components/dashboard/Officer_EventRequestManagement/EventRequestDetails.tsx +++ b/src/components/dashboard/Officer_EventRequestManagement/EventRequestDetails.tsx @@ -1,10 +1,13 @@ import React, { useState, useEffect } from 'react'; -import { motion } from 'framer-motion'; +import { motion, AnimatePresence } from 'framer-motion'; import toast from 'react-hot-toast'; import { DataSyncService } from '../../../scripts/database/DataSyncService'; import { Collections } from '../../../schemas/pocketbase/schema'; import type { EventRequest as SchemaEventRequest } from '../../../schemas/pocketbase/schema'; import { FileManager } from '../../../scripts/pocketbase/FileManager'; +import { Icon } from "@iconify/react"; +import CustomAlert from '../universal/CustomAlert'; +import { Authentication } from '../../../scripts/pocketbase/Authentication'; // Extended EventRequest interface with additional properties needed for this component interface ExtendedEventRequest extends SchemaEventRequest { @@ -12,8 +15,20 @@ interface ExtendedEventRequest extends SchemaEventRequest { name: string; email: string; }; + expand?: { + requested_user?: { + id: string; + name: string; + email: string; + emailVisibility?: boolean; + [key: string]: any; + }; + [key: string]: any; + }; invoice_data?: string | any; invoice_files?: string[]; // Array of invoice file IDs + flyer_files?: string[]; // Add this for PR-related files + files?: string[]; // Generic files field } interface EventRequestDetailsProps { @@ -43,41 +58,33 @@ const FilePreviewModal: React.FC = ({ const [fileUrl, setFileUrl] = useState(''); const [isLoading, setIsLoading] = useState(true); const [error, setError] = useState(null); - const [fileType, setFileType] = useState<'image' | 'pdf' | 'other'>('other'); + const [fileType, setFileType] = useState(''); useEffect(() => { + if (!isOpen) return; + const loadFile = async () => { - if (!isOpen) return; - - setIsLoading(true); - setError(null); - try { - const fileManager = FileManager.getInstance(); + setIsLoading(true); + setError(null); - // Get file URL with token for secure access - const url = await fileManager.getFileUrlWithToken( - collectionName, - recordId, - fileName, - true // Use token for secure access - ); + // Construct the secure file URL + const auth = Authentication.getInstance(); + const token = auth.getAuthToken(); + const pbUrl = import.meta.env.PUBLIC_POCKETBASE_URL; - setFileUrl(url); + const secureUrl = `${pbUrl}/api/files/${collectionName}/${recordId}/${fileName}?token=${token}`; - // Determine file type based on extension + setFileUrl(secureUrl); + + // Determine file type from extension const extension = fileName.split('.').pop()?.toLowerCase() || ''; - if (['jpg', 'jpeg', 'png', 'gif', 'webp'].includes(extension)) { - setFileType('image'); - } else if (extension === 'pdf') { - setFileType('pdf'); - } else { - setFileType('other'); - } + setFileType(extension); + + setIsLoading(false); } catch (err) { console.error('Error loading file:', err); setError('Failed to load file. Please try again.'); - } finally { setIsLoading(false); } }; @@ -87,46 +94,35 @@ const FilePreviewModal: React.FC = ({ const handleDownload = async () => { try { - const fileManager = FileManager.getInstance(); - const blob = await fileManager.downloadFile(collectionName, recordId, fileName); - - // Create a download link - const url = window.URL.createObjectURL(blob); - const a = document.createElement('a'); - a.href = url; - a.download = displayName || fileName; - document.body.appendChild(a); - a.click(); - - // Clean up - window.URL.revokeObjectURL(url); - document.body.removeChild(a); - - toast.success('File downloaded successfully'); + // Create a temporary link element + const link = document.createElement('a'); + link.href = fileUrl; + link.download = displayName || fileName; + document.body.appendChild(link); + link.click(); + document.body.removeChild(link); } catch (err) { console.error('Error downloading file:', err); - toast.error('Failed to download file'); + setError('Failed to download file. Please try again.'); } }; if (!isOpen) return null; return ( -
- - {/* Header */} -
-

{displayName || fileName}

-
+ +
+
+

{displayName || fileName}

+
- {/* Content */} -
+
{isLoading ? ( -
+
-

Loading file...

) : error ? ( -
- - - - {error} +
+

{error}

) : ( <> - {fileType === 'image' && ( -
+ {(fileType === 'jpg' || fileType === 'jpeg' || fileType === 'png' || fileType === 'gif') ? ( +
{displayName setError('Failed to load image')} + className="max-w-full max-h-[60vh] object-contain rounded-lg" + onError={() => setError('Failed to load image.')} />
- )} - - {fileType === 'pdf' && ( + ) : fileType === 'pdf' ? ( - )} - - {fileType === 'other' && ( -
- + ) : ( +
+ -

This file type cannot be previewed directly.

+

File Preview Not Available

+

+ This file type ({fileType}) cannot be previewed in the browser. +

@@ -196,8 +186,8 @@ const FilePreviewModal: React.FC = ({ )}
- -
+
+ ); }; @@ -273,9 +263,7 @@ const FilePreview: React.FC<{
- - - + {displayName}
) : ( -
- - No invoice files have been uploaded. -
+ )} {/* Display invoice data if available */} @@ -593,10 +583,12 @@ const InvoiceTable: React.FC<{ invoiceData: any }> = ({ invoiceData }) => { // If no invoice data is provided, show a message if (!invoiceData) { return ( -
- - No invoice data available. -
+ ); } @@ -610,10 +602,12 @@ const InvoiceTable: React.FC<{ invoiceData: any }> = ({ invoiceData }) => { } catch (e) { console.error('Failed to parse invoice data string:', e); return ( -
- - Invalid invoice data format. -
+ ); } } else if (typeof invoiceData === 'object' && invoiceData !== null) { @@ -623,10 +617,12 @@ const InvoiceTable: React.FC<{ invoiceData: any }> = ({ invoiceData }) => { // Check if we have valid invoice data if (!parsedInvoice || typeof parsedInvoice !== 'object') { return ( -
- - No structured invoice data available. -
+ ); } @@ -656,10 +652,12 @@ const InvoiceTable: React.FC<{ invoiceData: any }> = ({ invoiceData }) => { // If we still don't have items, show a message if (items.length === 0) { return ( -
- - No invoice items found in the data. -
+ ); } @@ -741,14 +739,366 @@ const InvoiceTable: React.FC<{ invoiceData: any }> = ({ invoiceData }) => { } catch (error) { console.error('Error rendering invoice table:', error); return ( -
- - An unexpected error occurred while processing the invoice. -
+ ); } }; +// Now, add a new component for the PR Materials tab +const PRMaterialsTab: React.FC<{ request: ExtendedEventRequest }> = ({ request }) => { + const [isPreviewModalOpen, setIsPreviewModalOpen] = useState(false); + const [selectedFile, setSelectedFile] = useState<{ name: string, displayName: string }>({ name: '', displayName: '' }); + + // Format date for display + const formatDate = (dateString: string) => { + if (!dateString) return 'Not specified'; + try { + const date = new Date(dateString); + return date.toLocaleDateString('en-US', { + year: 'numeric', + month: 'short', + day: 'numeric', + hour: '2-digit', + minute: '2-digit' + }); + } catch (e) { + return dateString; + } + }; + + // Use the same utility functions as in the ASFundingTab + const getFileExtension = (filename: string): string => { + const parts = filename.split('.'); + return parts.length > 1 ? parts.pop()?.toLowerCase() || '' : ''; + }; + + const isImageFile = (filename: string): boolean => { + const extension = getFileExtension(filename); + return ['jpg', 'jpeg', 'png', 'gif', 'webp', 'svg'].includes(extension); + }; + + const isPdfFile = (filename: string): boolean => { + return getFileExtension(filename) === 'pdf'; + }; + + const getFriendlyFileName = (filename: string, maxLength: number = 20): string => { + const basename = filename.split('/').pop() || filename; + if (basename.length <= maxLength) return basename; + const extension = getFileExtension(basename); + const name = basename.substring(0, basename.length - extension.length - 1); + const truncatedName = name.substring(0, maxLength - 3 - extension.length) + '...'; + return extension ? `${truncatedName}.${extension}` : truncatedName; + }; + + // Check if we have any PR-related files (flyer_files or related files) + const hasFiles = ( + request.flyer_files && request.flyer_files.length > 0 || + request.files && request.files.length > 0 || + request.other_logos && request.other_logos.length > 0 + ); + + // Open file preview modal + const openFilePreview = (fileName: string, displayName: string) => { + setSelectedFile({ name: fileName, displayName }); + setIsPreviewModalOpen(true); + }; + + // Close file preview modal + const closeFilePreview = () => { + setIsPreviewModalOpen(false); + }; + + return ( + +
+
+
+

Flyers Needed

+
+ {request.flyers_needed ? ( + Yes + ) : ( + No + )} +
+
+ + {request.flyers_needed && ( + +
+

Flyer Types

+
    + {request.flyer_type?.map((type, index) => ( + + + {type} + + ))} + {request.other_flyer_type && ( + + + {request.other_flyer_type} + + )} +
+
+ +
+

Advertising Start Date

+

{formatDate(request.flyer_advertising_start_date || '')}

+
+ +
+

Advertising Format

+

{request.advertising_format || 'Not specified'}

+
+
+ )} +
+ +
+
+

Photography Needed

+
+ {request.photography_needed ? ( + Yes + ) : ( + No + )} +
+
+ + {/* Logo Requirements Section */} + +

Required Logos

+
    + {request.required_logos?.map((logo, index) => ( + + + {logo} + + ))} + {(!request.required_logos || request.required_logos.length === 0) && ( + + + No specific logos required + + )} +
+
+ + {/* Display custom logos if available */} + {request.other_logos && request.other_logos.length > 0 && ( + +

Custom Logos

+
+ {request.other_logos.map((logoId, index) => { + const displayName = getFriendlyFileName(logoId, 15); + + return ( + openFilePreview(logoId, displayName)} + initial={{ opacity: 0, y: 5 }} + animate={{ opacity: 1, y: 0 }} + transition={{ delay: 0.2 + (index * 0.05) }} + whileHover={{ scale: 1.02 }} + whileTap={{ scale: 0.98 }} + > +
+ {isImageFile(logoId) ? ( + + ) : ( + + )} +
+

+ {displayName} +

+
+ +
+
+ ); + })} +
+
+ )} + +
+

Additional Requests

+

{request.flyer_additional_requests || 'None'}

+
+
+
+ + {/* Display PR-related files if available */} + {hasFiles && ( + +

Related Files

+
+ {/* Display flyer_files if available */} + {request.flyer_files && request.flyer_files.map((fileId, index) => { + const extension = getFileExtension(fileId); + const displayName = getFriendlyFileName(fileId, 25); + + return ( + openFilePreview(fileId, displayName)} + initial={{ opacity: 0, y: 10 }} + animate={{ opacity: 1, y: 0 }} + transition={{ delay: 0.2 + (index * 0.05) }} + whileHover={{ scale: 1.02 }} + whileTap={{ scale: 0.98 }} + > +
+ {isImageFile(fileId) ? ( + + ) : isPdfFile(fileId) ? ( + + ) : ( + + )} +
+

+ {displayName} +

+

+ {extension ? extension.toUpperCase() : 'FILE'} +

+
+ +
+
+ ); + })} + + {/* Display general files if available */} + {request.files && request.files.map((fileId, index) => { + const extension = getFileExtension(fileId); + const displayName = getFriendlyFileName(fileId, 25); + + return ( + openFilePreview(fileId, displayName)} + initial={{ opacity: 0, y: 10 }} + animate={{ opacity: 1, y: 0 }} + transition={{ delay: 0.2 + ((request.flyer_files?.length || 0) + index) * 0.05 }} + whileHover={{ scale: 1.02 }} + whileTap={{ scale: 0.98 }} + > +
+ {isImageFile(fileId) ? ( + + ) : isPdfFile(fileId) ? ( + + ) : ( + + )} +
+

+ {displayName} +

+

+ {extension ? extension.toUpperCase() : 'FILE'} +

+
+ +
+
+ ); + })} +
+
+ )} + + {/* No files message */} + {!hasFiles && ( + + + + )} + + {/* File Preview Modal */} + +
+ ); +}; + +// Now, update the EventRequestDetails component to use the new PRMaterialsTab const EventRequestDetails = ({ request, onClose, @@ -801,47 +1151,63 @@ const EventRequestDetails = ({ setIsStatusChanging(false); }; - return ( -
- - {/* Header */} -
-

{request.name}

- -
+ // Animation variants + const fadeInVariants = { + hidden: { opacity: 0, y: 20 }, + visible: { opacity: 1, y: 0, transition: { duration: 0.4 } } + }; - {/* Status and controls */} -
+ const tabVariants = { + hidden: { opacity: 0, y: 10 }, + visible: { opacity: 1, y: 0, transition: { duration: 0.3 } } + }; + + return ( + +
+
Status: - + {status || 'Pending'} - +
- Requested by: {request.requested_user_expand?.name || request.requested_user || 'Unknown'} + Requested by: + {request.requested_user_expand?.name || + (request.expand?.requested_user?.name) || + 'Unknown'} + + {" - "} + + {request.requested_user_expand?.email || + (request.expand?.requested_user?.email) || + 'No email available'} +
- -
{/* Tabs */} - {/* Content */} -
+ {/* Event Details Tab */} {activeTab === 'details' && ( -
+
-
+

Event Name

{request.name}

-
+

Location

{request.location || 'Not specified'}

-
+

Start Date & Time

{formatDate(request.start_date_time)}

-
+

End Date & Time

{formatDate(request.end_date_time)}

-
+

Expected Attendance

{request.expected_attendance || 'Not specified'}

-
+

Event Description

{request.event_description || 'No description provided'}

-
+

Room Booking

-

{request.will_or_have_room_booking ? 'Yes' : 'No'}

+
+ {request.will_or_have_room_booking ? ( + Yes + ) : ( + No + )} +
+ + {/* Display room booking file if available */} + {request.room_booking && ( +
+ +
+ )}
-
+

Food/Drinks Served

-

{request.food_drinks_being_served ? 'Yes' : 'No'}

+
+ {request.food_drinks_being_served ? ( + Yes + ) : ( + No + )} +
-
+

Submission Date

{formatDate(request.created)}

-
+ )} {/* PR Materials Tab */} {activeTab === 'pr' && ( -
-
-
-
-

Flyers Needed

-

{request.flyers_needed ? 'Yes' : 'No'}

-
- {request.flyers_needed && ( - <> -
-

Flyer Types

-
    - {request.flyer_type?.map((type, index) => ( -
  • {type}
  • - ))} - {request.other_flyer_type &&
  • {request.other_flyer_type}
  • } -
-
-
-

Advertising Start Date

-

{formatDate(request.flyer_advertising_start_date || '')}

-
-
-

Advertising Format

-

{request.advertising_format || 'Not specified'}

-
- - )} -
-
-
-

Photography Needed

-

{request.photography_needed ? 'Yes' : 'No'}

-
- {request.flyers_needed && ( - <> -
-

Required Logos

-
    - {request.required_logos?.map((logo, index) => ( -
  • {logo}
  • - ))} - {(!request.required_logos || request.required_logos.length === 0) && -
  • No specific logos required
  • - } -
-
-
-

Additional Requests

-

{request.flyer_additional_requests || 'None'}

-
- - )} -
-
-
+ + + )} {/* AS Funding Tab */} {activeTab === 'funding' && ( - + + + )} -
- -
+ +
+ ); }; diff --git a/src/components/dashboard/Officer_EventRequestManagement/EventRequestManagementTable.tsx b/src/components/dashboard/Officer_EventRequestManagement/EventRequestManagementTable.tsx index 4e80aff..772b2e0 100644 --- a/src/components/dashboard/Officer_EventRequestManagement/EventRequestManagementTable.tsx +++ b/src/components/dashboard/Officer_EventRequestManagement/EventRequestManagementTable.tsx @@ -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; } -const EventRequestManagementTable = ({ eventRequests: initialEventRequests }: EventRequestManagementTableProps) => { +const EventRequestManagementTable = ({ + eventRequests: initialEventRequests, + onRequestSelect, + onStatusChange +}: EventRequestManagementTableProps) => { const [eventRequests, setEventRequests] = useState(initialEventRequests); const [filteredRequests, setFilteredRequests] = useState(initialEventRequests); - const [selectedRequest, setSelectedRequest] = useState(null); - const [isModalOpen, setIsModalOpen] = useState(false); const [isRefreshing, setIsRefreshing] = useState(false); const [statusFilter, setStatusFilter] = useState('all'); const [searchTerm, setSearchTerm] = useState(''); @@ -135,8 +139,7 @@ const EventRequestManagementTable = ({ eventRequests: initialEventRequests }: Ev // Update event request status const updateEventRequestStatus = async (id: string, status: "submitted" | "pending" | "completed" | "declined"): Promise => { 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(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 {filteredRequests.map((request) => ( - {request.name} + +
+ {truncateText(request.name, 30)} +
+ {formatDate(request.start_date_time)} -
- {request.expand?.requested_user?.name || 'Unknown'} - {request.expand?.requested_user?.email} -
+ {(() => { + const { name, email } = getUserDisplayInfo(request); + return ( +
+ {name} + {email} +
+ ); + })()} {request.flyers_needed ? ( @@ -554,12 +556,6 @@ const EventRequestManagementTable = ({ eventRequests: initialEventRequests }: Ev
-
@@ -577,85 +574,6 @@ const EventRequestManagementTable = ({ eventRequests: initialEventRequests }: Ev
- - {/* Event request details modal - Now outside the main component div */} - {isModalOpen && selectedRequest && ( - - - - )} - - {/* Update status modal */} - {isUpdateModalOpen && requestToUpdate && ( - -
- - {/* Header */} -
-

Update Status

- -
- - {/* Content */} -
-

- Update status for event: {requestToUpdate.name} -

-
- - - -
-
- - {/* Footer */} -
- -
-
-
-
- )} ); }; diff --git a/src/components/dashboard/Officer_EventRequestManagement/EventRequestModal.tsx b/src/components/dashboard/Officer_EventRequestManagement/EventRequestModal.tsx new file mode 100644 index 0000000..9b74c38 --- /dev/null +++ b/src/components/dashboard/Officer_EventRequestManagement/EventRequestModal.tsx @@ -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 { + 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 => { + try { + const get = Get.getInstance(); + const updatedRequests = [...requests]; + const userCache: Record = {}; // 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; +}> = ({ eventRequests, handleSelectRequest, handleStatusChange }) => { + return ( + + ); +}; + +const EventRequestModal: React.FC = ({ eventRequests }) => { + // Define animation delay as a constant to keep it consistent + const ANIMATION_DELAY = "0.3s"; + + const [selectedRequest, setSelectedRequest] = useState(null); + const [isModalOpen, setIsModalOpen] = useState(false); + const [localEventRequests, setLocalEventRequests] = useState(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 => { + 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 */} +
+
+
+ {isLoadingUserData ? ( +
+
+ Loading user data... +
+ ) : ( +
+ )} + +
+
+ +
+
+
+ + {/* Modal */} + + {isModalOpen && selectedRequest && ( + +
+
+ +
+ +
+
+ )} +
+ + ); +}; + +export default EventRequestModal; \ No newline at end of file diff --git a/src/schemas/pocketbase/schema.ts b/src/schemas/pocketbase/schema.ts index 57aa85c..0d1232a 100644 --- a/src/schemas/pocketbase/schema.ts +++ b/src/schemas/pocketbase/schema.ts @@ -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