update the files using the appropriate schema
This commit is contained in:
parent
f4db576400
commit
701854e633
24 changed files with 222 additions and 398 deletions
|
@ -1,6 +1,4 @@
|
||||||
---
|
---
|
||||||
import { Icon } from "@iconify/react";
|
|
||||||
import JSZip from "jszip";
|
|
||||||
import FilePreview from "./universal/FilePreview";
|
import FilePreview from "./universal/FilePreview";
|
||||||
import EventCheckIn from "./EventsSection/EventCheckIn";
|
import EventCheckIn from "./EventsSection/EventCheckIn";
|
||||||
import EventLoad from "./EventsSection/EventLoad";
|
import EventLoad from "./EventsSection/EventLoad";
|
||||||
|
|
|
@ -4,26 +4,11 @@ import { Authentication } from "../../../scripts/pocketbase/Authentication";
|
||||||
import { Update } from "../../../scripts/pocketbase/Update";
|
import { Update } from "../../../scripts/pocketbase/Update";
|
||||||
import { SendLog } from "../../../scripts/pocketbase/SendLog";
|
import { SendLog } from "../../../scripts/pocketbase/SendLog";
|
||||||
import { Icon } from "@iconify/react";
|
import { Icon } from "@iconify/react";
|
||||||
|
import type { Event, AttendeeEntry } from "../../../schemas/pocketbase";
|
||||||
|
|
||||||
|
// Extended Event interface with additional properties needed for this component
|
||||||
interface Event {
|
interface ExtendedEvent extends Event {
|
||||||
id: string;
|
description?: string; // This component uses 'description' but schema has 'event_description'
|
||||||
event_name: string;
|
|
||||||
event_code: string;
|
|
||||||
location: string;
|
|
||||||
points_to_reward: number;
|
|
||||||
attendees: AttendeeEntry[];
|
|
||||||
start_date: string;
|
|
||||||
end_date: string;
|
|
||||||
has_food: boolean;
|
|
||||||
description: string;
|
|
||||||
files: string[];
|
|
||||||
}
|
|
||||||
|
|
||||||
interface AttendeeEntry {
|
|
||||||
user_id: string;
|
|
||||||
time_checked_in: string;
|
|
||||||
food: string;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Toast management system
|
// Toast management system
|
||||||
|
@ -109,7 +94,8 @@ const EventCheckIn = () => {
|
||||||
|
|
||||||
const currentUser = auth.getCurrentUser();
|
const currentUser = auth.getCurrentUser();
|
||||||
if (!currentUser) {
|
if (!currentUser) {
|
||||||
throw new Error("You must be logged in to check in to events");
|
createToast("You must be logged in to check in to events", "error");
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Find the event with the given code
|
// Find the event with the given code
|
||||||
|
@ -122,7 +108,8 @@ const EventCheckIn = () => {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if user is already checked in
|
// Check if user is already checked in
|
||||||
if (event.attendees.some((entry) => entry.user_id === currentUser.id)) {
|
const attendees = event.attendees || [];
|
||||||
|
if (attendees.some((entry) => entry.user_id === currentUser.id)) {
|
||||||
throw new Error("You have already checked in to this event");
|
throw new Error("You have already checked in to this event");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -157,6 +144,27 @@ const EventCheckIn = () => {
|
||||||
throw new Error("You must be logged in to check in to events");
|
throw new Error("You must be logged in to check in to events");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check if user is already checked in
|
||||||
|
const userId = auth.getUserId();
|
||||||
|
|
||||||
|
if (!userId) {
|
||||||
|
createToast("You must be logged in to check in to an event", "error");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize attendees array if it doesn't exist
|
||||||
|
const attendees = event.attendees || [];
|
||||||
|
|
||||||
|
// Check if user is already checked in
|
||||||
|
const isAlreadyCheckedIn = attendees.some(
|
||||||
|
(attendee) => attendee.user_id === userId
|
||||||
|
);
|
||||||
|
|
||||||
|
if (isAlreadyCheckedIn) {
|
||||||
|
createToast("You are already checked in to this event", "warning");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Create attendee entry with check-in details
|
// Create attendee entry with check-in details
|
||||||
const attendeeEntry: AttendeeEntry = {
|
const attendeeEntry: AttendeeEntry = {
|
||||||
user_id: currentUser.id,
|
user_id: currentUser.id,
|
||||||
|
|
|
@ -2,30 +2,16 @@ import { useEffect, useState } from "react";
|
||||||
import { Icon } from "@iconify/react";
|
import { Icon } from "@iconify/react";
|
||||||
import { Get } from "../../../scripts/pocketbase/Get";
|
import { Get } from "../../../scripts/pocketbase/Get";
|
||||||
import { Authentication } from "../../../scripts/pocketbase/Authentication";
|
import { Authentication } from "../../../scripts/pocketbase/Authentication";
|
||||||
|
import type { Event, AttendeeEntry } from "../../../schemas/pocketbase";
|
||||||
|
|
||||||
interface Event {
|
// Extended Event interface with additional properties needed for this component
|
||||||
id: string;
|
interface ExtendedEvent extends Event {
|
||||||
event_name: string;
|
description?: string; // This component uses 'description' but schema has 'event_description'
|
||||||
event_code: string;
|
|
||||||
location: string;
|
|
||||||
points_to_reward: number;
|
|
||||||
attendees: AttendeeEntry[];
|
|
||||||
start_date: string;
|
|
||||||
end_date: string;
|
|
||||||
has_food: boolean;
|
|
||||||
description: string;
|
|
||||||
files: string[];
|
|
||||||
}
|
|
||||||
|
|
||||||
interface AttendeeEntry {
|
|
||||||
user_id: string;
|
|
||||||
time_checked_in: string;
|
|
||||||
food: string;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
interface Window {
|
interface Window {
|
||||||
openDetailsModal: (event: Event) => void;
|
openDetailsModal: (event: ExtendedEvent) => void;
|
||||||
downloadAllFiles: () => Promise<void>;
|
downloadAllFiles: () => Promise<void>;
|
||||||
currentEventId: string;
|
currentEventId: string;
|
||||||
[key: string]: any;
|
[key: string]: any;
|
||||||
|
@ -118,13 +104,13 @@ const EventLoad = () => {
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="text-xs sm:text-sm text-base-content/70 my-2 line-clamp-2">
|
<div className="text-xs sm:text-sm text-base-content/70 my-2 line-clamp-2">
|
||||||
{event.description || "No description available"}
|
{event.event_description || "No description available"}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex flex-wrap items-center gap-2 mt-auto pt-2">
|
<div className="flex flex-wrap items-center gap-2 mt-auto pt-2">
|
||||||
{event.files && event.files.length > 0 && (
|
{event.files && event.files.length > 0 && (
|
||||||
<button
|
<button
|
||||||
onClick={() => window.openDetailsModal(event)}
|
onClick={() => window.openDetailsModal(event as ExtendedEvent)}
|
||||||
className="btn btn-ghost btn-sm text-xs sm:text-sm gap-1 h-8 min-h-0 px-2"
|
className="btn btn-ghost btn-sm text-xs sm:text-sm gap-1 h-8 min-h-0 px-2"
|
||||||
>
|
>
|
||||||
<Icon icon="heroicons:document-duplicate" className="h-3 w-3 sm:h-4 sm:w-4" />
|
<Icon icon="heroicons:document-duplicate" className="h-3 w-3 sm:h-4 sm:w-4" />
|
||||||
|
|
|
@ -5,32 +5,12 @@ import { Authentication } from "../../scripts/pocketbase/Authentication";
|
||||||
import EventEditor from "./Officer_EventManagement/EventEditor";
|
import EventEditor from "./Officer_EventManagement/EventEditor";
|
||||||
import FilePreview from "./universal/FilePreview";
|
import FilePreview from "./universal/FilePreview";
|
||||||
import Attendees from "./Officer_EventManagement/Attendees";
|
import Attendees from "./Officer_EventManagement/Attendees";
|
||||||
|
import type { Event, AttendeeEntry } from "../../schemas/pocketbase";
|
||||||
|
|
||||||
// Get instances
|
// Get instances
|
||||||
const get = Get.getInstance();
|
const get = Get.getInstance();
|
||||||
const auth = Authentication.getInstance();
|
const auth = Authentication.getInstance();
|
||||||
|
|
||||||
interface Event {
|
|
||||||
id: string;
|
|
||||||
event_name: string;
|
|
||||||
event_description: string;
|
|
||||||
event_code: string;
|
|
||||||
location: string;
|
|
||||||
files: string[];
|
|
||||||
points_to_reward: number;
|
|
||||||
start_date: string;
|
|
||||||
end_date: string;
|
|
||||||
published: boolean;
|
|
||||||
has_food: boolean;
|
|
||||||
attendees: AttendeeEntry[];
|
|
||||||
}
|
|
||||||
|
|
||||||
interface AttendeeEntry {
|
|
||||||
user_id: string;
|
|
||||||
time_checked_in: string;
|
|
||||||
food: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface ListResponse<T> {
|
interface ListResponse<T> {
|
||||||
page: number;
|
page: number;
|
||||||
perPage: number;
|
perPage: number;
|
||||||
|
@ -64,7 +44,7 @@ const totalPages = eventResponse.totalPages;
|
||||||
const currentPage = eventResponse.page;
|
const currentPage = eventResponse.page;
|
||||||
---
|
---
|
||||||
|
|
||||||
<div >
|
<div>
|
||||||
<div
|
<div
|
||||||
class="mb-4 md:mb-6 flex flex-col md:flex-row md:justify-between md:items-center gap-2"
|
class="mb-4 md:mb-6 flex flex-col md:flex-row md:justify-between md:items-center gap-2"
|
||||||
>
|
>
|
||||||
|
|
|
@ -3,6 +3,12 @@ import { Get } from '../../../scripts/pocketbase/Get';
|
||||||
import { Authentication } from '../../../scripts/pocketbase/Authentication';
|
import { Authentication } from '../../../scripts/pocketbase/Authentication';
|
||||||
import { SendLog } from '../../../scripts/pocketbase/SendLog';
|
import { SendLog } from '../../../scripts/pocketbase/SendLog';
|
||||||
import { Icon } from "@iconify/react";
|
import { Icon } from "@iconify/react";
|
||||||
|
import type { Event, AttendeeEntry, User as SchemaUser } from "../../../schemas/pocketbase";
|
||||||
|
|
||||||
|
// Extended User interface with additional properties needed for this component
|
||||||
|
interface User extends SchemaUser {
|
||||||
|
member_type: string;
|
||||||
|
}
|
||||||
|
|
||||||
// Cache for storing user data
|
// Cache for storing user data
|
||||||
const userCache = new Map<string, {
|
const userCache = new Map<string, {
|
||||||
|
@ -46,29 +52,6 @@ const HighlightText = ({ text, searchTerms }: { text: string | number | null | u
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
interface AttendeeEntry {
|
|
||||||
user_id: string;
|
|
||||||
time_checked_in: string;
|
|
||||||
food: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface User {
|
|
||||||
id: string;
|
|
||||||
name: string;
|
|
||||||
email: string;
|
|
||||||
pid: string;
|
|
||||||
member_id: string;
|
|
||||||
member_type: string;
|
|
||||||
graduation_year: string;
|
|
||||||
major: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface Event {
|
|
||||||
id: string;
|
|
||||||
event_name: string;
|
|
||||||
attendees: AttendeeEntry[];
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add new interface for selected fields
|
// Add new interface for selected fields
|
||||||
interface EventFields {
|
interface EventFields {
|
||||||
id: true;
|
id: true;
|
||||||
|
|
|
@ -6,6 +6,13 @@ import { Update } from "../../../scripts/pocketbase/Update";
|
||||||
import { FileManager } from "../../../scripts/pocketbase/FileManager";
|
import { FileManager } from "../../../scripts/pocketbase/FileManager";
|
||||||
import { SendLog } from "../../../scripts/pocketbase/SendLog";
|
import { SendLog } from "../../../scripts/pocketbase/SendLog";
|
||||||
import FilePreview from "../universal/FilePreview";
|
import FilePreview from "../universal/FilePreview";
|
||||||
|
import type { Event as SchemaEvent, AttendeeEntry } from "../../../schemas/pocketbase";
|
||||||
|
|
||||||
|
// Extended Event interface with optional created and updated fields
|
||||||
|
interface Event extends Omit<SchemaEvent, 'created' | 'updated'> {
|
||||||
|
created?: string;
|
||||||
|
updated?: string;
|
||||||
|
}
|
||||||
|
|
||||||
// Extend Window interface
|
// Extend Window interface
|
||||||
declare global {
|
declare global {
|
||||||
|
@ -17,27 +24,6 @@ declare global {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Event {
|
|
||||||
id: string;
|
|
||||||
event_name: string;
|
|
||||||
event_description: string;
|
|
||||||
event_code: string;
|
|
||||||
location: string;
|
|
||||||
files: string[];
|
|
||||||
points_to_reward: number;
|
|
||||||
start_date: string;
|
|
||||||
end_date: string;
|
|
||||||
published: boolean;
|
|
||||||
has_food: boolean;
|
|
||||||
attendees: AttendeeEntry[];
|
|
||||||
}
|
|
||||||
|
|
||||||
interface AttendeeEntry {
|
|
||||||
user_id: string;
|
|
||||||
time_checked_in: string;
|
|
||||||
food: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface EventEditorProps {
|
interface EventEditorProps {
|
||||||
onEventSaved?: () => void;
|
onEventSaved?: () => void;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,10 +1,6 @@
|
||||||
---
|
---
|
||||||
import { Authentication } from "../../scripts/pocketbase/Authentication";
|
import { Authentication } from "../../scripts/pocketbase/Authentication";
|
||||||
import { Update } from "../../scripts/pocketbase/Update";
|
|
||||||
import { FileManager } from "../../scripts/pocketbase/FileManager";
|
|
||||||
import { Get } from "../../scripts/pocketbase/Get";
|
import { Get } from "../../scripts/pocketbase/Get";
|
||||||
import { toast, Toaster } from "react-hot-toast";
|
|
||||||
import EventRequestFormPreview from "./Officer_EventRequestForm/EventRequestFormPreview";
|
|
||||||
import EventRequestForm from "./Officer_EventRequestForm/EventRequestForm";
|
import EventRequestForm from "./Officer_EventRequestForm/EventRequestForm";
|
||||||
import UserEventRequests from "./Officer_EventRequestForm/UserEventRequests";
|
import UserEventRequests from "./Officer_EventRequestForm/UserEventRequests";
|
||||||
|
|
||||||
|
|
|
@ -4,6 +4,7 @@ import toast from 'react-hot-toast';
|
||||||
import type { EventRequestFormData } from './EventRequestForm';
|
import type { EventRequestFormData } from './EventRequestForm';
|
||||||
import InvoiceBuilder from './InvoiceBuilder';
|
import InvoiceBuilder from './InvoiceBuilder';
|
||||||
import type { InvoiceData } from './InvoiceBuilder';
|
import type { InvoiceData } from './InvoiceBuilder';
|
||||||
|
import type { EventRequest } from '../../../schemas/pocketbase';
|
||||||
|
|
||||||
// Animation variants
|
// Animation variants
|
||||||
const itemVariants = {
|
const itemVariants = {
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { motion } from 'framer-motion';
|
import { motion } from 'framer-motion';
|
||||||
import type { EventRequestFormData } from './EventRequestForm';
|
import type { EventRequestFormData } from './EventRequestForm';
|
||||||
|
import type { EventRequest } from '../../../schemas/pocketbase';
|
||||||
|
|
||||||
// Animation variants
|
// Animation variants
|
||||||
const itemVariants = {
|
const itemVariants = {
|
||||||
|
|
|
@ -5,6 +5,7 @@ import { Authentication } from '../../../scripts/pocketbase/Authentication';
|
||||||
import { Update } from '../../../scripts/pocketbase/Update';
|
import { Update } from '../../../scripts/pocketbase/Update';
|
||||||
import { FileManager } from '../../../scripts/pocketbase/FileManager';
|
import { FileManager } from '../../../scripts/pocketbase/FileManager';
|
||||||
import { Get } from '../../../scripts/pocketbase/Get';
|
import { Get } from '../../../scripts/pocketbase/Get';
|
||||||
|
import type { EventRequest } from '../../../schemas/pocketbase';
|
||||||
|
|
||||||
// Form sections
|
// Form sections
|
||||||
import PRSection from './PRSection';
|
import PRSection from './PRSection';
|
||||||
|
@ -43,34 +44,42 @@ const itemVariants = {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Form data interface
|
// Form data interface - based on the schema EventRequest but with form-specific fields
|
||||||
export interface EventRequestFormData {
|
export interface EventRequestFormData {
|
||||||
|
// Fields from EventRequest
|
||||||
name: string;
|
name: string;
|
||||||
location: string;
|
location: string;
|
||||||
start_date_time: string;
|
start_date_time: string;
|
||||||
end_date_time: string;
|
end_date_time: string;
|
||||||
event_description: string;
|
event_description: string;
|
||||||
flyers_needed: boolean;
|
flyers_needed: boolean;
|
||||||
|
photography_needed: boolean;
|
||||||
|
as_funding_required: boolean;
|
||||||
|
food_drinks_being_served: boolean;
|
||||||
|
itemized_invoice?: string;
|
||||||
|
status?: string;
|
||||||
|
created_by?: string;
|
||||||
|
id?: string;
|
||||||
|
created?: string;
|
||||||
|
updated?: string;
|
||||||
|
|
||||||
|
// Additional form-specific fields
|
||||||
flyer_type: string[];
|
flyer_type: string[];
|
||||||
other_flyer_type: string;
|
other_flyer_type: string;
|
||||||
flyer_advertising_start_date: string;
|
flyer_advertising_start_date: string;
|
||||||
flyer_additional_requests: string;
|
flyer_additional_requests: string;
|
||||||
photography_needed: boolean;
|
|
||||||
required_logos: string[];
|
required_logos: string[];
|
||||||
other_logos: File[];
|
other_logos: File[]; // Form uses File objects, schema uses strings
|
||||||
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: File | null;
|
room_booking: File | null;
|
||||||
as_funding_required: boolean;
|
|
||||||
food_drinks_being_served: boolean;
|
|
||||||
itemized_invoice: string;
|
|
||||||
invoice: File | null;
|
invoice: File | null;
|
||||||
invoice_files: File[]; // Support for multiple invoice files
|
invoice_files: File[];
|
||||||
needs_graphics: boolean | null;
|
|
||||||
needs_as_funding: boolean;
|
|
||||||
invoiceData: InvoiceData;
|
invoiceData: InvoiceData;
|
||||||
formReviewed: boolean; // New field to track if the form has been reviewed
|
needs_graphics?: boolean | null;
|
||||||
|
needs_as_funding?: boolean | null;
|
||||||
|
formReviewed?: boolean; // Track if the form has been reviewed
|
||||||
}
|
}
|
||||||
|
|
||||||
const EventRequestForm: React.FC = () => {
|
const EventRequestForm: React.FC = () => {
|
||||||
|
|
|
@ -2,6 +2,7 @@ import React, { useState, useEffect } from 'react';
|
||||||
import { motion } from 'framer-motion';
|
import { motion } from 'framer-motion';
|
||||||
import type { EventRequestFormData } from './EventRequestForm';
|
import type { EventRequestFormData } from './EventRequestForm';
|
||||||
import type { InvoiceItem } from './InvoiceBuilder';
|
import type { InvoiceItem } from './InvoiceBuilder';
|
||||||
|
import type { EventRequest } from '../../../schemas/pocketbase';
|
||||||
|
|
||||||
interface EventRequestFormPreviewProps {
|
interface EventRequestFormPreviewProps {
|
||||||
formData?: EventRequestFormData; // Optional prop to directly pass form data
|
formData?: EventRequestFormData; // Optional prop to directly pass form data
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
import { motion } from 'framer-motion';
|
import { motion } from 'framer-motion';
|
||||||
import type { EventRequestFormData } from './EventRequestForm';
|
import type { EventRequestFormData } from './EventRequestForm';
|
||||||
|
import type { EventRequest } from '../../../schemas/pocketbase';
|
||||||
|
|
||||||
// Animation variants
|
// Animation variants
|
||||||
const itemVariants = {
|
const itemVariants = {
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
import { motion } from 'framer-motion';
|
import { motion } from 'framer-motion';
|
||||||
import type { EventRequestFormData } from './EventRequestForm';
|
import type { EventRequestFormData } from './EventRequestForm';
|
||||||
|
import type { EventRequest } from '../../../schemas/pocketbase';
|
||||||
|
|
||||||
// Animation variants
|
// Animation variants
|
||||||
const itemVariants = {
|
const itemVariants = {
|
||||||
|
|
|
@ -3,31 +3,10 @@ import { motion, AnimatePresence } from 'framer-motion';
|
||||||
import { Get } from '../../../scripts/pocketbase/Get';
|
import { Get } from '../../../scripts/pocketbase/Get';
|
||||||
import { Authentication } from '../../../scripts/pocketbase/Authentication';
|
import { Authentication } from '../../../scripts/pocketbase/Authentication';
|
||||||
import toast from 'react-hot-toast';
|
import toast from 'react-hot-toast';
|
||||||
|
import type { EventRequest as SchemaEventRequest } from '../../../schemas/pocketbase';
|
||||||
|
|
||||||
// Define the event request interface
|
// Extended EventRequest interface with additional properties needed for this component
|
||||||
interface EventRequest {
|
export interface EventRequest extends SchemaEventRequest {
|
||||||
id: string;
|
|
||||||
name: string;
|
|
||||||
location: string;
|
|
||||||
start_date_time: string;
|
|
||||||
end_date_time: string;
|
|
||||||
event_description: string;
|
|
||||||
flyers_needed: boolean;
|
|
||||||
photography_needed: boolean;
|
|
||||||
as_funding_required: boolean;
|
|
||||||
food_drinks_being_served: boolean;
|
|
||||||
created: string;
|
|
||||||
updated: string;
|
|
||||||
status?: string; // Status might not be in the schema yet
|
|
||||||
flyer_type?: string[];
|
|
||||||
other_flyer_type?: string;
|
|
||||||
flyer_advertising_start_date?: string;
|
|
||||||
flyer_additional_requests?: string;
|
|
||||||
required_logos?: string[];
|
|
||||||
advertising_format?: string;
|
|
||||||
will_or_have_room_booking?: boolean;
|
|
||||||
expected_attendance?: number;
|
|
||||||
itemized_invoice?: string;
|
|
||||||
invoice_data?: any;
|
invoice_data?: any;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -3,27 +3,14 @@ import { Authentication } from "../../scripts/pocketbase/Authentication";
|
||||||
import { Get } from "../../scripts/pocketbase/Get";
|
import { Get } from "../../scripts/pocketbase/Get";
|
||||||
import { Toaster } from "react-hot-toast";
|
import { Toaster } from "react-hot-toast";
|
||||||
import EventRequestManagementTable from "./Officer_EventRequestManagement/EventRequestManagementTable";
|
import EventRequestManagementTable from "./Officer_EventRequestManagement/EventRequestManagementTable";
|
||||||
|
import type { EventRequest } from "../../schemas/pocketbase";
|
||||||
|
|
||||||
// Get instances
|
// Get instances
|
||||||
const get = Get.getInstance();
|
const get = Get.getInstance();
|
||||||
const auth = Authentication.getInstance();
|
const auth = Authentication.getInstance();
|
||||||
|
|
||||||
// Define the EventRequest interface
|
// Extended EventRequest interface with additional properties needed for this component
|
||||||
interface EventRequest {
|
interface ExtendedEventRequest extends EventRequest {
|
||||||
id: string;
|
|
||||||
name: string;
|
|
||||||
location: string;
|
|
||||||
start_date_time: string;
|
|
||||||
end_date_time: string;
|
|
||||||
event_description: string;
|
|
||||||
flyers_needed: boolean;
|
|
||||||
photography_needed: boolean;
|
|
||||||
as_funding_required: boolean;
|
|
||||||
food_drinks_being_served: boolean;
|
|
||||||
created: string;
|
|
||||||
updated: string;
|
|
||||||
status: string;
|
|
||||||
requested_user: string;
|
|
||||||
requested_user_expand?: {
|
requested_user_expand?: {
|
||||||
name: string;
|
name: string;
|
||||||
email: string;
|
email: string;
|
||||||
|
@ -42,26 +29,39 @@ interface EventRequest {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initialize variables for all event requests
|
// Initialize variables for all event requests
|
||||||
let allEventRequests: EventRequest[] = [];
|
let allEventRequests: ExtendedEventRequest[] = [];
|
||||||
let error = null;
|
let error = null;
|
||||||
|
|
||||||
// Fetch all event requests if authenticated
|
try {
|
||||||
if (auth.isAuthenticated()) {
|
// Expand the requested_user field to get user details
|
||||||
try {
|
allEventRequests = await get.getAll<ExtendedEventRequest>(
|
||||||
// Expand the requested_user field to get user details
|
"event_request",
|
||||||
allEventRequests = await get.getAll<EventRequest>(
|
"",
|
||||||
"event_request",
|
"-created",
|
||||||
"",
|
);
|
||||||
"-created",
|
|
||||||
{
|
// Process the event requests to add the requested_user_expand property
|
||||||
fields: ["*"],
|
allEventRequests = allEventRequests.map((request) => {
|
||||||
expand: ["requested_user"],
|
const requestWithExpand = { ...request };
|
||||||
},
|
|
||||||
);
|
// Add the requested_user_expand property if the expand data is available
|
||||||
} catch (err) {
|
if (
|
||||||
console.error("Failed to fetch event requests:", err);
|
request.expand &&
|
||||||
error = "Failed to load event requests. Please try again later.";
|
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;
|
||||||
}
|
}
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
|
@ -1,49 +1,27 @@
|
||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
import { motion } from 'framer-motion';
|
import { motion } from 'framer-motion';
|
||||||
import toast from 'react-hot-toast';
|
import toast from 'react-hot-toast';
|
||||||
|
import type { EventRequest as SchemaEventRequest } from '../../../schemas/pocketbase';
|
||||||
|
|
||||||
// Define the EventRequest interface
|
// Extended EventRequest interface with additional properties needed for this component
|
||||||
interface EventRequest {
|
interface ExtendedEventRequest extends SchemaEventRequest {
|
||||||
id: string;
|
|
||||||
name: string;
|
|
||||||
location: string;
|
|
||||||
start_date_time: string;
|
|
||||||
end_date_time: string;
|
|
||||||
event_description: string;
|
|
||||||
flyers_needed: boolean;
|
|
||||||
photography_needed: boolean;
|
|
||||||
as_funding_required: boolean;
|
|
||||||
food_drinks_being_served: boolean;
|
|
||||||
created: string;
|
|
||||||
updated: string;
|
|
||||||
status: string;
|
|
||||||
requested_user: string;
|
|
||||||
requested_user_expand?: {
|
requested_user_expand?: {
|
||||||
name: string;
|
name: string;
|
||||||
email: string;
|
email: string;
|
||||||
};
|
};
|
||||||
flyer_type?: string[];
|
|
||||||
other_flyer_type?: string;
|
|
||||||
flyer_advertising_start_date?: string;
|
|
||||||
flyer_additional_requests?: string;
|
|
||||||
required_logos?: string[];
|
|
||||||
advertising_format?: string;
|
|
||||||
will_or_have_room_booking?: boolean;
|
|
||||||
expected_attendance?: number;
|
|
||||||
itemized_invoice?: string;
|
|
||||||
invoice_data?: string | any;
|
invoice_data?: string | any;
|
||||||
feedback?: string;
|
feedback?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface EventRequestDetailsProps {
|
interface EventRequestDetailsProps {
|
||||||
request: EventRequest;
|
request: ExtendedEventRequest;
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
onStatusChange: (id: string, status: string) => Promise<void>;
|
onStatusChange: (id: string, status: string) => Promise<void>;
|
||||||
onFeedbackChange: (id: string, feedback: string) => Promise<boolean>;
|
onFeedbackChange: (id: string, feedback: string) => Promise<boolean>;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Separate component for AS Funding tab to isolate any issues
|
// Separate component for AS Funding tab to isolate any issues
|
||||||
const ASFundingTab: React.FC<{ request: EventRequest }> = ({ request }) => {
|
const ASFundingTab: React.FC<{ request: ExtendedEventRequest }> = ({ request }) => {
|
||||||
if (!request.as_funding_required) {
|
if (!request.as_funding_required) {
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
|
|
|
@ -5,23 +5,10 @@ import { Update } from '../../../scripts/pocketbase/Update';
|
||||||
import { Authentication } from '../../../scripts/pocketbase/Authentication';
|
import { Authentication } from '../../../scripts/pocketbase/Authentication';
|
||||||
import toast from 'react-hot-toast';
|
import toast from 'react-hot-toast';
|
||||||
import EventRequestDetails from './EventRequestDetails';
|
import EventRequestDetails from './EventRequestDetails';
|
||||||
|
import type { EventRequest as SchemaEventRequest } from '../../../schemas/pocketbase';
|
||||||
|
|
||||||
// Define the EventRequest interface
|
// Extended EventRequest interface with additional properties needed for this component
|
||||||
interface EventRequest {
|
interface ExtendedEventRequest extends SchemaEventRequest {
|
||||||
id: string;
|
|
||||||
name: string;
|
|
||||||
location: string;
|
|
||||||
start_date_time: string;
|
|
||||||
end_date_time: string;
|
|
||||||
event_description: string;
|
|
||||||
flyers_needed: boolean;
|
|
||||||
photography_needed: boolean;
|
|
||||||
as_funding_required: boolean;
|
|
||||||
food_drinks_being_served: boolean;
|
|
||||||
created: string;
|
|
||||||
updated: string;
|
|
||||||
status: string;
|
|
||||||
requested_user: string;
|
|
||||||
requested_user_expand?: {
|
requested_user_expand?: {
|
||||||
name: string;
|
name: string;
|
||||||
email: string;
|
email: string;
|
||||||
|
@ -35,27 +22,18 @@ interface EventRequest {
|
||||||
};
|
};
|
||||||
[key: string]: any;
|
[key: string]: any;
|
||||||
};
|
};
|
||||||
flyer_type?: string[];
|
|
||||||
other_flyer_type?: string;
|
|
||||||
flyer_advertising_start_date?: string;
|
|
||||||
flyer_additional_requests?: string;
|
|
||||||
required_logos?: string[];
|
|
||||||
advertising_format?: string;
|
|
||||||
will_or_have_room_booking?: boolean;
|
|
||||||
expected_attendance?: number;
|
|
||||||
itemized_invoice?: string;
|
|
||||||
invoice_data?: any;
|
invoice_data?: any;
|
||||||
feedback?: string;
|
feedback?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface EventRequestManagementTableProps {
|
interface EventRequestManagementTableProps {
|
||||||
eventRequests: EventRequest[];
|
eventRequests: ExtendedEventRequest[];
|
||||||
}
|
}
|
||||||
|
|
||||||
const EventRequestManagementTable: React.FC<EventRequestManagementTableProps> = ({ eventRequests: initialEventRequests }) => {
|
const EventRequestManagementTable = ({ eventRequests: initialEventRequests }: EventRequestManagementTableProps) => {
|
||||||
const [eventRequests, setEventRequests] = useState<EventRequest[]>(initialEventRequests);
|
const [eventRequests, setEventRequests] = useState<ExtendedEventRequest[]>(initialEventRequests);
|
||||||
const [filteredRequests, setFilteredRequests] = useState<EventRequest[]>(initialEventRequests);
|
const [filteredRequests, setFilteredRequests] = useState<ExtendedEventRequest[]>(initialEventRequests);
|
||||||
const [selectedRequest, setSelectedRequest] = useState<EventRequest | null>(null);
|
const [selectedRequest, setSelectedRequest] = useState<ExtendedEventRequest | null>(null);
|
||||||
const [isModalOpen, setIsModalOpen] = useState<boolean>(false);
|
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');
|
||||||
|
@ -77,7 +55,7 @@ const EventRequestManagementTable: React.FC<EventRequestManagementTableProps> =
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const updatedRequests = await get.getAll<EventRequest>(
|
const updatedRequests = await get.getAll<ExtendedEventRequest>(
|
||||||
'event_request',
|
'event_request',
|
||||||
'',
|
'',
|
||||||
'-created',
|
'-created',
|
||||||
|
@ -123,8 +101,8 @@ const EventRequestManagementTable: React.FC<EventRequestManagementTableProps> =
|
||||||
|
|
||||||
// Apply sorting
|
// Apply sorting
|
||||||
filtered.sort((a, b) => {
|
filtered.sort((a, b) => {
|
||||||
let aValue: any = a[sortField as keyof EventRequest];
|
let aValue: any = a[sortField as keyof ExtendedEventRequest];
|
||||||
let bValue: any = b[sortField as keyof EventRequest];
|
let bValue: any = b[sortField as keyof ExtendedEventRequest];
|
||||||
|
|
||||||
// Handle special cases
|
// Handle special cases
|
||||||
if (sortField === 'requested_user') {
|
if (sortField === 'requested_user') {
|
||||||
|
@ -247,7 +225,7 @@ const EventRequestManagementTable: React.FC<EventRequestManagementTableProps> =
|
||||||
};
|
};
|
||||||
|
|
||||||
// Open modal with event request details
|
// Open modal with event request details
|
||||||
const openDetailModal = (request: EventRequest) => {
|
const openDetailModal = (request: ExtendedEventRequest) => {
|
||||||
setSelectedRequest(request);
|
setSelectedRequest(request);
|
||||||
setIsModalOpen(true);
|
setIsModalOpen(true);
|
||||||
};
|
};
|
||||||
|
|
|
@ -2,14 +2,7 @@ import { useEffect, useState, useCallback, useMemo } from "react";
|
||||||
import { Get } from "../../../scripts/pocketbase/Get";
|
import { Get } from "../../../scripts/pocketbase/Get";
|
||||||
import { Authentication } from "../../../scripts/pocketbase/Authentication";
|
import { Authentication } from "../../../scripts/pocketbase/Authentication";
|
||||||
import debounce from 'lodash/debounce';
|
import debounce from 'lodash/debounce';
|
||||||
|
import type { Log } from "../../../schemas/pocketbase";
|
||||||
interface Log {
|
|
||||||
id: string;
|
|
||||||
message: string;
|
|
||||||
created: string;
|
|
||||||
user_id?: string;
|
|
||||||
[key: string]: any;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface PaginatedResponse {
|
interface PaginatedResponse {
|
||||||
page: number;
|
page: number;
|
||||||
|
|
|
@ -1,25 +1,7 @@
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { Get } from "../../../scripts/pocketbase/Get";
|
import { Get } from "../../../scripts/pocketbase/Get";
|
||||||
import { Authentication } from "../../../scripts/pocketbase/Authentication";
|
import { Authentication } from "../../../scripts/pocketbase/Authentication";
|
||||||
|
import type { Event, Log } from "../../../schemas/pocketbase";
|
||||||
interface Event {
|
|
||||||
id: string;
|
|
||||||
event_name: string;
|
|
||||||
attendees: Array<{
|
|
||||||
food: string;
|
|
||||||
time_checked_in: string;
|
|
||||||
user_id: string;
|
|
||||||
}>;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface Log {
|
|
||||||
id: string;
|
|
||||||
message: string;
|
|
||||||
created: string;
|
|
||||||
type: string;
|
|
||||||
part: string;
|
|
||||||
user_id: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function Stats() {
|
export function Stats() {
|
||||||
const [eventsAttended, setEventsAttended] = useState(0);
|
const [eventsAttended, setEventsAttended] = useState(0);
|
||||||
|
|
|
@ -3,16 +3,11 @@ import { Icon } from '@iconify/react';
|
||||||
import FilePreview from '../universal/FilePreview';
|
import FilePreview from '../universal/FilePreview';
|
||||||
import { toast } from 'react-hot-toast';
|
import { toast } from 'react-hot-toast';
|
||||||
import { motion, AnimatePresence } from 'framer-motion';
|
import { motion, AnimatePresence } from 'framer-motion';
|
||||||
|
import type { ItemizedExpense } from '../../../schemas/pocketbase';
|
||||||
interface ExpenseItem {
|
|
||||||
description: string;
|
|
||||||
amount: number;
|
|
||||||
category: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface ReceiptFormData {
|
interface ReceiptFormData {
|
||||||
field: File;
|
field: File;
|
||||||
itemized_expenses: ExpenseItem[];
|
itemized_expenses: ItemizedExpense[];
|
||||||
tax: number;
|
tax: number;
|
||||||
date: string;
|
date: string;
|
||||||
location_name: string;
|
location_name: string;
|
||||||
|
@ -62,7 +57,7 @@ const itemVariants = {
|
||||||
export default function ReceiptForm({ onSubmit, onCancel }: ReceiptFormProps) {
|
export default function ReceiptForm({ onSubmit, onCancel }: ReceiptFormProps) {
|
||||||
const [file, setFile] = useState<File | null>(null);
|
const [file, setFile] = useState<File | null>(null);
|
||||||
const [previewUrl, setPreviewUrl] = useState<string>('');
|
const [previewUrl, setPreviewUrl] = useState<string>('');
|
||||||
const [itemizedExpenses, setItemizedExpenses] = useState<ExpenseItem[]>([
|
const [itemizedExpenses, setItemizedExpenses] = useState<ItemizedExpense[]>([
|
||||||
{ description: '', amount: 0, category: '' }
|
{ description: '', amount: 0, category: '' }
|
||||||
]);
|
]);
|
||||||
const [tax, setTax] = useState<number>(0);
|
const [tax, setTax] = useState<number>(0);
|
||||||
|
@ -106,7 +101,7 @@ export default function ReceiptForm({ onSubmit, onCancel }: ReceiptFormProps) {
|
||||||
setItemizedExpenses(itemizedExpenses.filter((_, i) => i !== index));
|
setItemizedExpenses(itemizedExpenses.filter((_, i) => i !== index));
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleExpenseItemChange = (index: number, field: keyof ExpenseItem, value: string | number) => {
|
const handleExpenseItemChange = (index: number, field: keyof ItemizedExpense, value: string | number) => {
|
||||||
const newItems = [...itemizedExpenses];
|
const newItems = [...itemizedExpenses];
|
||||||
newItems[index] = {
|
newItems[index] = {
|
||||||
...newItems[index],
|
...newItems[index],
|
||||||
|
|
|
@ -6,16 +6,11 @@ import { toast } from 'react-hot-toast';
|
||||||
import { motion, AnimatePresence } from 'framer-motion';
|
import { motion, AnimatePresence } from 'framer-motion';
|
||||||
import FilePreview from '../universal/FilePreview';
|
import FilePreview from '../universal/FilePreview';
|
||||||
import ToastProvider from './ToastProvider';
|
import ToastProvider from './ToastProvider';
|
||||||
|
import type { ItemizedExpense, Reimbursement } from '../../../schemas/pocketbase';
|
||||||
interface ExpenseItem {
|
|
||||||
description: string;
|
|
||||||
amount: number;
|
|
||||||
category: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface ReceiptFormData {
|
interface ReceiptFormData {
|
||||||
field: File;
|
field: File;
|
||||||
itemized_expenses: ExpenseItem[];
|
itemized_expenses: ItemizedExpense[];
|
||||||
tax: number;
|
tax: number;
|
||||||
date: string;
|
date: string;
|
||||||
location_name: string;
|
location_name: string;
|
||||||
|
@ -23,14 +18,13 @@ interface ReceiptFormData {
|
||||||
notes: string;
|
notes: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ReimbursementRequest {
|
// Extended Reimbursement interface with form-specific fields
|
||||||
id?: string;
|
interface ReimbursementRequest extends Partial<Omit<Reimbursement, 'receipts'>> {
|
||||||
title: string;
|
title: string;
|
||||||
total_amount: number;
|
total_amount: number;
|
||||||
date_of_purchase: string;
|
date_of_purchase: string;
|
||||||
payment_method: string;
|
payment_method: string;
|
||||||
status: 'submitted' | 'under_review' | 'approved' | 'rejected' | 'paid' | 'in_progress';
|
status: 'submitted' | 'under_review' | 'approved' | 'rejected' | 'paid' | 'in_progress';
|
||||||
submitted_by?: string;
|
|
||||||
additional_info: string;
|
additional_info: string;
|
||||||
receipts: string[];
|
receipts: string[];
|
||||||
department: 'internal' | 'external' | 'projects' | 'events' | 'other';
|
department: 'internal' | 'external' | 'projects' | 'events' | 'other';
|
||||||
|
|
|
@ -7,12 +7,7 @@ import FilePreview from '../universal/FilePreview';
|
||||||
import { toast } from 'react-hot-toast';
|
import { toast } from 'react-hot-toast';
|
||||||
import { motion, AnimatePresence } from 'framer-motion';
|
import { motion, AnimatePresence } from 'framer-motion';
|
||||||
import ToastProvider from './ToastProvider';
|
import ToastProvider from './ToastProvider';
|
||||||
|
import type { ItemizedExpense, Reimbursement, Receipt } from '../../../schemas/pocketbase';
|
||||||
interface ExpenseItem {
|
|
||||||
description: string;
|
|
||||||
amount: number;
|
|
||||||
category: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface AuditNote {
|
interface AuditNote {
|
||||||
note: string;
|
note: string;
|
||||||
|
@ -21,32 +16,14 @@ interface AuditNote {
|
||||||
is_private: boolean;
|
is_private: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ReimbursementRequest {
|
// Extended Reimbursement interface with component-specific properties
|
||||||
id: string;
|
interface ReimbursementRequest extends Omit<Reimbursement, 'audit_notes'> {
|
||||||
title: string;
|
|
||||||
total_amount: number;
|
|
||||||
date_of_purchase: string;
|
|
||||||
payment_method: string;
|
|
||||||
status: 'submitted' | 'under_review' | 'approved' | 'rejected' | 'paid' | 'in_progress';
|
|
||||||
submitted_by: string;
|
|
||||||
additional_info: string;
|
|
||||||
receipts: string[];
|
|
||||||
department: 'internal' | 'external' | 'projects' | 'events' | 'other';
|
|
||||||
created: string;
|
|
||||||
updated: string;
|
|
||||||
audit_notes: AuditNote[] | null;
|
audit_notes: AuditNote[] | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ReceiptDetails {
|
// Extended Receipt interface with component-specific properties
|
||||||
id: string;
|
interface ReceiptDetails extends Omit<Receipt, 'itemized_expenses' | 'audited_by'> {
|
||||||
field: string;
|
itemized_expenses: ItemizedExpense[];
|
||||||
created_by: string;
|
|
||||||
itemized_expenses: ExpenseItem[];
|
|
||||||
tax: number;
|
|
||||||
date: string;
|
|
||||||
location_name: string;
|
|
||||||
location_address: string;
|
|
||||||
notes: string;
|
|
||||||
audited_by: string[];
|
audited_by: string[];
|
||||||
created: string;
|
created: string;
|
||||||
updated: string;
|
updated: string;
|
||||||
|
|
|
@ -6,49 +6,23 @@ import { Update } from '../../../scripts/pocketbase/Update';
|
||||||
import { Authentication } from '../../../scripts/pocketbase/Authentication';
|
import { Authentication } from '../../../scripts/pocketbase/Authentication';
|
||||||
import { toast } from 'react-hot-toast';
|
import { toast } from 'react-hot-toast';
|
||||||
import { motion, AnimatePresence } from 'framer-motion';
|
import { motion, AnimatePresence } from 'framer-motion';
|
||||||
|
import type { Receipt as SchemaReceipt, User, Reimbursement } from '../../../schemas/pocketbase';
|
||||||
|
|
||||||
interface Receipt {
|
// Extended Receipt interface with additional properties needed for this component
|
||||||
id: string;
|
interface ExtendedReceipt extends Omit<SchemaReceipt, 'audited_by'> {
|
||||||
field: string;
|
audited_by: string[]; // In schema it's a string, but in this component it's used as string[]
|
||||||
created_by: string;
|
|
||||||
itemized_expenses: string; // JSON string
|
|
||||||
tax: number;
|
|
||||||
date: string;
|
|
||||||
location_name: string;
|
|
||||||
location_address: string;
|
|
||||||
notes: string;
|
|
||||||
audited_by: string[];
|
|
||||||
auditor_names?: string[]; // Names of auditors
|
auditor_names?: string[]; // Names of auditors
|
||||||
created: string;
|
|
||||||
updated: string;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
interface User {
|
// Extended User interface with additional properties needed for this component
|
||||||
id: string;
|
interface ExtendedUser extends User {
|
||||||
name: string;
|
|
||||||
email: string;
|
|
||||||
avatar: string;
|
avatar: string;
|
||||||
zelle_information: string;
|
zelle_information: string;
|
||||||
created: string;
|
|
||||||
updated: string;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Reimbursement {
|
// Extended Reimbursement interface with additional properties needed for this component
|
||||||
id: string;
|
interface ExtendedReimbursement extends Reimbursement {
|
||||||
title: string;
|
submitter?: ExtendedUser;
|
||||||
total_amount: number;
|
|
||||||
date_of_purchase: string;
|
|
||||||
payment_method: string;
|
|
||||||
status: 'submitted' | 'under_review' | 'approved' | 'rejected' | 'in_progress' | 'paid';
|
|
||||||
submitted_by: string;
|
|
||||||
additional_info: string;
|
|
||||||
receipts: string[]; // Array of Receipt IDs
|
|
||||||
department: 'internal' | 'external' | 'projects' | 'events' | 'other';
|
|
||||||
audit_notes: string | null; // JSON string for user-submitted notes
|
|
||||||
audit_logs: string | null; // JSON string for system-generated logs
|
|
||||||
created: string;
|
|
||||||
updated: string;
|
|
||||||
submitter?: User;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
interface FilterOptions {
|
interface FilterOptions {
|
||||||
|
@ -66,10 +40,10 @@ interface ItemizedExpense {
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function ReimbursementManagementPortal() {
|
export default function ReimbursementManagementPortal() {
|
||||||
const [reimbursements, setReimbursements] = useState<Reimbursement[]>([]);
|
const [reimbursements, setReimbursements] = useState<ExtendedReimbursement[]>([]);
|
||||||
const [receipts, setReceipts] = useState<Record<string, Receipt>>({});
|
const [receipts, setReceipts] = useState<Record<string, ExtendedReceipt>>({});
|
||||||
const [selectedReimbursement, setSelectedReimbursement] = useState<Reimbursement | null>(null);
|
const [selectedReimbursement, setSelectedReimbursement] = useState<ExtendedReimbursement | null>(null);
|
||||||
const [selectedReceipt, setSelectedReceipt] = useState<Receipt | null>(null);
|
const [selectedReceipt, setSelectedReceipt] = useState<ExtendedReceipt | null>(null);
|
||||||
const [showReceiptModal, setShowReceiptModal] = useState(false);
|
const [showReceiptModal, setShowReceiptModal] = useState(false);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
@ -84,7 +58,7 @@ export default function ReimbursementManagementPortal() {
|
||||||
const [loadingStatus, setLoadingStatus] = useState(false);
|
const [loadingStatus, setLoadingStatus] = useState(false);
|
||||||
const [expandedReceipts, setExpandedReceipts] = useState<Set<string>>(new Set());
|
const [expandedReceipts, setExpandedReceipts] = useState<Set<string>>(new Set());
|
||||||
const [auditingReceipt, setAuditingReceipt] = useState<string | null>(null);
|
const [auditingReceipt, setAuditingReceipt] = useState<string | null>(null);
|
||||||
const [users, setUsers] = useState<Record<string, User>>({});
|
const [users, setUsers] = useState<Record<string, ExtendedUser>>({});
|
||||||
const [showUserProfile, setShowUserProfile] = useState<string | null>(null);
|
const [showUserProfile, setShowUserProfile] = useState<string | null>(null);
|
||||||
const [auditNotes, setAuditNotes] = useState<string[]>([]);
|
const [auditNotes, setAuditNotes] = useState<string[]>([]);
|
||||||
const userDropdownRef = React.useRef<HTMLDivElement>(null);
|
const userDropdownRef = React.useRef<HTMLDivElement>(null);
|
||||||
|
@ -157,7 +131,7 @@ export default function ReimbursementManagementPortal() {
|
||||||
filter = filter ? `${filter} && ${dateFilter}` : dateFilter;
|
filter = filter ? `${filter} && ${dateFilter}` : dateFilter;
|
||||||
}
|
}
|
||||||
|
|
||||||
const records = await get.getAll<Reimbursement>('reimbursement', filter, sort);
|
const records = await get.getAll<ExtendedReimbursement>('reimbursement', filter, sort);
|
||||||
console.log('Loaded reimbursements:', records);
|
console.log('Loaded reimbursements:', records);
|
||||||
|
|
||||||
// Load user data for submitters
|
// Load user data for submitters
|
||||||
|
@ -165,7 +139,7 @@ export default function ReimbursementManagementPortal() {
|
||||||
const userRecords = await Promise.all(
|
const userRecords = await Promise.all(
|
||||||
Array.from(userIds).map(async id => {
|
Array.from(userIds).map(async id => {
|
||||||
try {
|
try {
|
||||||
return await get.getOne<User>('users', id);
|
return await get.getOne<ExtendedUser>('users', id);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`Failed to load user ${id}:`, error);
|
console.error(`Failed to load user ${id}:`, error);
|
||||||
return null;
|
return null;
|
||||||
|
@ -173,7 +147,7 @@ export default function ReimbursementManagementPortal() {
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
const validUsers = userRecords.filter((u): u is User => u !== null);
|
const validUsers = userRecords.filter((u): u is ExtendedUser => u !== null);
|
||||||
const userMap = Object.fromEntries(
|
const userMap = Object.fromEntries(
|
||||||
validUsers.map(user => [user.id, user])
|
validUsers.map(user => [user.id, user])
|
||||||
);
|
);
|
||||||
|
@ -197,7 +171,7 @@ export default function ReimbursementManagementPortal() {
|
||||||
const receiptRecords = await Promise.all(
|
const receiptRecords = await Promise.all(
|
||||||
receiptIds.map(async id => {
|
receiptIds.map(async id => {
|
||||||
try {
|
try {
|
||||||
const receipt = await get.getOne<Receipt>('receipts', id);
|
const receipt = await get.getOne<ExtendedReceipt>('receipts', id);
|
||||||
// Get auditor names from the users collection
|
// Get auditor names from the users collection
|
||||||
if (receipt.audited_by.length > 0) {
|
if (receipt.audited_by.length > 0) {
|
||||||
const auditorUsers = await Promise.all(
|
const auditorUsers = await Promise.all(
|
||||||
|
@ -216,7 +190,7 @@ export default function ReimbursementManagementPortal() {
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
const validReceipts = receiptRecords.filter((r): r is Receipt => r !== null);
|
const validReceipts = receiptRecords.filter((r): r is ExtendedReceipt => r !== null);
|
||||||
console.log('Successfully loaded receipt records:', validReceipts);
|
console.log('Successfully loaded receipt records:', validReceipts);
|
||||||
|
|
||||||
const receiptMap = Object.fromEntries(
|
const receiptMap = Object.fromEntries(
|
||||||
|
@ -295,11 +269,11 @@ export default function ReimbursementManagementPortal() {
|
||||||
const refreshAuditData = async (reimbursementId: string) => {
|
const refreshAuditData = async (reimbursementId: string) => {
|
||||||
try {
|
try {
|
||||||
const get = Get.getInstance();
|
const get = Get.getInstance();
|
||||||
const updatedReimbursement = await get.getOne<Reimbursement>('reimbursement', reimbursementId);
|
const updatedReimbursement = await get.getOne<ExtendedReimbursement>('reimbursement', reimbursementId);
|
||||||
|
|
||||||
// Get updated user data if needed
|
// Get updated user data if needed
|
||||||
if (!users[updatedReimbursement.submitted_by]) {
|
if (!users[updatedReimbursement.submitted_by]) {
|
||||||
const user = await get.getOne<User>('users', updatedReimbursement.submitted_by);
|
const user = await get.getOne<ExtendedUser>('users', updatedReimbursement.submitted_by);
|
||||||
setUsers(prev => ({
|
setUsers(prev => ({
|
||||||
...prev,
|
...prev,
|
||||||
[user.id]: user
|
[user.id]: user
|
||||||
|
@ -310,13 +284,13 @@ export default function ReimbursementManagementPortal() {
|
||||||
const updatedReceipts = await Promise.all(
|
const updatedReceipts = await Promise.all(
|
||||||
updatedReimbursement.receipts.map(async id => {
|
updatedReimbursement.receipts.map(async id => {
|
||||||
try {
|
try {
|
||||||
const receipt = await get.getOne<Receipt>('receipts', id);
|
const receipt = await get.getOne<ExtendedReceipt>('receipts', id);
|
||||||
// Get updated auditor names
|
// Get updated auditor names
|
||||||
if (receipt.audited_by.length > 0) {
|
if (receipt.audited_by.length > 0) {
|
||||||
const auditorUsers = await Promise.all(
|
const auditorUsers = await Promise.all(
|
||||||
receipt.audited_by.map(async auditorId => {
|
receipt.audited_by.map(async auditorId => {
|
||||||
try {
|
try {
|
||||||
const user = await get.getOne<User>('users', auditorId);
|
const user = await get.getOne<ExtendedUser>('users', auditorId);
|
||||||
// Update users state with any new auditors
|
// Update users state with any new auditors
|
||||||
setUsers(prev => ({
|
setUsers(prev => ({
|
||||||
...prev,
|
...prev,
|
||||||
|
@ -324,7 +298,7 @@ export default function ReimbursementManagementPortal() {
|
||||||
}));
|
}));
|
||||||
return user;
|
return user;
|
||||||
} catch {
|
} catch {
|
||||||
return { name: 'Unknown User' } as User;
|
return { name: 'Unknown User' } as ExtendedUser;
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
@ -338,7 +312,7 @@ export default function ReimbursementManagementPortal() {
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
const validReceipts = updatedReceipts.filter((r): r is Receipt => r !== null);
|
const validReceipts = updatedReceipts.filter((r): r is ExtendedReceipt => r !== null);
|
||||||
const receiptMap = Object.fromEntries(
|
const receiptMap = Object.fromEntries(
|
||||||
validReceipts.map(receipt => [receipt.id, receipt])
|
validReceipts.map(receipt => [receipt.id, receipt])
|
||||||
);
|
);
|
||||||
|
@ -476,7 +450,7 @@ export default function ReimbursementManagementPortal() {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const canApproveOrReject = (reimbursement: Reimbursement): boolean => {
|
const canApproveOrReject = (reimbursement: ExtendedReimbursement): boolean => {
|
||||||
const auth = Authentication.getInstance();
|
const auth = Authentication.getInstance();
|
||||||
const userId = auth.getUserId();
|
const userId = auth.getUserId();
|
||||||
|
|
||||||
|
@ -489,14 +463,14 @@ export default function ReimbursementManagementPortal() {
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const getReceiptUrl = (receipt: Receipt): string => {
|
const getReceiptUrl = (receipt: ExtendedReceipt): string => {
|
||||||
const auth = Authentication.getInstance();
|
const auth = Authentication.getInstance();
|
||||||
const pb = auth.getPocketBase();
|
const pb = auth.getPocketBase();
|
||||||
return pb.files.getURL(receipt, receipt.field);
|
return pb.files.getURL(receipt, receipt.field);
|
||||||
};
|
};
|
||||||
|
|
||||||
// Add this function to get the user avatar URL
|
// Add this function to get the user avatar URL
|
||||||
const getUserAvatarUrl = (user: User): string => {
|
const getUserAvatarUrl = (user: ExtendedUser): string => {
|
||||||
const auth = Authentication.getInstance();
|
const auth = Authentication.getInstance();
|
||||||
const pb = auth.getPocketBase();
|
const pb = auth.getPocketBase();
|
||||||
return pb.files.getURL(user, user.avatar);
|
return pb.files.getURL(user, user.avatar);
|
||||||
|
|
|
@ -1,17 +1,19 @@
|
||||||
import { Authentication } from "./Authentication";
|
import { Authentication } from "./Authentication";
|
||||||
|
import { Collections } from "../../schemas/pocketbase";
|
||||||
|
import type { Log } from "../../schemas/pocketbase";
|
||||||
|
|
||||||
// Log interface
|
// Log data interface for creating new logs
|
||||||
interface LogData {
|
interface LogData {
|
||||||
user_id: string;
|
user: string; // Relation to User
|
||||||
type: string; // Standard types: "error", "update", "delete", "create", "login", "logout"
|
type: string; // Standard types: "error", "update", "delete", "create", "login", "logout"
|
||||||
part: string; // The specific part/section being logged (can be multiple words, e.g., "profile settings", "resume upload")
|
part: string; // The specific part/section being logged
|
||||||
message: string;
|
message: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class SendLog {
|
export class SendLog {
|
||||||
private auth: Authentication;
|
private auth: Authentication;
|
||||||
private static instance: SendLog;
|
private static instance: SendLog;
|
||||||
private readonly COLLECTION_NAME = "logs"; // Make collection name a constant
|
private readonly COLLECTION_NAME = Collections.LOGS;
|
||||||
|
|
||||||
private constructor() {
|
private constructor() {
|
||||||
this.auth = Authentication.getInstance();
|
this.auth = Authentication.getInstance();
|
||||||
|
@ -49,7 +51,12 @@ export class SendLog {
|
||||||
* @param overrideUserId Optional user ID to override the current user
|
* @param overrideUserId Optional user ID to override the current user
|
||||||
* @returns Promise that resolves when the log is created
|
* @returns Promise that resolves when the log is created
|
||||||
*/
|
*/
|
||||||
public async send(type: string, part: string, message: string, overrideUserId?: string): Promise<void> {
|
public async send(
|
||||||
|
type: string,
|
||||||
|
part: string,
|
||||||
|
message: string,
|
||||||
|
overrideUserId?: string,
|
||||||
|
): Promise<void> {
|
||||||
try {
|
try {
|
||||||
// Check authentication first
|
// Check authentication first
|
||||||
if (!this.auth.isAuthenticated()) {
|
if (!this.auth.isAuthenticated()) {
|
||||||
|
@ -61,30 +68,32 @@ export class SendLog {
|
||||||
const userId = overrideUserId || this.getCurrentUserId();
|
const userId = overrideUserId || this.getCurrentUserId();
|
||||||
if (!userId) {
|
if (!userId) {
|
||||||
console.error("SendLog: No user ID available");
|
console.error("SendLog: No user ID available");
|
||||||
throw new Error("No user ID available. User must be authenticated to create logs.");
|
throw new Error(
|
||||||
|
"No user ID available. User must be authenticated to create logs.",
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Prepare log data
|
// Prepare log data
|
||||||
const logData: LogData = {
|
const logData: LogData = {
|
||||||
user_id: userId,
|
user: userId,
|
||||||
type,
|
type,
|
||||||
part,
|
part,
|
||||||
message
|
message,
|
||||||
};
|
};
|
||||||
|
|
||||||
console.debug("SendLog: Preparing to send log:", {
|
console.debug("SendLog: Preparing to send log:", {
|
||||||
collection: this.COLLECTION_NAME,
|
collection: this.COLLECTION_NAME,
|
||||||
data: logData,
|
data: logData,
|
||||||
authValid: this.auth.isAuthenticated(),
|
authValid: this.auth.isAuthenticated(),
|
||||||
userId
|
userId,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Get PocketBase instance
|
// Get PocketBase instance
|
||||||
const pb = this.auth.getPocketBase();
|
const pb = this.auth.getPocketBase();
|
||||||
|
|
||||||
// Create the log entry
|
// Create the log entry
|
||||||
await pb.collection(this.COLLECTION_NAME).create(logData);
|
await pb.collection(this.COLLECTION_NAME).create(logData);
|
||||||
|
|
||||||
console.debug("SendLog: Log created successfully");
|
console.debug("SendLog: Log created successfully");
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// Enhanced error logging
|
// Enhanced error logging
|
||||||
|
@ -94,7 +103,7 @@ export class SendLog {
|
||||||
stack: error.stack,
|
stack: error.stack,
|
||||||
type,
|
type,
|
||||||
part,
|
part,
|
||||||
message
|
message,
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
console.error("SendLog: Unknown error:", error);
|
console.error("SendLog: Unknown error:", error);
|
||||||
|
@ -110,20 +119,27 @@ export class SendLog {
|
||||||
* @param part Optional part/section to filter by
|
* @param part Optional part/section to filter by
|
||||||
* @returns Array of log entries
|
* @returns Array of log entries
|
||||||
*/
|
*/
|
||||||
public async getUserLogs(userId: string, type?: string, part?: string): Promise<LogData[]> {
|
public async getUserLogs(
|
||||||
|
userId: string,
|
||||||
|
type?: string,
|
||||||
|
part?: string,
|
||||||
|
): Promise<Log[]> {
|
||||||
if (!this.auth.isAuthenticated()) {
|
if (!this.auth.isAuthenticated()) {
|
||||||
throw new Error("User must be authenticated to retrieve logs");
|
throw new Error("User must be authenticated to retrieve logs");
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
let filter = `user_id = "${userId}"`;
|
let filter = `user = "${userId}"`;
|
||||||
if (type) filter += ` && type = "${type}"`;
|
if (type) filter += ` && type = "${type}"`;
|
||||||
if (part) filter += ` && part = "${part}"`;
|
if (part) filter += ` && part = "${part}"`;
|
||||||
|
|
||||||
const result = await this.auth.getPocketBase().collection(this.COLLECTION_NAME).getFullList<LogData>({
|
const result = await this.auth
|
||||||
filter,
|
.getPocketBase()
|
||||||
sort: "-created"
|
.collection(this.COLLECTION_NAME)
|
||||||
});
|
.getFullList<Log>({
|
||||||
|
filter,
|
||||||
|
sort: "-created",
|
||||||
|
});
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
@ -139,7 +155,11 @@ export class SendLog {
|
||||||
* @param part Optional part/section to filter by
|
* @param part Optional part/section to filter by
|
||||||
* @returns Array of recent log entries
|
* @returns Array of recent log entries
|
||||||
*/
|
*/
|
||||||
public async getRecentLogs(limit: number = 10, type?: string, part?: string): Promise<LogData[]> {
|
public async getRecentLogs(
|
||||||
|
limit: number = 10,
|
||||||
|
type?: string,
|
||||||
|
part?: string,
|
||||||
|
): Promise<Log[]> {
|
||||||
if (!this.auth.isAuthenticated()) {
|
if (!this.auth.isAuthenticated()) {
|
||||||
throw new Error("User must be authenticated to retrieve logs");
|
throw new Error("User must be authenticated to retrieve logs");
|
||||||
}
|
}
|
||||||
|
@ -150,14 +170,17 @@ export class SendLog {
|
||||||
throw new Error("No user ID available");
|
throw new Error("No user ID available");
|
||||||
}
|
}
|
||||||
|
|
||||||
let filter = `user_id = "${userId}"`;
|
let filter = `user = "${userId}"`;
|
||||||
if (type) filter += ` && type = "${type}"`;
|
if (type) filter += ` && type = "${type}"`;
|
||||||
if (part) filter += ` && part = "${part}"`;
|
if (part) filter += ` && part = "${part}"`;
|
||||||
|
|
||||||
const result = await this.auth.getPocketBase().collection(this.COLLECTION_NAME).getList<LogData>(1, limit, {
|
const result = await this.auth
|
||||||
filter,
|
.getPocketBase()
|
||||||
sort: "-created"
|
.collection(this.COLLECTION_NAME)
|
||||||
});
|
.getList<Log>(1, limit, {
|
||||||
|
filter,
|
||||||
|
sort: "-created",
|
||||||
|
});
|
||||||
|
|
||||||
return result.items;
|
return result.items;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
@ -165,4 +188,4 @@ export class SendLog {
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue