Skip to content

Commit

Permalink
Merge pull request #445 from TAMUSHPE/dev
Browse files Browse the repository at this point in the history
Dev
  • Loading branch information
JasonIsAzn authored Aug 21, 2024
2 parents 8fd4ec9 + 1b1bc68 commit 287fd7d
Show file tree
Hide file tree
Showing 11 changed files with 738 additions and 441 deletions.
2 changes: 1 addition & 1 deletion app.json
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@
"permissions": [
"android.permission.RECORD_AUDIO"
],
"versionCode": 52,
"versionCode": 56,
"userInterfaceStyle": "automatic"
},
"ios": {
Expand Down
190 changes: 188 additions & 2 deletions shpe-app-web/app/(main)/membership/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,119 @@
import { useEffect, useState } from "react";
import { useRouter } from "next/navigation";
import Header from "@/components/Header";
import { getMembers, getMembersToVerify } from "@/api/firebaseUtils";
import { SHPEEventLog } from '@/types/events';
import { User } from "@/types/user"
import { isMemberVerified, RequestWithDoc } from "@/types/membership";
import { deleteDoc, deleteField, doc, Timestamp, updateDoc } from "firebase/firestore";
import { auth, db, functions } from "@/config/firebaseConfig";
import { httpsCallable } from "firebase/functions";
import MemberCard from "@/components/MemberCard";
import { onAuthStateChanged } from "firebase/auth";
import { auth } from "@/config/firebaseConfig";


interface UserWithLogs extends User {
eventLogs?: SHPEEventLog[];
}



const Membership = () => {
const router = useRouter();
const [loading, setLoading] = useState(true);
const [members, setMembers] = useState<UserWithLogs[]>([]);
const [students, setStudents] = useState<UserWithLogs[]>([]);
const [tab, setTab] = useState("members");
// make a state to make a make the RequestWithDoc array
const [requestsWithDocuments, setRequestsWithDocuments] = useState<RequestWithDoc[]>([])

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

const incomingReqs = await getMembersToVerify()
setRequestsWithDocuments(incomingReqs)
setLoading(false)

}

const handleApprove = async (member: RequestWithDoc) => {
const userDocRef = doc(db, 'users', member.uid);

await updateDoc(userDocRef, {
chapterExpiration: member?.chapterExpiration,
nationalExpiration: member?.nationalExpiration,
});


const memberDocRef = doc(db, 'memberSHPE', member.uid);
await deleteDoc(memberDocRef);
const filteredRequests = requestsWithDocuments.filter(req => req.uid !== member.uid);
setRequestsWithDocuments(filteredRequests);

const sendNotificationToMember = httpsCallable(functions, 'sendNotificationMemberSHPE');
await sendNotificationToMember({
uid: member.uid,
type: "approved",
});
};

const handleDeny = async (member: RequestWithDoc) => {
const userDocRef = doc(db, 'users', member.uid);

await updateDoc(userDocRef, {
chapterExpiration: deleteField(),
nationalExpiration: deleteField()
});

const memberDocRef = doc(db, 'memberSHPE', member.uid);
await deleteDoc(memberDocRef);

//get rid of the member from the lists
const filteredRequests = requestsWithDocuments.filter(req => req.uid !== member.uid);
setRequestsWithDocuments(filteredRequests);
const sendNotificationToMember = httpsCallable(functions, 'sendNotificationMemberSHPE');
await sendNotificationToMember({
uid: member.uid,
type: "denied",
});
};

const getRole = (user : User) => {
if(user.publicInfo?.roles?.admin){
return "Admin"
}
if(user.publicInfo?.roles?.developer){
return "Developer"
}
if(user.publicInfo?.roles?.lead){
return "Lead"
}
if(user.publicInfo?.roles?.officer){
return "Officer"
}
if(user.publicInfo?.roles?.representative){
return "Representative"
}
if(user.publicInfo?.roles?.reader){
if(user.publicInfo.isStudent && isMemberVerified(user.publicInfo?.chapterExpiration, user.publicInfo?.nationalExpiration)){
return "SHPE Member"
}
else if(user.publicInfo.isStudent){
return "Student"
}
return "Guest"
}
}

useEffect(() => {
fetchMembers()
setLoading(false);
}, []);


useEffect(() => {
const unsubscribe = onAuthStateChanged(auth, (currentUser) => {
Expand All @@ -31,8 +138,87 @@ const Membership = () => {
}

return (
<div className="w-full h-full">
<div className="w-full h-full flex flex-col">
<Header title="MemberSHPE" iconPath="calendar-solid-gray.svg" />
<div className="text-white bg-[#500000] text-center text-2xl flex">
<button onClick={() => setTab("members")} className="w-1/2">Offical Members</button>
<button onClick={() => setTab("requests")} className="w-1/2">Requests</button>
<button onClick={() => setTab("users")} className="w-1/2">All Users</button>
</div>
{/* Display the offical members */}

{ tab == "members" &&
<table className="text-center">
<tr className="bg-gray-700">
<th className=" px-4 py-2">Name</th>
<th className=" px-4 py-2">Major</th>
<th className=" px-4 py-2">Class Year</th>
<th className=" px-4 py-2">Role</th>
<th className=" px-4 py-2">Email</th>
</tr>
{members && members.map((member) => {

return (<tr className="bg-gray-300">
<td className="bg-gray-600 px-4 py-2 "> {member.publicInfo?.displayName} </td>
<td className="bg-gray-300 px-4 py-2 "> {member.publicInfo?.major} </td>
<td className="bg-gray-300 px-4 py-2 "> {member.publicInfo?.classYear} </td>
<td className="bg-gray-300 px-4 py-2 "> {getRole(member)} </td>
<td className="bg-gray-300 px-4 py-2 "> {member.private?.privateInfo?.email} </td>
</tr>)
})}
</table>
}




{/* flex flex-col items-center w-full content-center */}
{tab == "requests" &&
<table className=" text-center">
<tr className="bg-gray-700">
<th className=" px-4 py-2">Name</th>
<th className=" px-4 py-2" colSpan={2}>Links</th>
<th className=" px-4 py-2" colSpan={2}>Action</th>
</tr>
{!loading && requestsWithDocuments.length > 0 &&
requestsWithDocuments.map((member) => {
return ((
(
<MemberCard request={member}
onApprove={handleApprove} onDeny={handleDeny}>
</MemberCard>
)
))
}
)
}
{
!loading && requestsWithDocuments.length === 0 &&
(<div className="text-center text-2xl text-gray-500">No pending requests</div>)
}
</table>}
{ tab == "users" &&
<table className="text-center">
<tr className="bg-gray-700">
<th className=" px-4 py-2">Name</th>
<th className=" px-4 py-2">Major</th>
<th className=" px-4 py-2">Class Year</th>
<th className=" px-4 py-2">Role</th>
<th className=" px-4 py-2">Email</th>
</tr>
{students && students.map((member) => {

return (<tr className="bg-gray-300">
<td className="bg-gray-500 px-4 py-2 "> {member.publicInfo?.displayName} </td>
<td className="bg-gray-300 px-4 py-2 "> {member.publicInfo?.major} </td>
<td className="bg-gray-300 px-4 py-2 "> {member.publicInfo?.classYear} </td>
<td className="bg-blue-300 px-4 py-2 "> {getRole(member)} </td>
<td className="bg-gray-300 px-4 py-2 "> {member.private?.privateInfo?.email} </td>
</tr>)
})}
</table>
}

</div>
);
}
Expand Down
46 changes: 43 additions & 3 deletions shpe-app-web/app/api/firebaseUtils.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,15 @@
import { db, auth } from "@/config/firebaseConfig";
import { collection, getDocs, getDoc, doc, query, orderBy, writeBatch } from 'firebase/firestore';
import { collection, getDocs, getDoc, doc, query, orderBy, where, Timestamp,writeBatch } from 'firebase/firestore';
import { RequestWithDoc } from "@/types/membership";
import { PrivateUserInfo, PublicUserInfo, User } from "@/types/user"
import { SHPEEvent, SHPEEventLog } from "@/types/events";
import { Committee } from "@/types/committees";

export const getMembers = async (): Promise<User[]> => {
interface UserWithLogs extends User {
eventLogs?: SHPEEventLog[];
}

export const getMembers = async (): Promise<UserWithLogs[]> => {
try {
const userRef = collection(db, 'users');
const q = query(userRef, orderBy("points", "desc"));
Expand Down Expand Up @@ -94,6 +99,39 @@ export const getEventLogs = async (eventId: string): Promise<SHPEEventLog[]> =>
}
};

export const getMembersToVerify = async (): Promise<RequestWithDoc[]> => {
const memberSHPERef = collection(db, 'memberSHPE');
const memberSHPEQuery = query(memberSHPERef, where('nationalURL', '!=', ''));
const memberSHPESnapshot = await getDocs(memberSHPEQuery);

const members: RequestWithDoc[] = [];
for (const document of memberSHPESnapshot.docs) {

const memberSHPEData = document.data();
if (memberSHPEData.chapterURL && memberSHPEData.nationalURL) {
const userId = document.id;
const userDocRef = doc(db, 'users', userId);
const userDocSnap = await getDoc(userDocRef);
if (userDocSnap.exists()) {
//get the name from the doc snap
const name = userDocSnap.data().name;
const member: RequestWithDoc = {
name: name,
uid: userId,
chapterURL: memberSHPEData.chapterURL,
nationalURL: memberSHPEData.nationalURL,
chapterExpiration: memberSHPEData.chapterExpiration,
nationalExpiration: memberSHPEData.nationalExpiration,
shirtSize: memberSHPEData.shirtSize,
// Add other properties as needed
};
members.push(member);
}

}
}
return members;
}
export const updatePointsInFirebase = async (changesToSave: { userId: string, eventId: string, newPoints: number | null }[]) => {
const batch = writeBatch(db);

Expand Down Expand Up @@ -235,4 +273,6 @@ export const getPrivateUserData = async (uid: string = ""): Promise<PrivateUserI
console.error(err);
return undefined;
});
};
};


28 changes: 28 additions & 0 deletions shpe-app-web/app/components/MemberCard.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
'use client'
import { useState } from "react";
import { RequestWithDoc } from "@/types/membership";


interface MemberCardProps{
request: RequestWithDoc,
onApprove: (member: RequestWithDoc) => void,
onDeny: (member: RequestWithDoc) => void,
}

const MemberCard : React.FC<MemberCardProps> = ({request, onApprove, onDeny}) => {


return(
// flex text-gray-400 justify-evenly text-2xl
<tr className="bg-gray-300">
<td className="bg-gray-500 px-4 py-2">{request.name}</td>
<td className="px-4 py-2"><a href={request.chapterURL}>Chapter</a></td>
<td className="px-4 py-2"><a href={request.nationalURL}>National</a></td>
<td className="bg-green-400 px-4 py-2"><button onClick={() => {onApprove(request); }}>Approve</button></td>
<td className="bg-red-400 px-4 py-2"><button onClick={() => {onDeny(request); }}>Deny</button></td>
</tr>
)
//}

}
export default MemberCard;
57 changes: 57 additions & 0 deletions shpe-app-web/app/types/membership.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import { Timestamp } from "firebase/firestore";

export interface RequestWithDoc {
name: string,
uid: string,
chapterURL: string;
nationalURL: string;
chapterExpiration: Timestamp;
nationalExpiration: Timestamp;
shirtSize: string;
}
/** ===================================================================================
* The content below must only be changed to match MobileApp/src/types/membership.ts
*
* You may manually add any imports above at the top of this file if needed
* ===================================================================================
* */
export const isMemberVerified = (nationalExpiration: Timestamp | { seconds: number; nanoseconds: number; } | undefined, chapterExpiration: Timestamp | { seconds: number; nanoseconds: number; } | undefined) => {
if (!nationalExpiration || !chapterExpiration) return false;

const nationalExpirationDate = nationalExpiration instanceof Timestamp ? nationalExpiration.toDate() : nationalExpiration ? new Date(nationalExpiration.seconds * 1000) : undefined;
const chapterExpirationDate = chapterExpiration instanceof Timestamp ? chapterExpiration.toDate() : chapterExpiration ? new Date(chapterExpiration.seconds * 1000) : undefined;

const currentDate = new Date();
let isNationalValid = true;
let isChapterValid = true;

if (nationalExpirationDate) {
isNationalValid = currentDate <= nationalExpirationDate;
}

if (chapterExpirationDate) {
isChapterValid = currentDate <= chapterExpirationDate;
}

return isNationalValid && isChapterValid
};

export const getBadgeColor = (isOfficer: boolean, isVerified: boolean) => {
if (isOfficer) return '#FCE300';
if (isVerified) return '#500000';
return '';
};


export const formatExpirationDate = (expiration: Timestamp | { seconds: number; nanoseconds: number; } | undefined): string => {
if (!expiration) return ''; // Handle undefined cases
const options: Intl.DateTimeFormatOptions = {
year: 'numeric',
month: 'long',
day: 'numeric'
};

const expirationDate = expiration instanceof Timestamp ? expiration.toDate() : new Date(expiration.seconds * 1000);

return new Intl.DateTimeFormat('en-US', options).format(expirationDate);
};
2 changes: 1 addition & 1 deletion src/api/firebaseUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -486,7 +486,7 @@ export const getEvent = async (eventID: string): Promise<null | SHPEEvent> => {
export const getUpcomingEvents = async () => {
const currentTime = new Date();
const eventsRef = collection(db, "events");
const q = query(eventsRef, where("endTime", ">", currentTime));
const q = query(eventsRef, where("endTime", ">=", currentTime));
const querySnapshot = await getDocs(q);
const events: SHPEEvent[] = [];

Expand Down
Loading

0 comments on commit 287fd7d

Please sign in to comment.