diff --git a/shpe-app-web/app/(main)/membership/page.tsx b/shpe-app-web/app/(main)/membership/page.tsx
index 0f007882..51beddef 100644
--- a/shpe-app-web/app/(main)/membership/page.tsx
+++ b/shpe-app-web/app/(main)/membership/page.tsx
@@ -3,6 +3,7 @@ import { useEffect, useState } from 'react';
import { useRouter } from 'next/navigation';
import Header from '@/components/Header';
import { getMembers, getMembersToVerify } from '@/api/firebaseUtils';
+import { FaSync } from "react-icons/fa";
import { SHPEEventLog } from '@/types/events';
import { User } from '@/types/user';
import { isMemberVerified, RequestWithDoc } from '@/types/membership';
@@ -27,19 +28,71 @@ const Membership = () => {
const fetchMembers = async () => {
setLoading(true);
- const response = await getMembers();
- setStudents(response);
- const filteredMembers = response.filter((member) => {
- console.log(member.publicInfo?.chapterExpiration);
- return isMemberVerified(member.publicInfo?.chapterExpiration, member.publicInfo?.nationalExpiration);
- });
- setMembers(filteredMembers);
+ try {
+ const response = await getMembers();
+ setStudents(response);
+
+ const filteredMembers = response.filter((member) => {
+ console.log(member.publicInfo?.chapterExpiration);
+ return isMemberVerified(
+ member.publicInfo?.chapterExpiration,
+ member.publicInfo?.nationalExpiration
+ );
+ });
+ setMembers(filteredMembers);
+
+ localStorage.setItem('cachedMembers', JSON.stringify(response));
+ localStorage.setItem('cachedOfficialMembers', JSON.stringify(filteredMembers));
+ localStorage.setItem('cachedMembersTimestamp', Date.now().toString());
- const incomingReqs = await getMembersToVerify();
- setRequestsWithDocuments(incomingReqs);
- setLoading(false);
+ const incomingReqs = await getMembersToVerify();
+ setRequestsWithDocuments(incomingReqs);
+ localStorage.setItem('cachedRequests', JSON.stringify(incomingReqs));
+ localStorage.setItem('cachedRequestsTimestamp', Date.now().toString());
+
+ } catch (error) {
+ console.error('Error fetching members:', error);
+ } finally {
+ setLoading(false);
+ }
};
+ const checkCacheAndFetchMembers = () => {
+ const cachedMembers = localStorage.getItem('cachedMembers');
+ const cachedOfficialMembers = localStorage.getItem('cachedOfficialMembers');
+ const cachedMembersTimestamp = localStorage.getItem('cachedMembersTimestamp');
+ const cachedRequests = localStorage.getItem('cachedRequests');
+ const cachedRequestsTimestamp = localStorage.getItem('cachedRequestsTimestamp');
+
+ const isCacheValid = (timestamp: string): boolean => {
+ return Date.now() - parseInt(timestamp, 10) < 24 * 60 * 60 * 1000;
+ };
+
+ if (
+ cachedMembers &&
+ cachedOfficialMembers &&
+ cachedMembersTimestamp &&
+ isCacheValid(cachedMembersTimestamp) &&
+ cachedRequests &&
+ cachedRequestsTimestamp &&
+ isCacheValid(cachedRequestsTimestamp)
+ ) {
+ const studentsData = JSON.parse(cachedMembers);
+ setStudents(studentsData);
+
+ setMembers(JSON.parse(cachedOfficialMembers));
+
+ setRequestsWithDocuments(JSON.parse(cachedRequests));
+ setLoading(false);
+ } else {
+ fetchMembers();
+ }
+ };
+
+ useEffect(() => {
+ checkCacheAndFetchMembers();
+ }, []);
+
const handleApprove = async (member: RequestWithDoc) => {
const userDocRef = doc(db, 'users', member.uid);
@@ -110,10 +163,12 @@ const Membership = () => {
}
};
- useEffect(() => {
- fetchMembers();
- setLoading(false);
- }, []);
+ const handleReload = async () => {
+ if (window.confirm("Are you sure you want to reload the members?")) {
+ setLoading(true);
+ await fetchMembers();
+ }
+ };
useEffect(() => {
const unsubscribe = onAuthStateChanged(auth, (currentUser) => {
@@ -142,7 +197,6 @@ const Membership = () => {
return (
-
);
};
diff --git a/shpe-app-web/app/(main)/points/page.tsx b/shpe-app-web/app/(main)/points/page.tsx
index 22461d5e..826c91aa 100644
--- a/shpe-app-web/app/(main)/points/page.tsx
+++ b/shpe-app-web/app/(main)/points/page.tsx
@@ -5,7 +5,7 @@ import { getEvents, getMembers, updatePointsInFirebase } from "@/api/firebaseUti
import { SHPEEvent, SHPEEventLog } from '@/types/events';
import { format } from 'date-fns';
import { User } from "@/types/user";
-import { FaChevronLeft, FaChevronRight, FaFilter, FaUndo, FaSave } from "react-icons/fa";
+import { FaChevronLeft, FaChevronRight, FaFilter, FaUndo, FaSave, FaSync } from "react-icons/fa";
import { httpsCallable } from "firebase/functions";
import { auth, functions } from "@/config/firebaseConfig";
import { onAuthStateChanged } from "firebase/auth";
@@ -35,51 +35,79 @@ const Points = () => {
const months = generateSchoolYearMonths();
const updateAllUserPoints = httpsCallable(functions, 'updateAllUserPoints');
- // Set the initial current month index to the real current month
const currentMonthDate = new Date();
const realCurrentMonthIndex = months.findIndex(
(month) => month.getFullYear() === currentMonthDate.getFullYear() && month.getMonth() === currentMonthDate.getMonth()
);
const [currentMonthIndex, setCurrentMonthIndex] = useState
(realCurrentMonthIndex !== -1 ? realCurrentMonthIndex : 0);
-
const fetchMembers = async () => {
- const response = await getMembers() as UserWithLogs[];
- setMembers(response);
-
- // Initialize the original points state
- const initialPoints = response.reduce((acc: PointsRecord, member) => {
- if (member.publicInfo?.uid && member.eventLogs) {
- member.eventLogs.forEach(log => {
- if (log.eventId) {
- acc[`${member.publicInfo?.uid}-${log.eventId}`] = log.points || 0;
- }
-
- // Calculate Instagram points per month
- if (log.instagramLogs) {
- months.forEach((month, index) => {
- const pointsForInstagram = calculateInstagramPoints(log.instagramLogs!, month);
- acc[`${member.publicInfo?.uid}-instagram-${index}`] = pointsForInstagram;
- });
- }
- });
- }
-
- return acc;
- }, {});
+ setLoading(true);
+ try {
+ const response = await getMembers() as UserWithLogs[];
+ setMembers(response);
+
+ const initialPoints = response.reduce((acc: PointsRecord, member) => {
+ if (member.publicInfo?.uid && member.eventLogs) {
+ member.eventLogs.forEach(log => {
+ if (log.eventId) {
+ acc[`${member.publicInfo?.uid}-${log.eventId}`] = log.points || 0;
+ }
+
+ if (log.instagramLogs) {
+ months.forEach((month, index) => {
+ const pointsForInstagram = calculateInstagramPoints(log.instagramLogs!, month);
+ acc[`${member.publicInfo?.uid}-instagram-${index}`] = pointsForInstagram;
+ });
+ }
+ });
+ }
+ return acc;
+ }, {});
- setOriginalPoints(initialPoints);
+ setOriginalPoints(initialPoints);
+ localStorage.setItem('cachedMembers', JSON.stringify(response));
+ localStorage.setItem('cachedPoints', JSON.stringify(initialPoints));
+ localStorage.setItem('cachedMembersTimestamp', Date.now().toString());
+ } catch (error) {
+ console.error('Error fetching members:', error);
+ } finally {
+ setLoading(false);
+ }
};
+ const checkCacheAndFetchMembers = () => {
+ const cachedMembers = localStorage.getItem('cachedMembers');
+ const cachedPoints = localStorage.getItem('cachedPoints');
+ const cachedTimestamp = localStorage.getItem('cachedMembersTimestamp');
+
+ const isCacheValid = (timestamp: string): boolean => {
+ return Date.now() - parseInt(timestamp, 10) < 24 * 60 * 60 * 1000;
+ };
+
+ if (cachedMembers && cachedPoints && cachedTimestamp && isCacheValid(cachedTimestamp)) {
+ const members = JSON.parse(cachedMembers);
+ const convertedMembers = convertMembersLogsToTimestamps(members);
+ setMembers(convertedMembers);
+ setOriginalPoints(JSON.parse(cachedPoints));
+ } else {
+ fetchMembers();
+ }
+ };
const fetchEvents = async () => {
- const response = await getEvents();
- setEvents(response);
+ try {
+ const response = await getEvents();
+ setEvents(response);
+ } catch (error) {
+ console.error('Error fetching events:', error);
+ }
};
useEffect(() => {
- fetchMembers();
+ checkCacheAndFetchMembers();
fetchEvents();
}, []);
+
useEffect(() => {
if (isEditing && inputRef.current) {
inputRef.current.focus();
@@ -411,6 +439,14 @@ const Points = () => {
};
+ const handleReload = async () => {
+ if (window.confirm("Are you sure you want to reload the points?")) {
+ setLoading(true);
+ await fetchMembers();
+ }
+ };
+
+
if (loading) {
return (
@@ -636,6 +672,14 @@ const Points = () => {
+
+ {/* Reload Button */}
+
+
+
);
}
@@ -682,8 +726,55 @@ const getColumnColor = (index: number): string => {
return colors[index % colors.length];
};
-const sleep = (ms: number) => new Promise(resolve => setTimeout(resolve, ms));
+const isPlainDateObject = (obj: any): obj is Date => {
+ return (
+ obj &&
+ typeof obj === 'object' &&
+ typeof obj.getFullYear === 'function' &&
+ typeof obj.getMonth === 'function' &&
+ typeof obj.getDate === 'function'
+ );
+};
+// Helper function to check if an object is structured like a Firebase Timestamp
+const isPlainTimestampObject = (obj: any): obj is { seconds: number; nanoseconds: number } => {
+ return (
+ obj &&
+ typeof obj === 'object' &&
+ typeof obj.seconds === 'number' &&
+ typeof obj.nanoseconds === 'number'
+ );
+};
+// Convert the object back to a Timestamp if it's structured like one
+const convertToTimestamp = (obj: any): Timestamp | null => {
+ if (isPlainDateObject(obj)) {
+ return Timestamp.fromDate(obj);
+ } else if (isPlainTimestampObject(obj)) {
+ return new Timestamp(obj.seconds, obj.nanoseconds);
+ }
+ return null;
+};
+
+
+
+// Conversion function to convert Date objects or Timestamp-like objects back to Timestamps
+const convertDatesToTimestamps = (log: SHPEEventLog): SHPEEventLog => {
+ return {
+ ...log,
+ signInTime: convertToTimestamp(log.signInTime) || log.signInTime,
+ signOutTime: convertToTimestamp(log.signOutTime) || log.signOutTime,
+ creationTime: convertToTimestamp(log.creationTime) || log.creationTime,
+ instagramLogs: log.instagramLogs?.map(log => convertToTimestamp(log) || log),
+ };
+};
+
+// Utility function to convert all event logs in members
+const convertMembersLogsToTimestamps = (members: UserWithLogs[]): UserWithLogs[] => {
+ return members.map(member => ({
+ ...member,
+ eventLogs: member.eventLogs?.map(convertDatesToTimestamps),
+ }));
+};
export default Points;
diff --git a/shpe-app-web/app/(main)/tools/shirt-tracker/page.tsx b/shpe-app-web/app/(main)/tools/shirt-tracker/page.tsx
index 9832a840..fb67c912 100644
--- a/shpe-app-web/app/(main)/tools/shirt-tracker/page.tsx
+++ b/shpe-app-web/app/(main)/tools/shirt-tracker/page.tsx
@@ -5,34 +5,34 @@ import Header from '@/components/Header';
import { onAuthStateChanged } from 'firebase/auth';
import { db, auth } from '@/config/firebaseConfig';
import { getMembers, getShirtsToVerify } from '@/api/firebaseUtils';
-import { User } from '@/types/user';
+import { PublicUserInfo, User } from '@/types/user';
import { SHPEEventLog } from '@/types/events';
import { doc, updateDoc, Timestamp } from 'firebase/firestore';
import { isMemberVerified } from '@/types/membership';
+import { FaSync } from 'react-icons/fa';
const ShirtTracker = () => {
const router = useRouter();
const [loading, setLoading] = useState(true);
const [members, setMembers] = useState([]);
const [shirtList, setShirtList] = useState([]);
-
- const fetchMembers = async () => {
- setLoading(true);
- const response = await getMembers();
- setMembers(response);
- };
-
const fetchShirts = async () => {
const shirts = await getShirtsToVerify();
const updatedShirtList: ShirtWithMember[] = shirts.map((shirt) => {
- const matchedMember = members.find((member) => member.publicInfo?.uid === shirt.uid);
+ const shirtUid = shirt.uid?.trim().toLowerCase();
+ const matchedMember = members.find((member) =>
+ member.publicInfo?.uid?.trim().toLowerCase() === shirtUid
+ );
const email = matchedMember?.publicInfo?.email?.trim()
? matchedMember.publicInfo.email
: matchedMember?.private?.privateInfo?.email || 'N/A';
const isOfficialMember = matchedMember
- ? isMemberVerified(matchedMember.publicInfo?.chapterExpiration, matchedMember.publicInfo?.nationalExpiration)
+ ? isMemberVerified(
+ matchedMember.publicInfo?.chapterExpiration,
+ matchedMember.publicInfo?.nationalExpiration
+ )
? 'Yes'
: 'No'
: 'No';
@@ -78,8 +78,38 @@ const ShirtTracker = () => {
}
};
+ const fetchMembers = async () => {
+ setLoading(true);
+ try {
+ const response = await getMembers() as UserWithLogs[];
+ setMembers(response);
+
+ localStorage.setItem('cachedMembers', JSON.stringify(response));
+ localStorage.setItem('cachedMembersTimestamp', Date.now().toString());
+
+ } catch (error) {
+ console.error('Error fetching members:', error);
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ const checkCacheAndFetchMembers = () => {
+ const cachedMembers = localStorage.getItem('cachedMembers');
+ const cachedTimestamp = localStorage.getItem('cachedMembersTimestamp');
+
+ if (cachedMembers && cachedTimestamp && Date.now() - parseInt(cachedTimestamp, 10) < 24 * 60 * 60 * 1000) {
+ const membersData = JSON.parse(cachedMembers) as UserWithLogs[];
+ const convertedMembers = convertMembersLogsAndPublicInfoToTimestamps(membersData);
+ setMembers(convertedMembers);
+ setLoading(false);
+ } else {
+ fetchMembers();
+ }
+ };
+
useEffect(() => {
- fetchMembers();
+ checkCacheAndFetchMembers();
}, []);
useEffect(() => {
@@ -100,6 +130,14 @@ const ShirtTracker = () => {
return () => unsubscribe();
}, [router]);
+ const handleReload = async () => {
+ if (window.confirm("Are you sure you want to reload the points?")) {
+ setLoading(true);
+ await fetchMembers();
+ }
+ };
+
+
if (loading) {
return (
@@ -153,6 +191,14 @@ const ShirtTracker = () => {
+
+ {/* Reload Button */}
+
+
+
);
};
@@ -174,5 +220,61 @@ interface ShirtWithMember extends ShirtData {
isOfficialMember: string;
}
+const isPlainDateObject = (obj: any): obj is Date => {
+ return (
+ obj &&
+ typeof obj === 'object' &&
+ typeof obj.getFullYear === 'function' &&
+ typeof obj.getMonth === 'function' &&
+ typeof obj.getDate === 'function'
+ );
+};
+
+
+const isPlainTimestampObject = (obj: any): obj is { seconds: number; nanoseconds: number } => {
+ return (
+ obj &&
+ typeof obj === 'object' &&
+ typeof obj.seconds === 'number' &&
+ typeof obj.nanoseconds === 'number'
+ );
+};
+
+const convertToTimestamp = (obj: any): Timestamp | null => {
+ if (isPlainDateObject(obj)) {
+ return Timestamp.fromDate(obj);
+ } else if (isPlainTimestampObject(obj)) {
+ return new Timestamp(obj.seconds, obj.nanoseconds);
+ }
+ return null;
+};
+
+const convertPublicUserInfoDatesToTimestamps = (publicInfo: PublicUserInfo): PublicUserInfo => {
+ return {
+ ...publicInfo,
+ chapterExpiration: convertToTimestamp(publicInfo.chapterExpiration) || publicInfo.chapterExpiration,
+ nationalExpiration: convertToTimestamp(publicInfo.nationalExpiration) || publicInfo.nationalExpiration,
+ };
+};
+
+const convertDatesToTimestamps = (log: SHPEEventLog): SHPEEventLog => {
+ return {
+ ...log,
+ signInTime: convertToTimestamp(log.signInTime) || log.signInTime,
+ signOutTime: convertToTimestamp(log.signOutTime) || log.signOutTime,
+ creationTime: convertToTimestamp(log.creationTime) || log.creationTime,
+ instagramLogs: log.instagramLogs?.map(log => convertToTimestamp(log) || log),
+ };
+};
+
+const convertMembersLogsAndPublicInfoToTimestamps = (members: UserWithLogs[]): UserWithLogs[] => {
+ return members.map(member => ({
+ ...member,
+ publicInfo: member.publicInfo ? convertPublicUserInfoDatesToTimestamps(member.publicInfo) : undefined,
+ eventLogs: member.eventLogs?.map(convertDatesToTimestamps),
+ }));
+};
+
+
export default ShirtTracker;