more reliable updates to the modal
This commit is contained in:
parent
e2933b1c5f
commit
fd68c47e9a
7 changed files with 1273 additions and 525 deletions
3
bun.lock
3
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=="],
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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>
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -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>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -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;
|
|
@ -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
|
||||
|
|
Loading…
Reference in a new issue