update the files using the appropriate schema

This commit is contained in:
chark1es 2025-02-28 18:27:59 -08:00
parent f4db576400
commit 701854e633
24 changed files with 222 additions and 398 deletions

View file

@ -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";

View file

@ -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,

View file

@ -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" />

View file

@ -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"
> >

View file

@ -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;

View file

@ -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;
} }

View file

@ -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";

View file

@ -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 = {

View file

@ -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 = {

View file

@ -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 = () => {

View file

@ -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

View file

@ -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 = {

View file

@ -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 = {

View file

@ -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;
} }

View file

@ -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;
} }
--- ---

View file

@ -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>

View file

@ -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);
}; };

View file

@ -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;

View file

@ -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);

View file

@ -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],

View file

@ -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';

View file

@ -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;

View file

@ -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);

View file

@ -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;
} }
} }
} }