Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
91 changes: 91 additions & 0 deletions Api/MeetingsApi.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
import { AxiosInstance } from 'axios';

export type TeamInfo<T extends object = NonNullable<unknown>> = {
id: number;
name: string;
self_serviceable: boolean;
visible: boolean;
visible_on_kiosk: boolean;
attendable: boolean;
description: string;
mailing_list_name: string | null;
slack_channel_id: string;
slack_channel_name: string;
slack_private_channel_id: string;
google_group: string;
created_at: string;
updated_at: string;
deleted_at: string | null;
} & T;

export type EventInfo<T extends object = NonNullable<unknown>> = {
id: number;
name: string;
allow_anonymous_rsvp: boolean;
location: string;
start_time: string;
end_time: string;
created_at: string;
updated_at: string;
deleted_at: string | null;
} & T;

export type AttendanceInfo<T extends object = NonNullable<unknown>> = {
attendable_type: string;
attendable_id: number;
gtid: number;
source: string;
} & T;

export type AttendanceResponse = {
attendance: {
attendee?: {
name: string;
};
};
};

export async function getTeamInfo(api: AxiosInstance): Promise<TeamInfo[] | null> {
try {
const teams = await api.get('/api/v1/teams');
//TODO: Incorporate Sentry
const teamInfos: TeamInfo[] = teams.data.teams;
return teamInfos;
} catch (error) {
//TODO: incorporate logging
}
return null;
}

export async function getEventInfo(api: AxiosInstance): Promise<EventInfo[] | null> {
try {
const events = await api.get('/api/v1/events');
//TODO: Incorporate Sentry
const eventInfos: EventInfo[] = events.data.events;
return eventInfos;
} catch (error) {
//TODO: incorporate logging
}
return null;
}

export async function postAttendance(
api: AxiosInstance,
props: AttendanceInfo,
): Promise<{ success: true; data: AttendanceResponse } | { success: false; error: string }> {
try {
const response = await api.post('/api/v1/attendance?include=attendee', props);

return {
success: true,
data: response.data,
};
} catch (error) {
//TODO: incorporate logging
console.error(error);
return {
success: false,
error: error instanceof Error ? error.message : 'Unknown error',
};
}
}
10 changes: 10 additions & 0 deletions Api/Models/Attendance.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
export enum NfcSource {
NFC = 'Nfc',
KEYBOARD = 'Keyboard',
}

export enum AttendableType {
TEAM = 'team',
EVENT = 'event',
NONE = 'none',
}
5 changes: 5 additions & 0 deletions AppEnvironment.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,11 @@ export const APP_ENVIRONMENTS: AppEnvironmentList = {
production: true,
baseUrl: 'https://my.robojackets.org',
},
test: {
name: 'Test',
production: false,
baseUrl: 'https://apiary-test.robojackets.org',
},
};

type EnvironmentContextType = {
Expand Down
84 changes: 84 additions & 0 deletions Attendance/AttendableSelect.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import React, { useEffect, useState } from 'react';
import { ScrollView } from 'react-native';
import { useApi } from '../Api/ApiContextProvider';
import { EventInfo, getEventInfo, getTeamInfo, TeamInfo } from '../Api/MeetingsApi';
import { AttendableType } from '../Api/Models/Attendance';
import LoadingScreen from '../Components/LoadingScreen';
import MenuHeader from '../Components/MenuHeader';
import MenuLink from '../Components/MenuLink';
import RoundedButton from '../Components/RoundedButton';
import TapABuzzCard from './TapABuzzCard';

type AttendanceProps = {
attendanceType: AttendableType;
setAttendanceType: (state: AttendableType) => void;
};

function AttendableSelect({ attendanceType, setAttendanceType }: AttendanceProps) {
const api = useApi();
const [attendables, setAttendables] = useState<TeamInfo[] | EventInfo[] | null | undefined>(
undefined,
);
const [attendable, setAttendable] = useState<TeamInfo | EventInfo | undefined>(undefined);

async function onRefreshAttendables(forceRefresh: boolean = false) {
if (forceRefresh) {
if (attendanceType === AttendableType.TEAM) {
setAttendables(await getTeamInfo(api));
} else {
setAttendables(await getEventInfo(api));
}
}
}

useEffect(() => {
onRefreshAttendables(true);
}, []);

function AttendableList() {
if (attendables) {
const attendableList = [];
for (const attendable of attendables) {
attendableList.push(
<MenuLink
key={attendable.id}
icon="people"
title={attendable.name}
onClick={() => {
setAttendable(attendable);
}}
></MenuLink>,
);
}
return attendableList;
} else {
return <LoadingScreen></LoadingScreen>;
}
}

return (
<>
{attendable ? (
<TapABuzzCard
attendanceType={attendanceType}
setAttendanceType={setAttendanceType}
attendable={attendable}
setAttendable={setAttendable}
></TapABuzzCard>
) : (
<ScrollView>
<RoundedButton
title="Change team or event"
onPress={() => {
setAttendanceType(AttendableType.NONE);
}}
/>
<MenuHeader title={`Select a ${attendanceType}`}></MenuHeader>
<AttendableList></AttendableList>
</ScrollView>
)}
</>
);
}

export default AttendableSelect;
102 changes: 96 additions & 6 deletions Attendance/AttendanceScreen.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,103 @@
import React from 'react';
import { Text, View } from 'react-native';
import React, { useEffect, useState } from 'react';
import { SafeAreaView, StyleSheet } from 'react-native';
import { useApi } from '../Api/ApiContextProvider';
import { AttendableType } from '../Api/Models/Attendance';
import { Permission } from '../Api/Models/Permission';
import { getUserInfo, UserInfo } from '../Api/UserApi';
import InsufficientPermissions from '../Auth/InsufficientPermissions';
import LoadingScreen from '../Components/LoadingScreen';
import MenuHeader from '../Components/MenuHeader';
import MenuLink from '../Components/MenuLink';
import { useTheme } from '../Themes/ThemeContextProvider';
import AttendableSelect from './AttendableSelect';

const requiredPermissions: Permission[] = [Permission.CREATE_ATTENDANCE, Permission.READ_USERS];

/*
Architecture:
Attendance Screen > AttendableSelect > TapABuzzCard > BuzzCardPrompt
*/
function AttendanceScreen() {
const api = useApi();
const [user, setUser] = useState<UserInfo | null | undefined>(undefined);
const [missingPermissions, setMissingPermissions] = useState<Permission[] | undefined>(undefined);
const { currentTheme } = useTheme();

async function onRefreshUser(forceRefresh: boolean = false) {
if (!user || forceRefresh) {
setUser(await getUserInfo(api));
}
}

function getPermissions() {
Comment thread
elysseaa marked this conversation as resolved.
let missingPermissions: Permission[] = [];
if (user && user.allPermissions) {
const permissions = user.allPermissions;
missingPermissions = requiredPermissions.filter((item) => !permissions.includes(item));
}
setMissingPermissions(missingPermissions);
}

useEffect(() => {
onRefreshUser(true);
getPermissions();
}, []);

function AttendableSelectionScreen() {
const [attendanceType, setAttendanceType] = useState<AttendableType>(AttendableType.NONE);

return (
<SafeAreaView style={[styles.container, { backgroundColor: currentTheme.background }]}>
{attendanceType === AttendableType.NONE ? (
<>
<MenuHeader title={'What do you want to take attendance for?'}></MenuHeader>
<MenuLink
title="Team"
icon="group"
onClick={() => {
setAttendanceType(AttendableType.TEAM);
}}
></MenuLink>
<MenuLink
title="Event"
icon="event"
onClick={() => {
setAttendanceType(AttendableType.EVENT);
}}
></MenuLink>
</>
) : (
<AttendableSelect
attendanceType={attendanceType}
setAttendanceType={setAttendanceType}
></AttendableSelect>
)}
</SafeAreaView>
);
}

return (
// eslint-disable-next-line react-native/no-inline-styles
<View style={{ flex: 1, alignItems: 'center', justifyContent: 'center' }}>
<Text>Attendance Screen</Text>
</View>
<>
{user && missingPermissions ? (
missingPermissions.length != 0 ? (
<InsufficientPermissions
featureName="Attendance"
requiredPermissions={requiredPermissions}
missingPermissions={missingPermissions}
onRetry={() => {}}
></InsufficientPermissions>
) : (
<AttendableSelectionScreen></AttendableSelectionScreen>
)
) : (
<LoadingScreen></LoadingScreen>
)}
</>
);
}

const styles = StyleSheet.create({
container: { flex: 1, padding: 10 },
});

export default AttendanceScreen;
Loading
Loading