From b252434de2d30dc5deb49821fa0a42058009333c Mon Sep 17 00:00:00 2001 From: Andrew-Gibson42 Date: Wed, 23 Sep 2020 17:58:10 -0500 Subject: [PATCH 01/10] Finished back end stuff --- apps/api/src/resources/Event/entity.ts | 7 +++++++ apps/api/src/resources/Event/resolver.ts | 16 +++++++++++++++ apps/api/src/resources/User/entity.ts | 8 ++++++++ apps/api/src/resources/User/resolver.ts | 25 ++++++++++++++++++++++++ 4 files changed, 56 insertions(+) diff --git a/apps/api/src/resources/Event/entity.ts b/apps/api/src/resources/Event/entity.ts index 3be925ad..86cb39c3 100644 --- a/apps/api/src/resources/Event/entity.ts +++ b/apps/api/src/resources/Event/entity.ts @@ -93,4 +93,11 @@ export class Event extends BaseEntity { nullable: true }) public attendees: Lazy; + + @Field(() => [User], { nullable: true }) + @ManyToMany(() => User, (user: User) => user.interestedEvents, { + lazy: true, + nullable: true + }) + public usersInterested: Lazy; } diff --git a/apps/api/src/resources/Event/resolver.ts b/apps/api/src/resources/Event/resolver.ts index 1b2ad0bf..f9931e47 100644 --- a/apps/api/src/resources/Event/resolver.ts +++ b/apps/api/src/resources/Event/resolver.ts @@ -204,4 +204,20 @@ export class EventResolver { protected async eventsWithKey(@Arg("urlKey", () => String) urlKey: string): Promise { return this.repository.find({ urlKey }); } + + @Query(() => [Event]) + protected async getAttendees(@Arg("urlKey", () => String) urlKey: string): Promise { + const event: Event = await Event.findOneOrFail({ urlKey: urlKey }); + let users: User[] = await event.attendees; + + return users; + } + + @Query(() => [Event]) + protected async getInterestedUsers(@Arg("urlKey", () => String) urlKey: string): Promise { + const event: Event = await Event.findOneOrFail({ urlKey: urlKey }); + let users: User[] = await event.usersInterested; + + return users; + } } \ No newline at end of file diff --git a/apps/api/src/resources/User/entity.ts b/apps/api/src/resources/User/entity.ts index c2968f87..05c8102c 100644 --- a/apps/api/src/resources/User/entity.ts +++ b/apps/api/src/resources/User/entity.ts @@ -147,4 +147,12 @@ export class User extends BaseEntity { }) @JoinTable() public eventsAttended: Lazy; + + @Field(() => [Event], { nullable: true }) + @ManyToMany(() => Event, (event: Event) => event.usersInterested, { + lazy: true, + nullable: true + }) + @JoinTable() + public interestedEvents: Lazy; } diff --git a/apps/api/src/resources/User/resolver.ts b/apps/api/src/resources/User/resolver.ts index f0bd2f4e..288a54f6 100644 --- a/apps/api/src/resources/User/resolver.ts +++ b/apps/api/src/resources/User/resolver.ts @@ -167,6 +167,31 @@ export class UserResolver extends ResourceResolver( return event.save(); } + @Authorized() + @Mutation((_: void) => Event) + public async addEventInterested( + @Ctx() context: IContext, + @Arg("eventId") eventId: number, + //@Arg("userId") userId: string, //for testing purposes + ): Promise { + const curUser: User | undefined = context.state.user; + //const curUser: User | undefined = await User.findOneOrFail({ id: userId }); //also for testing purposes + + const event: Event = await Event.findOneOrFail({ id: eventId }); + let users: User[] = await event.usersInterested; + + if(curUser == null) + return event; + + for(let i = 0; i < users.length; i++) + if(users[i].id == curUser.id) + return event; + + users.push(await User.findOneOrFail({ id: curUser.id })); + event.usersInterested = users; + return event.save(); + } + @Query((_: void) => resource, { nullable: true }) protected async me(@Ctx() context: IContext) { const user: User | undefined = context.state.user; From e3a91f08825b537eb1717f1ff34cfdc51c2635f1 Mon Sep 17 00:00:00 2001 From: Andrew-Gibson42 Date: Wed, 7 Oct 2020 11:26:30 -0500 Subject: [PATCH 02/10] Implemented most of front end --- apps/admin-web/src/App.tsx | 2 + apps/admin-web/src/generated/graphql.tsx | 83 ++++++++++++++ .../src/screens/EventData/helpers.ts | 19 ++++ .../admin-web/src/screens/EventData/index.tsx | 107 ++++++++++++++++++ .../src/screens/EventData/interfaces.ts | 13 +++ apps/api/src/resources/Event/resolver.ts | 4 +- 6 files changed, 226 insertions(+), 2 deletions(-) create mode 100644 apps/admin-web/src/screens/EventData/helpers.ts create mode 100644 apps/admin-web/src/screens/EventData/index.tsx create mode 100644 apps/admin-web/src/screens/EventData/interfaces.ts diff --git a/apps/admin-web/src/App.tsx b/apps/admin-web/src/App.tsx index 0d8e691f..789ee23b 100644 --- a/apps/admin-web/src/App.tsx +++ b/apps/admin-web/src/App.tsx @@ -4,6 +4,7 @@ import { Layout, Menu, PageHeader, Spin } from "antd"; import { ToolList } from "./Tools"; import { Events } from "./screens/Events"; +import { EventData } from "./screens/EventData"; import { Membership } from "./screens/Membership"; import { RedemptionCodes } from "./screens/RedemptionCodes"; import { config } from "./config"; @@ -22,6 +23,7 @@ const MainContent: React.SFC<{}> = (): JSX.Element => { + diff --git a/apps/admin-web/src/generated/graphql.tsx b/apps/admin-web/src/generated/graphql.tsx index 5d19d855..f3e8831e 100644 --- a/apps/admin-web/src/generated/graphql.tsx +++ b/apps/admin-web/src/generated/graphql.tsx @@ -40,6 +40,7 @@ export type Event = { eventLink?: Maybe; urlKey?: Maybe; attendees?: Maybe>; + usersInterested?: Maybe>; }; export type EventCreateInput = { @@ -119,6 +120,7 @@ export type Mutation = { updateShirtReceived: User; resetShirtReceived: Array; attendEvent: Event; + addEventInterested: Event; }; @@ -247,6 +249,11 @@ export type MutationAttendEventArgs = { eventId: Scalars['Float']; }; + +export type MutationAddEventInterestedArgs = { + eventId: Scalars['Float']; +}; + export type Permission = { __typename?: 'Permission'; name: Scalars['ID']; @@ -299,6 +306,8 @@ export type Query = { currentEvents: Array; event: Event; eventsWithKey: Array; + getAttendees: Array; + getInterestedUsers: Array; groups: Array; permissions: Array; products: Array; @@ -328,6 +337,16 @@ export type QueryEventsWithKeyArgs = { urlKey: Scalars['String']; }; + +export type QueryGetAttendeesArgs = { + urlKey: Scalars['String']; +}; + + +export type QueryGetInterestedUsersArgs = { + urlKey: Scalars['String']; +}; + export type RedemptionCode = { __typename?: 'RedemptionCode'; id: Scalars['ID']; @@ -408,6 +427,7 @@ export type User = { permissions?: Maybe>; groups: Array; eventsAttended?: Maybe>; + interestedEvents?: Maybe>; }; export type UserCreateInput = { @@ -428,6 +448,26 @@ export type UserUpdateInput = { email?: Maybe; }; +export type EventsWithKeyQueryVariables = Exact<{ + urlKey: Scalars['String']; +}>; + + +export type EventsWithKeyQuery = ( + { __typename?: 'Query' } + & { eventsWithKey: Array<( + { __typename?: 'Event' } + & Pick + & { attendees?: Maybe + )>>, usersInterested?: Maybe + )>> } + )> } +); + export type SigsQueryVariables = Exact<{ [key: string]: never; }>; @@ -621,6 +661,49 @@ export type GetProductsQuery = ( ); +export const EventsWithKeyDocument = gql` + query eventsWithKey($urlKey: String!) { + eventsWithKey(urlKey: $urlKey) { + eventTitle + attendees { + firstName + lastName + email + } + usersInterested { + firstName + lastName + email + } + } +} + `; + +/** + * __useEventsWithKeyQuery__ + * + * To run a query within a React component, call `useEventsWithKeyQuery` and pass it any options that fit your needs. + * When your component renders, `useEventsWithKeyQuery` returns an object from Apollo Client that contains loading, error, and data properties + * you can use to render your UI. + * + * @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options; + * + * @example + * const { data, loading, error } = useEventsWithKeyQuery({ + * variables: { + * urlKey: // value for 'urlKey' + * }, + * }); + */ +export function useEventsWithKeyQuery(baseOptions?: ApolloReactHooks.QueryHookOptions) { + return ApolloReactHooks.useQuery(EventsWithKeyDocument, baseOptions); + } +export function useEventsWithKeyLazyQuery(baseOptions?: ApolloReactHooks.LazyQueryHookOptions) { + return ApolloReactHooks.useLazyQuery(EventsWithKeyDocument, baseOptions); + } +export type EventsWithKeyQueryHookResult = ReturnType; +export type EventsWithKeyLazyQueryHookResult = ReturnType; +export type EventsWithKeyQueryResult = ApolloReactCommon.QueryResult; export const SigsDocument = gql` query sigs { sigs { diff --git a/apps/admin-web/src/screens/EventData/helpers.ts b/apps/admin-web/src/screens/EventData/helpers.ts new file mode 100644 index 00000000..0f93f457 --- /dev/null +++ b/apps/admin-web/src/screens/EventData/helpers.ts @@ -0,0 +1,19 @@ +import gql from "graphql-tag"; + +export const EVENTS_WITH_KEY: any = gql` + query eventsWithKey($urlKey: String!) { + eventsWithKey(urlKey: $urlKey) { + eventTitle + attendees { + firstName + lastName + email + } + usersInterested { + firstName + lastName + email + } + } + } +`; \ No newline at end of file diff --git a/apps/admin-web/src/screens/EventData/index.tsx b/apps/admin-web/src/screens/EventData/index.tsx new file mode 100644 index 00000000..e526c84d --- /dev/null +++ b/apps/admin-web/src/screens/EventData/index.tsx @@ -0,0 +1,107 @@ +import React, { useEffect, useState } from "react"; +import { Statistic, Row, Col, Table, message } from 'antd'; +import { IEvent, IUser } from "./interfaces"; +import { useEventsWithKeyQuery } from "../../generated/graphql"; + +const EventData: React.FC<{match: any}> = ({match}: any) => { +// const statStyle = { +// fontSize: 35, +// fontWeight: 100, +// } + + const eventUrlKey: string = match.params.eventId; + + const [event, setEvent] = useState(); + const [attendees, setAttendees] = useState(); + const [usersInterested, setUsersInterested] = useState(); + + const { + loading: eventLoading, + error: eventError, + data: eventData, + }: any = useEventsWithKeyQuery({variables: { urlKey: eventUrlKey }}); + + useEffect(() => { + if (eventLoading) + message.info("Event data loading..."); + else if (eventError) + message.info("An error occured loading event data."); + else if (eventData) { + setEvent((eventData.eventsWithKey)[0]); + //message.success("Event data loading complete!"); + } + }, [eventData, eventError, eventLoading]); + + useEffect(() => { + if(event != undefined) { + setAttendees(event.attendees); + setUsersInterested(event.usersInterested); + } + }, [event]); + + if(eventUrlKey == null) + return

This event does not exist.

+ + const ordinal_suffix: Function = (num: number) => { + let j = num % 10, + k = num % 100; + if (j == 1 && k != 11) + return "st"; + if (j == 2 && k != 12) + return "nd"; + if (j == 3 && k != 13) + return "rd"; + + return "th"; + }; + + const columns = [ + { + title: "Name", + key: "name", + render: (record: IUser) => ( + {`${record.firstName} ${record.lastName}`} + ), + }, + { + title: "Email", + dataIndex: "email", + key: "email", + } + ]; + + return ( +
+

{event?.eventTitle} Data

+ + + + + + + + + + + + + + + + +
+ +

Interested Users

+ + + +
+ +

Attendees

+ +
+ + ); +}; + +export { EventData }; \ No newline at end of file diff --git a/apps/admin-web/src/screens/EventData/interfaces.ts b/apps/admin-web/src/screens/EventData/interfaces.ts new file mode 100644 index 00000000..ae5ed20f --- /dev/null +++ b/apps/admin-web/src/screens/EventData/interfaces.ts @@ -0,0 +1,13 @@ +interface IUser { + firstName: string; + lastName: string; + email: string; +} + +interface IEvent { + eventTitle: string; + attendees: IUser[]; + usersInterested: IUser[]; +} + +export type { IUser, IEvent }; \ No newline at end of file diff --git a/apps/api/src/resources/Event/resolver.ts b/apps/api/src/resources/Event/resolver.ts index f9931e47..ae615aee 100644 --- a/apps/api/src/resources/Event/resolver.ts +++ b/apps/api/src/resources/Event/resolver.ts @@ -205,7 +205,7 @@ export class EventResolver { return this.repository.find({ urlKey }); } - @Query(() => [Event]) + @Query(() => [User]) protected async getAttendees(@Arg("urlKey", () => String) urlKey: string): Promise { const event: Event = await Event.findOneOrFail({ urlKey: urlKey }); let users: User[] = await event.attendees; @@ -213,7 +213,7 @@ export class EventResolver { return users; } - @Query(() => [Event]) + @Query(() => [User]) protected async getInterestedUsers(@Arg("urlKey", () => String) urlKey: string): Promise { const event: Event = await Event.findOneOrFail({ urlKey: urlKey }); let users: User[] = await event.usersInterested; From 64ec1f55b0e7093247948ee0be789673e1fd7981 Mon Sep 17 00:00:00 2001 From: Andrew-Gibson42 Date: Mon, 19 Oct 2020 13:29:08 -0500 Subject: [PATCH 03/10] Added QR/link to data page, began csv download work --- apps/admin-web/package.json | 4 +- .../src/screens/EventData/QRModal.tsx | 49 +++++++++++ .../admin-web/src/screens/EventData/index.tsx | 86 +++++++++++++++++-- yarn.lock | 5 ++ 4 files changed, 137 insertions(+), 7 deletions(-) create mode 100644 apps/admin-web/src/screens/EventData/QRModal.tsx diff --git a/apps/admin-web/package.json b/apps/admin-web/package.json index ce441d99..3b55de43 100644 --- a/apps/admin-web/package.json +++ b/apps/admin-web/package.json @@ -6,6 +6,8 @@ "@apollo/react-hooks": "^3.0.0", "@auth0/auth0-spa-js": "^1.2.2", "antd": "^4.1.3", + "export-to-csv": "^0.2.1", + "graphql": "^14", "graphql-tag": "^2.10.1", "qrcode.react": "^1.0.0", "react": "^16.7.0", @@ -56,4 +58,4 @@ "not ie <= 11", "not op_mini all" ] -} +} \ No newline at end of file diff --git a/apps/admin-web/src/screens/EventData/QRModal.tsx b/apps/admin-web/src/screens/EventData/QRModal.tsx new file mode 100644 index 00000000..c7f63bcf --- /dev/null +++ b/apps/admin-web/src/screens/EventData/QRModal.tsx @@ -0,0 +1,49 @@ +import React from "react"; +import Modal from "antd/lib/modal/Modal"; + +var QRCode = require('qrcode.react'); + +interface IQRModalProps { + visible: boolean; + setVisible: (arg: boolean) => void; + eventUrlKey?: string; +} + +const QRModal: React.FC = ({ + visible, + setVisible, + eventUrlKey +}: IQRModalProps): JSX.Element => { + const handleCancel: () => void = (): void => { + setVisible(false); + }; + + const registrationLink: string = "https://mstacm.org/e/" + eventUrlKey; + + if(eventUrlKey == null) { + return ( + +

This event does not have a registration URL key.

+
+ ); + } + + return ( + +

{registrationLink}

+ +
+ ); +}; + +export { QRModal }; \ No newline at end of file diff --git a/apps/admin-web/src/screens/EventData/index.tsx b/apps/admin-web/src/screens/EventData/index.tsx index e526c84d..26873cca 100644 --- a/apps/admin-web/src/screens/EventData/index.tsx +++ b/apps/admin-web/src/screens/EventData/index.tsx @@ -1,6 +1,8 @@ import React, { useEffect, useState } from "react"; import { Statistic, Row, Col, Table, message } from 'antd'; +import { ExportToCsv } from "export-to-csv"; import { IEvent, IUser } from "./interfaces"; +import { QRModal } from "./QRModal" import { useEventsWithKeyQuery } from "../../generated/graphql"; const EventData: React.FC<{match: any}> = ({match}: any) => { @@ -14,6 +16,23 @@ const EventData: React.FC<{match: any}> = ({match}: any) => { const [event, setEvent] = useState(); const [attendees, setAttendees] = useState(); const [usersInterested, setUsersInterested] = useState(); + const [QRVisible, setQRVisible] = useState(false); + + let attendeeEmails: string[] = []; + + const csvOptions = { + filename: 'email addresses', + fieldSeparator: ',', + quoteStrings: '"', + decimalSeparator: '.', + showLabels: true, + showTitle: true, + title: 'Attendee Emails', + useTextFile: false, + useBom: true, + useKeysAsHeaders: true, + // headers: ['Column 1', 'Column 2', etc...] <-- Won't work with useKeysAsHeaders present! + }; const { loading: eventLoading, @@ -39,9 +58,22 @@ const EventData: React.FC<{match: any}> = ({match}: any) => { } }, [event]); + useEffect(() => { + if(attendees != undefined) { + attendeeEmails = attendees?.map(attendees => attendees.email); + console.log(attendeeEmails) + } + }, [attendees]); + if(eventUrlKey == null) return

This event does not exist.

+ const get_ratio: Function = () => { + if(usersInterested != undefined && attendees != undefined && attendees.length != 0) + return (usersInterested.length / attendees.length) * 100 + return null + }; + const ordinal_suffix: Function = (num: number) => { let j = num % 10, k = num % 100; @@ -70,19 +102,28 @@ const EventData: React.FC<{match: any}> = ({match}: any) => { } ]; + const handleQR: () => void = (): void => { + setQRVisible(true); + }; + + const downloadAttendeeCSV: () => void = (): void => { + const csvExporter = new ExportToCsv(csvOptions); + csvExporter.generateCsv(attendeeEmails); + }; + return (
-

{event?.eventTitle} Data

+

{event?.eventTitle} Data

- + - + - + @@ -90,16 +131,49 @@ const EventData: React.FC<{match: any}> = ({match}: any) => {
+
+ + +
+

Interested Users

+ + + + + + -

Interested Users

+ {/*

Interested Users

*/} + {/* */} + {/*
*/}

-

Attendees

+ + +

Attendees

+ + + Registration Link / QR + + + Download Email CSV + + + + {/*

Attendees

+ */} + {/*div style={{float: "left", marginLeft: 50, marginTop: 2}}>Download Email CSV*/}
+ + ); }; diff --git a/yarn.lock b/yarn.lock index e5acdd3e..6f42dd88 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7196,6 +7196,11 @@ expect@^24.9.0: jest-message-util "^24.9.0" jest-regex-util "^24.9.0" +export-to-csv@^0.2.1: + version "0.2.1" + resolved "https://registry.yarnpkg.com/export-to-csv/-/export-to-csv-0.2.1.tgz#8f997156feebc1cf995096da16341aa0100cce4e" + integrity sha512-KTbrd3CAZ0cFceJEZr1e5uiMasabeCpXq1/5uvVxDl53o4jXJHnltasQoj2NkzrxD8hU9kdwjnMhoir/7nNx/A== + express@^4.17.1: version "4.17.1" resolved "https://registry.yarnpkg.com/express/-/express-4.17.1.tgz#4491fc38605cf51f8629d39c2b5d026f98a4c134" From d966940e9f322dbe098cf7955ad2e22452b8d32f Mon Sep 17 00:00:00 2001 From: Andrew-Gibson42 Date: Mon, 26 Oct 2020 11:49:21 -0500 Subject: [PATCH 04/10] Added attendance ranking --- apps/admin-web/src/generated/graphql.tsx | 46 +++++++++++++++++++ .../src/screens/EventData/helpers.ts | 31 ++++++++----- .../admin-web/src/screens/EventData/index.tsx | 36 +++++++++++++-- .../src/screens/EventData/interfaces.ts | 7 ++- apps/api/src/resources/Event/entity.ts | 5 ++ apps/api/src/resources/Event/resolver.ts | 24 +++++++++- 6 files changed, 131 insertions(+), 18 deletions(-) diff --git a/apps/admin-web/src/generated/graphql.tsx b/apps/admin-web/src/generated/graphql.tsx index f3e8831e..65fe302d 100644 --- a/apps/admin-web/src/generated/graphql.tsx +++ b/apps/admin-web/src/generated/graphql.tsx @@ -41,6 +41,7 @@ export type Event = { urlKey?: Maybe; attendees?: Maybe>; usersInterested?: Maybe>; + numAttendees?: Maybe; }; export type EventCreateInput = { @@ -308,6 +309,7 @@ export type Query = { eventsWithKey: Array; getAttendees: Array; getInterestedUsers: Array; + yearEvents: Array; groups: Array; permissions: Array; products: Array; @@ -468,6 +470,17 @@ export type EventsWithKeyQuery = ( )> } ); +export type YearEventsQueryVariables = Exact<{ [key: string]: never; }>; + + +export type YearEventsQuery = ( + { __typename?: 'Query' } + & { yearEvents: Array<( + { __typename?: 'Event' } + & Pick + )> } +); + export type SigsQueryVariables = Exact<{ [key: string]: never; }>; @@ -704,6 +717,39 @@ export function useEventsWithKeyLazyQuery(baseOptions?: ApolloReactHooks.LazyQue export type EventsWithKeyQueryHookResult = ReturnType; export type EventsWithKeyLazyQueryHookResult = ReturnType; export type EventsWithKeyQueryResult = ApolloReactCommon.QueryResult; +export const YearEventsDocument = gql` + query yearEvents { + yearEvents { + urlKey + numAttendees + } +} + `; + +/** + * __useYearEventsQuery__ + * + * To run a query within a React component, call `useYearEventsQuery` and pass it any options that fit your needs. + * When your component renders, `useYearEventsQuery` returns an object from Apollo Client that contains loading, error, and data properties + * you can use to render your UI. + * + * @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options; + * + * @example + * const { data, loading, error } = useYearEventsQuery({ + * variables: { + * }, + * }); + */ +export function useYearEventsQuery(baseOptions?: ApolloReactHooks.QueryHookOptions) { + return ApolloReactHooks.useQuery(YearEventsDocument, baseOptions); + } +export function useYearEventsLazyQuery(baseOptions?: ApolloReactHooks.LazyQueryHookOptions) { + return ApolloReactHooks.useLazyQuery(YearEventsDocument, baseOptions); + } +export type YearEventsQueryHookResult = ReturnType; +export type YearEventsLazyQueryHookResult = ReturnType; +export type YearEventsQueryResult = ApolloReactCommon.QueryResult; export const SigsDocument = gql` query sigs { sigs { diff --git a/apps/admin-web/src/screens/EventData/helpers.ts b/apps/admin-web/src/screens/EventData/helpers.ts index 0f93f457..6fd562d6 100644 --- a/apps/admin-web/src/screens/EventData/helpers.ts +++ b/apps/admin-web/src/screens/EventData/helpers.ts @@ -3,17 +3,26 @@ import gql from "graphql-tag"; export const EVENTS_WITH_KEY: any = gql` query eventsWithKey($urlKey: String!) { eventsWithKey(urlKey: $urlKey) { - eventTitle - attendees { - firstName - lastName - email - } - usersInterested { - firstName - lastName - email - } + eventTitle + attendees { + firstName + lastName + email + } + usersInterested { + firstName + lastName + email + } + } + } +`; + +export const YEAR_EVENTS: any = gql` + query yearEvents { + yearEvents { + urlKey + numAttendees } } `; \ No newline at end of file diff --git a/apps/admin-web/src/screens/EventData/index.tsx b/apps/admin-web/src/screens/EventData/index.tsx index 26873cca..c5ca6d2f 100644 --- a/apps/admin-web/src/screens/EventData/index.tsx +++ b/apps/admin-web/src/screens/EventData/index.tsx @@ -1,9 +1,10 @@ import React, { useEffect, useState } from "react"; import { Statistic, Row, Col, Table, message } from 'antd'; import { ExportToCsv } from "export-to-csv"; -import { IEvent, IUser } from "./interfaces"; +import { IUser, IEvent, IYearEvent } from "./interfaces"; import { QRModal } from "./QRModal" -import { useEventsWithKeyQuery } from "../../generated/graphql"; +import { useEventsWithKeyQuery, + useYearEventsQuery } from "../../generated/graphql"; const EventData: React.FC<{match: any}> = ({match}: any) => { // const statStyle = { @@ -16,6 +17,8 @@ const EventData: React.FC<{match: any}> = ({match}: any) => { const [event, setEvent] = useState(); const [attendees, setAttendees] = useState(); const [usersInterested, setUsersInterested] = useState(); + const [yearEvents, setYearEvents] = useState(); + const [attendancePlace, setAttendancePlace] = useState(); const [QRVisible, setQRVisible] = useState(false); let attendeeEmails: string[] = []; @@ -40,6 +43,12 @@ const EventData: React.FC<{match: any}> = ({match}: any) => { data: eventData, }: any = useEventsWithKeyQuery({variables: { urlKey: eventUrlKey }}); + const { + loading: yearEventsLoading, + error: yearEventsError, + data: yearEventsData, + }: any = useYearEventsQuery(); + useEffect(() => { if (eventLoading) message.info("Event data loading..."); @@ -51,6 +60,19 @@ const EventData: React.FC<{match: any}> = ({match}: any) => { } }, [eventData, eventError, eventLoading]); + useEffect(() => { + if (yearEventsLoading) { + //message.info("Year event data loading..."); + } + else if (yearEventsError) { + message.info("An error occured loading year event data."); + } + else if (yearEventsData) { + setYearEvents(yearEventsData.yearEvents); + //message.success("Event data loading complete!"); + } + }, [yearEventsData, yearEventsError, yearEventsLoading]); + useEffect(() => { if(event != undefined) { setAttendees(event.attendees); @@ -61,10 +83,16 @@ const EventData: React.FC<{match: any}> = ({match}: any) => { useEffect(() => { if(attendees != undefined) { attendeeEmails = attendees?.map(attendees => attendees.email); - console.log(attendeeEmails) } }, [attendees]); + useEffect(() => { + if(yearEvents != undefined) { + setYearEvents(yearEvents.sort((a,b) => (a.numAttendees > b.numAttendees) ? -1 : ((b.numAttendees > a.numAttendees) ? 1 : 0))); + setAttendancePlace(yearEvents.map(function(e) { return e.urlKey }).indexOf(eventUrlKey) + 1); + } + }, [yearEvents]); + if(eventUrlKey == null) return

This event does not exist.

@@ -126,7 +154,7 @@ const EventData: React.FC<{match: any}> = ({match}: any) => {
- + diff --git a/apps/admin-web/src/screens/EventData/interfaces.ts b/apps/admin-web/src/screens/EventData/interfaces.ts index ae5ed20f..95f59db9 100644 --- a/apps/admin-web/src/screens/EventData/interfaces.ts +++ b/apps/admin-web/src/screens/EventData/interfaces.ts @@ -10,4 +10,9 @@ interface IEvent { usersInterested: IUser[]; } -export type { IUser, IEvent }; \ No newline at end of file +interface IYearEvent { + urlKey: string; + numAttendees: number; +} + +export type { IUser, IEvent, IYearEvent }; \ No newline at end of file diff --git a/apps/api/src/resources/Event/entity.ts b/apps/api/src/resources/Event/entity.ts index 86cb39c3..37d53e96 100644 --- a/apps/api/src/resources/Event/entity.ts +++ b/apps/api/src/resources/Event/entity.ts @@ -100,4 +100,9 @@ export class Event extends BaseEntity { nullable: true }) public usersInterested: Lazy; + + @Field({ defaultValue: 0 }) + @Column({ default: 0 }) + public numAttendees: number; + } diff --git a/apps/api/src/resources/Event/resolver.ts b/apps/api/src/resources/Event/resolver.ts index ae615aee..49409600 100644 --- a/apps/api/src/resources/Event/resolver.ts +++ b/apps/api/src/resources/Event/resolver.ts @@ -1,10 +1,10 @@ import { AuthenticationError, UserInputError } from "apollo-server"; -import { Arg, Authorized, Ctx, Mutation, Query, Resolver } from "type-graphql"; +import { Arg, Authorized, Ctx, Mutation, Query, Resolver, FieldResolver, Root } from "type-graphql"; import { DeepPartial, getRepository, MoreThanOrEqual, - Repository + Repository, } from "typeorm"; import { GraphQLUpload } from "graphql-upload"; @@ -220,4 +220,24 @@ export class EventResolver { return users; } + + @Query(() => [Event]) + protected async yearEvents(): Promise { + const startDate: Date = new Date(Date.now()); + startDate.setMonth(7); + startDate.setDate(0); //August 1st, {year} + return this.repository.find({ + where: { + dateExpire: MoreThanOrEqual(startDate) + } + }); + } + + @FieldResolver((_: void) => Number, { nullable: false }) + public async numAttendees( + @Root() event: Event, + ): Promise { + const attendeeList: User[] = await event.attendees; + return attendeeList.length; + } } \ No newline at end of file From 9bcb8e290f6d0448be9e9f75b1afd6d7b62e8cd7 Mon Sep 17 00:00:00 2001 From: Andrew-Gibson42 Date: Fri, 30 Oct 2020 10:30:31 -0500 Subject: [PATCH 05/10] Finished email csv downloading --- .../admin-web/src/screens/EventData/index.tsx | 35 ++++++++++++++----- 1 file changed, 27 insertions(+), 8 deletions(-) diff --git a/apps/admin-web/src/screens/EventData/index.tsx b/apps/admin-web/src/screens/EventData/index.tsx index c5ca6d2f..65e99ef7 100644 --- a/apps/admin-web/src/screens/EventData/index.tsx +++ b/apps/admin-web/src/screens/EventData/index.tsx @@ -17,23 +17,27 @@ const EventData: React.FC<{match: any}> = ({match}: any) => { const [event, setEvent] = useState(); const [attendees, setAttendees] = useState(); const [usersInterested, setUsersInterested] = useState(); + const [yearEvents, setYearEvents] = useState(); const [attendancePlace, setAttendancePlace] = useState(); + const [QRVisible, setQRVisible] = useState(false); - let attendeeEmails: string[] = []; + const [attendeeEmails, setAttendeeEmails] = useState(); + const [interestEmails, setInterestEmails] = useState(); const csvOptions = { filename: 'email addresses', fieldSeparator: ',', - quoteStrings: '"', + quoteStrings: '', decimalSeparator: '.', showLabels: true, showTitle: true, - title: 'Attendee Emails', + title: 'Emails', useTextFile: false, useBom: true, - useKeysAsHeaders: true, + //useKeysAsHeaders: true, + headers: ['Emails'] // headers: ['Column 1', 'Column 2', etc...] <-- Won't work with useKeysAsHeaders present! }; @@ -81,9 +85,13 @@ const EventData: React.FC<{match: any}> = ({match}: any) => { }, [event]); useEffect(() => { - if(attendees != undefined) { - attendeeEmails = attendees?.map(attendees => attendees.email); - } + if(usersInterested != undefined) + setInterestEmails(usersInterested?.map(usersInterested => usersInterested.email)); + }, [usersInterested]); + + useEffect(() => { + if(attendees != undefined) + setAttendeeEmails(attendees?.map(attendees => attendees.email)); }, [attendees]); useEffect(() => { @@ -134,9 +142,18 @@ const EventData: React.FC<{match: any}> = ({match}: any) => { setQRVisible(true); }; + const downloadInterestCSV: () => void = (): void => { + csvOptions.filename = "interest_emails"; + csvOptions.title = "Interested Users"; + const csvExporter = new ExportToCsv(csvOptions); + csvExporter.generateCsv(interestEmails?.map(interestEmails => ({ interestEmails }))); + }; + const downloadAttendeeCSV: () => void = (): void => { + csvOptions.filename = "attendee_emails"; + csvOptions.title = "Attendees"; const csvExporter = new ExportToCsv(csvOptions); - csvExporter.generateCsv(attendeeEmails); + csvExporter.generateCsv(attendeeEmails?.map(attendeeEmails => ({ attendeeEmails }))); }; return ( @@ -166,8 +183,10 @@ const EventData: React.FC<{match: any}> = ({match}: any) => {

Interested Users

+ + Download Email CSV From 82dbe689c7b13703e9e8cdb6f76c2da1095788c8 Mon Sep 17 00:00:00 2001 From: Andrew-Gibson42 Date: Fri, 30 Oct 2020 11:08:07 -0500 Subject: [PATCH 06/10] Added interest redirect in web/record interest page in profile-web --- apps/admin-web/src/generated/graphql.tsx | 4 +- .../admin-web/src/screens/EventData/index.tsx | 5 - apps/api/src/resources/User/resolver.ts | 2 +- apps/profile-web/src/App.tsx | 3 +- .../components/EventRegistration/helpers.ts | 10 ++ .../components/EventRegistration/index.tsx | 121 +++++++++++++++++- apps/profile-web/src/generated/graphql.tsx | 72 +++++++++++ apps/web/src/App.tsx | 3 +- apps/web/src/generated/graphql.tsx | 23 ++++ .../screens/EventRegistrationPage/index.tsx | 19 ++- apps/web/src/screens/index.ts | 4 +- 11 files changed, 252 insertions(+), 14 deletions(-) diff --git a/apps/admin-web/src/generated/graphql.tsx b/apps/admin-web/src/generated/graphql.tsx index 65fe302d..64f36c89 100644 --- a/apps/admin-web/src/generated/graphql.tsx +++ b/apps/admin-web/src/generated/graphql.tsx @@ -121,7 +121,7 @@ export type Mutation = { updateShirtReceived: User; resetShirtReceived: Array; attendEvent: Event; - addEventInterested: Event; + recordInterest: Event; }; @@ -251,7 +251,7 @@ export type MutationAttendEventArgs = { }; -export type MutationAddEventInterestedArgs = { +export type MutationRecordInterestArgs = { eventId: Scalars['Float']; }; diff --git a/apps/admin-web/src/screens/EventData/index.tsx b/apps/admin-web/src/screens/EventData/index.tsx index 65e99ef7..43456d7d 100644 --- a/apps/admin-web/src/screens/EventData/index.tsx +++ b/apps/admin-web/src/screens/EventData/index.tsx @@ -7,11 +7,6 @@ import { useEventsWithKeyQuery, useYearEventsQuery } from "../../generated/graphql"; const EventData: React.FC<{match: any}> = ({match}: any) => { -// const statStyle = { -// fontSize: 35, -// fontWeight: 100, -// } - const eventUrlKey: string = match.params.eventId; const [event, setEvent] = useState(); diff --git a/apps/api/src/resources/User/resolver.ts b/apps/api/src/resources/User/resolver.ts index 288a54f6..7a4ac83a 100644 --- a/apps/api/src/resources/User/resolver.ts +++ b/apps/api/src/resources/User/resolver.ts @@ -169,7 +169,7 @@ export class UserResolver extends ResourceResolver( @Authorized() @Mutation((_: void) => Event) - public async addEventInterested( + public async recordInterest( @Ctx() context: IContext, @Arg("eventId") eventId: number, //@Arg("userId") userId: string, //for testing purposes diff --git a/apps/profile-web/src/App.tsx b/apps/profile-web/src/App.tsx index c35d53a2..032619fa 100644 --- a/apps/profile-web/src/App.tsx +++ b/apps/profile-web/src/App.tsx @@ -2,7 +2,7 @@ import React, { useEffect } from "react"; import { BrowserRouter, Link, Route, Switch } from "react-router-dom"; import { SubmitResume } from "./components/SubmitResume"; -import { EventRegistration } from "./components/EventRegistration"; +import { EventRegistration, EventInterest } from "./components/EventRegistration"; import { config } from "./config"; import "./static/css/App.css"; @@ -17,6 +17,7 @@ const MainContent: React.SFC = (): JSX.Element => { + ); }; diff --git a/apps/profile-web/src/components/EventRegistration/helpers.ts b/apps/profile-web/src/components/EventRegistration/helpers.ts index 57e02296..049c3c56 100644 --- a/apps/profile-web/src/components/EventRegistration/helpers.ts +++ b/apps/profile-web/src/components/EventRegistration/helpers.ts @@ -28,4 +28,14 @@ export const ATTEND_EVENT: any = gql` } } } +`; + +export const RECORD_INTEREST: any = gql` + mutation recordInterest($eventId: Float!) { + recordInterest(eventId: $eventId) { + usersInterested { + id + } + } + } `; \ No newline at end of file diff --git a/apps/profile-web/src/components/EventRegistration/index.tsx b/apps/profile-web/src/components/EventRegistration/index.tsx index e142eb7b..5867a1f5 100644 --- a/apps/profile-web/src/components/EventRegistration/index.tsx +++ b/apps/profile-web/src/components/EventRegistration/index.tsx @@ -3,6 +3,7 @@ import { message } from "antd"; import { useMeQuery, useEventsQuery, useAttendEventMutation, + useRecordInterestMutation, Event as IEvent } from "../../generated/graphql"; const EventRegistration: React.FC<{match: any}> = ({match}: any) => { @@ -123,4 +124,122 @@ const EventRegistration: React.FC<{match: any}> = ({match}: any) => { ); }; -export { EventRegistration } \ No newline at end of file +const EventInterest: React.FC<{match: any}> = ({match}: any) => { + //style sheets for output + const divStyle = { + margin: "auto", + justifyContent: "center", + textAlign: "center" as const, + marginTop: "10%", + } + + const errorStyle = { + color: "red", + fontSize: 32, + margin: "auto", + fontWeight: 100, + marginBottom: "10%", + } as React.CSSProperties; + + const successStyle = { + color: "#3c2eff", + fontSize: 32, + margin: "auto", + fontWeight: 100, + marginBottom: "10%", + } as React.CSSProperties; + + const linkStyle = { + fontSize: 30, + margin: "auto", + fontWeight: 100, + fontStyle: "italic", + } + + const { + loading: userLoading, + error: userError, + }: any = useMeQuery(); + + const { + loading: eventLoading, + error: eventError, + data: eventData, + }: any = useEventsQuery(); + + const [recordInterest]: any = useRecordInterestMutation(); + + const [curEvents, setCurEvents] = useState([]); + + const result: any = useMeQuery(); + + useEffect(() => { + if (eventLoading) { + //message.info("Event data loading..."); + } else if (eventError) { + message.info("An error occured loading event data."); + } else if (eventData) { + const events: IEvent[] = eventData.events; + setCurEvents(events); + //message.success("Event data loaded."); + } + }, [eventData, eventLoading, eventError]); + + useEffect(() => { + if (userLoading) { + //message.info("User data loading..."); + } else if (userError) { + message.info("An error occured loading user data."); + } + }, [userLoading, userError]); + + //once all mutations/queries are loaded, the actual attendance is tracked + useEffect(() => { + if(curEvents && recordInterest && result.data) { + if(result.data.me.id != null) + if(getEventId(eventUrlKey) != -1) { + recordInterest({ + variables: { + eventId: Number(getEventId(eventUrlKey)) + } + }); + } + else + message.error("Event does with URL key {" + eventUrlKey + "} does not exist."); + else + message.error("User ID is null."); + } + }, [curEvents, recordInterest]); + + const eventUrlKey: string | null = match.params.eventId; + + const getEventId: Function = (urlArg: string) => { + for (let i = 0; i < curEvents.length; i++) { + if (curEvents[i].urlKey === urlArg) + return curEvents[i].id; + } + return -1; + }; + + if(eventLoading || eventError || userLoading || userError) + return

Loading...

+ + if(result.data == null || getEventId(eventUrlKey) == -1 || !recordInterest) + return ( +
+

An error has occured.

+ Return to homepage +
+ ); + + //this default return statement can only be reached with + // the same conditions under which the attendance is tracked + return ( +
+

Interest has been successfully recorded!

+ Return to homepage +
+ ); +}; + +export { EventRegistration, EventInterest } \ No newline at end of file diff --git a/apps/profile-web/src/generated/graphql.tsx b/apps/profile-web/src/generated/graphql.tsx index 8e98e964..a7665778 100644 --- a/apps/profile-web/src/generated/graphql.tsx +++ b/apps/profile-web/src/generated/graphql.tsx @@ -40,6 +40,8 @@ export type Event = { eventLink?: Maybe; urlKey?: Maybe; attendees?: Maybe>; + usersInterested?: Maybe>; + numAttendees?: Maybe; }; export type EventCreateInput = { @@ -119,6 +121,7 @@ export type Mutation = { updateShirtReceived: User; resetShirtReceived: Array; attendEvent: Event; + recordInterest: Event; }; @@ -247,6 +250,11 @@ export type MutationAttendEventArgs = { eventId: Scalars['Float']; }; + +export type MutationRecordInterestArgs = { + eventId: Scalars['Float']; +}; + export type Permission = { __typename?: 'Permission'; name: Scalars['ID']; @@ -299,6 +307,9 @@ export type Query = { currentEvents: Array; event: Event; eventsWithKey: Array; + getAttendees: Array; + getInterestedUsers: Array; + yearEvents: Array; groups: Array; permissions: Array; products: Array; @@ -328,6 +339,16 @@ export type QueryEventsWithKeyArgs = { urlKey: Scalars['String']; }; + +export type QueryGetAttendeesArgs = { + urlKey: Scalars['String']; +}; + + +export type QueryGetInterestedUsersArgs = { + urlKey: Scalars['String']; +}; + export type RedemptionCode = { __typename?: 'RedemptionCode'; id: Scalars['ID']; @@ -408,6 +429,7 @@ export type User = { permissions?: Maybe>; groups: Array; eventsAttended?: Maybe>; + interestedEvents?: Maybe>; }; export type UserCreateInput = { @@ -459,6 +481,22 @@ export type AttendEventMutation = ( ) } ); +export type RecordInterestMutationVariables = Exact<{ + eventId: Scalars['Float']; +}>; + + +export type RecordInterestMutation = ( + { __typename?: 'Mutation' } + & { recordInterest: ( + { __typename?: 'Event' } + & { usersInterested?: Maybe + )>> } + ) } +); + export type UploadResumeMutationVariables = Exact<{ resume: Scalars['Upload']; grad: Scalars['DateTime']; @@ -565,6 +603,40 @@ export function useAttendEventMutation(baseOptions?: ApolloReactHooks.MutationHo export type AttendEventMutationHookResult = ReturnType; export type AttendEventMutationResult = ApolloReactCommon.MutationResult; export type AttendEventMutationOptions = ApolloReactCommon.BaseMutationOptions; +export const RecordInterestDocument = gql` + mutation recordInterest($eventId: Float!) { + recordInterest(eventId: $eventId) { + usersInterested { + id + } + } +} + `; +export type RecordInterestMutationFn = ApolloReactCommon.MutationFunction; + +/** + * __useRecordInterestMutation__ + * + * To run a mutation, you first call `useRecordInterestMutation` within a React component and pass it any options that fit your needs. + * When your component renders, `useRecordInterestMutation` returns a tuple that includes: + * - A mutate function that you can call at any time to execute the mutation + * - An object with fields that represent the current status of the mutation's execution + * + * @param baseOptions options that will be passed into the mutation, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options-2; + * + * @example + * const [recordInterestMutation, { data, loading, error }] = useRecordInterestMutation({ + * variables: { + * eventId: // value for 'eventId' + * }, + * }); + */ +export function useRecordInterestMutation(baseOptions?: ApolloReactHooks.MutationHookOptions) { + return ApolloReactHooks.useMutation(RecordInterestDocument, baseOptions); + } +export type RecordInterestMutationHookResult = ReturnType; +export type RecordInterestMutationResult = ApolloReactCommon.MutationResult; +export type RecordInterestMutationOptions = ApolloReactCommon.BaseMutationOptions; export const UploadResumeDocument = gql` mutation uploadResume($resume: Upload!, $grad: DateTime!, $fname: String!, $lname: String!) { uploadResume(resume: $resume, graduationDate: $grad, firstName: $fname, lastName: $lname) { diff --git a/apps/web/src/App.tsx b/apps/web/src/App.tsx index b9c68d87..bf899f93 100644 --- a/apps/web/src/App.tsx +++ b/apps/web/src/App.tsx @@ -4,7 +4,7 @@ import { BrowserRouter, Route, Switch } from "react-router-dom"; import ReactGA from 'react-ga'; import { config } from "./config"; -import { HomePage, NotFoundPage, EventRegistration } from "./screens"; +import { HomePage, NotFoundPage, EventRegistration, EventInterest } from "./screens"; import { useAuth0 } from "./utils/react-auth0-wrapper"; import "./static/css/App.css"; @@ -46,6 +46,7 @@ const App: React.FC<{}> = (): JSX.Element => { + diff --git a/apps/web/src/generated/graphql.tsx b/apps/web/src/generated/graphql.tsx index 2ec1e323..7a8d3431 100644 --- a/apps/web/src/generated/graphql.tsx +++ b/apps/web/src/generated/graphql.tsx @@ -40,6 +40,8 @@ export type Event = { eventLink?: Maybe; urlKey?: Maybe; attendees?: Maybe>; + usersInterested?: Maybe>; + numAttendees?: Maybe; }; export type EventCreateInput = { @@ -118,6 +120,7 @@ export type Mutation = { updateShirtReceived: User; resetShirtReceived: Array; attendEvent: Event; + recordInterest: Event; }; @@ -240,6 +243,11 @@ export type MutationAttendEventArgs = { eventId: Scalars['Float']; }; + +export type MutationRecordInterestArgs = { + eventId: Scalars['Float']; +}; + export type Permission = { __typename?: 'Permission'; name: Scalars['ID']; @@ -291,6 +299,10 @@ export type Query = { events: Array; currentEvents: Array; event: Event; + eventsWithKey: Array; + getAttendees: Array; + getInterestedUsers: Array; + yearEvents: Array; groups: Array; permissions: Array; products: Array; @@ -320,6 +332,16 @@ export type QueryEventsWithKeyArgs = { urlKey: Scalars['String']; }; + +export type QueryGetAttendeesArgs = { + urlKey: Scalars['String']; +}; + + +export type QueryGetInterestedUsersArgs = { + urlKey: Scalars['String']; +}; + export type RedemptionCode = { __typename?: 'RedemptionCode'; id: Scalars['ID']; @@ -400,6 +422,7 @@ export type User = { permissions?: Maybe>; groups: Array; eventsAttended?: Maybe>; + interestedEvents?: Maybe>; }; export type UserCreateInput = { diff --git a/apps/web/src/screens/EventRegistrationPage/index.tsx b/apps/web/src/screens/EventRegistrationPage/index.tsx index 949a84ea..98af37b1 100644 --- a/apps/web/src/screens/EventRegistrationPage/index.tsx +++ b/apps/web/src/screens/EventRegistrationPage/index.tsx @@ -17,4 +17,21 @@ const EventRegistration: React.FC<{match: any}> = ({match}: any) => { ); }; -export { EventRegistration } \ No newline at end of file +const EventInterest: React.FC<{match: any}> = ({match}: any) => { + useEffect(() => { + window.location.replace(redirectUrl); + }, []); + + const eventID: string | null = match.params.eventId; + + const redirectUrl: string = "https://profile.mstacm.org/interest/" + eventID; + + if(eventID === null) + return

Something went wrong while redirecting to your event.

; + + return ( + null + ); +}; + +export { EventRegistration, EventInterest } \ No newline at end of file diff --git a/apps/web/src/screens/index.ts b/apps/web/src/screens/index.ts index daeb6cc4..fe81217b 100644 --- a/apps/web/src/screens/index.ts +++ b/apps/web/src/screens/index.ts @@ -1,4 +1,4 @@ import { HomePage } from "./HomePage"; import { NotFoundPage } from "./NotFoundPage"; -import { EventRegistration } from "./EventRegistrationPage"; -export { HomePage, NotFoundPage, EventRegistration }; +import { EventRegistration, EventInterest } from "./EventRegistrationPage"; +export { HomePage, NotFoundPage, EventRegistration, EventInterest }; From fe91ff4d1ab7706abd1bcc2a8ebd326eadb508d6 Mon Sep 17 00:00:00 2001 From: Andrew-Gibson42 Date: Wed, 11 Nov 2020 11:40:13 -0600 Subject: [PATCH 07/10] Added qr/link for interest on event data page --- .../src/screens/EventData/QRModal.tsx | 2 +- .../admin-web/src/screens/EventData/index.tsx | 23 +++++++++---------- 2 files changed, 12 insertions(+), 13 deletions(-) diff --git a/apps/admin-web/src/screens/EventData/QRModal.tsx b/apps/admin-web/src/screens/EventData/QRModal.tsx index c7f63bcf..09b40b43 100644 --- a/apps/admin-web/src/screens/EventData/QRModal.tsx +++ b/apps/admin-web/src/screens/EventData/QRModal.tsx @@ -18,7 +18,7 @@ const QRModal: React.FC = ({ setVisible(false); }; - const registrationLink: string = "https://mstacm.org/e/" + eventUrlKey; + const registrationLink: string = "https://mstacm.org/" + eventUrlKey; if(eventUrlKey == null) { return ( diff --git a/apps/admin-web/src/screens/EventData/index.tsx b/apps/admin-web/src/screens/EventData/index.tsx index 43456d7d..5dbc6b62 100644 --- a/apps/admin-web/src/screens/EventData/index.tsx +++ b/apps/admin-web/src/screens/EventData/index.tsx @@ -21,6 +21,8 @@ const EventData: React.FC<{match: any}> = ({match}: any) => { const [attendeeEmails, setAttendeeEmails] = useState(); const [interestEmails, setInterestEmails] = useState(); + const [qrKey, setQrKey] = useState(); + const csvOptions = { filename: 'email addresses', fieldSeparator: ',', @@ -133,7 +135,12 @@ const EventData: React.FC<{match: any}> = ({match}: any) => { } ]; - const handleQR: () => void = (): void => { + const handleQR: Function = (choice: number) => { + if (choice == 0) + setQrKey("e/" + eventUrlKey); + else if (choice == 1) + setQrKey("i/" + eventUrlKey); + setQRVisible(true); }; @@ -178,17 +185,13 @@ const EventData: React.FC<{match: any}> = ({match}: any) => {

Interested Users

- + handleQR(1)}>Interest Link / QRDownload Email CSV - {/*

Interested Users

*/} - {/* */} - {/*
*/} -

@@ -198,23 +201,19 @@ const EventData: React.FC<{match: any}> = ({match}: any) => {

Attendees

- Registration Link / QR + handleQR(0)}>Registration Link / QRDownload Email CSV - {/*

Attendees

- */} - {/*div style={{float: "left", marginLeft: 50, marginTop: 2}}>Download Email CSV*/} -
); From 717ab29e55edc5151b11cb0ec0b887e40ace5759 Mon Sep 17 00:00:00 2001 From: Andrew-Gibson42 Date: Sat, 14 Nov 2020 13:55:23 -0600 Subject: [PATCH 08/10] Added polling to event data page queries --- apps/admin-web/src/screens/EventData/index.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/admin-web/src/screens/EventData/index.tsx b/apps/admin-web/src/screens/EventData/index.tsx index 5dbc6b62..74c3b98b 100644 --- a/apps/admin-web/src/screens/EventData/index.tsx +++ b/apps/admin-web/src/screens/EventData/index.tsx @@ -42,13 +42,13 @@ const EventData: React.FC<{match: any}> = ({match}: any) => { loading: eventLoading, error: eventError, data: eventData, - }: any = useEventsWithKeyQuery({variables: { urlKey: eventUrlKey }}); + }: any = useEventsWithKeyQuery({pollInterval: 500, variables: { urlKey: eventUrlKey }}); const { loading: yearEventsLoading, error: yearEventsError, data: yearEventsData, - }: any = useYearEventsQuery(); + }: any = useYearEventsQuery({pollInterval: 500}); useEffect(() => { if (eventLoading) From 4af14e97a49d5d537cd951cbf65c3faa93f9b5eb Mon Sep 17 00:00:00 2001 From: Andrew-Gibson42 Date: Tue, 19 Jan 2021 22:16:16 -0600 Subject: [PATCH 09/10] Fixed various pull request issues --- apps/admin-web/src/App.tsx | 4 +- .../admin-web/src/screens/EventData/index.tsx | 72 ++++++++----------- .../components/EventRegistration/index.tsx | 28 +++++--- 3 files changed, 52 insertions(+), 52 deletions(-) diff --git a/apps/admin-web/src/App.tsx b/apps/admin-web/src/App.tsx index 789ee23b..cdb9f104 100644 --- a/apps/admin-web/src/App.tsx +++ b/apps/admin-web/src/App.tsx @@ -22,8 +22,8 @@ const MainContent: React.SFC<{}> = (): JSX.Element => { return ( - - + + diff --git a/apps/admin-web/src/screens/EventData/index.tsx b/apps/admin-web/src/screens/EventData/index.tsx index 74c3b98b..633d116e 100644 --- a/apps/admin-web/src/screens/EventData/index.tsx +++ b/apps/admin-web/src/screens/EventData/index.tsx @@ -3,11 +3,18 @@ import { Statistic, Row, Col, Table, message } from 'antd'; import { ExportToCsv } from "export-to-csv"; import { IUser, IEvent, IYearEvent } from "./interfaces"; import { QRModal } from "./QRModal" -import { useEventsWithKeyQuery, - useYearEventsQuery } from "../../generated/graphql"; +import { EventsWithKeyQueryHookResult, + useEventsWithKeyQuery, + useYearEventsQuery, + YearEventsQueryHookResult} from "../../generated/graphql"; +import { RouteComponentProps } from "react-router-dom"; -const EventData: React.FC<{match: any}> = ({match}: any) => { - const eventUrlKey: string = match.params.eventId; +interface IMatchParams { + eventId: string; +} + +const EventData: React.FC> = (props) => { + const eventUrlKey: string = props.match.params.eventId; const [event, setEvent] = useState(); const [attendees, setAttendees] = useState(); @@ -18,9 +25,6 @@ const EventData: React.FC<{match: any}> = ({match}: any) => { const [QRVisible, setQRVisible] = useState(false); - const [attendeeEmails, setAttendeeEmails] = useState(); - const [interestEmails, setInterestEmails] = useState(); - const [qrKey, setQrKey] = useState(); const csvOptions = { @@ -33,22 +37,21 @@ const EventData: React.FC<{match: any}> = ({match}: any) => { title: 'Emails', useTextFile: false, useBom: true, - //useKeysAsHeaders: true, + useKeysAsHeaders: true, headers: ['Emails'] - // headers: ['Column 1', 'Column 2', etc...] <-- Won't work with useKeysAsHeaders present! }; const { loading: eventLoading, error: eventError, data: eventData, - }: any = useEventsWithKeyQuery({pollInterval: 500, variables: { urlKey: eventUrlKey }}); + }: EventsWithKeyQueryHookResult = useEventsWithKeyQuery({pollInterval: 500, variables: { urlKey: eventUrlKey }}); const { loading: yearEventsLoading, error: yearEventsError, data: yearEventsData, - }: any = useYearEventsQuery({pollInterval: 500}); + }: YearEventsQueryHookResult = useYearEventsQuery({pollInterval: 500}); useEffect(() => { if (eventLoading) @@ -56,65 +59,50 @@ const EventData: React.FC<{match: any}> = ({match}: any) => { else if (eventError) message.info("An error occured loading event data."); else if (eventData) { - setEvent((eventData.eventsWithKey)[0]); - //message.success("Event data loading complete!"); + setEvent((eventData.eventsWithKey)[0] as IEvent); } }, [eventData, eventError, eventLoading]); useEffect(() => { - if (yearEventsLoading) { - //message.info("Year event data loading..."); - } - else if (yearEventsError) { + if (yearEventsError) { message.info("An error occured loading year event data."); } else if (yearEventsData) { - setYearEvents(yearEventsData.yearEvents); - //message.success("Event data loading complete!"); + setYearEvents(yearEventsData.yearEvents as IYearEvent[]); } }, [yearEventsData, yearEventsError, yearEventsLoading]); useEffect(() => { - if(event != undefined) { + if(event !== undefined) { setAttendees(event.attendees); setUsersInterested(event.usersInterested); } }, [event]); useEffect(() => { - if(usersInterested != undefined) - setInterestEmails(usersInterested?.map(usersInterested => usersInterested.email)); - }, [usersInterested]); - - useEffect(() => { - if(attendees != undefined) - setAttendeeEmails(attendees?.map(attendees => attendees.email)); - }, [attendees]); - - useEffect(() => { - if(yearEvents != undefined) { + if(yearEvents !== undefined) { setYearEvents(yearEvents.sort((a,b) => (a.numAttendees > b.numAttendees) ? -1 : ((b.numAttendees > a.numAttendees) ? 1 : 0))); setAttendancePlace(yearEvents.map(function(e) { return e.urlKey }).indexOf(eventUrlKey) + 1); } }, [yearEvents]); - if(eventUrlKey == null) + if(eventUrlKey === null || !event) return

This event does not exist.

const get_ratio: Function = () => { - if(usersInterested != undefined && attendees != undefined && attendees.length != 0) + if(usersInterested !== undefined && attendees !== undefined && attendees.length !== 0) return (usersInterested.length / attendees.length) * 100 - return null + return "N/A" }; const ordinal_suffix: Function = (num: number) => { let j = num % 10, k = num % 100; - if (j == 1 && k != 11) + if (j === 1 && k !== 11) return "st"; - if (j == 2 && k != 12) + if (j === 2 && k !== 12) return "nd"; - if (j == 3 && k != 13) + if (j === 3 && k !== 13) return "rd"; return "th"; @@ -136,9 +124,9 @@ const EventData: React.FC<{match: any}> = ({match}: any) => { ]; const handleQR: Function = (choice: number) => { - if (choice == 0) + if (choice === 0) setQrKey("e/" + eventUrlKey); - else if (choice == 1) + else if (choice === 1) setQrKey("i/" + eventUrlKey); setQRVisible(true); @@ -148,14 +136,16 @@ const EventData: React.FC<{match: any}> = ({match}: any) => { csvOptions.filename = "interest_emails"; csvOptions.title = "Interested Users"; const csvExporter = new ExportToCsv(csvOptions); - csvExporter.generateCsv(interestEmails?.map(interestEmails => ({ interestEmails }))); + const columns: any = usersInterested?.map(obj => ({Email: obj.email, Name: obj.firstName + " " + obj.lastName})) + csvExporter.generateCsv(columns); }; const downloadAttendeeCSV: () => void = (): void => { csvOptions.filename = "attendee_emails"; csvOptions.title = "Attendees"; const csvExporter = new ExportToCsv(csvOptions); - csvExporter.generateCsv(attendeeEmails?.map(attendeeEmails => ({ attendeeEmails }))); + const columns: any = attendees?.map(obj => ({Email: obj.email, Name: obj.firstName + " " + obj.lastName})) + csvExporter.generateCsv(columns); }; return ( diff --git a/apps/profile-web/src/components/EventRegistration/index.tsx b/apps/profile-web/src/components/EventRegistration/index.tsx index 5867a1f5..1fa0b1ad 100644 --- a/apps/profile-web/src/components/EventRegistration/index.tsx +++ b/apps/profile-web/src/components/EventRegistration/index.tsx @@ -78,8 +78,8 @@ const EventRegistration: React.FC<{match: any}> = ({match}: any) => { //once all mutations/queries are loaded, the actual attendance is tracked useEffect(() => { if(curEvents && attendEvent && result.data) { - if(result.data.me.id != null) - if(getEventId(eventUrlKey) != -1) { + if(result.data.me.id !== null) + if(getEventId(eventUrlKey) !== -1) { attendEvent({ variables: { eventId: Number(getEventId(eventUrlKey)) @@ -173,6 +173,11 @@ const EventInterest: React.FC<{match: any}> = ({match}: any) => { const result: any = useMeQuery(); + const onRecordInterestError: (e: Error) => void = (e: Error): void => { + message.error("Upload failed. Please try again."); + console.log(e); + }; + useEffect(() => { if (eventLoading) { //message.info("Event data loading..."); @@ -196,13 +201,18 @@ const EventInterest: React.FC<{match: any}> = ({match}: any) => { //once all mutations/queries are loaded, the actual attendance is tracked useEffect(() => { if(curEvents && recordInterest && result.data) { - if(result.data.me.id != null) - if(getEventId(eventUrlKey) != -1) { - recordInterest({ - variables: { - eventId: Number(getEventId(eventUrlKey)) - } - }); + if(result.data.me.id !== null) + if(getEventId(eventUrlKey) !== -1) { + try { + recordInterest({ + variables: { + eventId: Number(getEventId(eventUrlKey)) + } + }); + } + catch(e) { + onRecordInterestError(e); + } } else message.error("Event does with URL key {" + eventUrlKey + "} does not exist."); From f59c5120c320f798d720e61012d444b62295c553 Mon Sep 17 00:00:00 2001 From: Deniz Date: Mon, 23 Aug 2021 17:00:27 -0500 Subject: [PATCH 10/10] remove console log, prettier format --- .../components/EventRegistration/index.tsx | 49 ++++++++++--------- 1 file changed, 25 insertions(+), 24 deletions(-) diff --git a/apps/profile-web/src/components/EventRegistration/index.tsx b/apps/profile-web/src/components/EventRegistration/index.tsx index 1fa0b1ad..316a44b4 100644 --- a/apps/profile-web/src/components/EventRegistration/index.tsx +++ b/apps/profile-web/src/components/EventRegistration/index.tsx @@ -1,12 +1,14 @@ import React, { useEffect, useState } from "react"; import { message } from "antd"; -import { useMeQuery, - useEventsQuery, - useAttendEventMutation, - useRecordInterestMutation, - Event as IEvent } from "../../generated/graphql"; - -const EventRegistration: React.FC<{match: any}> = ({match}: any) => { +import { + useMeQuery, + useEventsQuery, + useAttendEventMutation, + useRecordInterestMutation, + Event as IEvent +} from "../../generated/graphql"; + +const EventRegistration: React.FC<{ match: any }> = ({ match }: any) => { //style sheets for output const divStyle = { margin: "auto", @@ -37,12 +39,12 @@ const EventRegistration: React.FC<{match: any}> = ({match}: any) => { fontWeight: 100, fontStyle: "italic", } - + const { loading: userLoading, error: userError, }: any = useMeQuery(); - + const { loading: eventLoading, error: eventError, @@ -77,9 +79,9 @@ const EventRegistration: React.FC<{match: any}> = ({match}: any) => { //once all mutations/queries are loaded, the actual attendance is tracked useEffect(() => { - if(curEvents && attendEvent && result.data) { - if(result.data.me.id !== null) - if(getEventId(eventUrlKey) !== -1) { + if (curEvents && attendEvent && result.data) { + if (result.data.me.id !== null) + if (getEventId(eventUrlKey) !== -1) { attendEvent({ variables: { eventId: Number(getEventId(eventUrlKey)) @@ -103,10 +105,10 @@ const EventRegistration: React.FC<{match: any}> = ({match}: any) => { return -1; }; - if(eventLoading || eventError || userLoading || userError) + if (eventLoading || eventError || userLoading || userError) return

Loading...

- if(result.data == null || getEventId(eventUrlKey) == -1 || !attendEvent) + if (result.data == null || getEventId(eventUrlKey) == -1 || !attendEvent) return (

An error has occured.

@@ -124,7 +126,7 @@ const EventRegistration: React.FC<{match: any}> = ({match}: any) => { ); }; -const EventInterest: React.FC<{match: any}> = ({match}: any) => { +const EventInterest: React.FC<{ match: any }> = ({ match }: any) => { //style sheets for output const divStyle = { margin: "auto", @@ -155,12 +157,12 @@ const EventInterest: React.FC<{match: any}> = ({match}: any) => { fontWeight: 100, fontStyle: "italic", } - + const { loading: userLoading, error: userError, }: any = useMeQuery(); - + const { loading: eventLoading, error: eventError, @@ -175,7 +177,6 @@ const EventInterest: React.FC<{match: any}> = ({match}: any) => { const onRecordInterestError: (e: Error) => void = (e: Error): void => { message.error("Upload failed. Please try again."); - console.log(e); }; useEffect(() => { @@ -200,9 +201,9 @@ const EventInterest: React.FC<{match: any}> = ({match}: any) => { //once all mutations/queries are loaded, the actual attendance is tracked useEffect(() => { - if(curEvents && recordInterest && result.data) { - if(result.data.me.id !== null) - if(getEventId(eventUrlKey) !== -1) { + if (curEvents && recordInterest && result.data) { + if (result.data.me.id !== null) + if (getEventId(eventUrlKey) !== -1) { try { recordInterest({ variables: { @@ -210,7 +211,7 @@ const EventInterest: React.FC<{match: any}> = ({match}: any) => { } }); } - catch(e) { + catch (e) { onRecordInterestError(e); } } @@ -231,10 +232,10 @@ const EventInterest: React.FC<{match: any}> = ({match}: any) => { return -1; }; - if(eventLoading || eventError || userLoading || userError) + if (eventLoading || eventError || userLoading || userError) return

Loading...

- if(result.data == null || getEventId(eventUrlKey) == -1 || !recordInterest) + if (result.data == null || getEventId(eventUrlKey) == -1 || !recordInterest) return (

An error has occured.