fix database fetching
This commit is contained in:
parent
10d3f32fbc
commit
733f3dc931
6 changed files with 274 additions and 84 deletions
|
@ -7,7 +7,7 @@ import { DataSyncService } from "../../../scripts/database/DataSyncService";
|
||||||
import { Collections } from "../../../schemas/pocketbase/schema";
|
import { Collections } from "../../../schemas/pocketbase/schema";
|
||||||
import { Icon } from "@iconify/react";
|
import { Icon } from "@iconify/react";
|
||||||
import toast from "react-hot-toast";
|
import toast from "react-hot-toast";
|
||||||
import type { Event, EventAttendee } from "../../../schemas/pocketbase";
|
import type { Event, EventAttendee, LimitedUser } from "../../../schemas/pocketbase";
|
||||||
|
|
||||||
// Extended Event interface with additional properties needed for this component
|
// Extended Event interface with additional properties needed for this component
|
||||||
interface ExtendedEvent extends Event {
|
interface ExtendedEvent extends Event {
|
||||||
|
@ -264,20 +264,48 @@ const EventCheckIn = () => {
|
||||||
totalPoints += attendee.points_earned || 0;
|
totalPoints += attendee.points_earned || 0;
|
||||||
});
|
});
|
||||||
|
|
||||||
// Log the points update
|
// Update the LimitedUser record with the new points total
|
||||||
// console.log(`Updating user points to: ${totalPoints}`);
|
try {
|
||||||
|
// Try to get the LimitedUser record to check if it exists
|
||||||
|
let limitedUserExists = false;
|
||||||
|
try {
|
||||||
|
const limitedUser = await get.getOne(Collections.LIMITED_USERS, userId);
|
||||||
|
limitedUserExists = !!limitedUser;
|
||||||
|
} catch (e) {
|
||||||
|
// Record doesn't exist
|
||||||
|
limitedUserExists = false;
|
||||||
|
}
|
||||||
|
|
||||||
// Update the user record with the new total points
|
// Create or update the LimitedUser record
|
||||||
await update.updateFields(Collections.USERS, userId, {
|
if (limitedUserExists) {
|
||||||
points: totalPoints
|
await update.updateFields(Collections.LIMITED_USERS, userId, {
|
||||||
});
|
points: JSON.stringify(totalPoints),
|
||||||
|
total_events_attended: JSON.stringify(userAttendance.totalItems)
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// Get user data to create LimitedUser record
|
||||||
|
const userData = await get.getOne(Collections.USERS, userId);
|
||||||
|
if (userData) {
|
||||||
|
await update.create(Collections.LIMITED_USERS, {
|
||||||
|
id: userId, // Use same ID as user record
|
||||||
|
name: userData.name || 'Anonymous User',
|
||||||
|
major: userData.major || '',
|
||||||
|
points: JSON.stringify(totalPoints),
|
||||||
|
total_events_attended: JSON.stringify(userAttendance.totalItems)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to update LimitedUser record:', error);
|
||||||
|
}
|
||||||
|
|
||||||
// Ensure local data is in sync with backend
|
// Ensure local data is in sync with backend
|
||||||
// First sync the new attendance record
|
// First sync the new attendance record
|
||||||
await dataSync.syncCollection(Collections.EVENT_ATTENDEES);
|
await dataSync.syncCollection(Collections.EVENT_ATTENDEES);
|
||||||
|
|
||||||
// Then sync the updated user data to ensure points are correctly reflected locally
|
// Then sync the updated user and LimitedUser data
|
||||||
await dataSync.syncCollection(Collections.USERS);
|
await dataSync.syncCollection(Collections.USERS);
|
||||||
|
await dataSync.syncCollection(Collections.LIMITED_USERS);
|
||||||
|
|
||||||
// Clear event code from local storage
|
// Clear event code from local storage
|
||||||
await dataSync.clearEventCode();
|
await dataSync.clearEventCode();
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
import { useState, useEffect } from 'react';
|
import { useState, useEffect } 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 { Collections } from '../../../schemas/pocketbase/schema';
|
||||||
|
import type { LimitedUser } from '../../../schemas/pocketbase/schema';
|
||||||
|
|
||||||
interface LeaderboardStats {
|
interface LeaderboardStats {
|
||||||
totalUsers: number;
|
totalUsers: number;
|
||||||
|
@ -54,34 +56,50 @@ export default function LeaderboardStats() {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
|
|
||||||
// Get all users without sorting - we'll sort on client side
|
// Get all users without sorting - we'll sort on client side
|
||||||
const response = await get.getList('limitedUser', 1, 500, '', '', {
|
const response = await get.getList(Collections.LIMITED_USERS, 1, 500, '', '', {
|
||||||
fields: ['id', 'name', 'points']
|
fields: ['id', 'name', 'points']
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Parse points from JSON string and convert to number
|
||||||
|
const processedUsers = response.items.map((user: Partial<LimitedUser>) => {
|
||||||
|
let pointsValue = 0;
|
||||||
|
try {
|
||||||
|
if (user.points) {
|
||||||
|
// Parse the JSON string to get the points value
|
||||||
|
const pointsData = JSON.parse(user.points);
|
||||||
|
pointsValue = typeof pointsData === 'number' ? pointsData : 0;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Error parsing points data:', e);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: user.id,
|
||||||
|
name: user.name,
|
||||||
|
parsedPoints: pointsValue
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
// Filter out users with no points for the leaderboard stats
|
// Filter out users with no points for the leaderboard stats
|
||||||
const leaderboardUsers = response.items
|
const leaderboardUsers = processedUsers
|
||||||
.filter((user: any) =>
|
.filter(user => user.parsedPoints > 0)
|
||||||
user.points !== undefined &&
|
|
||||||
user.points !== null &&
|
|
||||||
user.points > 0
|
|
||||||
)
|
|
||||||
// Sort by points descending
|
// Sort by points descending
|
||||||
.sort((a: any, b: any) => b.points - a.points);
|
.sort((a, b) => b.parsedPoints - a.parsedPoints);
|
||||||
|
|
||||||
const totalUsers = leaderboardUsers.length;
|
const totalUsers = leaderboardUsers.length;
|
||||||
const totalPoints = leaderboardUsers.reduce((sum: number, user: any) => sum + (user.points || 0), 0);
|
const totalPoints = leaderboardUsers.reduce((sum: number, user) => sum + user.parsedPoints, 0);
|
||||||
const topScore = leaderboardUsers.length > 0 ? leaderboardUsers[0].points : 0;
|
const topScore = leaderboardUsers.length > 0 ? leaderboardUsers[0].parsedPoints : 0;
|
||||||
|
|
||||||
// Find current user's points and rank - BUT don't filter by points > 0 for the current user
|
// Find current user's points and rank - BUT don't filter by points > 0 for the current user
|
||||||
let yourPoints = 0;
|
let yourPoints = 0;
|
||||||
let yourRank = null;
|
let yourRank = null;
|
||||||
|
|
||||||
if (isAuthenticated && currentUserId) {
|
if (isAuthenticated && currentUserId) {
|
||||||
// Look for the current user in ALL users, not just those with points > 0
|
// Look for the current user in ALL processed users, not just those with points > 0
|
||||||
const currentUser = response.items.find((user: any) => user.id === currentUserId);
|
const currentUser = processedUsers.find(user => user.id === currentUserId);
|
||||||
|
|
||||||
if (currentUser) {
|
if (currentUser) {
|
||||||
yourPoints = currentUser.points || 0;
|
yourPoints = currentUser.parsedPoints || 0;
|
||||||
|
|
||||||
// Only calculate rank if user has points
|
// Only calculate rank if user has points
|
||||||
if (yourPoints > 0) {
|
if (yourPoints > 0) {
|
||||||
|
@ -119,7 +137,7 @@ export default function LeaderboardStats() {
|
||||||
};
|
};
|
||||||
|
|
||||||
fetchStats();
|
fetchStats();
|
||||||
}, [isAuthenticated, currentUserId]);
|
}, [get, isAuthenticated, currentUserId]);
|
||||||
|
|
||||||
if (loading) {
|
if (loading) {
|
||||||
return (
|
return (
|
||||||
|
|
|
@ -1,7 +1,8 @@
|
||||||
import { useState, useEffect } from 'react';
|
import { useState, useEffect } 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 { User } from '../../../schemas/pocketbase/schema';
|
import type { User, LimitedUser } from '../../../schemas/pocketbase/schema';
|
||||||
|
import { Collections } from '../../../schemas/pocketbase/schema';
|
||||||
|
|
||||||
interface LeaderboardUser {
|
interface LeaderboardUser {
|
||||||
id: string;
|
id: string;
|
||||||
|
@ -63,21 +64,44 @@ export default function LeaderboardTable() {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
|
|
||||||
// Fetch users without sorting - we'll sort on client side
|
// Fetch users without sorting - we'll sort on client side
|
||||||
const response = await get.getList('limitedUser', 1, 100, '', '', {
|
const response = await get.getList(Collections.LIMITED_USERS, 1, 100, '', '', {
|
||||||
fields: ['id', 'name', 'points', 'avatar', 'major']
|
fields: ['id', 'name', 'points', 'avatar', 'major']
|
||||||
});
|
});
|
||||||
|
|
||||||
// First get the current user separately so we can include them even if they have 0 points
|
// First get the current user separately so we can include them even if they have 0 points
|
||||||
let currentUserData = null;
|
let currentUserData = null;
|
||||||
if (isAuthenticated && currentUserId) {
|
if (isAuthenticated && currentUserId) {
|
||||||
currentUserData = response.items.find((user: Partial<User>) => user.id === currentUserId);
|
currentUserData = response.items.find((user: Partial<LimitedUser>) => user.id === currentUserId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Parse points from JSON string and convert to number
|
||||||
|
const processedUsers = response.items.map((user: any) => {
|
||||||
|
let pointsValue = 0;
|
||||||
|
try {
|
||||||
|
if (user.points) {
|
||||||
|
// Parse the JSON string to get the points value
|
||||||
|
const pointsData = JSON.parse(user.points);
|
||||||
|
pointsValue = typeof pointsData === 'number' ? pointsData : 0;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Error parsing points data:', e);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: user.id,
|
||||||
|
name: user.name,
|
||||||
|
major: user.major,
|
||||||
|
avatar: user.avatar, // Include avatar if it exists
|
||||||
|
points: user.points,
|
||||||
|
parsedPoints: pointsValue
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
// Filter and map to our leaderboard user format, and sort client-side
|
// Filter and map to our leaderboard user format, and sort client-side
|
||||||
let leaderboardUsers = response.items
|
let leaderboardUsers = processedUsers
|
||||||
.filter((user: Partial<User>) => user.points !== undefined && user.points !== null && user.points > 0)
|
.filter(user => user.parsedPoints > 0)
|
||||||
.sort((a: Partial<User>, b: Partial<User>) => (b.points || 0) - (a.points || 0))
|
.sort((a, b) => (b.parsedPoints || 0) - (a.parsedPoints || 0))
|
||||||
.map((user: Partial<User>, index: number) => {
|
.map((user, index: number) => {
|
||||||
// Check if this is the current user
|
// Check if this is the current user
|
||||||
if (isAuthenticated && user.id === currentUserId) {
|
if (isAuthenticated && user.id === currentUserId) {
|
||||||
setCurrentUserRank(index + 1);
|
setCurrentUserRank(index + 1);
|
||||||
|
@ -86,7 +110,7 @@ export default function LeaderboardTable() {
|
||||||
return {
|
return {
|
||||||
id: user.id || '',
|
id: user.id || '',
|
||||||
name: user.name || 'Anonymous User',
|
name: user.name || 'Anonymous User',
|
||||||
points: user.points || 0,
|
points: user.parsedPoints,
|
||||||
avatar: user.avatar,
|
avatar: user.avatar,
|
||||||
major: user.major
|
major: user.major
|
||||||
};
|
};
|
||||||
|
@ -94,16 +118,20 @@ export default function LeaderboardTable() {
|
||||||
|
|
||||||
// Include current user even if they have 0 points,
|
// Include current user even if they have 0 points,
|
||||||
// but don't include in ranking if they have no points
|
// but don't include in ranking if they have no points
|
||||||
if (isAuthenticated && currentUserData &&
|
if (isAuthenticated && currentUserId) {
|
||||||
!leaderboardUsers.some(user => user.id === currentUserId)) {
|
// Find current user in processed users
|
||||||
// User isn't already in the list (has 0 points)
|
const currentUserProcessed = processedUsers.find(user => user.id === currentUserId);
|
||||||
leaderboardUsers.push({
|
|
||||||
id: currentUserData.id || '',
|
// If current user exists and isn't already in the leaderboard (has 0 points)
|
||||||
name: currentUserData.name || 'Anonymous User',
|
if (currentUserProcessed && !leaderboardUsers.some(user => user.id === currentUserId)) {
|
||||||
points: currentUserData.points || 0,
|
leaderboardUsers.push({
|
||||||
avatar: currentUserData.avatar,
|
id: currentUserProcessed.id || '',
|
||||||
major: currentUserData.major
|
name: currentUserProcessed.name || 'Anonymous User',
|
||||||
});
|
points: currentUserProcessed.parsedPoints || 0,
|
||||||
|
avatar: currentUserProcessed.avatar,
|
||||||
|
major: currentUserProcessed.major
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
setUsers(leaderboardUsers);
|
setUsers(leaderboardUsers);
|
||||||
|
@ -117,7 +145,7 @@ export default function LeaderboardTable() {
|
||||||
};
|
};
|
||||||
|
|
||||||
fetchLeaderboard();
|
fetchLeaderboard();
|
||||||
}, [isAuthenticated, currentUserId]);
|
}, [get, isAuthenticated, currentUserId]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (searchQuery.trim() === '') {
|
if (searchQuery.trim() === '') {
|
||||||
|
|
|
@ -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 { SendLog } from "../../../scripts/pocketbase/SendLog";
|
import { SendLog } from "../../../scripts/pocketbase/SendLog";
|
||||||
|
import { Realtime } from "../../../scripts/pocketbase/Realtime";
|
||||||
import FilePreview from "../universal/FilePreview";
|
import FilePreview from "../universal/FilePreview";
|
||||||
import type { Event as SchemaEvent, AttendeeEntry } from "../../../schemas/pocketbase";
|
import type { Event as SchemaEvent, AttendeeEntry } from "../../../schemas/pocketbase";
|
||||||
import { DataSyncService } from '../../../scripts/database/DataSyncService';
|
import { DataSyncService } from '../../../scripts/database/DataSyncService';
|
||||||
|
@ -240,7 +241,15 @@ const EventForm = memo(({
|
||||||
// Show error for rejected files
|
// Show error for rejected files
|
||||||
if (rejectedFiles.length > 0) {
|
if (rejectedFiles.length > 0) {
|
||||||
const errorMessage = `The following files were not added:\n${rejectedFiles.map(f => `${f.name}: ${f.reason}`).join('\n')}`;
|
const errorMessage = `The following files were not added:\n${rejectedFiles.map(f => `${f.name}: ${f.reason}`).join('\n')}`;
|
||||||
toast.error(errorMessage);
|
// Use toast with custom styling to ensure visibility above modal
|
||||||
|
toast.error(errorMessage, {
|
||||||
|
duration: 5000,
|
||||||
|
style: {
|
||||||
|
zIndex: 9999, // Ensure it's above the modal
|
||||||
|
maxWidth: '500px',
|
||||||
|
whiteSpace: 'pre-line' // Preserve line breaks
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
setSelectedFiles(newFiles);
|
setSelectedFiles(newFiles);
|
||||||
|
@ -293,6 +302,31 @@ const EventForm = memo(({
|
||||||
>
|
>
|
||||||
<Icon icon="heroicons:eye" className="h-4 w-4" />
|
<Icon icon="heroicons:eye" className="h-4 w-4" />
|
||||||
</button>
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="btn btn-ghost btn-xs"
|
||||||
|
onClick={async () => {
|
||||||
|
if (event?.id) {
|
||||||
|
try {
|
||||||
|
// Get file URL with token for protected files
|
||||||
|
const url = await fileManager.getFileUrlWithToken(
|
||||||
|
"events",
|
||||||
|
event.id,
|
||||||
|
filename,
|
||||||
|
true
|
||||||
|
);
|
||||||
|
|
||||||
|
// Open file in new tab
|
||||||
|
window.open(url, '_blank');
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to open file:", error);
|
||||||
|
toast.error("Failed to open file. Please try again.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Icon icon="heroicons:arrow-top-right-on-square" className="h-4 w-4" />
|
||||||
|
</button>
|
||||||
<div className="text-error">
|
<div className="text-error">
|
||||||
{filesToDelete.has(filename) ? (
|
{filesToDelete.has(filename) ? (
|
||||||
<button
|
<button
|
||||||
|
@ -571,7 +605,8 @@ export default function EventEditor({ onEventSaved }: EventEditorProps) {
|
||||||
auth: Authentication.getInstance(),
|
auth: Authentication.getInstance(),
|
||||||
update: Update.getInstance(),
|
update: Update.getInstance(),
|
||||||
fileManager: FileManager.getInstance(),
|
fileManager: FileManager.getInstance(),
|
||||||
sendLog: SendLog.getInstance()
|
sendLog: SendLog.getInstance(),
|
||||||
|
realtime: Realtime.getInstance()
|
||||||
}), []);
|
}), []);
|
||||||
|
|
||||||
// Handle field changes
|
// Handle field changes
|
||||||
|
@ -590,17 +625,35 @@ export default function EventEditor({ onEventSaved }: EventEditorProps) {
|
||||||
const initializeEventData = useCallback(async (eventId: string) => {
|
const initializeEventData = useCallback(async (eventId: string) => {
|
||||||
try {
|
try {
|
||||||
if (eventId) {
|
if (eventId) {
|
||||||
|
// Show loading state
|
||||||
|
setIsSubmitting(true);
|
||||||
|
|
||||||
// Clear cache to ensure fresh data
|
// Clear cache to ensure fresh data
|
||||||
const dataSync = DataSyncService.getInstance();
|
const dataSync = DataSyncService.getInstance();
|
||||||
await dataSync.clearCache();
|
await dataSync.clearCache();
|
||||||
|
|
||||||
// Fetch fresh event data
|
// Fetch fresh event data with expanded relations if needed
|
||||||
const eventData = await services.get.getOne<Event>(Collections.EVENTS, eventId);
|
const eventData = await services.get.getOne<Event>(
|
||||||
|
Collections.EVENTS,
|
||||||
|
eventId,
|
||||||
|
{
|
||||||
|
disableAutoCancellation: true,
|
||||||
|
// Add any fields to expand if needed
|
||||||
|
// expand: ['related_field1', 'related_field2']
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
if (!eventData) {
|
if (!eventData) {
|
||||||
throw new Error("Event not found");
|
throw new Error("Event not found");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Log successful data fetch
|
||||||
|
await services.sendLog.send(
|
||||||
|
"view",
|
||||||
|
"event",
|
||||||
|
`Loaded event data: ${eventData.event_name} (${eventId})`
|
||||||
|
);
|
||||||
|
|
||||||
// Ensure dates are properly formatted for datetime-local input
|
// Ensure dates are properly formatted for datetime-local input
|
||||||
if (eventData.start_date) {
|
if (eventData.start_date) {
|
||||||
// Convert to Date object first to ensure proper formatting
|
// Convert to Date object first to ensure proper formatting
|
||||||
|
@ -631,8 +684,36 @@ export default function EventEditor({ onEventSaved }: EventEditorProps) {
|
||||||
has_food: eventData.has_food || false
|
has_food: eventData.has_food || false
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Set up realtime subscription for this event
|
||||||
|
const realtime = services.realtime;
|
||||||
|
|
||||||
|
// Define the RealtimeEvent type for proper typing
|
||||||
|
interface RealtimeEvent<T> {
|
||||||
|
action: "create" | "update" | "delete";
|
||||||
|
record: T;
|
||||||
|
}
|
||||||
|
|
||||||
|
const subscriptionId = realtime.subscribeToRecord<RealtimeEvent<Event>>(
|
||||||
|
Collections.EVENTS,
|
||||||
|
eventId,
|
||||||
|
(data) => {
|
||||||
|
if (data.action === "update") {
|
||||||
|
// Auto-refresh data when event is updated elsewhere
|
||||||
|
initializeEventData(eventId);
|
||||||
|
toast.success("Event data has been updated");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// Store subscription ID for cleanup
|
||||||
|
(window as any).eventSubscriptionId = subscriptionId;
|
||||||
|
|
||||||
// console.log("Event data loaded successfully:", eventData);
|
// console.log("Event data loaded successfully:", eventData);
|
||||||
} else {
|
} else {
|
||||||
|
// Creating a new event
|
||||||
|
const now = new Date();
|
||||||
|
const oneHourLater = new Date(now.getTime() + 60 * 60 * 1000);
|
||||||
|
|
||||||
setEvent({
|
setEvent({
|
||||||
id: '',
|
id: '',
|
||||||
created: '',
|
created: '',
|
||||||
|
@ -643,8 +724,8 @@ export default function EventEditor({ onEventSaved }: EventEditorProps) {
|
||||||
location: '',
|
location: '',
|
||||||
files: [],
|
files: [],
|
||||||
points_to_reward: 0,
|
points_to_reward: 0,
|
||||||
start_date: '',
|
start_date: Get.formatLocalDate(now, false),
|
||||||
end_date: '',
|
end_date: Get.formatLocalDate(oneHourLater, false),
|
||||||
published: false,
|
published: false,
|
||||||
has_food: false
|
has_food: false
|
||||||
});
|
});
|
||||||
|
@ -656,8 +737,10 @@ export default function EventEditor({ onEventSaved }: EventEditorProps) {
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Failed to initialize event data:", error);
|
console.error("Failed to initialize event data:", error);
|
||||||
toast.error("Failed to load event data. Please try again.");
|
toast.error("Failed to load event data. Please try again.");
|
||||||
|
} finally {
|
||||||
|
setIsSubmitting(false);
|
||||||
}
|
}
|
||||||
}, [services.get]);
|
}, [services]);
|
||||||
|
|
||||||
// Expose initializeEventData to window
|
// Expose initializeEventData to window
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
@ -698,6 +781,12 @@ export default function EventEditor({ onEventSaved }: EventEditorProps) {
|
||||||
if (!confirmed) return;
|
if (!confirmed) return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Clean up realtime subscription if it exists
|
||||||
|
if ((window as any).eventSubscriptionId) {
|
||||||
|
services.realtime.unsubscribe((window as any).eventSubscriptionId);
|
||||||
|
delete (window as any).eventSubscriptionId;
|
||||||
|
}
|
||||||
|
|
||||||
setEvent({
|
setEvent({
|
||||||
id: "",
|
id: "",
|
||||||
created: "",
|
created: "",
|
||||||
|
@ -719,12 +808,24 @@ export default function EventEditor({ onEventSaved }: EventEditorProps) {
|
||||||
setPreviewUrl("");
|
setPreviewUrl("");
|
||||||
setPreviewFilename("");
|
setPreviewFilename("");
|
||||||
|
|
||||||
|
// Clear file input element to reset filename display
|
||||||
|
const fileInput = document.querySelector('input[name="editEventFiles"]') as HTMLInputElement;
|
||||||
|
if (fileInput) {
|
||||||
|
fileInput.value = "";
|
||||||
|
}
|
||||||
|
|
||||||
const modal = document.getElementById("editEventModal") as HTMLDialogElement;
|
const modal = document.getElementById("editEventModal") as HTMLDialogElement;
|
||||||
if (modal) modal.close();
|
if (modal) modal.close();
|
||||||
}, [hasUnsavedChanges, isSubmitting]);
|
}, [hasUnsavedChanges, isSubmitting, services.realtime]);
|
||||||
|
|
||||||
// Function to close modal after saving (without confirmation)
|
// Function to close modal after saving (without confirmation)
|
||||||
const closeModalAfterSave = useCallback(() => {
|
const closeModalAfterSave = useCallback(() => {
|
||||||
|
// Clean up realtime subscription if it exists
|
||||||
|
if ((window as any).eventSubscriptionId) {
|
||||||
|
services.realtime.unsubscribe((window as any).eventSubscriptionId);
|
||||||
|
delete (window as any).eventSubscriptionId;
|
||||||
|
}
|
||||||
|
|
||||||
setEvent({
|
setEvent({
|
||||||
id: "",
|
id: "",
|
||||||
created: "",
|
created: "",
|
||||||
|
@ -746,9 +847,15 @@ export default function EventEditor({ onEventSaved }: EventEditorProps) {
|
||||||
setPreviewUrl("");
|
setPreviewUrl("");
|
||||||
setPreviewFilename("");
|
setPreviewFilename("");
|
||||||
|
|
||||||
|
// Reset the file input element to clear the filename display
|
||||||
|
const fileInput = document.querySelector('input[name="editEventFiles"]') as HTMLInputElement;
|
||||||
|
if (fileInput) {
|
||||||
|
fileInput.value = "";
|
||||||
|
}
|
||||||
|
|
||||||
const modal = document.getElementById("editEventModal") as HTMLDialogElement;
|
const modal = document.getElementById("editEventModal") as HTMLDialogElement;
|
||||||
if (modal) modal.close();
|
if (modal) modal.close();
|
||||||
}, []);
|
}, [services.realtime]);
|
||||||
|
|
||||||
const handleSubmit = useCallback(async (e: React.FormEvent<HTMLFormElement>) => {
|
const handleSubmit = useCallback(async (e: React.FormEvent<HTMLFormElement>) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
|
@ -1,14 +1,13 @@
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { Authentication } from "../../../scripts/pocketbase/Authentication";
|
import { Authentication } from "../../../scripts/pocketbase/Authentication";
|
||||||
import { Collections } from "../../../schemas/pocketbase/schema";
|
import { Collections } from "../../../schemas/pocketbase/schema";
|
||||||
import type { Event, User } from "../../../schemas/pocketbase";
|
import type { Event, User, LimitedUser } from "../../../schemas/pocketbase";
|
||||||
import { Get } from "../../../scripts/pocketbase/Get";
|
import { Get } from "../../../scripts/pocketbase/Get";
|
||||||
import type { EventAttendee } from "../../../schemas/pocketbase";
|
import type { EventAttendee } from "../../../schemas/pocketbase";
|
||||||
import { Update } from "../../../scripts/pocketbase/Update";
|
import { Update } from "../../../scripts/pocketbase/Update";
|
||||||
|
|
||||||
// Extended User interface with points property
|
// Extended User interface with member_type property
|
||||||
interface ExtendedUser extends User {
|
interface ExtendedUser extends User {
|
||||||
points?: number;
|
|
||||||
member_type?: string;
|
member_type?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -83,47 +82,44 @@ export function Stats() {
|
||||||
|
|
||||||
setEventsAttended(attendedEvents.totalItems);
|
setEventsAttended(attendedEvents.totalItems);
|
||||||
|
|
||||||
// Get user points - either from the user record or calculate from attendees
|
// Calculate points from attendees
|
||||||
let totalPoints = 0;
|
let totalPoints = 0;
|
||||||
|
|
||||||
// Calculate quarterly points
|
// Calculate quarterly points
|
||||||
const quarterStartDate = getCurrentQuarterStartDate();
|
const quarterStartDate = getCurrentQuarterStartDate();
|
||||||
let pointsThisQuarter = 0;
|
let pointsThisQuarter = 0;
|
||||||
|
|
||||||
// If user has points field, use that for total points
|
// Calculate both total and quarterly points from attendees
|
||||||
if (currentUser && currentUser.points !== undefined) {
|
attendedEvents.items.forEach(attendee => {
|
||||||
totalPoints = currentUser.points;
|
const points = attendee.points_earned || 0;
|
||||||
|
totalPoints += points;
|
||||||
|
|
||||||
// Still need to calculate quarterly points from attendees
|
const checkinDate = new Date(attendee.time_checked_in);
|
||||||
attendedEvents.items.forEach(attendee => {
|
if (checkinDate >= quarterStartDate) {
|
||||||
const checkinDate = new Date(attendee.time_checked_in);
|
pointsThisQuarter += points;
|
||||||
if (checkinDate >= quarterStartDate) {
|
}
|
||||||
pointsThisQuarter += attendee.points_earned || 0;
|
});
|
||||||
}
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
// Calculate both total and quarterly points from attendees
|
|
||||||
attendedEvents.items.forEach(attendee => {
|
|
||||||
const points = attendee.points_earned || 0;
|
|
||||||
totalPoints += points;
|
|
||||||
|
|
||||||
const checkinDate = new Date(attendee.time_checked_in);
|
// Try to get the LimitedUser record to check if points match
|
||||||
if (checkinDate >= quarterStartDate) {
|
try {
|
||||||
pointsThisQuarter += points;
|
const limitedUserRecord = await get.getOne(
|
||||||
}
|
Collections.LIMITED_USERS,
|
||||||
});
|
userId
|
||||||
|
);
|
||||||
|
|
||||||
// Update the user record with calculated points if needed
|
if (limitedUserRecord && limitedUserRecord.points) {
|
||||||
if (currentUser) {
|
|
||||||
try {
|
try {
|
||||||
const update = Update.getInstance();
|
// Parse the points JSON string
|
||||||
await update.updateFields(Collections.USERS, currentUser.id, {
|
const parsedPoints = JSON.parse(limitedUserRecord.points);
|
||||||
points: totalPoints
|
if (parsedPoints !== totalPoints) {
|
||||||
});
|
console.log(`Points mismatch: LimitedUser has ${parsedPoints}, calculated ${totalPoints}`);
|
||||||
} catch (error) {
|
}
|
||||||
console.error("Error updating user points:", error);
|
} catch (e) {
|
||||||
|
console.error('Error parsing points from LimitedUser:', e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} catch (e) {
|
||||||
|
// LimitedUser record might not exist yet, that's okay
|
||||||
}
|
}
|
||||||
|
|
||||||
setPointsEarned(totalPoints);
|
setPointsEarned(totalPoints);
|
||||||
|
|
|
@ -32,7 +32,7 @@ export interface User extends BaseRecord {
|
||||||
major?: string;
|
major?: string;
|
||||||
zelle_information?: string;
|
zelle_information?: string;
|
||||||
last_login?: string;
|
last_login?: string;
|
||||||
points?: number; // Total points earned from events
|
// points?: number; // Total points earned from events (DEPRECATED)
|
||||||
notification_preferences?: string; // JSON string of notification settings
|
notification_preferences?: string; // JSON string of notification settings
|
||||||
display_preferences?: string; // JSON string of display settings (theme, font size, etc.)
|
display_preferences?: string; // JSON string of display settings (theme, font size, etc.)
|
||||||
accessibility_settings?: string; // JSON string of accessibility settings (color blind mode, reduced motion)
|
accessibility_settings?: string; // JSON string of accessibility settings (color blind mode, reduced motion)
|
||||||
|
@ -41,6 +41,18 @@ export interface User extends BaseRecord {
|
||||||
requested_email?: boolean; // Whether the user has requested an IEEE email address
|
requested_email?: boolean; // Whether the user has requested an IEEE email address
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Limited User Collection
|
||||||
|
* Represents limited user information for public display
|
||||||
|
* Collection ID: pbc_2802685943
|
||||||
|
*/
|
||||||
|
export interface LimitedUser extends BaseRecord {
|
||||||
|
name: string;
|
||||||
|
major: string;
|
||||||
|
points: string; // JSON string
|
||||||
|
total_events_attended: string; // JSON string
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Events Collection
|
* Events Collection
|
||||||
* Represents events created in the system
|
* Represents events created in the system
|
||||||
|
@ -96,7 +108,7 @@ export interface EventRequest extends BaseRecord {
|
||||||
event_description: string;
|
event_description: string;
|
||||||
flyers_needed: boolean;
|
flyers_needed: boolean;
|
||||||
flyer_type?: string[]; // digital_with_social, digital_no_social, physical_with_advertising, physical_no_advertising, newsletter, other
|
flyer_type?: string[]; // digital_with_social, digital_no_social, physical_with_advertising, physical_no_advertising, newsletter, other
|
||||||
other_flyer_type?: string;
|
other_flyer_type?: string;
|
||||||
flyer_advertising_start_date?: string;
|
flyer_advertising_start_date?: string;
|
||||||
flyer_additional_requests?: string;
|
flyer_additional_requests?: string;
|
||||||
photography_needed: boolean;
|
photography_needed: boolean;
|
||||||
|
@ -217,6 +229,7 @@ export const Collections = {
|
||||||
REIMBURSEMENTS: "reimbursement",
|
REIMBURSEMENTS: "reimbursement",
|
||||||
RECEIPTS: "receipts",
|
RECEIPTS: "receipts",
|
||||||
SPONSORS: "sponsors",
|
SPONSORS: "sponsors",
|
||||||
|
LIMITED_USERS: "limitedUser",
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
Loading…
Reference in a new issue