Skip to content

Commit 5c5e41d

Browse files
authored
Merge pull request #38 from RoboJackets/attendance
Attendance Screen Implementation
2 parents 0054e71 + 51a31d3 commit 5c5e41d

19 files changed

Lines changed: 1004 additions & 66 deletions

Api/MeetingsApi.ts

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
import { AxiosInstance } from 'axios';
2+
3+
export type TeamInfo<T extends object = NonNullable<unknown>> = {
4+
id: number;
5+
name: string;
6+
self_serviceable: boolean;
7+
visible: boolean;
8+
visible_on_kiosk: boolean;
9+
attendable: boolean;
10+
description: string;
11+
mailing_list_name: string | null;
12+
slack_channel_id: string;
13+
slack_channel_name: string;
14+
slack_private_channel_id: string;
15+
google_group: string;
16+
created_at: string;
17+
updated_at: string;
18+
deleted_at: string | null;
19+
} & T;
20+
21+
export type EventInfo<T extends object = NonNullable<unknown>> = {
22+
id: number;
23+
name: string;
24+
allow_anonymous_rsvp: boolean;
25+
location: string;
26+
start_time: string;
27+
end_time: string;
28+
created_at: string;
29+
updated_at: string;
30+
deleted_at: string | null;
31+
} & T;
32+
33+
export type AttendanceInfo<T extends object = NonNullable<unknown>> = {
34+
attendable_type: string;
35+
attendable_id: number;
36+
gtid: number;
37+
source: string;
38+
} & T;
39+
40+
export type AttendanceResponse = {
41+
attendance: {
42+
attendee?: {
43+
name: string;
44+
};
45+
};
46+
};
47+
48+
export async function getTeamInfo(api: AxiosInstance): Promise<TeamInfo[] | null> {
49+
try {
50+
const teams = await api.get('/api/v1/teams');
51+
//TODO: Incorporate Sentry
52+
const teamInfos: TeamInfo[] = teams.data.teams;
53+
return teamInfos;
54+
} catch (error) {
55+
//TODO: incorporate logging
56+
}
57+
return null;
58+
}
59+
60+
export async function getEventInfo(api: AxiosInstance): Promise<EventInfo[] | null> {
61+
try {
62+
const events = await api.get('/api/v1/events');
63+
//TODO: Incorporate Sentry
64+
const eventInfos: EventInfo[] = events.data.events;
65+
return eventInfos;
66+
} catch (error) {
67+
//TODO: incorporate logging
68+
}
69+
return null;
70+
}
71+
72+
export async function postAttendance(
73+
api: AxiosInstance,
74+
props: AttendanceInfo,
75+
): Promise<{ success: true; data: AttendanceResponse } | { success: false; error: string }> {
76+
try {
77+
const response = await api.post('/api/v1/attendance?include=attendee', props);
78+
79+
return {
80+
success: true,
81+
data: response.data,
82+
};
83+
} catch (error) {
84+
//TODO: incorporate logging
85+
console.error(error);
86+
return {
87+
success: false,
88+
error: error instanceof Error ? error.message : 'Unknown error',
89+
};
90+
}
91+
}

Api/Models/Attendance.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
export enum NfcSource {
2+
NFC = 'Nfc',
3+
KEYBOARD = 'Keyboard',
4+
}
5+
6+
export enum AttendableType {
7+
TEAM = 'team',
8+
EVENT = 'event',
9+
NONE = 'none',
10+
}

AppEnvironment.tsx

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,11 @@ export const APP_ENVIRONMENTS: AppEnvironmentList = {
1616
production: true,
1717
baseUrl: 'https://my.robojackets.org',
1818
},
19+
test: {
20+
name: 'Test',
21+
production: false,
22+
baseUrl: 'https://apiary-test.robojackets.org',
23+
},
1924
};
2025

2126
type EnvironmentContextType = {

Attendance/AttendableSelect.tsx

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
import React, { useEffect, useState } from 'react';
2+
import { ScrollView } from 'react-native';
3+
import { useApi } from '../Api/ApiContextProvider';
4+
import { EventInfo, getEventInfo, getTeamInfo, TeamInfo } from '../Api/MeetingsApi';
5+
import { AttendableType } from '../Api/Models/Attendance';
6+
import LoadingScreen from '../Components/LoadingScreen';
7+
import MenuHeader from '../Components/MenuHeader';
8+
import MenuLink from '../Components/MenuLink';
9+
import RoundedButton from '../Components/RoundedButton';
10+
import TapABuzzCard from './TapABuzzCard';
11+
12+
type AttendanceProps = {
13+
attendanceType: AttendableType;
14+
setAttendanceType: (state: AttendableType) => void;
15+
};
16+
17+
function AttendableSelect({ attendanceType, setAttendanceType }: AttendanceProps) {
18+
const api = useApi();
19+
const [attendables, setAttendables] = useState<TeamInfo[] | EventInfo[] | null | undefined>(
20+
undefined,
21+
);
22+
const [attendable, setAttendable] = useState<TeamInfo | EventInfo | undefined>(undefined);
23+
24+
async function onRefreshAttendables(forceRefresh: boolean = false) {
25+
if (forceRefresh) {
26+
if (attendanceType === AttendableType.TEAM) {
27+
setAttendables(await getTeamInfo(api));
28+
} else {
29+
setAttendables(await getEventInfo(api));
30+
}
31+
}
32+
}
33+
34+
useEffect(() => {
35+
onRefreshAttendables(true);
36+
}, []);
37+
38+
function AttendableList() {
39+
if (attendables) {
40+
const attendableList = [];
41+
for (const attendable of attendables) {
42+
attendableList.push(
43+
<MenuLink
44+
key={attendable.id}
45+
icon="people"
46+
title={attendable.name}
47+
onClick={() => {
48+
setAttendable(attendable);
49+
}}
50+
></MenuLink>,
51+
);
52+
}
53+
return attendableList;
54+
} else {
55+
return <LoadingScreen></LoadingScreen>;
56+
}
57+
}
58+
59+
return (
60+
<>
61+
{attendable ? (
62+
<TapABuzzCard
63+
attendanceType={attendanceType}
64+
setAttendanceType={setAttendanceType}
65+
attendable={attendable}
66+
setAttendable={setAttendable}
67+
></TapABuzzCard>
68+
) : (
69+
<ScrollView>
70+
<RoundedButton
71+
title="Change team or event"
72+
onPress={() => {
73+
setAttendanceType(AttendableType.NONE);
74+
}}
75+
/>
76+
{attendanceType == AttendableType.EVENT ? (
77+
<MenuHeader title={`Select an ${attendanceType}`}></MenuHeader>
78+
) : (
79+
<MenuHeader title={`Select a ${attendanceType}`}></MenuHeader>
80+
)}
81+
<AttendableList></AttendableList>
82+
</ScrollView>
83+
)}
84+
</>
85+
);
86+
}
87+
88+
export default AttendableSelect;

Attendance/AttendanceScreen.tsx

Lines changed: 99 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,106 @@
1-
import React from 'react';
2-
import { Text, View } from 'react-native';
1+
import React, { useEffect, useState } from 'react';
2+
import { SafeAreaView, StyleSheet, View } from 'react-native';
3+
import { useApi } from '../Api/ApiContextProvider';
4+
import { AttendableType } from '../Api/Models/Attendance';
5+
import { Permission } from '../Api/Models/Permission';
6+
import { getUserInfo, UserInfo } from '../Api/UserApi';
7+
import InsufficientPermissions from '../Auth/InsufficientPermissions';
8+
import LoadingScreen from '../Components/LoadingScreen';
9+
import MenuHeader from '../Components/MenuHeader';
10+
import MenuLink from '../Components/MenuLink';
11+
import { useTheme } from '../Themes/ThemeContextProvider';
12+
import AttendableSelect from './AttendableSelect';
313

14+
const requiredPermissions: Permission[] = [Permission.CREATE_ATTENDANCE, Permission.READ_USERS];
15+
16+
/*
17+
Architecture:
18+
Attendance Screen > AttendableSelect > TapABuzzCard > BuzzCardPrompt
19+
*/
420
function AttendanceScreen() {
21+
const api = useApi();
22+
const [user, setUser] = useState<UserInfo | null | undefined>(undefined);
23+
const [missingPermissions, setMissingPermissions] = useState<Permission[] | undefined>(undefined);
24+
const { currentTheme } = useTheme();
25+
26+
async function onRefreshUser(forceRefresh: boolean = false) {
27+
if (!user || forceRefresh) {
28+
setUser(await getUserInfo(api));
29+
}
30+
}
31+
32+
function getPermissions() {
33+
let missingPermissions: Permission[] = [];
34+
if (user && user.allPermissions) {
35+
const permissions = user.allPermissions;
36+
missingPermissions = requiredPermissions.filter((item) => !permissions.includes(item));
37+
}
38+
setMissingPermissions(missingPermissions);
39+
}
40+
41+
useEffect(() => {
42+
onRefreshUser(true);
43+
getPermissions();
44+
}, []);
45+
46+
function AttendableSelectionScreen() {
47+
const [attendanceType, setAttendanceType] = useState<AttendableType>(AttendableType.NONE);
48+
49+
return (
50+
<SafeAreaView style={[styles.container, { backgroundColor: currentTheme.background }]}>
51+
<View style={styles.viewContainer}>
52+
{attendanceType === AttendableType.NONE ? (
53+
<>
54+
<MenuHeader title={'What do you want to take attendance for?'}></MenuHeader>
55+
<MenuLink
56+
title="Team"
57+
icon="group"
58+
onClick={() => {
59+
setAttendanceType(AttendableType.TEAM);
60+
}}
61+
></MenuLink>
62+
<MenuLink
63+
title="Event"
64+
icon="event"
65+
onClick={() => {
66+
setAttendanceType(AttendableType.EVENT);
67+
}}
68+
></MenuLink>
69+
</>
70+
) : (
71+
<AttendableSelect
72+
attendanceType={attendanceType}
73+
setAttendanceType={setAttendanceType}
74+
></AttendableSelect>
75+
)}
76+
</View>
77+
</SafeAreaView>
78+
);
79+
}
80+
581
return (
6-
// eslint-disable-next-line react-native/no-inline-styles
7-
<View style={{ flex: 1, alignItems: 'center', justifyContent: 'center' }}>
8-
<Text>Attendance Screen</Text>
9-
</View>
82+
<>
83+
{user && missingPermissions ? (
84+
missingPermissions.length != 0 ? (
85+
<InsufficientPermissions
86+
featureName="Attendance"
87+
requiredPermissions={requiredPermissions}
88+
missingPermissions={missingPermissions}
89+
onRetry={() => {}}
90+
></InsufficientPermissions>
91+
) : (
92+
<AttendableSelectionScreen></AttendableSelectionScreen>
93+
)
94+
) : (
95+
<LoadingScreen></LoadingScreen>
96+
)}
97+
</>
1098
);
1199
}
12100

101+
const styles = StyleSheet.create({
102+
container: { flex: 1 },
103+
viewContainer: { flex: 1, padding: 10 },
104+
});
105+
13106
export default AttendanceScreen;

0 commit comments

Comments
 (0)