+
{type !== APP_CONSTANTS.PENDING_LABEL && (
)}
- {dataList &&
- dataList.map((request, index) => {
- const borderClass =
- dataList.length == 1
- ? 'rounded-xl'
- : index == 0
- ? 'rounded-tl-xl rounded-tr-xl'
- : index == dataList.length - 1
- ? 'rounded-bl-xl rounded-br-xl'
- : '';
- return (
-
redirectToDetail(request)}
- >
-
-
- {request?.name}
-
- {request.status !== APP_CONSTANTS.PENDING_LABEL && (
-
- {request.status === 'REJECTED'
- ? APP_CONSTANTS.REJECTED_LABEL
- : APP_CONSTANTS.APPROVED_LABEL}
-
- )}
-
-
- );
- })}
- {dataList.length === 0 && (
+ {dataList.length > 0 && <>{isTeamActive &&
}>}
+ {!isTeamActive &&
}
+ {dataList.length === 0 && isTeamActive && (
-
- {APP_CONSTANTS.NO_DATA_AVAILABLE_LABEL}
-
+ {APP_CONSTANTS.NO_DATA_AVAILABLE_LABEL}
)}
diff --git a/apps/back-office/components/tab/tab.tsx b/apps/back-office/components/tab/tab.tsx
new file mode 100644
index 000000000..277f48f32
--- /dev/null
+++ b/apps/back-office/components/tab/tab.tsx
@@ -0,0 +1,16 @@
+
+
+const Tab = (props: any) => {
+ const name=props?.name ?? "";
+ const isSelected = props?.isSelected ?? false;
+ const onTabClickHandler = props?.onClick;
+ const count = props?.count ?? 0;
+
+
+ return (
+
+ )
+
+}
+
+export default Tab;
\ No newline at end of file
diff --git a/apps/back-office/components/team-request-list.tsx b/apps/back-office/components/team-request-list.tsx
new file mode 100644
index 000000000..21ec423f8
--- /dev/null
+++ b/apps/back-office/components/team-request-list.tsx
@@ -0,0 +1,71 @@
+import { useState } from "react";
+import APP_CONSTANTS, { ROUTE_CONSTANTS } from "../utils/constants";
+import Loader from "./common/loader";
+import { useNavbarContext } from "../context/navbar-context";
+import router from 'next/router';
+
+
+
+const TeamRequestList = (props: any) => {
+ const dataList = props?.teams;
+ const [isLoading, setIsLoading] = useState(false);
+
+
+ function redirectToDetail(request) {
+ setIsLoading(true);
+ const route = ROUTE_CONSTANTS.TEAM_VIEW
+ router.push({
+ pathname: route,
+ query: {
+ id: request.id,
+ },
+ });
+ }
+
+ return <>
+ {isLoading &&
}
+ {dataList &&
+ dataList.map((request, index) => {
+ const borderClass =
+ dataList.length == 1
+ ? 'rounded-xl'
+ : index == 0
+ ? 'rounded-tl-xl rounded-tr-xl'
+ : index == dataList.length - 1
+ ? 'rounded-bl-xl rounded-br-xl'
+ : '';
+ return (
+
redirectToDetail(request)}
+ >
+
+
+ {request?.name}
+
+ {request.status !== APP_CONSTANTS.PENDING_LABEL && (
+
+ {request.status === 'REJECTED'
+ ? APP_CONSTANTS.REJECTED_LABEL
+ : APP_CONSTANTS.APPROVED_LABEL}
+
+ )}
+
+
+ );
+ })}
+ >
+}
+
+export default TeamRequestList;
\ No newline at end of file
diff --git a/apps/back-office/layout/approval-layout.tsx b/apps/back-office/layout/approval-layout.tsx
index bd5a52537..9dc66795f 100644
--- a/apps/back-office/layout/approval-layout.tsx
+++ b/apps/back-office/layout/approval-layout.tsx
@@ -2,9 +2,9 @@ import { Navbar } from '../components/navbar/navbar';
export function ApprovalLayout({ children }) {
return (
-
+
-
+
{children}
diff --git a/apps/back-office/pages/closed-list.tsx b/apps/back-office/pages/closed-list.tsx
index f1ab96773..eb0f3a7e9 100644
--- a/apps/back-office/pages/closed-list.tsx
+++ b/apps/back-office/pages/closed-list.tsx
@@ -13,31 +13,19 @@ type RequestList = {
};
export default function ClosedList(props) {
- const {
- setIsOpenRequest,
- setMemberList,
- setTeamList,
- isTeamActive,
- setShowMenu,
- } = useNavbarContext();
+ const { setIsOpenRequest, setMemberList, setTeamList, isTeamActive, setShowMenu } = useNavbarContext();
setShowMenu(true);
useEffect(() => {
setMemberList(props.memberList);
setTeamList(props.teamList);
setIsOpenRequest(false);
- }, [
- isTeamActive,
- setMemberList,
- props.memberList,
- props.teamList,
- setTeamList,
- setIsOpenRequest,
- ]);
+ }, [isTeamActive, setMemberList, props.memberList, props.teamList, setTeamList, setIsOpenRequest]);
return (
@@ -45,9 +33,7 @@ export default function ClosedList(props) {
);
}
-export const getServerSideProps: GetServerSideProps = async (
- context
-) => {
+export const getServerSideProps: GetServerSideProps = async (context) => {
const { plnadmin } = parseCookies(context);
if (!plnadmin) {
@@ -76,7 +62,7 @@ export const getServerSideProps: GetServerSideProps = async (
teamResponse = listData.data.filter(
(item) =>
item.participantType === ENROLLMENT_TYPE.TEAM &&
- item.status !== APP_CONSTANTS.PENDING_LABEL &&
+ item.status !== APP_CONSTANTS.PENDING_LABEL &&
item.status !== APP_CONSTANTS.AUTO_APPROVED_LABEL
);
memberResponse = listData.data.filter(
@@ -88,14 +74,14 @@ export const getServerSideProps: GetServerSideProps = async (
member = memberResponse?.map((data) => {
return {
id: data.uid,
- name: data.newData.name,
+ name: data.newData.name ?? '',
status: data.status,
};
});
team = teamResponse?.map((data) => {
return {
id: data.uid,
- name: data.newData.name,
+ name: data.newData.name ?? '',
status: data.status,
};
});
@@ -107,6 +93,7 @@ export const getServerSideProps: GetServerSideProps = async (
teamList: team,
teamCount: team?.length ?? 0,
memberCount: member?.length ?? 0,
+ plnadmin,
},
};
};
diff --git a/apps/back-office/pages/member-view.tsx b/apps/back-office/pages/member-view.tsx
index 612fa2cea..e26f91c95 100644
--- a/apps/back-office/pages/member-view.tsx
+++ b/apps/back-office/pages/member-view.tsx
@@ -29,32 +29,32 @@ function validateBasicForm(formValues, imageUrl) {
if (!formValues.email.trim() || !formValues.email?.trim().match(emailRE)) {
errors.push('Please add valid Email');
}
- if (
- !formValues.requestorEmail?.trim() ||
- !formValues.requestorEmail?.trim().match(emailRE)
- ) {
- errors.push('Please add a valid Requestor Email');
- }
+ // if (
+ // !formValues.requestorEmail?.trim() ||
+ // !formValues.requestorEmail?.trim().match(emailRE)
+ // ) {
+ // errors.push('Please add a valid Requestor Email');
+ // }
return errors;
}
-function validateSkillForm(formValues) {
- const errors = [];
- if (!formValues.teamAndRoles.length) {
- errors.push('Please add your Team and Role details');
- } else {
- const missingValues = formValues.teamAndRoles.filter(
- (item) => item.teamUid == '' || item.role == ''
- );
- if (missingValues.length) {
- errors.push('Please add missing Team(s)/Role(s)');
- }
- }
- if (!formValues.skills.length) {
- errors.push('Please add your skill details');
- }
- return errors;
-}
+// function validateSkillForm(formValues) {
+// const errors = [];
+// if (!formValues.teamAndRoles.length) {
+// errors.push('Please add your Team and Role details');
+// } else {
+// const missingValues = formValues.teamAndRoles.filter(
+// (item) => item.teamUid == '' || item.role == ''
+// );
+// if (missingValues.length) {
+// errors.push('Please add missing Team(s)/Role(s)');
+// }
+// }
+// if (!formValues.skills.length) {
+// errors.push('Please add your skill details');
+// }
+// return errors;
+// }
function validateForm(formValues, imageUrl) {
let errors = [];
@@ -62,10 +62,10 @@ function validateForm(formValues, imageUrl) {
if (basicFormErrors.length) {
errors = [...errors, ...basicFormErrors];
}
- const skillFormErrors = validateSkillForm(formValues);
- if (skillFormErrors.length) {
- errors = [...errors, ...skillFormErrors];
- }
+ // const skillFormErrors = validateSkillForm(formValues);
+ // if (skillFormErrors.length) {
+ // errors = [...errors, ...skillFormErrors];
+ // }
return errors;
}
@@ -85,7 +85,7 @@ export default function MemberView(props) {
const [disableSave, setDisableSave] = useState(false);
const [formValues, setFormValues] = useState(props?.formValues);
const [isLoading, setIsLoading] = useState(false);
- const [resetImg, setResetImg] = useState(false);
+ const [resetImg, setResetImg] = useState(false);
const {
setIsOpenRequest,
setMemberList,
@@ -102,7 +102,7 @@ export default function MemberView(props) {
useEffect(() => {
setDropDownValues({ skillValues: props?.skills, teamNames: props?.teams });
}, [props]);
-
+
const handleResetImg = () => {
setResetImg(false);
}
@@ -128,15 +128,16 @@ export default function MemberView(props) {
twitterHandler: formValues.twitterHandler?.trim(),
githubHandler: formValues.githubHandler?.trim(),
telegramHandler: formValues.telegramHandler?.trim(),
- officeHours: formValues.officeHours?.trim() === ''? null : formValues.officeHours?.trim(),
+ officeHours: formValues.officeHours?.trim() === '' ? null : formValues.officeHours?.trim(),
comments: formValues.comments?.trim(),
+ teamOrProjectURL: formValues.teamOrProjectURL,
plnStartDate: formValues.plnStartDate
? new Date(formValues.plnStartDate)?.toISOString()
: null,
skills: skills,
teamAndRoles: formattedTeamAndRoles,
openToWork: formValues.openToWork,
- projectContributions:formValues.projectContributions,
+ projectContributions: formValues.projectContributions,
oldName: name,
};
delete formattedData.requestorEmail;
@@ -203,21 +204,33 @@ export default function MemberView(props) {
const data = {
participantType: ENROLLMENT_TYPE.MEMBER,
// referenceUid: props.id,
- requesterEmailId: requestorEmail,
+ requesterEmailId: requestorEmail ? requestorEmail : null,
uniqueIdentifier: values.email,
newData: {
...values,
imageUid: image?.uid ?? values.imageUid,
imageUrl: image?.url ?? imageUrl,
},
- };
- const configuration = {
+ };
+ const configuration = {
headers: {
authorization: `Bearer ${props.plnadmin}`,
},
};
- await api
+ if(props?.from === "approved") {
+ await api.patch(
+ `${API_ROUTE.ADMIN_APPROVAL}/${props.id}`,
+ data,
+ configuration
+ )
+ .then((response) => {
+ setSaveCompleted(true);
+ setIsEditEnabled(false);
+ setResetImg(true);
+ });
+ } else {
+ await api
.put(
`${API_ROUTE.PARTICIPANTS_REQUEST}/${props.id}`,
data,
@@ -228,6 +241,7 @@ export default function MemberView(props) {
setIsEditEnabled(false);
setResetImg(true);
});
+ }
} catch (err) {
toast(err?.message);
console.log('error', err);
@@ -329,7 +343,6 @@ export default function MemberView(props) {
)}
)}
>
@@ -393,9 +407,10 @@ export default function MemberView(props) {
}
export const getServerSideProps = async (context) => {
- const { id, backLink = ROUTE_CONSTANTS.PENDING_LIST } = context.query as {
+ const { id, from, backLink = ROUTE_CONSTANTS.PENDING_LIST } = context.query as {
id: string;
backLink: string;
+ from: string;
};
const { plnadmin } = parseCookies(context);
@@ -421,117 +436,176 @@ export const getServerSideProps = async (context) => {
let teamList = [];
let oldName = '';
- // Check if provided ID is an Airtable ID, and if so, get the corresponding backend UID
-
- const [
- requestDetailResponse,
- allRequestResponse,
- memberTeamsResponse,
- skillsResponse,
- ] = await Promise.all([
- api.get(`${API_ROUTE.PARTICIPANTS_REQUEST}/${id}`, config),
- api.get(API_ROUTE.PARTICIPANTS_REQUEST, config),
- api.get(API_ROUTE.TEAMS),
- api.get(API_ROUTE.SKILLS),
- ]);
-
- if (
- requestDetailResponse.status === 200 &&
- allRequestResponse.status === 200 &&
- memberTeamsResponse.status === 200 &&
- skillsResponse.status === 200
- ) {
- teamList = allRequestResponse?.data?.filter(
- (item) => item.participantType === ENROLLMENT_TYPE.TEAM
- );
- memberList = allRequestResponse?.data?.filter(
- (item) => item.participantType === ENROLLMENT_TYPE.MEMBER
- );
+
+ if (from !== "approved") {
+ const [
+ requestDetailResponse,
+ allRequestResponse,
+ memberTeamsResponse,
+ skillsResponse,
+ ] = await Promise.all([
+ api.get(`${API_ROUTE.PARTICIPANTS_REQUEST}/${id}`, config),
+ api.get(API_ROUTE.PARTICIPANTS_REQUEST, config),
+ api.get(API_ROUTE.TEAMS),
+ api.get(API_ROUTE.SKILLS),
+ ]);
- let counter = 1;
- referenceUid = requestDetailResponse?.data?.referenceUid ?? '';
- const requestData = requestDetailResponse?.data?.newData;
- oldName = requestData?.oldName ?? requestData?.name;
- status = requestDetailResponse?.data?.status;
- const teamAndRoles =
- requestData.teamAndRoles?.length &&
- requestData.teamAndRoles.map((team) => {
+ if (
+ requestDetailResponse.status === 200 &&
+ allRequestResponse.status === 200 &&
+ memberTeamsResponse.status === 200 &&
+ skillsResponse.status === 200
+ ) {
+ teamList = allRequestResponse?.data?.filter(
+ (item) => item.participantType === ENROLLMENT_TYPE.TEAM
+ );
+ memberList = allRequestResponse?.data?.filter(
+ (item) => item.participantType === ENROLLMENT_TYPE.MEMBER
+ );
+
+ let counter = 1;
+ referenceUid = requestDetailResponse?.data?.referenceUid ?? '';
+ const requestData = requestDetailResponse?.data?.newData;
+ oldName = requestData?.oldName ?? requestData?.name;
+ status = requestDetailResponse?.data?.status;
+ const teamAndRoles =
+ requestData?.teamAndRoles?.length &&
+ requestData?.teamAndRoles?.map((team) => {
+ return {
+ role: team.role ?? "",
+ teamUid: team.teamUid,
+ teamTitle: team.teamTitle,
+ rowId: counter++,
+ };
+ });
+
+ formValues = {
+ name: requestData?.name,
+ email: requestData?.email,
+ imageUid: requestData?.imageUid ?? '',
+ imageFile: null,
+ plnStartDate: requestData?.plnStartDate
+ ? new Date(requestData?.plnStartDate).toISOString().split('T')[0]
+ : null,
+ city: requestData?.city ?? '',
+ region: requestData?.region ?? '',
+ country: requestData?.country ?? '',
+ linkedinHandler: requestData?.linkedinHandler ?? '',
+ discordHandler: requestData?.discordHandler ?? '',
+ twitterHandler: requestData?.twitterHandler ?? '',
+ githubHandler: requestData?.githubHandler ?? '',
+ telegramHandler: requestData?.telegramHandler ?? '',
+ officeHours: requestData?.officeHours ?? '',
+ requestorEmail: requestDetailResponse?.data?.requesterEmailId ?? '',
+ comments: requestData?.comments ?? '',
+ teamAndRoles: teamAndRoles || [
+ // { teamUid: '', teamTitle: '', role: '', rowId: 1 },
+ ],
+ teamOrProjectURL: requestData?.teamOrProjectURL ?? '',
+ skills: requestData?.skills?.map((item) => {
+ return { value: item.uid, label: item.title };
+ }) || [],
+ openToWork: requestData?.openToWork ?? '',
+ projectContributions: requestData?.projectContributions ?? []
+ };
+ imageUrl = requestData?.imageUrl ?? '';
+
+ if (status == APP_CONSTANTS.PENDING_LABEL) {
+ teamList = allRequestResponse?.data
+ ?.filter((item) => item.participantType === ENROLLMENT_TYPE.TEAM)
+ ?.filter((item) => item.status === APP_CONSTANTS.PENDING_LABEL);
+ memberList = allRequestResponse?.data
+ ?.filter((item) => item.participantType === ENROLLMENT_TYPE.MEMBER)
+ .filter((item) => item.status === APP_CONSTANTS.PENDING_LABEL);
+ } else {
+ teamList = allRequestResponse?.data
+ ?.filter((item) => item.participantType === ENROLLMENT_TYPE.TEAM)
+ ?.filter((item) => item.status !== APP_CONSTANTS.PENDING_LABEL);
+ memberList = allRequestResponse?.data
+ ?.filter((item) => item.participantType === ENROLLMENT_TYPE.MEMBER)
+ .filter((item) => item.status !== APP_CONSTANTS.PENDING_LABEL);
+ }
+
+ teams = Array.isArray(memberTeamsResponse?.data) ?
+ memberTeamsResponse?.data?.map((item) => {
+ return { value: item.uid, label: item.name };
+ }) : [];
+ skills = skillsResponse?.data?.map((item) => {
+ return { value: item.uid, label: item.title };
+ });
+ }
+ } else {
+ const approvedApiResponse = await api.get(`${API_ROUTE.MEMBERS}/${id}?with=image`, config);
+ const skillsResponse = await api.get(API_ROUTE.SKILLS);
+
+ let counter = 1;
+ if (approvedApiResponse.status === 200) {
+ const requestData = approvedApiResponse?.data;
+ const teamAndRoles =
+ requestData?.teamMemberRoles?.length &&
+ requestData?.teamMemberRoles?.map((team) => {
return {
- role: team.role,
+ role: team.role ?? "",
teamUid: team.teamUid,
- teamTitle: team.teamTitle,
+ teamTitle: team.team.name,
rowId: counter++,
};
});
-
- formValues = {
- name: requestData?.name,
- email: requestData.email,
- imageUid: requestData.imageUid ?? '',
- imageFile: null,
- plnStartDate: requestData.plnStartDate
- ? new Date(requestData.plnStartDate).toISOString().split('T')[0]
- : null,
- city: requestData?.city ?? '',
- region: requestData?.region ?? '',
- country: requestData?.country ?? '',
- linkedinHandler: requestData.linkedinHandler ?? '',
- discordHandler: requestData.discordHandler ?? '',
- twitterHandler: requestData.twitterHandler ?? '',
- githubHandler: requestData.githubHandler ?? '',
- telegramHandler: requestData.telegramHandler ?? '',
- officeHours: requestData.officeHours ?? '',
- requestorEmail: requestDetailResponse?.data?.requesterEmailId ?? '',
- comments: requestData?.comments ?? '',
- teamAndRoles: teamAndRoles || [
- { teamUid: '', teamTitle: '', role: '', rowId: 1 },
- ],
- skills: requestData.skills?.map((item) => {
- return { value: item.uid, label: item.title };
- }),
- openToWork: requestData?.openToWork ?? '',
- projectContributions:requestData?.projectContributions ?? []
- };
- imageUrl = requestData?.imageUrl ?? '';
-
- if (status == APP_CONSTANTS.PENDING_LABEL) {
- teamList = allRequestResponse?.data
- ?.filter((item) => item.participantType === ENROLLMENT_TYPE.TEAM)
- ?.filter((item) => item.status === APP_CONSTANTS.PENDING_LABEL);
- memberList = allRequestResponse?.data
- ?.filter((item) => item.participantType === ENROLLMENT_TYPE.MEMBER)
- .filter((item) => item.status === APP_CONSTANTS.PENDING_LABEL);
- } else {
- teamList = allRequestResponse?.data
- ?.filter((item) => item.participantType === ENROLLMENT_TYPE.TEAM)
- ?.filter((item) => item.status !== APP_CONSTANTS.PENDING_LABEL);
- memberList = allRequestResponse?.data
- ?.filter((item) => item.participantType === ENROLLMENT_TYPE.MEMBER)
- .filter((item) => item.status !== APP_CONSTANTS.PENDING_LABEL);
+ formValues = {
+ name: requestData?.name,
+ email: requestData?.email,
+ imageUid: requestData?.imageUid ?? '',
+ imageFile: null,
+ plnStartDate: requestData?.plnStartDate
+ ? new Date(requestData?.plnStartDate).toISOString().split('T')[0]
+ : null,
+ city: requestData?.city ?? '',
+ region: requestData?.region ?? '',
+ country: requestData?.country ?? '',
+ linkedinHandler: requestData?.linkedinHandler ?? '',
+ discordHandler: requestData?.discordHandler ?? '',
+ twitterHandler: requestData?.twitterHandler ?? '',
+ githubHandler: requestData?.githubHandler ?? '',
+ telegramHandler: requestData?.telegramHandler ?? '',
+ officeHours: requestData?.officeHours ?? '',
+ comments: requestData?.comments ?? '',
+ teamAndRoles: teamAndRoles ||
+ [
+ // { teamUid: '', teamTitle: '', role: '', rowId: 1 },
+ ],
+ teamOrProjectURL: requestData?.teamOrProjectURL ?? '',
+ skills: requestData?.skills?.map((item) => {
+ return { value: item.uid, label: item.title };
+ }),
+ openToWork: requestData?.openToWork ?? '',
+ projectContributions: requestData?.projectContributions ?? []
+ };
+ imageUrl = requestData?.image?.url ?? '',
+ teamList = approvedApiResponse?.data?.teamList ?? [];
+ memberList = approvedApiResponse?.data?.memberList ?? [];
+ teams = approvedApiResponse?.data?.teams ?? [];
+ skills = skillsResponse?.data?.map((item) => {
+ return { value: item.uid, label: item.title };
+ });
+ status= APP_CONSTANTS.PENDING_LABEL;
+ }
}
- teams = memberTeamsResponse?.data?.map((item) => {
- return { value: item.uid, label: item.name };
- });
- skills = skillsResponse?.data?.map((item) => {
- return { value: item.uid, label: item.title };
- });
- }
-
- return {
- props: {
- formValues,
- teams,
- skills,
- id,
- referenceUid,
- imageUrl,
- status,
- backLink,
- teamList,
- memberList,
- plnadmin,
- oldName,
- },
+ return {
+ props: {
+ formValues,
+ teams,
+ skills,
+ id,
+ // referenceUid,
+ imageUrl,
+ status,
+ backLink,
+ teamList,
+ memberList,
+ plnadmin,
+ oldName,
+ from,
+ },
+ };
};
-};
diff --git a/apps/back-office/pages/pending-list.tsx b/apps/back-office/pages/pending-list.tsx
index b91ddbc1a..aaaafa1a6 100644
--- a/apps/back-office/pages/pending-list.tsx
+++ b/apps/back-office/pages/pending-list.tsx
@@ -9,41 +9,27 @@ import { ApprovalLayout } from '../layout/approval-layout';
import { parseCookies } from 'nookies';
export default function PendingList(props) {
- const {
- setIsOpenRequest,
- setMemberList,
- setTeamList,
- isTeamActive,
- setShowMenu,
- } = useNavbarContext();
+ const { setIsOpenRequest, setMemberList, setTeamList, isTeamActive, setShowMenu, memberList, teamList } = useNavbarContext();
setShowMenu(true);
useEffect(() => {
- setMemberList(props.memberList);
+ setMemberList([...props.memberList, ...props.unverifiedMembers]);
setTeamList(props.teamList);
setIsOpenRequest(true);
- }, [
- isTeamActive,
- setMemberList,
- props.memberList,
- props.teamList,
- setTeamList,
- setIsOpenRequest,
- ]);
+ }, [isTeamActive, setMemberList, props.memberList, props.teamList, setTeamList, setIsOpenRequest]);
return (
);
}
-export const getServerSideProps: GetServerSideProps
= async (
- context
-) => {
+export const getServerSideProps: GetServerSideProps = async (context) => {
const { plnadmin } = parseCookies(context);
if (!plnadmin) {
@@ -61,21 +47,18 @@ export const getServerSideProps: GetServerSideProps = async (
authorization: `Bearer ${plnadmin}`,
},
};
- const listData = await api.get(
- `${API_ROUTE.PARTICIPANTS_REQUEST}?status=PENDING`,
- config
- );
+ const listData = await api.get(`${API_ROUTE.PARTICIPANTS_REQUEST}?status=PENDING`, config);
+ const unVerifiedMembes = await api.get(`${API_ROUTE.MEMBERS}?isVerified=false&pagination=false`, config);
+
let memberResponse = [];
let teamResponse = [];
let team = [];
let member = [];
+ let membersCount = 0;
+ let unverifiedMembers = [];
if (listData.data) {
- teamResponse = listData.data.filter(
- (item) => item.participantType === ENROLLMENT_TYPE.TEAM
- );
- memberResponse = listData.data.filter(
- (item) => item.participantType === ENROLLMENT_TYPE.MEMBER
- );
+ teamResponse = listData.data.filter((item) => item.participantType === ENROLLMENT_TYPE.TEAM);
+ memberResponse = listData.data.filter((item) => item.participantType === ENROLLMENT_TYPE.MEMBER);
member = memberResponse?.map((data) => {
return {
id: data.uid,
@@ -83,6 +66,14 @@ export const getServerSideProps: GetServerSideProps = async (
status: data.status,
};
});
+ unverifiedMembers = unVerifiedMembes.data.members.map((data) => {
+ return {
+ id: data.uid,
+ name: data.name,
+ isVerified: data?.isVerified || false,
+ };
+ });
+
team = teamResponse?.map((data) => {
return {
id: data.uid,
@@ -91,12 +82,15 @@ export const getServerSideProps: GetServerSideProps = async (
};
});
}
+ membersCount = member?.length + unverifiedMembers?.length;
return {
props: {
memberList: member,
+ unverifiedMembers: unverifiedMembers,
teamList: team,
teamCount: team?.length ?? 0,
- memberCount: member?.length ?? 0,
+ memberCount: membersCount ?? 0,
+ plnadmin,
},
};
};
diff --git a/apps/back-office/public/assets/icons/TrashIcon.svg b/apps/back-office/public/assets/icons/TrashIcon.svg
new file mode 100644
index 000000000..de038b51c
--- /dev/null
+++ b/apps/back-office/public/assets/icons/TrashIcon.svg
@@ -0,0 +1,3 @@
+
diff --git a/apps/back-office/public/assets/icons/upgrade-rounded.svg b/apps/back-office/public/assets/icons/upgrade-rounded.svg
new file mode 100644
index 000000000..c9cec6c84
--- /dev/null
+++ b/apps/back-office/public/assets/icons/upgrade-rounded.svg
@@ -0,0 +1,3 @@
+
diff --git a/apps/back-office/public/assets/images/delete-disabled.svg b/apps/back-office/public/assets/images/delete-disabled.svg
new file mode 100644
index 000000000..31668aeae
--- /dev/null
+++ b/apps/back-office/public/assets/images/delete-disabled.svg
@@ -0,0 +1,3 @@
+
diff --git a/apps/back-office/public/assets/images/delete.svg b/apps/back-office/public/assets/images/delete.svg
new file mode 100644
index 000000000..353162086
--- /dev/null
+++ b/apps/back-office/public/assets/images/delete.svg
@@ -0,0 +1,3 @@
+
diff --git a/apps/back-office/public/assets/images/sort-unselected.svg b/apps/back-office/public/assets/images/sort-unselected.svg
new file mode 100644
index 000000000..3526a2a6e
--- /dev/null
+++ b/apps/back-office/public/assets/images/sort-unselected.svg
@@ -0,0 +1,5 @@
+
diff --git a/apps/back-office/public/assets/images/unverified-disabled.svg b/apps/back-office/public/assets/images/unverified-disabled.svg
new file mode 100644
index 000000000..500058f0a
--- /dev/null
+++ b/apps/back-office/public/assets/images/unverified-disabled.svg
@@ -0,0 +1,3 @@
+
diff --git a/apps/back-office/public/assets/images/unverified.svg b/apps/back-office/public/assets/images/unverified.svg
new file mode 100644
index 000000000..1af9691f9
--- /dev/null
+++ b/apps/back-office/public/assets/images/unverified.svg
@@ -0,0 +1,3 @@
+
diff --git a/apps/back-office/public/assets/images/verified-disabled.svg b/apps/back-office/public/assets/images/verified-disabled.svg
new file mode 100644
index 000000000..500058f0a
--- /dev/null
+++ b/apps/back-office/public/assets/images/verified-disabled.svg
@@ -0,0 +1,3 @@
+
diff --git a/apps/back-office/public/assets/images/verified.svg b/apps/back-office/public/assets/images/verified.svg
new file mode 100644
index 000000000..fda0c6a23
--- /dev/null
+++ b/apps/back-office/public/assets/images/verified.svg
@@ -0,0 +1,3 @@
+
diff --git a/apps/back-office/utils/constants.ts b/apps/back-office/utils/constants.ts
index e010c32c2..91ed19d14 100644
--- a/apps/back-office/utils/constants.ts
+++ b/apps/back-office/utils/constants.ts
@@ -2,6 +2,10 @@ const APP_CONSTANTS = {
AUTO_APPROVED_LABEL:'AUTOAPPROVED',
APPROVED_LABEL: 'Approved',
PENDING_LABEL: 'PENDING',
+ PENDING_FLAG: "Pending",
+ UNVERIFIED_LABEL: 'UNVERIFIED',
+ UNVERIFIED_FLAG: 'Unverified',
+ VERIFIED_FLAG: 'Verified',
REJECTED_LABEL: 'Rejected',
APPROVED_FLAG: 'APPROVED',
REJECTED_FLAG: 'REJECTED',
@@ -40,6 +44,7 @@ export const API_ROUTE = {
INDUSTRIES: APP_CONSTANTS.V1 + 'industry-tags',
TECHNOLOGIES: APP_CONSTANTS.V1 + 'technologies',
MEMBERS: APP_CONSTANTS.V1 + 'members',
+ ADMIN_APPROVAL: APP_CONSTANTS.V1 + 'admin/members',
};
export const TOKEN = 'plnetwork@1';
diff --git a/apps/back-office/utils/members.types.ts b/apps/back-office/utils/members.types.ts
index 0544c383a..1ad4560ff 100644
--- a/apps/back-office/utils/members.types.ts
+++ b/apps/back-office/utils/members.types.ts
@@ -66,4 +66,5 @@ export interface IFormValues {
skills: Skill[];
openToWork: boolean;
projectContributions: IProjectContribution[];
+ teamOrProjectURL: string;
}
diff --git a/apps/back-office/utils/services/team.ts b/apps/back-office/utils/services/team.ts
index ac1c2121f..4ca1390d9 100644
--- a/apps/back-office/utils/services/team.ts
+++ b/apps/back-office/utils/services/team.ts
@@ -18,7 +18,7 @@ export const fetchTeamsForAutocomplete = async (searchTerm) => {
try {
const response = await api.get(`/v1/teams?name__istartswith=${searchTerm}`);
if (response.data) {
- return response.data.map((item) => {
+ return response.data?.teams?.map((item) => {
return { value: item.uid, label: item.name };
});
}
diff --git a/apps/web-api/prisma/fixtures/members.ts b/apps/web-api/prisma/fixtures/members.ts
index a53ca402e..8413b96c1 100644
--- a/apps/web-api/prisma/fixtures/members.ts
+++ b/apps/web-api/prisma/fixtures/members.ts
@@ -53,8 +53,24 @@ const membersFactory = Factory.define>(
plnStartDate: faker.date.past(),
updatedAt: faker.date.recent(),
locationUid: '',
+ signUpSource: faker.company.name(),
+ signUpCampaign: faker.company.name(),
+ signUpMedium: faker.company.name(),
+ isVerified: faker.datatype.boolean(),
+ isUserConsent: faker.datatype.boolean(),
+ isSubscribedToNewsletter: faker.datatype.boolean(),
+ teamOrProjectURL: faker.internet.url(),
openToWork: faker.datatype.boolean(),
- preferences: {showEmail:true,showGithubHandle:true,showTelegram:true,showLinkedin:true,showDiscord:false,showGithubProjects:false,showTwitter:true}
+ preferences: {
+ showEmail:true,
+ showGithubHandle:true,
+ showTelegram:true,
+ showLinkedin:true,
+ showDiscord:false,
+ showGithubProjects:false,
+ showTwitter:true,
+ showSubscription:true
+ }
};
}
);
diff --git a/apps/web-api/prisma/migrations/20241127093907_member_signup/migration.sql b/apps/web-api/prisma/migrations/20241127093907_member_signup/migration.sql
new file mode 100644
index 000000000..ea6e2f518
--- /dev/null
+++ b/apps/web-api/prisma/migrations/20241127093907_member_signup/migration.sql
@@ -0,0 +1,22 @@
+/*
+ Warnings:
+
+ - A unique constraint covering the columns `[memberUid,teamUid,eventUid]` on the table `PLEventGuest` will be added. If there are existing duplicate values, this will fail.
+
+*/
+-- AlterTable
+ALTER TABLE "Member"
+ADD COLUMN "isVerified" BOOLEAN DEFAULT false,
+ADD COLUMN "signUpSource" TEXT,
+ADD COLUMN "isSubscribedToNewsletter" BOOLEAN DEFAULT false,
+ADD COLUMN "isUserConsent" BOOLEAN DEFAULT false,
+ADD COLUMN "teamOrProjectURL" TEXT,
+ADD COLUMN "signUpCampaign" TEXT,
+ADD COLUMN "signUpMedium" TEXT;
+
+-- Modify the "plnFriend" column to drop NOT NULL constraint
+ALTER TABLE "Member"
+ALTER COLUMN "plnFriend" DROP NOT NULL;
+
+
+
diff --git a/apps/web-api/prisma/schema.prisma b/apps/web-api/prisma/schema.prisma
index b08be33b8..6b83e72af 100644
--- a/apps/web-api/prisma/schema.prisma
+++ b/apps/web-api/prisma/schema.prisma
@@ -66,12 +66,19 @@ model Member {
officeHours String?
moreDetails String?
bio String?
- plnFriend Boolean @default(false)
+ plnFriend Boolean? @default(false)
plnStartDate DateTime?
airtableRecId String? @unique
externalId String? @unique
openToWork Boolean? @default(false)
isFeatured Boolean? @default(false)
+ isVerified Boolean? @default(false)
+ signUpSource String?
+ signUpMedium String?
+ signUpCampaign String?
+ isUserConsent Boolean? @default(false)
+ isSubscribedToNewsletter Boolean? @default(false)
+ teamOrProjectURL String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
approvedAt DateTime @default(now())
@@ -408,7 +415,6 @@ model PLEventGuest {
isHost Boolean @default(false)
isSpeaker Boolean @default(false)
isFeatured Boolean @default(false)
- @@unique([memberUid, teamUid, eventUid])
}
model FocusArea {
diff --git a/apps/web-api/src/admin/admin.module.ts b/apps/web-api/src/admin/admin.module.ts
index 35195ad6c..076a07066 100644
--- a/apps/web-api/src/admin/admin.module.ts
+++ b/apps/web-api/src/admin/admin.module.ts
@@ -5,12 +5,14 @@ import { ParticipantsRequestModule } from '../participants-request/participants-
import { SharedModule } from '../shared/shared.module';
import { AdminParticipantsRequestController } from './participants-request.controller';
import { AdminAuthController } from './auth.controller';
+import { MemberController } from './member.controller';
+import { MembersModule } from '../members/members.module';
@Module({
- imports: [CacheModule.register(), ParticipantsRequestModule, SharedModule],
- controllers: [AdminParticipantsRequestController, AdminAuthController],
+ imports: [CacheModule.register(), ParticipantsRequestModule, SharedModule, MembersModule],
+ controllers: [AdminParticipantsRequestController, AdminAuthController, MemberController],
providers: [
AdminService,
JwtService
],
})
-export class AdminModule {}
+export class AdminModule { }
diff --git a/apps/web-api/src/admin/member.controller.ts b/apps/web-api/src/admin/member.controller.ts
new file mode 100644
index 000000000..d5c13e50b
--- /dev/null
+++ b/apps/web-api/src/admin/member.controller.ts
@@ -0,0 +1,37 @@
+import { Body, Controller, Param, Patch, Post, UseGuards } from '@nestjs/common';
+import { AdminAuthGuard } from '../guards/admin-auth.guard';
+import { MembersService } from '../members/members.service';
+
+@Controller('v1/admin/members')
+export class MemberController {
+ constructor(private readonly membersService: MembersService) { }
+
+ /**
+ * Updates a member to a verfied user.
+ *
+ * @param body - array of memberIds to be updated.
+ * @returns Array of updation status of the provided memberIds.
+ */
+ @Post("/")
+ @UseGuards(AdminAuthGuard)
+ async verifyMembers(@Body() body) {
+ const requestor = await this.membersService.findMemberByRole();
+ const { memberIds } = body;
+ return await this.membersService.verifyMembers(memberIds, requestor?.email);
+ }
+
+ /**
+ * Updates a member to a verfied user.
+ *
+ * @param body - participation request data with updated member details
+ * @returns updated member object
+ */
+ @Patch("/:uid")
+ @UseGuards(AdminAuthGuard)
+ async updateMemberAndVerify(@Param('uid') uid, @Body() participantsRequest) {
+ const requestor = await this.membersService.findMemberByRole();
+ const requestorEmail = requestor?.email ?? '';
+ return await this.membersService.updateMemberFromParticipantsRequest(uid, participantsRequest, requestorEmail, true);
+ }
+
+}
diff --git a/apps/web-api/src/admin/participants-request.controller.ts b/apps/web-api/src/admin/participants-request.controller.ts
index 1a0a1d027..b633bcbcd 100644
--- a/apps/web-api/src/admin/participants-request.controller.ts
+++ b/apps/web-api/src/admin/participants-request.controller.ts
@@ -9,13 +9,14 @@ import {
UseGuards,
UsePipes,
BadRequestException,
- NotFoundException
+ NotFoundException,
+ Post
} from '@nestjs/common';
import { NoCache } from '../decorators/no-cache.decorator';
import { ParticipantsRequestService } from '../participants-request/participants-request.service';
import { AdminAuthGuard } from '../guards/admin-auth.guard';
import { ParticipantsReqValidationPipe } from '../pipes/participant-request-validation.pipe';
-import { ProcessParticipantReqDto } from 'libs/contracts/src/schema';
+import { ProcessBulkParticipantRequest, ProcessParticipantReqDto } from 'libs/contracts/src/schema';
import { ApprovalStatus, ParticipantsRequest, ParticipantType } from '@prisma/client';
@Controller('v1/admin/participants-request')
@@ -23,7 +24,20 @@ import { ApprovalStatus, ParticipantsRequest, ParticipantType } from '@prisma/cl
export class AdminParticipantsRequestController {
constructor(
private readonly participantsRequestService: ParticipantsRequestService
- ) {}
+ ) { }
+
+ /**
+ * Process (approve/reject) multiple pending participants requests.
+ * @param body - The request body containing array of uids and status of participants to be processed;
+ * @returns The result of processing the participants request
+ */
+ @Post('/')
+ async processBulkRequest(
+ @Body() body: ProcessBulkParticipantRequest[]
+ ): Promise {
+ const participationRequests = body;
+ return await this.participantsRequestService.processBulkRequest(participationRequests);
+ }
/**
* Retrieve all participants requests based on query parameters.
@@ -87,7 +101,7 @@ export class AdminParticipantsRequestController {
'Requester email is required for team participation requests. Please provide a valid email address.'
);
}
- return await this.participantsRequestService.processRequestByUid(uid, participantRequest, body.status);
+ return await this.participantsRequestService.processRequestByUid(uid, participantRequest, body.status, body.isVerified);
}
+
}
-
\ No newline at end of file
diff --git a/apps/web-api/src/auth/auth.module.ts b/apps/web-api/src/auth/auth.module.ts
index c3482180c..03ab0ff58 100644
--- a/apps/web-api/src/auth/auth.module.ts
+++ b/apps/web-api/src/auth/auth.module.ts
@@ -4,12 +4,11 @@ import { AuthController } from './auth.controller';
import { AuthService } from './auth.service';
import { HttpModule } from '@nestjs/axios';
import { PrismaService } from '../shared/prisma.service';
-import { RedisService } from '../utils/redis/redis.service';
import { OtpModule } from '../otp/otp.module';
@Module({
imports: [HttpModule, OtpModule],
controllers: [AuthController],
- providers: [AuthService, PrismaService, RedisService],
+ providers: [AuthService, PrismaService],
exports: [AuthService]
})
export class AuthModule {}
diff --git a/apps/web-api/src/home/home.controller.ts b/apps/web-api/src/home/home.controller.ts
index 30c352def..ecd0872a0 100644
--- a/apps/web-api/src/home/home.controller.ts
+++ b/apps/web-api/src/home/home.controller.ts
@@ -6,15 +6,17 @@ import { ApiQueryFromZod } from '../decorators/api-query-from-zod';
import { ApiOkResponseFromZod } from '../decorators/api-response-from-zod';
import { apiHome } from 'libs/contracts/src/lib/contract-home';
import { HomeService } from './home.service';
-import {
+import {
DiscoveryQuestionQueryParams,
ResponseDiscoveryQuestionSchemaWithRelations,
ResponseDiscoveryQuestionSchema,
CreateDiscoveryQuestionSchemaDto,
- UpdateDiscoveryQuestionSchemaDto
+ UpdateDiscoveryQuestionSchemaDto,
+ TeamQueryParams,
+ MemberQueryParams
} from 'libs/contracts/src/schema';
import { UserTokenValidation } from '../guards/user-token-validation.guard';
-import { MembersService } from '../members/members.service';
+import { MembersService } from '../members/members.service';
import { NoCache } from '../decorators/no-cache.decorator';
import { PrismaQueryBuilder } from '../utils/prisma-query-builder';
import { prismaQueryableFieldsFromZod } from '../utils/prisma-queryable-fields-from-zod';
@@ -28,15 +30,15 @@ export class HomeController {
constructor(
private homeService: HomeService,
private memberService: MembersService,
- private huskyService: HuskyService
- ) {}
-
+ private huskyService: HuskyService
+ ) { }
+
@Api(server.route.getAllFeaturedData)
async getAllFeaturedData() {
return await this.homeService.fetchAllFeaturedData();
}
- @Api(server.route.getAllDiscoveryQuestions)
+ @Api(server.route.getAllDiscoveryQuestions)
@ApiQueryFromZod(DiscoveryQuestionQueryParams)
@ApiOkResponseFromZod(ResponseDiscoveryQuestionSchemaWithRelations.array())
@NoCache()
@@ -50,12 +52,11 @@ export class HomeController {
}
- @Api(server.route.getDiscoveryQuestion)
+ @Api(server.route.getDiscoveryQuestion)
@ApiQueryFromZod(DiscoveryQuestionQueryParams)
@ApiOkResponseFromZod(ResponseDiscoveryQuestionSchemaWithRelations)
@NoCache()
- async getDiscoveryQuestion(@Param('slug') slug: string)
- {
+ async getDiscoveryQuestion(@Param('slug') slug: string) {
return await this.huskyService.fetchDiscoverQuestionBySlug(slug);
}
@@ -99,7 +100,7 @@ export class HomeController {
) {
const attribute = body.attribute;
switch (attribute) {
- case "shareCount":
+ case "shareCount":
return this.huskyService.updateDiscoveryQuestionShareCount(slug);
case "viewCount":
return this.huskyService.updateDiscoveryQuestionViewCount(slug);
@@ -107,4 +108,19 @@ export class HomeController {
throw new BadRequestException(`Invalid attribute: ${attribute}`);
}
}
+
+ /**
+ * Retrieves a list of teams and projects based on search query.
+ *
+ * @param request - HTTP request object containing query parameters
+ * @returns Array of projects and teams.
+ */
+ @Api(server.route.getTeamsAndProjects)
+ @ApiQueryFromZod(TeamQueryParams)
+ @ApiQueryFromZod(MemberQueryParams)
+ @NoCache()
+ async getTeamsAndProjects(@Req() request: Request) {
+ const queryParams = request.query;
+ return this.homeService.fetchTeamsAndProjects(queryParams);
+ }
}
diff --git a/apps/web-api/src/home/home.service.ts b/apps/web-api/src/home/home.service.ts
index 8954b2ba8..0a0e329d1 100644
--- a/apps/web-api/src/home/home.service.ts
+++ b/apps/web-api/src/home/home.service.ts
@@ -1,4 +1,4 @@
-import {
+import {
Injectable,
InternalServerErrorException
} from '@nestjs/common';
@@ -6,7 +6,6 @@ import { MembersService } from '../members/members.service';
import { TeamsService } from '../teams/teams.service';
import { PLEventsService } from '../pl-events/pl-events.service';
import { ProjectsService } from '../projects/projects.service';
-
@Injectable()
export class HomeService {
constructor(
@@ -14,7 +13,7 @@ export class HomeService {
private teamsService: TeamsService,
private plEventsService: PLEventsService,
private projectsService: ProjectsService
- ) {}
+ ) { }
async fetchAllFeaturedData() {
try {
@@ -45,4 +44,85 @@ export class HomeService {
throw new InternalServerErrorException(`Error occured while retrieving featured data: ${error.message}`);
}
}
+
+ /**
+ * Retrieves a list of teams and projects based on search term.
+ * Builds a Prisma query from the queryable fields and adds filters for team and project name.
+ *
+ * @param request - HTTP request object containing query parameters
+ * @returns Array of projects and teams.
+ */
+ async fetchTeamsAndProjects(queryParams) {
+ let result: any[] = []
+ const entities: string[] = queryParams.include?.split(",");
+ if (entities.includes('teams')) {
+ const resultantTeams = await this.fetchTeamsBySearchTerm(queryParams.name);
+ resultantTeams.teams.forEach((team) => result.push({ ...team, category: "TEAM" }));
+ }
+ if (entities.includes('projects')) {
+ const resultantProjects = await this.fetchProjectsBySearchTerm(queryParams.name);
+ resultantProjects?.projects.forEach((project) => result.push({ ...project, category: "PROJECT" }));
+ }
+ return [...result].sort((team, project) => team.name.localeCompare(project.name))
+ }
+
+ /**
+ * Retrieves a list of teams based on search term.
+ * Builds a Prisma query from the queryable fields and adds filters for team name.
+ *
+ * @param name - name of the team to be searched for.
+ * @returns Array of resultant teams.
+ */
+ private fetchTeamsBySearchTerm(name) {
+ return this.teamsService.findAll({
+ where: {
+ name: {
+ startsWith: name,
+ mode: 'insensitive'
+ }
+ },
+ select: {
+ uid: true,
+ name: true,
+ logo: {
+ select: {
+ url: true,
+ }
+ }
+ },
+ orderBy: {
+ name: 'asc'
+ }
+ })
+ }
+
+ /**
+ * Retrieves a list of projects based on search term.
+ * Builds a Prisma query from the queryable fields and adds filters for project name.
+ *
+ * @param name - name of the project to be searched for.
+ * @returns Array of resultant projects.
+ */
+ private fetchProjectsBySearchTerm(name) {
+ return this.projectsService.getProjects({
+ where: {
+ name: {
+ startsWith: name,
+ mode: 'insensitive'
+ }
+ },
+ select: {
+ uid: true,
+ name: true,
+ logo: {
+ select: {
+ url: true,
+ }
+ }
+ },
+ orderBy: {
+ name: 'asc'
+ }
+ })
+ }
}
diff --git a/apps/web-api/src/interceptors/verified-member.interceptor.ts b/apps/web-api/src/interceptors/verified-member.interceptor.ts
new file mode 100644
index 000000000..fd719b4f0
--- /dev/null
+++ b/apps/web-api/src/interceptors/verified-member.interceptor.ts
@@ -0,0 +1,15 @@
+import { Injectable, NestInterceptor, ExecutionContext, CallHandler } from '@nestjs/common';
+import { Observable } from 'rxjs';
+
+@Injectable()
+export class IsVerifiedMemberInterceptor implements NestInterceptor {
+ intercept(context: ExecutionContext, next: CallHandler): Observable {
+ const request = context.switchToHttp().getRequest();
+ if (request.query.isVerified === 'all') {
+ delete request.query.isVerified;
+ } else if (request.query.isVerified !== 'false') {
+ request.query.isVerified = 'true';
+ }
+ return next.handle();
+ }
+}
diff --git a/apps/web-api/src/members/members.controller.ts b/apps/web-api/src/members/members.controller.ts
index 5102acdfd..fd04d3616 100644
--- a/apps/web-api/src/members/members.controller.ts
+++ b/apps/web-api/src/members/members.controller.ts
@@ -1,4 +1,4 @@
-import { Body, Controller, Param, Req, UseGuards, UsePipes, BadRequestException, ForbiddenException } from '@nestjs/common';
+import { Body, Controller, Param, Req, UseGuards, UsePipes, UseInterceptors, BadRequestException, ForbiddenException } from '@nestjs/common';
import { ApiNotFoundResponse, ApiParam } from '@nestjs/swagger';
import { Api, ApiDecorator, initNestServer } from '@ts-rest/nest';
import { Request } from 'express';
@@ -24,6 +24,8 @@ import { AuthGuard } from '../guards/auth.guard';
import { UserAccessTokenValidateGuard } from '../guards/user-access-token-validate.guard';
import { LogService } from '../shared/log.service';
import { ParticipantsReqValidationPipe } from '../pipes/participant-request-validation.pipe';
+import { IsVerifiedMemberInterceptor } from '../interceptors/verified-member.interceptor';
+import { isEmpty } from 'lodash';
const server = initNestServer(apiMembers);
type RouteShape = typeof server.routeShapes;
@@ -31,8 +33,8 @@ type RouteShape = typeof server.routeShapes;
@Controller()
@NoCache()
export class MemberController {
- constructor(private readonly membersService: MembersService, private logger: LogService) {}
-
+ constructor(private readonly membersService: MembersService, private logger: LogService) { }
+
/**
* Retrieves a list of members based on query parameters.
* Builds a Prisma query from the queryable fields and adds filters for names, roles, and recent members.
@@ -43,6 +45,8 @@ export class MemberController {
@Api(server.route.getMembers)
@ApiQueryFromZod(MemberQueryParams)
@ApiOkResponseFromZod(ResponseMemberWithRelationsSchema.array())
+ @UseInterceptors(IsVerifiedMemberInterceptor)
+ @NoCache()
async findAll(@Req() request: Request) {
const queryableFields = prismaQueryableFieldsFromZod(ResponseMemberWithRelationsSchema);
const queryParams = request.query;
@@ -71,7 +75,9 @@ export class MemberController {
* @returns Array of roles with member counts
*/
@Api(server.route.getMemberRoles)
- async getMemberFilters(@Req() request: Request) {
+ @UseInterceptors(IsVerifiedMemberInterceptor)
+ @NoCache()
+ async getMemberRoleFilters(@Req() request: Request) {
const queryableFields = prismaQueryableFieldsFromZod(ResponseMemberWithRelationsSchema);
const queryParams = request.query;
const builder = new PrismaQueryBuilder(queryableFields);
@@ -90,6 +96,35 @@ export class MemberController {
return await this.membersService.getRolesWithCount(builtQuery, queryParams);
}
+ /**
+ * Retrieves member filters.
+ *
+ * @param request - HTTP request object containing query parameters
+ * @returns return list of member filters.
+ */
+ @Api(server.route.getMemberFilters)
+ @UseInterceptors(IsVerifiedMemberInterceptor)
+ @NoCache()
+ async getMembersFilter(@Req() request: Request) {
+ const queryableFields = prismaQueryableFieldsFromZod(ResponseMemberWithRelationsSchema);
+ const queryParams = request.query;
+ const builder = new PrismaQueryBuilder(queryableFields);
+ const builtQuery = builder.build(queryParams);
+ const { name__icontains } = queryParams;
+ if (name__icontains) {
+ delete builtQuery.where?.name;
+ }
+ builtQuery.where = {
+ AND: [
+ builtQuery.where,
+ this.membersService.buildNameFilters(queryParams),
+ this.membersService.buildRoleFilters(queryParams),
+ this.membersService.buildRecentMembersFilter(queryParams)
+ ],
+ };
+ return await this.membersService.getMemberFilters(builtQuery);
+ }
+
/**
* Retrieves details of a specific member by UID.
* Builds a query for member details including relations and profile data.
@@ -134,7 +169,10 @@ export class MemberController {
) {
throw new ForbiddenException(`Member isn't authorized to update the member`);
}
- return await this.membersService.updateMemberFromParticipantsRequest(uid, participantsRequest, requestor.email);
+ if (!isEmpty(participantsRequest.newData.isVerified) && !this.membersService.checkIfAdminUser(requestor)) {
+ throw new ForbiddenException(`Member isn't authorized to verify a member`);
+ }
+ return await this.membersService.updateMemberFromParticipantsRequest(uid, participantsRequest, requestor.email, requestor.isDirectoryAdmin);
}
/**
@@ -154,7 +192,18 @@ export class MemberController {
@Api(server.route.updateMember)
@UseGuards(UserTokenValidation)
- async updateMemberByUid(@Param('uid') uid, @Body() body) {
+ async updateMemberByUid(@Param('uid') uid, @Body() body, @Req() req) {
+ this.logger.info(`Member update request - Initated by -> ${req.userEmail}`);
+ const requestor = await this.membersService.findMemberByEmail(req.userEmail);
+ if (
+ !requestor.isDirectoryAdmin &&
+ uid !== requestor.uid
+ ) {
+ throw new ForbiddenException(`Member isn't authorized to update the member`);
+ }
+ if (!isEmpty(body.isVerified) && !this.membersService.checkIfAdminUser(requestor)) {
+ throw new ForbiddenException(`Member isn't authorized to verify a member`);
+ }
return await this.membersService.updateMemberByUid(uid, body);
}
@@ -209,7 +258,7 @@ export class MemberController {
@UsePipes(ZodValidationPipe)
async updateMemberEmail(@Body() changeEmailRequest: ChangeEmailRequestDto, @Req() req) {
const memberInfo = await this.membersService.findMemberByEmail(req.userEmail);
- if(!memberInfo || !memberInfo.externalId) {
+ if (!memberInfo || !memberInfo.externalId) {
throw new ForbiddenException("Please login again and try")
}
return await this.membersService.updateMemberEmail(changeEmailRequest.newEmail, req.userEmail, memberInfo);
diff --git a/apps/web-api/src/members/members.service.ts b/apps/web-api/src/members/members.service.ts
index 12cd54869..8c6852a9d 100644
--- a/apps/web-api/src/members/members.service.ts
+++ b/apps/web-api/src/members/members.service.ts
@@ -1,7 +1,6 @@
/* eslint-disable prettier/prettier */
import {
BadRequestException,
- CACHE_MANAGER,
ConflictException,
NotFoundException,
Inject,
@@ -11,7 +10,6 @@ import {
import { z } from 'zod';
import axios from 'axios';
import * as path from 'path';
-import { Cache } from 'cache-manager';
import { Prisma, Member, ParticipantsRequest } from '@prisma/client';
import { PrismaService } from '../shared/prisma.service';
import { ParticipantsRequestService } from '../participants-request/participants-request.service';
@@ -26,6 +24,7 @@ import { LogService } from '../shared/log.service';
import { DEFAULT_MEMBER_ROLES } from '../utils/constants';
import { hashFileName } from '../utils/hashing';
import { copyObj, buildMultiRelationMapping } from '../utils/helper/helper';
+import { CacheService } from '../utils/cache/cache.service';
@Injectable()
export class MembersService {
@@ -41,9 +40,8 @@ export class MembersService {
private participantsRequestService: ParticipantsRequestService,
@Inject(forwardRef(() => NotificationService))
private notificationService: NotificationService,
- @Inject(CACHE_MANAGER) private cacheService: Cache
-
- ) {}
+ private cacheService: CacheService
+ ) { }
/**
* Creates a new member in the database within a transaction.
@@ -60,9 +58,9 @@ export class MembersService {
return await tx.member.create({
data: member,
});
- } catch(error) {
+ } catch (error) {
return this.handleErrors(error);
- }
+ }
}
/**
@@ -75,10 +73,14 @@ export class MembersService {
* the members. These options are based on Prisma's `MemberFindManyArgs`.
* @returns A promise that resolves to an array of member records matching the query criteria.
*/
- async findAll(queryOptions: Prisma.MemberFindManyArgs): Promise {
+ async findAll(queryOptions: Prisma.MemberFindManyArgs): Promise<{ count: Number, members: Member[] }> {
try {
- return await this.prisma.member.findMany(queryOptions);
- } catch(error) {
+ const [members, membersCount] = await Promise.all([
+ this.prisma.member.findMany(queryOptions),
+ this.prisma.member.count({ where: queryOptions.where }),
+ ]);
+ return { count: membersCount, members: members }
+ } catch (error) {
return this.handleErrors(error);
}
}
@@ -174,6 +176,9 @@ export class MembersService {
formattedDefaultRoles.push({ ...defaultRole, count: 0 });
}
});
+ if (!searchTerm) {
+ return formattedDefaultRoles;
+ }
const result: any = await this.getRoleCountForExcludedAndNonSelectedRoles(selectedRoles, members, searchTerm);
return [...formattedDefaultRoles, ...result];
} catch (error) {
@@ -203,7 +208,7 @@ export class MembersService {
where: { uid },
data: member,
});
- } catch(error) {
+ } catch (error) {
return this.handleErrors(error);
}
}
@@ -252,7 +257,7 @@ export class MembersService {
},
},
});
- } catch(error) {
+ } catch (error) {
return this.handleErrors(error);
}
}
@@ -274,8 +279,8 @@ export class MembersService {
teamMemberRoles: true,
projectContributions: true,
}
- });
- } catch(error) {
+ });
+ } catch (error) {
return this.handleErrors(error);
}
}
@@ -285,20 +290,20 @@ export class MembersService {
* @param tx - Prisma transaction client or Prisma client.
* @param uid - Member UID to fetch.
*/
- async findMemberByUid(uid: string, tx: Prisma.TransactionClient = this.prisma){
+ async findMemberByUid(uid: string, tx: Prisma.TransactionClient = this.prisma) {
try {
return tx.member.findUniqueOrThrow({
where: { uid },
- include: {
- image: true,
- location: true,
- skills: true,
- teamMemberRoles: true,
- memberRoles: true,
+ include: {
+ image: true,
+ location: true,
+ skills: true,
+ teamMemberRoles: true,
+ memberRoles: true,
projectContributions: true
},
- });
- } catch(error) {
+ });
+ } catch (error) {
return this.handleErrors(error);
}
}
@@ -313,11 +318,11 @@ export class MembersService {
try {
return await this.prisma.member.findUniqueOrThrow({
where: { email: email.toLowerCase().trim() },
- include: {
- memberRoles: true
+ include: {
+ memberRoles: true
},
});
- } catch(error) {
+ } catch (error) {
return this.handleErrors(error);
}
}
@@ -357,7 +362,7 @@ export class MembersService {
.filter((role) => role.teamLead)
.map((role) => role.teamUid),
};
- } catch(error) {
+ } catch (error) {
return this.handleErrors(error);
}
}
@@ -390,7 +395,7 @@ export class MembersService {
*
* @throws If any operation within the transaction fails, the entire transaction is rolled back.
*/
- async updateMemberEmail(newEmail:string, oldEmail:string, memberInfo) {
+ async updateMemberEmail(newEmail: string, oldEmail: string, memberInfo) {
try {
let newTokens;
let newMemberInfo;
@@ -401,33 +406,34 @@ export class MembersService {
referenceUid: memberInfo.uid,
uniqueIdentifier: oldEmail,
participantType: 'MEMBER',
- newData: {
- oldEmail: oldEmail,
- email: newEmail
- }},
+ newData: {
+ oldEmail: oldEmail,
+ email: newEmail
+ }
+ },
false,
tx
);
newMemberInfo = await tx.member.update({
- where: { email: oldEmail.toLowerCase().trim()},
- data: { email: newEmail.toLowerCase().trim()},
- include: {
- memberRoles: true,
- image: true,
- teamMemberRoles: true,
- }
- })
+ where: { email: oldEmail.toLowerCase().trim() },
+ data: { email: newEmail.toLowerCase().trim() },
+ include: {
+ memberRoles: true,
+ image: true,
+ teamMemberRoles: true,
+ }
+ })
newTokens = await this.authService.updateEmailInAuth(newEmail, oldEmail, memberInfo.externalId)
});
this.logger.info(`Email has been successfully updated from ${oldEmail} to ${newEmail}`)
- await this.cacheService.reset();
+ await this.cacheService.reset({ service: 'members' });
return {
refreshToken: newTokens.refresh_token,
idToken: newTokens.id_token,
accessToken: newTokens.access_token,
userInfo: this.memberToUserInfo(newMemberInfo)
};
- } catch(error) {
+ } catch (error) {
return this.handleErrors(error);
}
}
@@ -481,7 +487,7 @@ export class MembersService {
where: { email: emailId.toLowerCase().trim() },
data: { externalId },
});
- } catch(error){
+ } catch (error) {
return this.handleErrors(error);
}
}
@@ -499,7 +505,7 @@ export class MembersService {
select: { githubHandler: true },
});
return member?.githubHandler || null;
- } catch(error) {
+ } catch (error) {
return this.handleErrors(error);
}
}
@@ -622,13 +628,16 @@ export class MembersService {
const memberData: any = memberParticipantRequest.newData;
const member = await this.prepareMemberFromParticipantRequest(null, memberData, null, tx);
await this.mapLocationToMember(memberData, null, member, tx);
- return await this.createMember(member, tx);
+ const createdMember = await this.createMember(member, tx);
+ await this.postCreateActions();
+ return createdMember;
}
-
+
async updateMemberFromParticipantsRequest(
memberUid: string,
memberParticipantsRequest: ParticipantsRequest,
- requestorEmail: string
+ requestorEmail: string,
+ isDirectoryAdmin = false
): Promise {
let result;
await this.prisma.$transaction(async (tx) => {
@@ -640,22 +649,24 @@ export class MembersService {
const member = await this.prepareMemberFromParticipantRequest(memberUid, memberData, existingMember, tx, 'Update');
await this.mapLocationToMember(memberData, existingMember, member, tx);
result = await this.updateMemberByUid(
- memberUid,
+ memberUid,
{
...member,
- ...(isEmailChanged && isExternalIdAvailable && { externalId: null })
- },
+ ...(isEmailChanged && isExternalIdAvailable && { externalId: null }),
+ },
tx
);
await this.updateMemberEmailChange(memberUid, isEmailChanged, isExternalIdAvailable, memberData, existingMember);
await this.logParticipantRequest(requestorEmail, memberData, existingMember.uid, tx);
- this.notificationService.notifyForMemberEditApproval(memberData.name, memberUid, requestorEmail);
+ if (isEmailChanged && isDirectoryAdmin) {
+ this.notificationService.notifyForMemberChangesByAdmin(memberData.name, memberUid, existingMember.email, memberData.email);
+ }
this.logger.info(`Member update request - completed, requestId -> ${result.uid}, requestor -> ${requestorEmail}`)
});
await this.postUpdateActions();
return result;
}
-
+
/**
* Checks if the email has changed during update and verifies if the new email is already in use.
*
@@ -700,16 +711,20 @@ export class MembersService {
const member: any = {};
const directFields = [
'name', 'email', 'githubHandler', 'discordHandler', 'bio',
- 'twitterHandler', 'linkedinHandler', 'telegramHandler',
- 'officeHours', 'moreDetails', 'plnStartDate', 'openToWork'
+ 'twitterHandler', 'linkedinHandler', 'telegramHandler',
+ 'officeHours', 'moreDetails', 'plnStartDate', 'openToWork',
+ 'isVerified', 'signUpSource', 'signUpMedium', 'signUpCampaign',
+ 'isUserConsent', 'isSubscribedToNewsletter', 'teamOrProjectURL',
];
copyObj(memberData, member, directFields);
member.email = member.email.toLowerCase().trim();
- member['image'] = memberData.imageUid ? { connect: { uid: memberData.imageUid } }
- : type === 'Update' ? { disconnect: true } : undefined ;
+ member['image'] = memberData.imageUid ? { connect: { uid: memberData.imageUid } }
+ : type === 'Update' ? { disconnect: true } : undefined;
member['skills'] = buildMultiRelationMapping('skills', memberData, type);
if (type === 'Create') {
- member['teamMemberRoles'] = this.buildTeamMemberRoles(memberData);
+ if (Array.isArray(memberData.teamAndRoles)) {
+ member['teamMemberRoles'] = this.buildTeamMemberRoles(memberData);
+ }
if (Array.isArray(memberData.projectContributions)) {
member['projectContributions'] = {
createMany: { data: memberData.projectContributions },
@@ -717,7 +732,7 @@ export class MembersService {
}
} else {
await this.updateProjectContributions(memberData, existingMember, memberUid, tx);
- await this.updateTeamMemberRoles(memberData, existingMember, memberUid, tx);
+ await this.updateTeamMemberRoles(memberData, existingMember, memberUid, tx);
}
return member;
}
@@ -759,7 +774,7 @@ export class MembersService {
throw new BadRequestException('Invalid Location info');
}
} else {
- if (existingMember) {
+ if (existingMember) {
member['location'] = { disconnect: true };
}
}
@@ -919,7 +934,7 @@ export class MembersService {
},
};
}
-
+
/**
* function to handle creation, updating, and deletion of project contributions
* with fewer database calls by using batch operations.
@@ -1101,10 +1116,51 @@ export class MembersService {
participantType: 'MEMBER',
newData: { ...newMemberData },
},
- tx
+ tx
);
}
+ /**
+ * Verify the list of members and log into participant request.
+ * @param memberIds array of member IDs
+ * @param userEmail logged in member email
+ * @returns result
+ */
+ async verifyMembers(memberIds: string[], userEmail): Promise {
+ return await this.prisma.$transaction(async (tx) => {
+ const result = await tx.member.updateMany({
+ where: { uid: { in: memberIds } },
+ data: {
+ isVerified: true
+ }
+ });
+ if (result.count !== memberIds.length) {
+ throw new NotFoundException('One or more member IDs are invalid.');
+ }
+ const members = await tx.member.findMany({
+ where: { uid: { in: memberIds } }
+ })
+ await Promise.all(members.map(async (member) => {
+ await this.participantsRequestService.add({
+ status: 'AUTOAPPROVED',
+ requesterEmailId: userEmail,
+ referenceUid: member.uid,
+ uniqueIdentifier: member?.email || '',
+ participantType: 'MEMBER',
+ oldData: {
+ isVerified: false
+ },
+ newData: {
+ isVerified: true
+ },
+ },
+ tx
+ );
+ }));
+ return result;
+ });
+ }
+
/**
* Updates the member's preferences and resets the cache.
*
@@ -1114,16 +1170,25 @@ export class MembersService {
*/
async updatePreference(id: string, preferences: any): Promise {
const updatedMember = await this.updateMemberByUid(id, { preferences });
- await this.cacheService.reset();
+ await this.cacheService.reset({ service: 'members' });
return updatedMember;
}
+ /**
+ * Executes post-create actions such as resetting the cache and triggering Airtable sync.
+ * This ensures that the system is up-to-date with the latest changes.
+ */
+ private async postCreateActions(): Promise {
+ await this.cacheService.reset({ service: 'members'});
+ await this.forestadminService.triggerAirtableSync();
+ }
+
/**
* Executes post-update actions such as resetting the cache and triggering Airtable sync.
* This ensures that the system is up-to-date with the latest changes.
*/
private async postUpdateActions(): Promise {
- await this.cacheService.reset();
+ await this.cacheService.reset({ service: 'members' });
await this.forestadminService.triggerAirtableSync();
}
@@ -1144,6 +1209,7 @@ export class MembersService {
linkedinHandler: true,
twitterHandler: true,
preferences: true,
+ isSubscribedToNewsletter: true
},
});
return this.buildPreferenceResponse(member);
@@ -1168,6 +1234,7 @@ export class MembersService {
preferences.discord = !!member.discordHandler;
preferences.linkedin = !!member.linkedinHandler;
preferences.twitter = !!member.twitterHandler;
+ preferences.subscription = !!member.isSubscribedToNewsletter;
return preferences;
}
@@ -1213,7 +1280,7 @@ export class MembersService {
* @returns Constructed query with a 'createdAt' filter if 'recent' is set to 'true',
* or an empty object if 'recent' is not provided or set to 'false'.
*/
- buildRecentMembersFilter(queryParams) {
+ buildRecentMembersFilter(queryParams) {
const { isRecent } = queryParams;
if (isRecent === 'true') {
return {
@@ -1232,7 +1299,7 @@ export class MembersService {
* @returns Constructed query based on given member role input
*/
buildRoleFilters(queryParams) {
- const { memberRoles } : any = queryParams;
+ const { memberRoles }: any = queryParams;
const roles = memberRoles?.split(',');
if (roles?.length > 0) {
return {
@@ -1257,41 +1324,92 @@ export class MembersService {
if (name__icontains) {
return {
OR: [
- { name: {
+ {
+ name: {
contains: name__icontains,
mode: 'insensitive'
}
},
- { teamMemberRoles : {
+ {
+ teamMemberRoles: {
some: {
- team: {
+ team: {
name: {
contains: name__icontains,
mode: 'insensitive'
- }
- }
- }
+ }
+ }
+ }
}
},
- {
- projectContributions : {
+ {
+ projectContributions: {
some: {
- project: {
+ project: {
name: {
contains: name__icontains,
mode: 'insensitive'
},
- isDeleted: false
- }
- }
+ isDeleted: false
+ }
+ }
}
}
]
}
}
- return { };
+ return {};
+ }
+
+ /**
+ * Fetches filter tags for members for facilitating easy searching.
+ * @param queryParams HTTP request query params object
+ * @returns Set of skills, locations that contain at least one member.
+ */
+ async getMemberFilters(queryParams) {
+ const skills = await this.prisma.skill.findMany({
+ where: {
+ members: {
+ some: { ...queryParams.where },
+ },
+ },
+ select: {
+ title: true,
+ },
+ });
+ const locations = await this.prisma.location.findMany({
+ where: {
+ members: {
+ some: { ...queryParams.where },
+ },
+ },
+ select: {
+ city: true,
+ continent: true,
+ country: true,
+ region: true,
+ metroArea: true
+ },
+ });
+
+ // Deduplicate cities, countries, and regions using Set
+ const uniqueCities = [...new Set(locations.map((location) => location.city).filter(Boolean))];
+ const uniqueCountries = [...new Set(locations.map((location) => location.country).filter(Boolean))];
+ const uniqueRegions = [...new Set(locations.map((location) => location.continent).filter(Boolean))];
+ const uniqueMetroAreas = [...new Set(locations.map((location) => location.metroArea).filter(Boolean))];
+
+ // Return deduplicated skills and locations
+ return {
+ skills: skills.map((skill) => skill.title),
+ cities: uniqueCities,
+ countries: uniqueCountries,
+ regions: uniqueRegions,
+ metroAreas: uniqueMetroAreas
+ };
}
+
+
/**
* Updates the member's field if the value has changed.
*
@@ -1380,7 +1498,7 @@ export class MembersService {
}
return error;
}
-
+
async insertManyWithLocationsFromAirtable(
airtableMembers: z.infer[]
@@ -1469,4 +1587,20 @@ export class MembersService {
});
}
}
+
+ async findMemberByRole() {
+ const member = await this.prisma.member.findFirst({
+ where: {
+ memberRoles: {
+ some: {
+ name: 'DIRECTORYADMIN', // Adjust this based on the actual field name in your schema
+ },
+ },
+ },
+ select: {
+ email: true
+ }
+ });
+ return member;
+ }
}
diff --git a/apps/web-api/src/participants-request/participants-request.service.ts b/apps/web-api/src/participants-request/participants-request.service.ts
index 7c022a551..e549454f4 100644
--- a/apps/web-api/src/participants-request/participants-request.service.ts
+++ b/apps/web-api/src/participants-request/participants-request.service.ts
@@ -2,14 +2,12 @@
import {
BadRequestException,
ConflictException,
- NotFoundException,
- Inject,
- Injectable,
- CACHE_MANAGER,
- forwardRef
+ NotFoundException,
+ Injectable,
+ forwardRef,
+ Inject
} from '@nestjs/common';
import { ApprovalStatus, ParticipantType } from '@prisma/client';
-import { Cache } from 'cache-manager';
import { Prisma, ParticipantsRequest, PrismaClient } from '@prisma/client';
import { generateProfileURL } from '../utils/helper/helper';
import { LogService } from '../shared/log.service';
@@ -19,6 +17,7 @@ import { TeamsService } from '../teams/teams.service';
import { NotificationService } from '../utils/notification/notification.service';
import { LocationTransferService } from '../utils/location-transfer/location-transfer.service';
import { ForestAdminService } from '../utils/forest-admin/forest-admin.service';
+import { CacheService } from '../utils/cache/cache.service';
@Injectable()
export class ParticipantsRequestService {
@@ -28,13 +27,12 @@ export class ParticipantsRequestService {
private locationTransferService: LocationTransferService,
private forestAdminService: ForestAdminService,
private notificationService: NotificationService,
- @Inject(CACHE_MANAGER)
- private cacheService: Cache,
+ private cacheService: CacheService,
@Inject(forwardRef(() => MembersService))
private membersService: MembersService,
- @Inject(forwardRef(() => TeamsService))
+ @Inject(forwardRef(() => TeamsService))
private teamsService: TeamsService,
- ) {}
+ ) { }
/**
* Find all participant requests based on the query.
@@ -63,7 +61,7 @@ export class ParticipantsRequestService {
where: filters,
orderBy: { createdAt: 'desc' },
});
- } catch(err) {
+ } catch (err) {
return this.handleErrors(err)
}
}
@@ -77,13 +75,13 @@ export class ParticipantsRequestService {
*/
async add(
newEntry: Prisma.ParticipantsRequestUncheckedCreateInput,
- tx?: Prisma.TransactionClient,
+ tx?: Prisma.TransactionClient,
): Promise {
try {
return await (tx || this.prisma).participantsRequest.create({
data: { ...newEntry },
});
- } catch(err) {
+ } catch (err) {
return this.handleErrors(err)
}
}
@@ -99,7 +97,7 @@ export class ParticipantsRequestService {
return await this.prisma.participantsRequest.findUnique({
where: { uid },
});
- } catch(err) {
+ } catch (err) {
return this.handleErrors(err, uid)
}
}
@@ -115,9 +113,9 @@ export class ParticipantsRequestService {
async checkIfIdentifierAlreadyExist(
type: ParticipantType,
identifier: string
- ): Promise<{
- isRequestPending: boolean;
- isUniqueIdentifierExist: boolean
+ ): Promise<{
+ isRequestPending: boolean;
+ isUniqueIdentifierExist: boolean
}> {
try {
const existingRequest = await this.prisma.participantsRequest.findFirst({
@@ -130,16 +128,16 @@ export class ParticipantsRequestService {
if (existingRequest) {
return { isRequestPending: true, isUniqueIdentifierExist: false };
}
- const existingEntry =
- type === ParticipantType.TEAM
- ? await this.teamsService.findTeamByName(identifier)
+ const existingEntry =
+ type === ParticipantType.TEAM
+ ? await this.teamsService.findTeamByName(identifier)
: await this.membersService.findMemberByEmail(identifier);
if (existingEntry) {
return { isRequestPending: false, isUniqueIdentifierExist: true };
}
return { isRequestPending: false, isUniqueIdentifierExist: false };
- }
- catch(err) {
+ }
+ catch (err) {
return this.handleErrors(err)
}
}
@@ -155,20 +153,20 @@ export class ParticipantsRequestService {
async updateByUid(
uid: string,
participantRequest: Prisma.ParticipantsRequestUncheckedUpdateInput,
- ):Promise {
+ ): Promise {
try {
const formattedData = { ...participantRequest };
delete formattedData.id;
delete formattedData.uid;
delete formattedData.status;
delete formattedData.participantType;
- const result:ParticipantsRequest = await this.prisma.participantsRequest.update({
+ const result: ParticipantsRequest = await this.prisma.participantsRequest.update({
where: { uid },
data: formattedData,
});
- await this.cacheService.reset();
+ await this.cacheService.reset({ service: "participants-requests" });
return result;
- } catch(err) {
+ } catch (err) {
return this.handleErrors(err)
}
}
@@ -183,13 +181,13 @@ export class ParticipantsRequestService {
*/
async rejectRequestByUid(uidToReject: string): Promise {
try {
- const result:ParticipantsRequest = await this.prisma.participantsRequest.update({
+ const result: ParticipantsRequest = await this.prisma.participantsRequest.update({
where: { uid: uidToReject },
data: { status: ApprovalStatus.REJECTED }
});
- await this.cacheService.reset();
+ await this.cacheService.reset({ service: "participants-requests" });
return result;
- } catch(err) {
+ } catch (err) {
return this.handleErrors(err)
}
}
@@ -209,12 +207,14 @@ export class ParticipantsRequestService {
* @returns The updated participant request with the status set to `APPROVED`.
*/
private async approveRequestByUid(
- uidToApprove: string,
- participantsRequest: ParticipantsRequest
+ uidToApprove: string,
+ participantsRequest: ParticipantsRequest,
+ isVerified: boolean
): Promise {
let result;
let createdItem;
const dataToProcess: any = participantsRequest;
+ dataToProcess.newData.isVerified = isVerified;
const participantType = participantsRequest.participantType;
// Add new member or team and update status to approved
await this.prisma.$transaction(async (tx) => {
@@ -248,7 +248,7 @@ export class ParticipantsRequestService {
participantsRequest.requesterEmailId
);
}
- await this.cacheService.reset();
+ await this.cacheService.reset({ service: "participants-requests" });
await this.forestAdminService.triggerAirtableSync();
return result;
}
@@ -259,11 +259,11 @@ export class ParticipantsRequestService {
* @param uid
* @returns
*/
- async processRequestByUid(uid:string, participantsRequest:ParticipantsRequest, statusToProcess) {
+ async processRequestByUid(uid: string, participantsRequest: ParticipantsRequest, statusToProcess, isVerified: boolean) {
if (statusToProcess === ApprovalStatus.REJECTED) {
return await this.rejectRequestByUid(uid);
} else {
- return await this.approveRequestByUid(uid, participantsRequest);
+ return await this.approveRequestByUid(uid, participantsRequest, isVerified);
}
}
@@ -288,13 +288,13 @@ export class ParticipantsRequestService {
// Add the new request
const result: ParticipantsRequest = await this.add({
...postData
- },
+ },
tx
);
if (!disableNotification) {
this.notifyForCreate(result);
}
- await this.cacheService.reset();
+ await this.cacheService.reset({ service: "participants-requests" });
return result;
}
@@ -313,7 +313,7 @@ export class ParticipantsRequestService {
}
}
}
-
+
/**
* Extract unique identifier based on participant type.
* @param requestData
@@ -324,7 +324,7 @@ export class ParticipantsRequestService {
? requestData.newData.name
: requestData.newData.email?.toLowerCase().trim();
}
-
+
/**
* Validate if the unique identifier already exists.
* @param participantType
@@ -332,7 +332,7 @@ export class ParticipantsRequestService {
* @throws BadRequestException if identifier already exists
*/
async validateUniqueIdentifier(
- participantType: ParticipantType,
+ participantType: ParticipantType,
uniqueIdentifier: string
): Promise {
const { isRequestPending, isUniqueIdentifierExist } = await this.checkIfIdentifierAlreadyExist(
@@ -344,7 +344,7 @@ export class ParticipantsRequestService {
throw new BadRequestException(`${typeLabel} already exists`);
}
}
-
+
/**
* Validate location for members or email for teams.
* @param requestData
@@ -360,7 +360,7 @@ export class ParticipantsRequestService {
);
}
}
-
+
/**
* Send notification based on the participant type.
* @param result
@@ -409,8 +409,57 @@ export class ParticipantsRequestService {
return error;
}
-
+
generateMemberProfileURL(value) {
return generateProfileURL(value);
}
+
+ /**
+ * Process (approve/reject) multiple pending participants requests.
+ * @param participantRequests - The request body containing array of uids and status of participants to be processed;
+ * @returns The result of processing the participants request along with the success count.
+ */
+ async processBulkRequest(participantRequests) {
+ let successCount = 0;
+ const results = await Promise.all(
+ participantRequests.map(async (request) => {
+ try {
+ const participantRequest: ParticipantsRequest | null =
+ await this.findOneByUid(request.uid);
+ if (!participantRequest) {
+ return {
+ uid: request.uid,
+ message: 'Request not found',
+ };
+ }
+ if (participantRequest.status !== ApprovalStatus.PENDING) {
+ return {
+ uid: request.uid,
+ message: `Request cannot be processed. It has already been ${participantRequest.status.toLowerCase()}.`,
+ };
+ }
+ if (participantRequest.participantType === ParticipantType.TEAM && !participantRequest.requesterEmailId) {
+ return {
+ uid: request.uid,
+ message: 'Requester email is required for team participation requests. Please provide a valid email address.',
+ };
+ }
+ await this.processRequestByUid(
+ request.uid,
+ participantRequest,
+ request.status,
+ request.isVerified
+ );
+ successCount++;
+ return { uid: request.uid, message: 'Processed successfully' };
+ } catch (error) {
+ return {
+ uid: request.uid,
+ message: 'An error occurred while processing the request',
+ };
+ }
+ })
+ );
+ return { count: successCount, results };
+ }
}
diff --git a/apps/web-api/src/pl-events/pl-event-guests.service.ts b/apps/web-api/src/pl-events/pl-event-guests.service.ts
index 8ea507220..e16df6f5e 100644
--- a/apps/web-api/src/pl-events/pl-event-guests.service.ts
+++ b/apps/web-api/src/pl-events/pl-event-guests.service.ts
@@ -1,9 +1,8 @@
-import { Injectable, NotFoundException, ConflictException, BadRequestException, Inject, CACHE_MANAGER } from '@nestjs/common';
+import { Injectable, NotFoundException, ConflictException, BadRequestException } from '@nestjs/common';
import { LogService } from '../shared/log.service';
import { PrismaService } from '../shared/prisma.service';
import { Prisma, Member } from '@prisma/client';
import { MembersService } from '../members/members.service';
-import { Cache } from 'cache-manager';
import { PLEventLocationsService } from './pl-event-locations.service';
import {
CreatePLEventGuestSchemaDto,
@@ -13,6 +12,7 @@ import {
FormattedLocationWithEvents,
PLEvent
} from './pl-event-locations.types';
+import { CacheService } from '../utils/cache/cache.service';
@Injectable()
export class PLEventGuestsService {
@@ -21,7 +21,7 @@ export class PLEventGuestsService {
private logger: LogService,
private memberService: MembersService,
private eventLocationsService: PLEventLocationsService,
- @Inject(CACHE_MANAGER) private cacheService: Cache
+ private cacheService: CacheService
) {}
/**
@@ -43,7 +43,7 @@ export class PLEventGuestsService {
data.memberUid = isAdmin ? data.memberUid : member.uid;
const guests = this.formatInputToEventGuests(data);
const result = await (tx || this.prisma).pLEventGuest.createMany({ data: guests });
- this.cacheService.reset();
+ this.cacheService.reset({ service: 'PLEventGuest' });
return result;
} catch(err) {
this.handleErrors(err);
@@ -100,7 +100,7 @@ export class PLEventGuestsService {
OR: deleteConditions
}
});
- await this.cacheService.reset();
+ await this.cacheService.reset({ service: 'PLEventGuest' });
return result;
} catch (err) {
this.handleErrors(err);
diff --git a/apps/web-api/src/projects/projects.controller.ts b/apps/web-api/src/projects/projects.controller.ts
index 3bb5881c0..ab0b9f482 100644
--- a/apps/web-api/src/projects/projects.controller.ts
+++ b/apps/web-api/src/projects/projects.controller.ts
@@ -19,7 +19,13 @@ type RouteShape = typeof server.routeShapes;
@Controller()
export class ProjectsController {
- constructor(private readonly projectsService: ProjectsService) {}
+ constructor(private readonly projectsService: ProjectsService) { }
+
+ @Api(server.route.getProjectFilters)
+ @NoCache()
+ async getProjectFilters() {
+ return await this.projectsService.getProjectFilters();
+ }
@Api(server.route.createProject)
@UsePipes(ZodValidationPipe)
@@ -40,7 +46,7 @@ export class ProjectsController {
) {
return this.projectsService.updateProjectByUid(uid, body as any, request.userEmail);
}
-
+
@Api(server.route.getProjects)
@ApiOkResponseFromZod(ResponseProjectWithRelationsSchema.array())
async findAll(@Req() req) {
@@ -49,7 +55,7 @@ export class ProjectsController {
);
const builder = new PrismaQueryBuilder(queryableFields);
const builtQuery = builder.build(req.query);
- const { focusAreas } : any = req.query;
+ const { focusAreas }: any = req.query;
builtQuery.where = {
AND: [
builtQuery.where ? builtQuery.where : {},
@@ -82,4 +88,5 @@ export class ProjectsController {
) {
return this.projectsService.removeProjectByUid(uid, request.userEmail);
}
+
}
diff --git a/apps/web-api/src/projects/projects.service.ts b/apps/web-api/src/projects/projects.service.ts
index ea189f53d..253c08924 100644
--- a/apps/web-api/src/projects/projects.service.ts
+++ b/apps/web-api/src/projects/projects.service.ts
@@ -1,9 +1,9 @@
-import { Inject, CACHE_MANAGER, BadRequestException, ConflictException, ForbiddenException, Injectable, NotFoundException, HttpException } from '@nestjs/common';
+import { BadRequestException, ConflictException, ForbiddenException, Injectable, NotFoundException } from '@nestjs/common';
import { LogService } from '../shared/log.service';
import { PrismaService } from '../shared/prisma.service';
import { Prisma } from '@prisma/client';
import { MembersService } from '../members/members.service';
-import { Cache } from 'cache-manager';
+import { CacheService } from '../utils/cache/cache.service';
@Injectable()
export class ProjectsService {
@@ -11,21 +11,21 @@ export class ProjectsService {
private prisma: PrismaService,
private memberService: MembersService,
private logger: LogService,
- @Inject(CACHE_MANAGER) private cacheService: Cache
- ) {}
+ private cacheService: CacheService
+ ) { }
async createProject(project: Prisma.ProjectUncheckedCreateInput, userEmail: string) {
try {
- const member:any = await this.getMemberInfo(userEmail);
- const { contributingTeams, contributions, focusAreas} : any = project;
+ const member: any = await this.getMemberInfo(userEmail);
+ const { contributingTeams, contributions, focusAreas }: any = project;
project.createdBy = member.uid;
- project['projectFocusAreas'] = {...await this.createProjectWithFocusAreas(focusAreas, this.prisma)};
+ project['projectFocusAreas'] = { ...await this.createProjectWithFocusAreas(focusAreas, this.prisma) };
delete project['focusAreas'];
const result = await this.prisma.project.create({
data: {
...project,
contributingTeams: {
- connect: contributingTeams?.map(team => { return { uid: team.uid }})
+ connect: contributingTeams?.map(team => { return { uid: team.uid } })
},
contributions: {
create: contributions?.map((contribution) => {
@@ -34,9 +34,9 @@ export class ProjectsService {
}
}
});
- await this.cacheService.reset();
+ await this.cacheService.reset({ service: 'projects'});
return result;
- } catch(err) {
+ } catch (err) {
this.handleErrors(err);
}
}
@@ -47,13 +47,13 @@ export class ProjectsService {
userEmail: string
) {
try {
- const member:any = await this.getMemberInfo(userEmail);
- const existingData:any = await this.getProjectByUid(uid);
+ const member: any = await this.getMemberInfo(userEmail);
+ const existingData: any = await this.getProjectByUid(uid);
const contributingTeamsUid = existingData?.contributingTeams?.map(team => team.uid) || [];
await this.isMemberAllowedToEdit(member, [existingData?.maintainingTeamUid, ...contributingTeamsUid], existingData);
- const { contributingTeams, contributions, focusAreas } : any = project;
- const contributionsToCreate:any = [];
- const contributionUidsToDelete:any = [];
+ const { contributingTeams, contributions, focusAreas }: any = project;
+ const contributionsToCreate: any = [];
+ const contributionUidsToDelete: any = [];
contributions?.map((contribution) => {
if (!contribution.uid) {
contributionsToCreate.push(contribution);
@@ -62,8 +62,8 @@ export class ProjectsService {
contributionUidsToDelete.push({ uid: contribution.uid });
}
});
- return await this.prisma.$transaction(async(tx) => {
- project['projectFocusAreas'] = {...await this.updateProjectWithFocusAreas(uid, focusAreas, tx)};
+ return await this.prisma.$transaction(async (tx) => {
+ project['projectFocusAreas'] = { ...await this.updateProjectWithFocusAreas(uid, focusAreas, tx) };
delete project['focusAreas'];
const result = await tx.project.update({
where: {
@@ -72,8 +72,8 @@ export class ProjectsService {
data: {
...project,
contributingTeams: {
- disconnect: contributingTeamsUid?.map(uid => { return { uid }}),
- connect: contributingTeams?.map(team => { return { uid: team.uid }}) || []
+ disconnect: contributingTeamsUid?.map(uid => { return { uid } }),
+ connect: contributingTeams?.map(team => { return { uid: team.uid } }) || []
},
contributions: {
create: contributionsToCreate,
@@ -81,39 +81,22 @@ export class ProjectsService {
}
}
});
- await this.cacheService.reset();
+ await this.cacheService.reset({ service: 'projects'});
return result;
});
- } catch(err) {
+ } catch (err) {
this.handleErrors(err, `${uid}`);
}
}
async getProjects(queryOptions: Prisma.ProjectFindManyArgs) {
try {
- queryOptions.where = {
- ...queryOptions.where,
- isDeleted: false
- };
- queryOptions.include = {
- contributions: {
- select: {
- uid: true,
- member: {
- select: {
- uid: true,
- name: true,
- image: true
- }
- }
- }
- },
- maintainingTeam: { select: { uid: true, name: true, logo: true }},
- creator: { select: { uid: true, name: true, image: true }},
- logo: true
- };
- return await this.prisma.project.findMany(queryOptions);
- } catch(err) {
+ const [projects, projectsCount] = await Promise.all([
+ this.prisma.project.findMany(queryOptions),
+ this.prisma.project.count({ where: queryOptions.where }),
+ ]);
+ return { count: projectsCount, projects: projects }
+ } catch (err) {
this.handleErrors(err);
}
}
@@ -125,23 +108,23 @@ export class ProjectsService {
const project = await this.prisma.project.findUniqueOrThrow({
where: { uid },
include: {
- maintainingTeam: { select: { uid: true, name: true, logo: true }},
- contributingTeams: { select: { uid: true, name: true, logo: true }},
- contributions: {
- select: {
+ maintainingTeam: { select: { uid: true, name: true, logo: true } },
+ contributingTeams: { select: { uid: true, name: true, logo: true } },
+ contributions: {
+ select: {
uid: true,
- member: {
- select: {
- uid: true,
- name: true,
- image: true ,
- teamMemberRoles:{
- select:{
- mainTeam:true,
- teamLead:true,
- role:true,
- team:{
- select:{
+ member: {
+ select: {
+ uid: true,
+ name: true,
+ image: true,
+ teamMemberRoles: {
+ select: {
+ mainTeam: true,
+ teamLead: true,
+ role: true,
+ team: {
+ select: {
uid: true,
name: true
}
@@ -149,18 +132,18 @@ export class ProjectsService {
}
}
}
- },
+ },
projectUid: true
}
},
- creator: { select: { uid: true, name: true, image: true }},
+ creator: { select: { uid: true, name: true, image: true } },
logo: true,
projectFocusAreas: {
select: {
focusArea: {
select: {
uid: true,
- title: true
+ title: true
}
}
}
@@ -169,7 +152,7 @@ export class ProjectsService {
});
project['projectFocusAreas'] = this.removeDuplicateFocusAreas(project?.projectFocusAreas);
return project;
- } catch(err) {
+ } catch (err) {
this.handleErrors(err, `${uid}`);
}
}
@@ -178,7 +161,7 @@ export class ProjectsService {
uid: string,
userEmail: string
) {
- const member:any = await this.getMemberInfo(userEmail);
+ const member: any = await this.getMemberInfo(userEmail);
const existingData = await this.getProjectByUid(uid);
await this.isMemberAllowedToDelete(member, existingData);
try {
@@ -186,9 +169,9 @@ export class ProjectsService {
where: { uid },
data: { isDeleted: true }
});
- await this.cacheService.reset();
+ await this.cacheService.reset({ service: 'projects'});
return result;
- } catch(err) {
+ } catch (err) {
this.handleErrors(err, `${uid}`);
}
}
@@ -216,7 +199,7 @@ export class ProjectsService {
return await this.memberService.findMemberByEmail(memberEmail)
};
- async isMemberAllowedToEdit(member, teams, project ) {
+ async isMemberAllowedToEdit(member, teams, project) {
const res = await this.memberService.isMemberPartOfTeams(member, teams);
if (res || this.memberService.checkIfAdminUser(member) || member.uid === project.createdBy) {
return true;
@@ -236,7 +219,7 @@ export class ProjectsService {
async createProjectWithFocusAreas(focusAreas, transaction) {
if (focusAreas && focusAreas.length) {
- const projectFocusAreas:any = [];
+ const projectFocusAreas: any = [];
const focusAreaHierarchies = await transaction.focusAreaHierarchy.findMany({
where: {
subFocusAreaUid: {
@@ -261,7 +244,7 @@ export class ProjectsService {
data: projectFocusAreas
}
}
- }
+ }
}
async isFocusAreaModified(projectId, focusAreas, transaction) {
@@ -275,10 +258,10 @@ export class ProjectsService {
if (newFocusAreaUIds.length !== focusAreasUIds.length) {
return true;
- }
+ }
if (projectFocusAreas.length === 0 && focusAreas.length === 0) {
- return false
+ return false
}
return !focusAreasUIds.every(area => newFocusAreaUIds.includes(area));
}
@@ -309,7 +292,7 @@ export class ProjectsService {
return {
projectFocusAreas: {
some: {
- ancestorArea:{
+ ancestorArea: {
title: {
in: focusAreas?.split(',')
}
@@ -324,56 +307,56 @@ export class ProjectsService {
removeDuplicateFocusAreas(focusAreas): any {
const uniqueFocusAreas = {};
focusAreas.forEach(item => {
- const uid = item.focusArea.uid;
- const title = item.focusArea.title;
- uniqueFocusAreas[uid] = { uid, title };
+ const uid = item.focusArea.uid;
+ const title = item.focusArea.title;
+ uniqueFocusAreas[uid] = { uid, title };
});
return Object.values(uniqueFocusAreas);
}
- buildProjectFilter(query){
- const {
+ buildProjectFilter(query) {
+ const {
name,
lookingForFunding,
team
} = query;
- const filter:any = [{
+ const filter: any = [{
isDeleted: false
}];
this.buildNameFilter(name, filter);
this.buildFundingFilter(lookingForFunding, filter);
this.buildMaintainingTeamFilter(team, filter);
this.buildRecentProjectsFilter(query, filter);
- return {
+ return {
AND: filter
};
}
buildNameFilter(name, filter) {
if (name) {
- filter.push({
+ filter.push({
name: {
contains: name,
mode: 'insensitive'
}
});
- }
+ }
}
buildFundingFilter(funding, filter) {
if (funding === "true") {
- filter.push({
+ filter.push({
lookingForFunding: true
});
- }
+ }
}
buildMaintainingTeamFilter(team, filter) {
if (team) {
- filter.push({
+ filter.push({
maintainingTeamUid: team
});
- }
+ }
}
/**
@@ -388,7 +371,7 @@ export class ProjectsService {
* @returns The constructed query with a 'createdAt' filter if 'is_recent' is 'true',
* or an empty object if 'is_recent' is not provided or set to 'false'.
*/
- buildRecentProjectsFilter(queryParams, filter?) {
+ buildRecentProjectsFilter(queryParams, filter?) {
const { isRecent } = queryParams;
const recentFilter = {
createdAt: {
@@ -397,10 +380,35 @@ export class ProjectsService {
};
if (isRecent === 'true' && !filter) {
return recentFilter;
- }
+ }
if (isRecent === 'true' && filter) {
filter.push(recentFilter);
}
return {};
}
+
+ /**
+ * Fetches team names that maintain atleast a single project.
+ *
+ * @returns Set of team names.
+ */
+ async getProjectFilters() {
+ const maintainingTeams = await this.prisma.team.findMany({
+ where: {
+ maintainingProjects: {
+ some: {},
+ }
+ },
+ select: {
+ uid: true,
+ name: true,
+ logo: {
+ select: {
+ url: true
+ }
+ }
+ }
+ })
+ return { maintainedBy: maintainingTeams.map((team) => ({ uid: team.uid, name: team.name, logo: team.logo?.url })) };
+ }
}
diff --git a/apps/web-api/src/shared/shared.module.ts b/apps/web-api/src/shared/shared.module.ts
index 26a424c30..a29c68c0a 100644
--- a/apps/web-api/src/shared/shared.module.ts
+++ b/apps/web-api/src/shared/shared.module.ts
@@ -10,6 +10,7 @@ import { ImagesController } from '../images/images.controller';
import { ImagesService } from '../images/images.service';
import { FileUploadService } from '../utils/file-upload/file-upload.service';
import { FileEncryptionService } from '../utils/file-encryption/file-encryption.service';
+import { CacheService } from '../utils/cache/cache.service';
@Global()
@Module({
@@ -26,6 +27,7 @@ import { FileEncryptionService } from '../utils/file-encryption/file-encryption.
ImagesService,
FileUploadService,
FileEncryptionService,
+ CacheService
],
exports: [
PrismaService,
@@ -39,6 +41,7 @@ import { FileEncryptionService } from '../utils/file-encryption/file-encryption.
ImagesService,
FileUploadService,
FileEncryptionService,
+ CacheService
],
})
export class SharedModule {}
\ No newline at end of file
diff --git a/apps/web-api/src/teams/teams.controller.ts b/apps/web-api/src/teams/teams.controller.ts
index a026e2138..68a813b12 100644
--- a/apps/web-api/src/teams/teams.controller.ts
+++ b/apps/web-api/src/teams/teams.controller.ts
@@ -23,19 +23,37 @@ const server = initNestServer(apiTeam);
type RouteShape = typeof server.routeShapes;
@Controller()
export class TeamsController {
- constructor(private readonly teamsService: TeamsService) {}
+ constructor(private readonly teamsService: TeamsService) { }
+
+ @Api(server.route.teamFilters)
+ @ApiQueryFromZod(TeamQueryParams)
+ async getTeamFilters(@Req() request: Request) {
+ const queryableFields = prismaQueryableFieldsFromZod(
+ ResponseTeamWithRelationsSchema
+ );
+ const builder = new PrismaQueryBuilder(queryableFields);
+ const builtQuery = builder.build(request.query);
+ const { focusAreas }: any = request.query;
+ builtQuery.where = {
+ AND: [
+ builtQuery.where ? builtQuery.where : {},
+ this.teamsService.buildFocusAreaFilters(focusAreas),
+ this.teamsService.buildRecentTeamsFilter(request.query)
+ ]
+ }
+ return await this.teamsService.getTeamFilters(builtQuery);
+ }
@Api(server.route.getTeams)
@ApiQueryFromZod(TeamQueryParams)
@ApiOkResponseFromZod(ResponseTeamWithRelationsSchema.array())
- @NoCache()
findAll(@Req() request: Request) {
const queryableFields = prismaQueryableFieldsFromZod(
ResponseTeamWithRelationsSchema
);
const builder = new PrismaQueryBuilder(queryableFields);
const builtQuery = builder.build(request.query);
- const { focusAreas } : any = request.query;
+ const { focusAreas }: any = request.query;
builtQuery.where = {
AND: [
builtQuery.where ? builtQuery.where : {},
@@ -74,4 +92,5 @@ export class TeamsController {
await this.teamsService.validateRequestor(req.userEmail, teamUid);
return await this.teamsService.updateTeamFromParticipantsRequest(teamUid, body, req.userEmail);
}
+
}
diff --git a/apps/web-api/src/teams/teams.service.ts b/apps/web-api/src/teams/teams.service.ts
index dc05703e2..8c9456ee3 100644
--- a/apps/web-api/src/teams/teams.service.ts
+++ b/apps/web-api/src/teams/teams.service.ts
@@ -4,9 +4,8 @@ import {
ForbiddenException,
BadRequestException,
NotFoundException,
- Inject,
forwardRef,
- CACHE_MANAGER
+ Inject
} from '@nestjs/common';
import * as path from 'path';
import { z } from 'zod';
@@ -19,9 +18,9 @@ import { ParticipantsRequestService } from '../participants-request/participants
import { hashFileName } from '../utils/hashing';
import { ForestAdminService } from '../utils/forest-admin/forest-admin.service';
import { MembersService } from '../members/members.service';
-import { LogService } from '../shared/log.service';
-import { Cache } from 'cache-manager';
+import { LogService } from '../shared/log.service';
import { copyObj, buildMultiRelationMapping, buildRelationMapping } from '../utils/helper/helper';
+import { CacheService } from '../utils/cache/cache.service';
@Injectable()
export class TeamsService {
@@ -35,8 +34,8 @@ export class TeamsService {
private logger: LogService,
private forestadminService: ForestAdminService,
private notificationService: NotificationService,
- @Inject(CACHE_MANAGER) private cacheService: Cache
- ) {}
+ private cacheService: CacheService
+ ) { }
/**
* Find all teams based on provided query options.
@@ -46,10 +45,14 @@ export class TeamsService {
* (filter, pagination, sorting, etc.)
* @returns A list of teams that match the query options
*/
- async findAll(queryOptions: Prisma.TeamFindManyArgs): Promise {
+ async findAll(queryOptions: Prisma.TeamFindManyArgs): Promise<{ count: Number, teams: Team[] }> {
try {
- return this.prisma.team.findMany({ ...queryOptions });
- } catch(err) {
+ const [teams, teamsCount] = await Promise.all([
+ this.prisma.team.findMany(queryOptions),
+ this.prisma.team.count({ where: queryOptions.where }),
+ ]);
+ return { count: teamsCount, teams: teams };
+ } catch (err) {
return this.handleErrors(err);
}
}
@@ -104,7 +107,7 @@ export class TeamsService {
});
team.teamFocusAreas = this.removeDuplicateFocusAreas(team.teamFocusAreas);
return team;
- } catch(err) {
+ } catch (err) {
return this.handleErrors(err, uid);
}
}
@@ -120,7 +123,7 @@ export class TeamsService {
return this.prisma.team.findUniqueOrThrow({
where: { name },
});
- } catch(err) {
+ } catch (err) {
return this.handleErrors(err);
}
};
@@ -133,14 +136,14 @@ export class TeamsService {
* @returns The created team record
*/
async createTeam(
- team: Prisma.TeamUncheckedCreateInput,
+ team: Prisma.TeamUncheckedCreateInput,
tx: Prisma.TransactionClient
): Promise {
try {
return await tx.team.create({
data: team,
});
- } catch(err) {
+ } catch (err) {
return this.handleErrors(err);
}
}
@@ -163,7 +166,7 @@ export class TeamsService {
where: { uid },
data: team,
});
- } catch(err) {
+ } catch (err) {
return this.handleErrors(err, `${uid}`);
}
}
@@ -211,7 +214,7 @@ export class TeamsService {
const formattedTeam = await this.formatTeam(null, newTeam, tx);
const createdTeam = await this.createTeam(formattedTeam, tx);
return createdTeam;
- }
+ }
/**
* Format team data for creation or update
@@ -243,7 +246,7 @@ export class TeamsService {
team['membershipSources'] = buildMultiRelationMapping('membershipSources', teamData, type);
if (type === 'create') {
team['teamFocusAreas'] = await this.createTeamWithFocusAreas(teamData, tx);
- }
+ }
if (teamUid) {
team['teamFocusAreas'] = await this.updateTeamWithFocusAreas(teamUid, teamData, tx);
}
@@ -277,7 +280,7 @@ export class TeamsService {
* @param focusAreas - An array of focus areas associated with the team
* @returns A deduplicated array of focus areas
*/
- private removeDuplicateFocusAreas(focusAreas):any {
+ private removeDuplicateFocusAreas(focusAreas): any {
const uniqueFocusAreas = {};
focusAreas.forEach(item => {
const { uid, title } = item.focusArea;
@@ -308,7 +311,7 @@ export class TeamsService {
participantType: 'TEAM',
newData: { ...newTeamData },
},
- tx
+ tx
);
}
@@ -317,7 +320,7 @@ export class TeamsService {
* This ensures that the system is up-to-date with the latest changes.
*/
private async postUpdateActions(): Promise {
- await this.cacheService.reset();
+ await this.cacheService.reset({ service: "teams" });
await this.forestadminService.triggerAirtableSync();
}
@@ -330,7 +333,7 @@ export class TeamsService {
*/
async createTeamWithFocusAreas(team, transaction: Prisma.TransactionClient) {
if (team.focusAreas && team.focusAreas.length > 0) {
- let teamFocusAreas:any = [];
+ let teamFocusAreas: any = [];
const focusAreaHierarchies = await transaction.focusAreaHierarchy.findMany({
where: {
subFocusAreaUid: {
@@ -358,7 +361,7 @@ export class TeamsService {
}
return {};
}
-
+
/**
* Updates focus areas for an existing team.
*
@@ -376,7 +379,7 @@ export class TeamsService {
}
return await this.createTeamWithFocusAreas(team, transaction);
}
-
+
/**
* Builds filter for focus areas by splitting the input and matching ancestor titles.
* @param focusAreas - Comma-separated focus area titles
@@ -387,7 +390,7 @@ export class TeamsService {
return {
teamFocusAreas: {
some: {
- ancestorArea:{
+ ancestorArea: {
title: {
in: focusAreas?.split(',')
}
@@ -398,23 +401,23 @@ export class TeamsService {
}
return {};
}
-
+
/**
* Constructs the team filter based on multiple query parameters.
* @param queryParams - Query parameters from the request
* @returns - Prisma AND filter combining all conditions
*/
- buildTeamFilter(queryParams){
- const {
+ buildTeamFilter(queryParams) {
+ const {
name,
- plnFriend,
- industryTags,
+ plnFriend,
+ industryTags,
technologies,
membershipSources,
fundingStage,
- officeHours
+ officeHours
} = queryParams;
- const filter:any = [];
+ const filter: any = [];
this.buildNameAndPLNFriendFilter(name, plnFriend, filter);
this.buildIndustryTagsFilter(industryTags, filter);
this.buildTechnologiesFilter(technologies, filter);
@@ -422,7 +425,7 @@ export class TeamsService {
this.buildFundingStageFilter(fundingStage, filter);
this.buildOfficeHoursFilter(officeHours, filter);
this.buildRecentTeamsFilter(queryParams, filter);
- return {
+ return {
AND: filter
};
};
@@ -435,17 +438,17 @@ export class TeamsService {
*/
buildNameAndPLNFriendFilter(name, plnFriend, filter) {
if (name) {
- filter.push({
+ filter.push({
name: {
contains: name,
mode: 'insensitive'
}
});
- }
+ }
if (!(plnFriend === "true")) {
- filter.push({
+ filter.push({
plnFriend: false
- });
+ });
}
}
@@ -455,14 +458,14 @@ export class TeamsService {
* @param filter - Filter array to be appended to
*/
buildIndustryTagsFilter(industryTags, filter) {
- const tags = industryTags?.split(',').map(tag=> tag.trim());
+ const tags = industryTags?.split(',').map(tag => tag.trim());
if (tags?.length > 0) {
- tags.map((tag)=> {
+ tags.map((tag) => {
filter.push({
- industryTags:{
+ industryTags: {
some: {
- title: {
- in: tag
+ title: {
+ in: tag
}
}
}
@@ -479,12 +482,12 @@ export class TeamsService {
buildTechnologiesFilter(technologies, filter) {
const tags = technologies?.split(',').map(tech => tech.trim());
if (tags?.length > 0) {
- tags.map((tag)=> {
+ tags.map((tag) => {
filter.push({
technologies: {
some: {
- title: {
- in: tag
+ title: {
+ in: tag
}
}
}
@@ -501,12 +504,12 @@ export class TeamsService {
buildMembershipSourcesFilter(membershipSources, filter) {
const sources = membershipSources?.split(',').map(source => source.trim());
if (sources?.length > 0) {
- sources.map((source)=> {
+ sources.map((source) => {
filter.push({
membershipSources: {
some: {
- title: {
- in: source
+ title: {
+ in: source
}
}
}
@@ -555,7 +558,7 @@ export class TeamsService {
* @returns The constructed query with a 'createdAt' filter if 'is_recent' is 'true',
* or an empty object if 'is_recent' is not provided or set to 'false'.
*/
- buildRecentTeamsFilter(queryParams, filter?) {
+ buildRecentTeamsFilter(queryParams, filter?) {
const { isRecent } = queryParams;
const recentFilter = {
createdAt: {
@@ -564,7 +567,7 @@ export class TeamsService {
};
if (isRecent === 'true' && !filter) {
return recentFilter;
- }
+ }
if (isRecent === 'true' && filter) {
filter.push(recentFilter);
}
@@ -720,4 +723,64 @@ export class TeamsService {
});
}
}
+
+ /**
+ * Fetches filter tags for teams for felicitating ease searching.
+ *
+ * @returns Set of industry tags, membership sources, funding stages
+ * and technologies that contains atleast one team.
+ */
+ async getTeamFilters(queryParams) {
+ const [industryTags, membershipSources, fundingStages, technologies] = await Promise.all([
+ this.prisma.industryTag.findMany({
+ where: {
+ teams: {
+ some: { ...queryParams.where },
+ },
+ },
+ select: {
+ title: true,
+ },
+ }),
+
+ this.prisma.membershipSource.findMany({
+ where: {
+ teams: {
+ some: { ...queryParams.where },
+ },
+ },
+ select: {
+ title: true,
+ },
+ }),
+
+ this.prisma.fundingStage.findMany({
+ where: {
+ teams: {
+ some: { ...queryParams.where },
+ },
+ },
+ select: {
+ title: true,
+ },
+ }),
+
+ this.prisma.technology.findMany({
+ where: {
+ teams: {
+ some: { ...queryParams.where },
+ },
+ },
+ select: {
+ title: true,
+ },
+ }),
+ ]);
+ return {
+ industryTags: industryTags.map((tag) => tag.title),
+ membershipSources: membershipSources.map((source) => source.title),
+ fundingStages: fundingStages.map((stage) => stage.title),
+ technologies: technologies.map((tech) => tech.title),
+ };
+ }
}
diff --git a/apps/web-api/src/utils/cache/cache.service.ts b/apps/web-api/src/utils/cache/cache.service.ts
new file mode 100644
index 000000000..0bfbd5eb5
--- /dev/null
+++ b/apps/web-api/src/utils/cache/cache.service.ts
@@ -0,0 +1,59 @@
+import { Injectable, Inject, CACHE_MANAGER } from '@nestjs/common';
+import { Cache } from 'cache-manager';
+import axios from 'axios';
+import { LogService } from '../../shared/log.service';
+
+@Injectable()
+export class CacheService {
+ constructor(
+ @Inject(CACHE_MANAGER) private cache: Cache,
+ private logService: LogService
+ ) {}
+
+ // Mapping service names to tags
+ private serviceTagsMap = {
+ members: ['member-filters', 'member-list'],
+ projects: ['project-list', 'focus-areas'],
+ teams: ['team-filters', 'team-list', 'focus-areas'],
+ 'participants-requests': ['member-filters', 'member-list','team-filters', 'team-list', 'focus-areas']
+ };
+
+ // Reset cache and call API based on service
+ async reset(data) {
+ const { service } = data;
+ await this.cache.reset(); // Reset the cache
+ const tags = this.serviceTagsMap[service];
+ if (tags) {
+ await this.revalidateCache(tags);
+ }
+ }
+
+ // Function to call the revalidate API
+ private async revalidateCache(tags: string[]) {
+ const baseUrl = process.env.WEB_UI_BASE_URL;
+ const token = process.env.REVALIDATE_API_TOKEN; // Assuming token is stored in env variable
+ if (!baseUrl) {
+ this.logService.error('WEB_UI_BASE_URL is not defined in the environment variables.');
+ return;
+ }
+ if (!token) {
+ this.logService.error('REVALIDATE_API_TOKEN is not defined in the environment variables.');
+ return;
+ }
+ const url = `${baseUrl}/api/revalidate`;
+ try {
+ const response = await axios.post(
+ url,
+ { tags },
+ {
+ headers: {
+ Authorization: `Bearer ${token}`, // Adding Bearer token to headers
+ },
+ },
+ );
+ this.logService.info(`Revalidation API called successfully with tags: ${tags.join(', ')}`);
+ } catch (error) {
+ this.logService.error('Error calling revalidate API:', error.message);
+ }
+ }
+}
diff --git a/apps/web-api/src/utils/redis/redis.service.ts b/apps/web-api/src/utils/redis/redis.service.ts
deleted file mode 100644
index a44562f50..000000000
--- a/apps/web-api/src/utils/redis/redis.service.ts
+++ /dev/null
@@ -1,24 +0,0 @@
-/* eslint-disable prettier/prettier */
-import { Injectable } from '@nestjs/common';
-import cacheManager from 'cache-manager';
-import redisStore from 'cache-manager-redis-store';
-
-@Injectable()
-export class RedisService {
- async resetAllCache() {
- const redisCache = cacheManager.caching({
- store: redisStore,
- host: process.env.REDIS_HOST,
- url: process.env.REDIS_TLS_URL,
- port: Number(process.env.REDIS_PORT),
- password: process.env.REDIS_PASSWORD,
- tls: process.env.REDIS_WITH_TLS
- ? {
- rejectUnauthorized: false,
- requestCert: true,
- }
- : null,
- });
- await redisCache.reset();
- }
-}
diff --git a/libs/contracts/src/lib/contract-home.ts b/libs/contracts/src/lib/contract-home.ts
index 8b3b25f4e..09a322c46 100644
--- a/libs/contracts/src/lib/contract-home.ts
+++ b/libs/contracts/src/lib/contract-home.ts
@@ -7,6 +7,15 @@ import {
const contract = initContract();
export const apiHome = contract.router({
+ getTeamsAndProjects: {
+ method: 'GET',
+ path: `${getAPIVersionAsPath('1')}/home/entities`,
+ query: contract.query,
+ responses: {
+ 200: contract.response()
+ },
+ summary: 'Get all featured members, projects, teams and events'
+ },
getAllFeaturedData: {
method: 'GET',
path: `${getAPIVersionAsPath('1')}/home/featured`,
diff --git a/libs/contracts/src/lib/contract-member.ts b/libs/contracts/src/lib/contract-member.ts
index 7e2b2352e..66eba60ac 100644
--- a/libs/contracts/src/lib/contract-member.ts
+++ b/libs/contracts/src/lib/contract-member.ts
@@ -28,6 +28,15 @@ export const apiMembers = contract.router({
},
summary: 'Get member roles',
},
+ getMemberFilters: {
+ method: 'GET',
+ path: `${getAPIVersionAsPath('1')}/members/filters`,
+ query: MemberQueryParams,
+ responses: {
+ 200: contract.response(),
+ },
+ summary: 'Get member filter values',
+ },
getMember: {
method: 'GET',
path: `${getAPIVersionAsPath('1')}/members/:uid`,
diff --git a/libs/contracts/src/lib/contract-project.ts b/libs/contracts/src/lib/contract-project.ts
index b5fabc1a7..85d6f9adc 100644
--- a/libs/contracts/src/lib/contract-project.ts
+++ b/libs/contracts/src/lib/contract-project.ts
@@ -1,12 +1,20 @@
import { initContract } from '@ts-rest/core';
import {
- ResponseProjectWithRelationsSchema
+ ResponseProjectWithRelationsSchema
} from '../schema';
import { getAPIVersionAsPath } from '../utils/versioned-path';
const contract = initContract();
export const apiProjects = contract.router({
+ getProjectFilters: {
+ method: 'GET',
+ path: `${getAPIVersionAsPath('1')}/projects/filters`,
+ responses: {
+ 200: contract.response(),
+ },
+ summary: 'Get project filters',
+ },
getProjects: {
method: 'GET',
path: `${getAPIVersionAsPath('1')}/projects`,
diff --git a/libs/contracts/src/lib/contract-team.ts b/libs/contracts/src/lib/contract-team.ts
index 4578d06f6..199bc93d7 100644
--- a/libs/contracts/src/lib/contract-team.ts
+++ b/libs/contracts/src/lib/contract-team.ts
@@ -9,6 +9,15 @@ import { getAPIVersionAsPath } from '../utils/versioned-path';
const contract = initContract();
export const apiTeam = contract.router({
+ teamFilters: {
+ method: 'GET',
+ path: `${getAPIVersionAsPath('1')}/teams/filters`,
+ query: TeamQueryParams,
+ responses: {
+ 200: contract.response(),
+ },
+ summary: 'filter teams',
+ },
getTeams: {
method: 'GET',
path: `${getAPIVersionAsPath('1')}/teams`,
diff --git a/libs/contracts/src/schema/member.ts b/libs/contracts/src/schema/member.ts
index 7a026bbdd..4cbcf8084 100644
--- a/libs/contracts/src/schema/member.ts
+++ b/libs/contracts/src/schema/member.ts
@@ -22,7 +22,8 @@ export const PreferenceSchema = z.object({
showLinkedin:z.boolean(),
showDiscord:z.boolean(),
showGithubProjects:z.boolean(),
- showTwitter:z.boolean()
+ showTwitter:z.boolean(),
+ showSubscription:z.boolean()
});
export const MemberSchema = z.object({
@@ -30,7 +31,7 @@ export const MemberSchema = z.object({
uid: z.string(),
name: z.string(),
email: z.string(),
- externalId: z.string(),
+ externalId: z.string().nullish(),
imageUid: z.string().nullish(),
githubHandler: z.string().nullish(),
discordHandler: z.string().nullish(),
@@ -38,17 +39,24 @@ export const MemberSchema = z.object({
telegramHandler: z.string().nullish(),
officeHours: z.string().nullish(),
airtableRecId: z.string().nullish(),
- plnFriend: z.boolean(),
+ plnFriend: z.boolean().nullish(),
bio: z.string().nullish(),
+ signUpSource: z.string().nullish(),
+ signUpMedium: z.string().nullish(),
+ signUpCampaign: z.string().nullish(),
isFeatured: z.boolean().nullish(),
createdAt: z.string(),
updatedAt: z.string(),
- locationUid: z.string(),
- openToWork: z.boolean(),
+ locationUid: z.string().nullable(),
+ openToWork: z.boolean().nullish(),
linkedinHandler: z.string().nullish(),
repositories: GitHubRepositorySchema.array().optional(),
preferences: PreferenceSchema.optional(),
- projectContributions: z.array(ProjectContributionSchema).optional()
+ projectContributions: z.array(ProjectContributionSchema).optional(),
+ isVerified:z.boolean().nullish(),
+ isUserConsent: z.boolean().nullish(),
+ isSubscribedToNewsletter: z.boolean().nullish(),
+ teamOrProjectURL: z.string().nullish()
});
@@ -74,7 +82,18 @@ export const CreateMemberSchema = MemberSchema.pick({
officeHours: true,
plnFriend: true,
locationUid: true,
- bio: true
+ bio: true,
+ signUpSource: true,
+ isFeatured: true,
+ openToWork: true,
+ linkedinHandler: true,
+ telegramHandler: true,
+ isVerified: true,
+ isUserConsent: true,
+ isSubscribedToNewsletter: true,
+ teamOrProjectURL: true,
+ preferences: true,
+ projectContributions: true
});
export const MemberRelationalFields = ResponseMemberWithRelationsSchema.pick({
diff --git a/libs/contracts/src/schema/participants-request.ts b/libs/contracts/src/schema/participants-request.ts
index 4d998cdb9..6d83db9c4 100644
--- a/libs/contracts/src/schema/participants-request.ts
+++ b/libs/contracts/src/schema/participants-request.ts
@@ -6,7 +6,7 @@ export const statusEnum = z.enum(['PENDING', 'APPROVED', 'REJECTED']);
export const participantTypeEnum = z.enum(['MEMBER', 'TEAM']);
const oldDataPostSchema = z.object({});
const teamMappingSchema = z.object({
- role: z.string(),
+ role: z.string().nullish().optional(),
teamUid: z.string(),
teamTitle: z.string(),
});
@@ -35,8 +35,8 @@ const newDataMemberSchema = z.object({
name: z.string(),
email: z.string(),
plnStartDate: z.string().optional().nullable(),
- teamAndRoles: z.array(teamMappingSchema).nonempty(),
- skills: z.array(skillsMappingSchema).nonempty(),
+ teamAndRoles: z.array(teamMappingSchema).optional(),
+ skills: z.array(skillsMappingSchema).optional(),
city: z.string().optional().nullable(),
country: z.string().optional().nullable(),
region: z.string().optional().nullable(),
@@ -46,9 +46,20 @@ const newDataMemberSchema = z.object({
linkedinHandler: z.string().optional().nullable(),
telegramHandler: z.string().optional().nullable(),
officeHours: z.string().optional().nullable(),
- imageUid: z.string().optional().nullable(),
- moreDetails: z.string().optional().nullable(),
- projectContributions: z.array(ProjectContributionSchema as any).optional()
+ imageUid: z.string().optional().nullish(),
+ moreDetails: z.string().optional().nullish(),
+ projectContributions: z.array(ProjectContributionSchema as any).optional(),
+ bio: z.string().nullish(),
+ signUpSource: z.string().nullish(),
+ signUpMedium: z.string().nullish(),
+ signUpCampaign: z.string().nullish(),
+ isFeatured: z.boolean().nullish(),
+ locationUid: z.string().nullish(),
+ openToWork: z.boolean().nullish(),
+ isVerified: z.boolean().nullish(),
+ isUserConsent: z.boolean().nullish(),
+ isSubscribedToNewsletter: z.boolean().nullish(),
+ teamOrProjectURL: z.string().nullish()
});
const newDataTeamSchema = z.object({
@@ -103,7 +114,15 @@ export const FindUniqueIdentiferSchema = z.object({
})
const ProcessParticipantRequest = z.object({
- status: statusEnum,
+ status: statusEnum,
+ isVerified: z.boolean()
+})
+const ProcessBulkRequest = z.object({
+ uid: z.string(),
+ status: statusEnum,
+ participantType: participantTypeEnum,
+ isVerified: z.boolean()
})
-export class ProcessParticipantReqDto extends createZodDto(ProcessParticipantRequest) {}
+export class ProcessBulkParticipantRequest extends createZodDto(ProcessBulkRequest) { }
+export class ProcessParticipantReqDto extends createZodDto(ProcessParticipantRequest) { }
export class FindUniqueIdentiferDto extends createZodDto(FindUniqueIdentiferSchema) { }
diff --git a/libs/contracts/src/schema/project-contribution.ts b/libs/contracts/src/schema/project-contribution.ts
index 0b1335565..5be0fd3a5 100644
--- a/libs/contracts/src/schema/project-contribution.ts
+++ b/libs/contracts/src/schema/project-contribution.ts
@@ -2,24 +2,16 @@ import { z } from 'zod';
import { compareDateWithoutTime, compareMonthYear } from '../../src/utils/date-utils';
const ProjectContribution = z.object({
- role: z.string(),
- currentProject: z.boolean(),
+ role: z.string().nullish(),
+ currentProject: z.boolean().nullish(),
startDate: z.string().nullish(),
- endDate: z.string().optional(),
- description: z.string().optional().nullish(),
+ endDate: z.string().nullish(),
+ description: z.string().nullish(),
projectUid: z.string(),
- uid: z.string().optional()
+ uid: z.string().nullish()
});
export const ProjectContributionSchema = ProjectContribution.superRefine((data, ctx) => {
- if (!data.currentProject && !data.endDate) {
- ctx.addIssue({
- code: z.ZodIssueCode.custom,
- message: 'End date should not be null for past contribution',
- fatal: true,
- });
- }
-
if (data.startDate && data.endDate && compareDateWithoutTime(data.startDate, data.endDate) >= 0) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
diff --git a/libs/contracts/src/schema/project.ts b/libs/contracts/src/schema/project.ts
index c31399961..312ee9115 100644
--- a/libs/contracts/src/schema/project.ts
+++ b/libs/contracts/src/schema/project.ts
@@ -1,5 +1,8 @@
-import { createZodDto } from 'nestjs-zod';
-import { z } from 'nestjs-zod/z';
+import { createZodDto } from '@abitia/zod-dto';
+import { z } from 'zod';
+import { ResponseTeamWithRelationsSchema } from './team';
+import { ResponseMemberWithRelationsSchema } from './member';
+import { ResponseImageWithRelationsSchema } from './image';
const TypeEnum = z.enum(['MAINTENER', 'COLLABORATOR']);
@@ -12,6 +15,7 @@ const ContributionSchema = z.object({
const ProjectSchema = z.object({
id: z.number().int(),
+ uid: z.string(),
logoUid: z.string().optional().nullable(),
name: z.string(),
tagline: z.string(),
@@ -40,11 +44,17 @@ const ProjectSchema = z.object({
uid: z.string(),
title: z.string()
}).array().optional(),
- contributions: ContributionSchema.array().optional()
+ contributions: ContributionSchema.array().optional(),
+ isDeleted: z.boolean().default(false)
});
export const ResponseProjectSchema = ProjectSchema.omit({ id: true }).strict();
-export const ResponseProjectWithRelationsSchema = ProjectSchema.extend({});
+export const ResponseProjectWithRelationsSchema = ResponseProjectSchema.extend({
+ logo: ResponseImageWithRelationsSchema.optional(),
+ maintainingTeam: ResponseTeamWithRelationsSchema.optional(),
+ contributingTeams: ResponseTeamWithRelationsSchema.array().optional(),
+ creator: ResponseMemberWithRelationsSchema.optional()
+});
export const ResponseProjectSuccessSchema = z.object({ success: z.boolean()});
// omit score and id to avoid update from request
export class UpdateProjectDto extends createZodDto(ProjectSchema.partial().omit({ id:true, score: true })) {}