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/react": "^4.2.0",
|
||||||
"@astrojs/tailwind": "5.1.4",
|
"@astrojs/tailwind": "5.1.4",
|
||||||
"@iconify-json/heroicons": "^1.2.2",
|
"@iconify-json/heroicons": "^1.2.2",
|
||||||
|
"@iconify-json/mdi": "^1.2.3",
|
||||||
"@iconify/react": "^5.2.0",
|
"@iconify/react": "^5.2.0",
|
||||||
"@types/highlight.js": "^10.1.0",
|
"@types/highlight.js": "^10.1.0",
|
||||||
"@types/js-yaml": "^4.0.9",
|
"@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/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/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=="],
|
"@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/react": "^4.2.0",
|
||||||
"@astrojs/tailwind": "5.1.4",
|
"@astrojs/tailwind": "5.1.4",
|
||||||
"@iconify-json/heroicons": "^1.2.2",
|
"@iconify-json/heroicons": "^1.2.2",
|
||||||
|
"@iconify-json/mdi": "^1.2.3",
|
||||||
"@iconify/react": "^5.2.0",
|
"@iconify/react": "^5.2.0",
|
||||||
"@types/highlight.js": "^10.1.0",
|
"@types/highlight.js": "^10.1.0",
|
||||||
"@types/js-yaml": "^4.0.9",
|
"@types/js-yaml": "^4.0.9",
|
||||||
|
|
|
@ -3,9 +3,11 @@ import { Authentication } from "../../scripts/pocketbase/Authentication";
|
||||||
import { Get } from "../../scripts/pocketbase/Get";
|
import { Get } from "../../scripts/pocketbase/Get";
|
||||||
import { toast } from "react-hot-toast";
|
import { toast } from "react-hot-toast";
|
||||||
import EventRequestManagementTable from "./Officer_EventRequestManagement/EventRequestManagementTable";
|
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 { Collections } from "../../schemas/pocketbase/schema";
|
||||||
import { Icon } from "astro-icon/components";
|
import { Icon } from "astro-icon/components";
|
||||||
|
import CustomAlert from "./universal/CustomAlert";
|
||||||
|
|
||||||
// Get instances
|
// Get instances
|
||||||
const get = Get.getInstance();
|
const get = Get.getInstance();
|
||||||
|
@ -22,6 +24,7 @@ interface ExtendedEventRequest extends EventRequest {
|
||||||
id: string;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
email: string;
|
email: string;
|
||||||
|
emailVisibility?: boolean; // Add this field to the interface
|
||||||
[key: string]: any;
|
[key: string]: any;
|
||||||
};
|
};
|
||||||
[key: string]: any;
|
[key: string]: any;
|
||||||
|
@ -29,6 +32,13 @@ interface ExtendedEventRequest extends EventRequest {
|
||||||
[key: string]: any; // For other optional properties
|
[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
|
// Initialize variables for all event requests
|
||||||
let allEventRequests: ExtendedEventRequest[] = [];
|
let allEventRequests: ExtendedEventRequest[] = [];
|
||||||
let error = null;
|
let error = null;
|
||||||
|
@ -37,37 +47,42 @@ try {
|
||||||
// Don't check authentication here - let the client component handle it
|
// 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
|
// 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
|
allEventRequests = await get
|
||||||
.getAll<ExtendedEventRequest>(Collections.EVENT_REQUESTS, "", "-created", {
|
.getAll<ExtendedEventRequest>(
|
||||||
|
Collections.EVENT_REQUESTS,
|
||||||
|
"",
|
||||||
|
"-created",
|
||||||
|
{
|
||||||
expand: ["requested_user"],
|
expand: ["requested_user"],
|
||||||
})
|
}
|
||||||
|
)
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
console.error("Error in get.getAll:", err);
|
console.error("Error in get.getAll:", err);
|
||||||
// Return empty array instead of throwing
|
// Return empty array instead of throwing
|
||||||
return [];
|
return [];
|
||||||
});
|
});
|
||||||
|
|
||||||
// console.log(
|
|
||||||
// `Fetched ${allEventRequests.length} event requests in Astro component`,
|
|
||||||
// );
|
|
||||||
|
|
||||||
// Process the event requests to add the requested_user_expand property
|
// Process the event requests to add the requested_user_expand property
|
||||||
allEventRequests = allEventRequests.map((request) => {
|
allEventRequests = allEventRequests.map((request) => {
|
||||||
const requestWithExpand = { ...request };
|
const requestWithExpand = { ...request };
|
||||||
|
|
||||||
// Add the requested_user_expand property if the expand data is available
|
// Add the requested_user_expand property if the expand data is available
|
||||||
if (
|
if (
|
||||||
request.expand &&
|
request.expand?.requested_user &&
|
||||||
request.expand.requested_user &&
|
request.expand.requested_user.name
|
||||||
request.expand.requested_user.name &&
|
|
||||||
request.expand.requested_user.email
|
|
||||||
) {
|
) {
|
||||||
|
// Always include email regardless of emailVisibility setting
|
||||||
requestWithExpand.requested_user_expand = {
|
requestWithExpand.requested_user_expand = {
|
||||||
name: request.expand.requested_user.name,
|
name: request.expand.requested_user.name,
|
||||||
email: request.expand.requested_user.email,
|
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;
|
return requestWithExpand;
|
||||||
|
@ -78,7 +93,7 @@ try {
|
||||||
}
|
}
|
||||||
---
|
---
|
||||||
|
|
||||||
<div class="w-full max-w-7xl mx-auto py-8 px-4">
|
<div class="w-full max-w-7xl mx-auto py-8 px-4 sm:px-6">
|
||||||
<style>
|
<style>
|
||||||
.event-table-container {
|
.event-table-container {
|
||||||
min-height: 600px;
|
min-height: 600px;
|
||||||
|
@ -91,90 +106,198 @@ try {
|
||||||
.event-table-container .overflow-x-auto {
|
.event-table-container .overflow-x-auto {
|
||||||
max-height: none !important;
|
max-height: none !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Modern table styles */
|
||||||
|
.modern-table thead th {
|
||||||
|
position: sticky;
|
||||||
|
top: 0;
|
||||||
|
z-index: 10;
|
||||||
|
font-weight: 600;
|
||||||
|
letter-spacing: 0.02em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modern-table tbody tr {
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 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>
|
</style>
|
||||||
|
|
||||||
<div class="mb-8">
|
<div
|
||||||
<h1 class="text-3xl font-bold text-white mb-2">Event Request Management</h1>
|
class="mb-8 space-y-4 card-enter"
|
||||||
<p class="text-gray-300 mb-4">
|
style={`animation-delay: ${ANIMATION_DELAYS.heading};`}
|
||||||
Review and manage event requests submitted by officers. Update status and
|
>
|
||||||
coordinate with the team.
|
<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>
|
||||||
|
|
||||||
|
<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>
|
</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>
|
<div
|
||||||
<ul class="list-disc list-inside space-y-1 ml-2">
|
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"
|
||||||
<li>View all submitted event requests</li>
|
style={`animation-delay: ${ANIMATION_DELAYS.info};`}
|
||||||
<li>Update the status of requests (Pending, Completed, Declined)</li>
|
>
|
||||||
<li>Filter and sort requests by various criteria</li>
|
<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>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{
|
{
|
||||||
error && (
|
error && (
|
||||||
<div class="alert alert-error mb-6">
|
<div
|
||||||
<svg
|
class="mb-6 card-enter"
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
style={`animation-delay: ${ANIMATION_DELAYS.content};`}
|
||||||
class="h-6 w-6 stroke-current shrink-0"
|
|
||||||
fill="none"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
>
|
>
|
||||||
<path
|
<CustomAlert
|
||||||
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>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
!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
|
client:load
|
||||||
eventRequests={allEventRequests}
|
type="error"
|
||||||
|
title="Error fetching event requests"
|
||||||
|
message={error.toString()}
|
||||||
|
icon="heroicons:exclamation-triangle"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
<!-- Main page content including table and modal -->
|
||||||
|
<EventRequestModal client:load eventRequests={allEventRequests} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script>
|
<script define:vars={{ ANIMATION_DELAYS }}>
|
||||||
// Import the DataSyncService for client-side use
|
// Import the DataSyncService for client-side use
|
||||||
import { DataSyncService } from "../../scripts/database/DataSyncService";
|
import { DataSyncService } from "../../scripts/database/DataSyncService";
|
||||||
import { Collections } from "../../schemas/pocketbase/schema";
|
import { Collections } from "../../schemas/pocketbase/schema";
|
||||||
|
|
||||||
// Remove the visibilitychange event listener that causes full page refresh
|
// Use a more efficient approach to refresh data only when needed
|
||||||
// Instead, we'll use a more efficient approach to refresh data only when needed
|
|
||||||
document.addEventListener("visibilitychange", () => {
|
document.addEventListener("visibilitychange", () => {
|
||||||
if (document.visibilityState === "visible") {
|
if (document.visibilityState === "visible") {
|
||||||
// Instead of reloading the entire page, dispatch a custom event
|
// Dispatch a custom event that components can listen for
|
||||||
// that components can listen for to refresh their data
|
|
||||||
document.dispatchEvent(new CustomEvent("dashboardTabVisible"));
|
document.dispatchEvent(new CustomEvent("dashboardTabVisible"));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Handle authentication errors
|
// Handle authentication errors and initial data loading
|
||||||
document.addEventListener("DOMContentLoaded", async () => {
|
document.addEventListener("DOMContentLoaded", async () => {
|
||||||
// Initialize DataSyncService for client-side
|
// Initialize DataSyncService for client-side
|
||||||
const dataSync = DataSyncService.getInstance();
|
const dataSync = DataSyncService.getInstance();
|
||||||
|
|
||||||
|
// 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;
|
||||||
|
|
||||||
|
card.setAttribute("style", `animation-delay: ${delay}`);
|
||||||
|
});
|
||||||
|
|
||||||
// Prefetch data into IndexedDB
|
// Prefetch data into IndexedDB
|
||||||
try {
|
try {
|
||||||
await dataSync.syncCollection(
|
await dataSync.syncCollection(
|
||||||
Collections.EVENT_REQUESTS,
|
Collections.EVENT_REQUESTS,
|
||||||
"",
|
"",
|
||||||
"-created",
|
"-created",
|
||||||
{ expand: "requested_user" },
|
{ expand: "requested_user" }
|
||||||
);
|
);
|
||||||
// console.log("Initial data sync complete");
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
// console.error("Error during initial data sync:", err);
|
console.error("Error during initial data sync:", err);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check for error message in the UI
|
// Check for error message in the UI
|
||||||
|
@ -183,26 +306,10 @@ try {
|
||||||
errorElement &&
|
errorElement &&
|
||||||
errorElement.textContent?.includes("Authentication error")
|
errorElement.textContent?.includes("Authentication error")
|
||||||
) {
|
) {
|
||||||
// console.log(
|
|
||||||
// "Authentication error detected in UI, redirecting to login...",
|
|
||||||
// );
|
|
||||||
// Redirect to login page after a short delay
|
// Redirect to login page after a short delay
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
window.location.href = "/login";
|
window.location.href = "/login";
|
||||||
}, 3000);
|
}, 3000);
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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",
|
|
||||||
// );
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -31,13 +31,17 @@ interface ExtendedEventRequest extends SchemaEventRequest {
|
||||||
|
|
||||||
interface EventRequestManagementTableProps {
|
interface EventRequestManagementTableProps {
|
||||||
eventRequests: ExtendedEventRequest[];
|
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 [eventRequests, setEventRequests] = useState<ExtendedEventRequest[]>(initialEventRequests);
|
||||||
const [filteredRequests, setFilteredRequests] = 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 [isRefreshing, setIsRefreshing] = useState<boolean>(false);
|
||||||
const [statusFilter, setStatusFilter] = useState<string>('all');
|
const [statusFilter, setStatusFilter] = useState<string>('all');
|
||||||
const [searchTerm, setSearchTerm] = useState<string>('');
|
const [searchTerm, setSearchTerm] = useState<string>('');
|
||||||
|
@ -135,8 +139,7 @@ const EventRequestManagementTable = ({ eventRequests: initialEventRequests }: Ev
|
||||||
// Update event request status
|
// Update event request status
|
||||||
const updateEventRequestStatus = async (id: string, status: "submitted" | "pending" | "completed" | "declined"): Promise<void> => {
|
const updateEventRequestStatus = async (id: string, status: "submitted" | "pending" | "completed" | "declined"): Promise<void> => {
|
||||||
try {
|
try {
|
||||||
const update = Update.getInstance();
|
await onStatusChange(id, status);
|
||||||
const result = await update.updateField('event_request', id, 'status', status);
|
|
||||||
|
|
||||||
// Find the event request to get its name
|
// Find the event request to get its name
|
||||||
const eventRequest = eventRequests.find(req => req.id === id);
|
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
|
// Force sync to update IndexedDB
|
||||||
await dataSync.syncCollection<ExtendedEventRequest>(Collections.EVENT_REQUESTS);
|
await dataSync.syncCollection<ExtendedEventRequest>(Collections.EVENT_REQUESTS);
|
||||||
|
|
||||||
|
@ -211,43 +209,38 @@ const EventRequestManagementTable = ({ eventRequests: initialEventRequests }: Ev
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Open modal with event request details
|
// Helper function to truncate text
|
||||||
|
const truncateText = (text: string, maxLength: number) => {
|
||||||
|
if (!text) return '';
|
||||||
|
return text.length > maxLength ? `${text.substring(0, maxLength)}...` : text;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 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) => {
|
const openDetailModal = (request: ExtendedEventRequest) => {
|
||||||
setSelectedRequest(request);
|
onRequestSelect(request);
|
||||||
setIsModalOpen(true);
|
|
||||||
};
|
|
||||||
|
|
||||||
// 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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Handle sort change
|
// Handle sort change
|
||||||
|
@ -524,13 +517,22 @@ const EventRequestManagementTable = ({ eventRequests: initialEventRequests }: Ev
|
||||||
<tbody>
|
<tbody>
|
||||||
{filteredRequests.map((request) => (
|
{filteredRequests.map((request) => (
|
||||||
<tr key={request.id} className="hover">
|
<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 className="hidden md:table-cell">{formatDate(request.start_date_time)}</td>
|
||||||
<td>
|
<td>
|
||||||
|
{(() => {
|
||||||
|
const { name, email } = getUserDisplayInfo(request);
|
||||||
|
return (
|
||||||
<div className="flex flex-col">
|
<div className="flex flex-col">
|
||||||
<span>{request.expand?.requested_user?.name || 'Unknown'}</span>
|
<span>{name}</span>
|
||||||
<span className="text-xs text-gray-400">{request.expand?.requested_user?.email}</span>
|
<span className="text-xs text-gray-400">{email}</span>
|
||||||
</div>
|
</div>
|
||||||
|
);
|
||||||
|
})()}
|
||||||
</td>
|
</td>
|
||||||
<td className="hidden lg:table-cell">
|
<td className="hidden lg:table-cell">
|
||||||
{request.flyers_needed ? (
|
{request.flyers_needed ? (
|
||||||
|
@ -554,12 +556,6 @@ const EventRequestManagementTable = ({ eventRequests: initialEventRequests }: Ev
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<button
|
|
||||||
className="btn btn-sm btn-outline"
|
|
||||||
onClick={() => openUpdateModal(request)}
|
|
||||||
>
|
|
||||||
Update
|
|
||||||
</button>
|
|
||||||
<button
|
<button
|
||||||
className="btn btn-sm btn-ghost"
|
className="btn btn-sm btn-ghost"
|
||||||
onClick={() => openDetailModal(request)}
|
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="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" />
|
<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>
|
</svg>
|
||||||
|
View Details
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
|
@ -577,85 +574,6 @@ const EventRequestManagementTable = ({ eventRequests: initialEventRequests }: Ev
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
</motion.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;
|
|
@ -100,11 +100,11 @@ export interface EventRequest extends BaseRecord {
|
||||||
flyer_additional_requests?: string;
|
flyer_additional_requests?: string;
|
||||||
photography_needed: boolean;
|
photography_needed: boolean;
|
||||||
required_logos?: string[]; // IEEE, AS, HKN, TESC, PIB, TNT, SWE, OTHER
|
required_logos?: string[]; // IEEE, AS, HKN, TESC, PIB, TNT, SWE, OTHER
|
||||||
other_logos?: string[];
|
other_logos?: string[]; // Array of logo IDs
|
||||||
advertising_format?: string;
|
advertising_format?: string;
|
||||||
will_or_have_room_booking?: boolean;
|
will_or_have_room_booking?: boolean;
|
||||||
expected_attendance?: number;
|
expected_attendance?: number;
|
||||||
room_booking?: string;
|
room_booking?: string; // signle file
|
||||||
as_funding_required: boolean;
|
as_funding_required: boolean;
|
||||||
food_drinks_being_served: boolean;
|
food_drinks_being_served: boolean;
|
||||||
itemized_invoice?: string; // JSON string
|
itemized_invoice?: string; // JSON string
|
||||||
|
|
Loading…
Reference in a new issue