Commit 7bd410b9 authored by Gijs Hendriksen's avatar Gijs Hendriksen
Browse files

Added admin screen for event registrations

parent 87ed510b
......@@ -3,6 +3,17 @@
exports[`event actions should create an action to load an event 1`] = `
Object {
"payload": Object {
"navigateToEventScreen": true,
"pk": 1,
},
"type": "EVENT_EVENT",
}
`;
exports[`event actions should create an action to load an event without opening the event screen 1`] = `
Object {
"payload": Object {
"navigateToEventScreen": false,
"pk": 1,
},
"type": "EVENT_EVENT",
......@@ -36,3 +47,26 @@ Object {
"type": "EVENT_FETCHING",
}
`;
exports[`event actions should create an action to open the admin screen of the event 1`] = `
Object {
"type": "EVENT_ADMIN",
}
`;
exports[`event actions should create an action to open the event screen 1`] = `
Object {
"type": "EVENT_OPEN",
}
`;
exports[`event actions should create an action to update a registration 1`] = `
Object {
"payload": Object {
"payment": "no_payment",
"pk": 1,
"present": true,
},
"type": "EVENT_UPDATE_REGISTRATION",
}
`;
......@@ -7,10 +7,21 @@ describe('event actions', () => {
expect(actions.FAILURE).toEqual('EVENT_FAILURE');
expect(actions.EVENT).toEqual('EVENT_EVENT');
expect(actions.DONE).toEqual('EVENT_DONE');
expect(actions.OPEN).toEqual('EVENT_OPEN');
expect(actions.ADMIN).toEqual('EVENT_ADMIN');
expect(actions.UPDATE_REGISTRATION).toEqual('EVENT_UPDATE_REGISTRATION');
});
it('should create an action to load an event', () => {
expect(actions.event(1)).toMatchSnapshot();
const action = actions.event(1);
expect(action).toMatchSnapshot();
expect(action.payload.navigateToEventScreen).toEqual(true);
});
it('should create an action to load an event without opening the event screen', () => {
const action = actions.event(1, false);
expect(action).toMatchSnapshot();
expect(action.payload.navigateToEventScreen).toEqual(false);
});
it('should create an action to notify of that an event is fetching', () => {
......@@ -28,4 +39,16 @@ describe('event actions', () => {
it('should create an action to notify of a completed fetch of an event', () => {
expect(actions.done()).toMatchSnapshot();
});
});
\ No newline at end of file
it('should create an action to open the event screen', () => {
expect(actions.open()).toMatchSnapshot();
});
it('should create an action to open the admin screen of the event', () => {
expect(actions.admin()).toMatchSnapshot();
});
it('should create an action to update a registration', () => {
expect(actions.updateRegistration(1, true, 'no_payment')).toMatchSnapshot();
});
});
......@@ -27,6 +27,7 @@ Object {
"Pizza": "Pizza",
"Profile": "Profiel",
"Registration": "Registratie",
"Registrations": "Inschrijvingen",
"Settings": "Instellingen",
"Welcome": "Welkom",
},
......@@ -39,6 +40,20 @@ Object {
"Sorry, we couldn't load any data.": "Sorry, we konden geen gegevens laden.",
"day": "dag",
},
"screens/events/EventAdminScreen": Object {
"Card": "Pin",
"Cash": "Cash",
"Could not load the event...": "Kon het evenement niet laden...",
"Disabled filter": "Filter uitgeschakeld",
"Filtering on payment": "Filteren op betaling",
"Filtering on presence": "Filteren op aanwezigheid",
"Find a member": "Zoek een lid",
"No registrations found with this filter.": "Geen inschrijvingen gevonden met dit filter.",
"No registrations found...": "Geen inschrijvingen gevonden...",
"Not paid": "Niet betaald",
"Present": "Aanwezig",
"Registrations": "Inschrijvingen",
},
"screens/events/EventScreen": Object {
", that you understand them and that you agree to be bound by them.": "hebt gelezen, dat je ze begrijpt en dat je accepteert eraan gebonden te zijn.",
"Are you sure you want to cancel your registration?": "Weet je je zeker dat je je wilt afmelden?",
......
......@@ -8,6 +8,7 @@ Object {
"end": "",
"google_maps_url": "",
"has_fields": false,
"is_admin": false,
"is_pizza_event": false,
"location": "",
"map_location": "",
......
......@@ -3,10 +3,9 @@ import * as actions from '../../app/actions/event';
describe('events reducer', () => {
const emptyState = {};
const initialState = reducer();
describe('initially', () => {
const initialState = reducer();
it('should not be fetched', () => {
expect(initialState).toMatchSnapshot();
});
......@@ -23,6 +22,14 @@ describe('events reducer', () => {
});
});
describe('is opened', () => {
const state = reducer(emptyState, actions.open());
it('should reset', () => {
expect(state).toEqual(initialState);
});
});
describe('is successful', () => {
const state = reducer(
emptyState,
......
......@@ -24,56 +24,119 @@ jest.mock('../../app/selectors/session', () => ({
describe('event saga', () => {
const error = new Error('error');
it('should start fetching', () => expectSaga(eventSaga)
.provide([
[select(tokenSelector), 'token'],
[matchers.call.fn(apiRequest), []],
])
.dispatch(eventActions.event(1))
.put(eventActions.fetching())
.silentRun());
it('should put an error when the api request fails', () => expectSaga(eventSaga)
.provide([
[select(tokenSelector), 'token'],
[matchers.call.fn(apiRequest), throwError(error)],
])
.dispatch(eventActions.event(1))
.put(eventActions.failure())
.silentRun());
it('should put the result data when the request succeeds', () => expectSaga(eventSaga)
.provide([
[select(tokenSelector), 'token'],
[matchers.call.like({ fn: apiRequest, args: ['events/1'] }), 'eventData'],
[matchers.call.like({ fn: apiRequest, args: ['events/1/registrations'] }), 'regData'],
])
.dispatch(eventActions.event(1))
.put(eventActions.success('eventData', 'regData'))
.silentRun());
it('should do two GET requests', () => expectSaga(eventSaga)
.provide([
[select(tokenSelector), 'usertoken'],
])
.dispatch(eventActions.event(1))
.silentRun()
.then(() => {
expect(apiRequest).toBeCalledWith('events/1', {
headers: {
Accept: 'application/json',
Authorization: 'Token usertoken',
'Content-Type': 'application/json',
},
method: 'GET',
});
expect(apiRequest).toBeCalledWith('events/1/registrations', {
headers: {
Accept: 'application/json',
Authorization: 'Token usertoken',
'Content-Type': 'application/json',
},
method: 'GET',
}, { status: 'registered' });
}));
describe('load event', () => {
it('should start fetching', () => expectSaga(eventSaga)
.provide([
[select(tokenSelector), 'token'],
])
.dispatch(eventActions.event(1))
.put(eventActions.fetching())
.silentRun());
it('should open the event screen when specified', () => expectSaga(eventSaga)
.provide([
[select(tokenSelector), 'token'],
])
.dispatch(eventActions.event(1))
.put(eventActions.open())
.silentRun());
it('should not open the event screen when told not to', () => expectSaga(eventSaga)
.provide([
[select(tokenSelector), 'token'],
])
.dispatch(eventActions.event(1, false))
.not.put(eventActions.open())
.silentRun());
it('should put an error when the api request fails', () => expectSaga(eventSaga)
.provide([
[select(tokenSelector), 'token'],
[matchers.call.fn(apiRequest), throwError(error)],
])
.dispatch(eventActions.event(1))
.put(eventActions.failure())
.silentRun());
it('should put the result data when the request succeeds', () => expectSaga(eventSaga)
.provide([
[select(tokenSelector), 'token'],
[matchers.call.like({ fn: apiRequest, args: ['events/1'] }), 'eventData'],
[matchers.call.like({ fn: apiRequest, args: ['events/1/registrations'] }), 'regData'],
])
.dispatch(eventActions.event(1))
.put(eventActions.success('eventData', 'regData'))
.silentRun());
it('should do two GET requests', () => expectSaga(eventSaga)
.provide([
[select(tokenSelector), 'usertoken'],
])
.dispatch(eventActions.event(1))
.silentRun()
.then(() => {
expect(apiRequest).toBeCalledWith('events/1', {
headers: {
Accept: 'application/json',
Authorization: 'Token usertoken',
'Content-Type': 'application/json',
},
method: 'GET',
});
expect(apiRequest).toBeCalledWith('events/1/registrations', {
headers: {
Accept: 'application/json',
Authorization: 'Token usertoken',
'Content-Type': 'application/json',
},
method: 'GET',
}, { status: 'registered' });
}));
});
describe('update registration', () => {
it('should start fetching', () => expectSaga(eventSaga)
.provide([
[select(tokenSelector), 'token'],
])
.dispatch(eventActions.updateRegistration(1, true, 'payment'))
.put(eventActions.fetching())
.silentRun());
it('should do a PATCH request', () => expectSaga(eventSaga)
.provide([
[select(tokenSelector), 'token'],
])
.dispatch(eventActions.updateRegistration(1, true, 'payment'))
.silentRun()
.then(() => {
expect(apiRequest).toBeCalledWith('registrations/1', {
headers: {
Accept: 'application/json',
Authorization: 'Token token',
'Content-Type': 'application/json',
},
method: 'PATCH',
body: '{"present":true,"payment":"payment"}',
});
}));
it('should be done when the request is successful', () => expectSaga(eventSaga)
.provide([
[select(tokenSelector), 'token'],
[matchers.call.like({ fn: apiRequest, args: ['registrations/1'] }), 'response'],
])
.dispatch(eventActions.updateRegistration(1, true, 'payment'))
.put(eventActions.done())
.silentRun());
it('should put an error when the request fails', () => expectSaga(eventSaga)
.provide([
[select(tokenSelector), 'token'],
[matchers.call.like({ fn: apiRequest, args: ['registrations/1'] }), throwError(error)],
])
.dispatch(eventActions.updateRegistration(1, true, 'payment'))
.put(eventActions.failure())
.silentRun());
});
});
......@@ -80,12 +80,19 @@ describe('navigation saga', () => {
}));
it('should open the event screen', () => expectSaga(navigationSaga)
.dispatch(eventActions.event(1))
.dispatch(eventActions.open())
.silentRun()
.then(() => {
expect(NavigationService.navigate).toBeCalledWith('Event');
}));
it('should open the event admin screen', () => expectSaga(navigationSaga)
.dispatch(eventActions.admin())
.silentRun()
.then(() => {
expect(NavigationService.navigate).toBeCalledWith('EventAdmin');
}));
it('should open the profile screen', () => expectSaga(navigationSaga)
.dispatch(profileActions.profile())
.silentRun()
......
......@@ -79,7 +79,8 @@ exports[`StandardHeader component renders correctly 1`] = `
<View
style={
Object {
"width": 50,
"position": "absolute",
"right": 0,
}
}
/>
......
export const EVENT = 'EVENT_EVENT';
export const OPEN = 'EVENT_OPEN';
export const FETCHING = 'EVENT_FETCHING';
export const SUCCESS = 'EVENT_SUCCESS';
export const DONE = 'EVENT_DONE';
export const FAILURE = 'EVENT_FAILURE';
export const ADMIN = 'EVENT_ADMIN';
export const UPDATE_REGISTRATION = 'EVENT_UPDATE_REGISTRATION';
export function event(pk) {
export function event(pk, navigateToEventScreen = true) {
return {
type: EVENT,
payload: { pk },
payload: { pk, navigateToEventScreen },
};
}
export function open() {
return {
type: OPEN,
};
}
......@@ -35,3 +44,16 @@ export function failure() {
type: FAILURE,
};
}
export function admin() {
return {
type: ADMIN,
};
}
export function updateRegistration(pk, present, payment) {
return {
type: UPDATE_REGISTRATION,
payload: { pk, present, payment },
};
}
......@@ -8,6 +8,7 @@ files['app/ui/screens/pizza/PizzaScreenNL'] = require('./nl/app/ui/screens/pizza
files['app/ui/screens/settings/NotificationsSectionNL'] = require('./nl/app/ui/screens/settings/NotificationsSection.json');
files['app/ui/screens/events/CalendarScreenNL'] = require('./nl/app/ui/screens/events/CalendarScreen.json');
files['app/ui/screens/events/RegistrationScreenNL'] = require('./nl/app/ui/screens/events/RegistrationScreen.json');
files['app/ui/screens/events/EventAdminScreenNL'] = require('./nl/app/ui/screens/events/EventAdminScreen.json');
files['app/ui/screens/events/CalendarItemNL'] = require('./nl/app/ui/screens/events/CalendarItem.json');
files['app/ui/screens/events/EventScreenNL'] = require('./nl/app/ui/screens/events/EventScreen.json');
files['app/ui/components/standardHeader/StandardHeaderNL'] = require('./nl/app/ui/components/standardHeader/StandardHeader.json');
......@@ -26,6 +27,7 @@ export default {
'screens/settings/NotificationsSection': files['app/ui/screens/settings/NotificationsSectionNL'],
'screens/events/CalendarScreen': files['app/ui/screens/events/CalendarScreenNL'],
'screens/events/RegistrationScreen': files['app/ui/screens/events/RegistrationScreenNL'],
'screens/events/EventAdminScreen': files['app/ui/screens/events/EventAdminScreenNL'],
'screens/events/CalendarItem': files['app/ui/screens/events/CalendarItemNL'],
'screens/events/EventScreen': files['app/ui/screens/events/EventScreenNL'],
'components/standardHeader/StandardHeader': files['app/ui/components/standardHeader/StandardHeaderNL'],
......
......@@ -5,5 +5,6 @@
"Pizza": "Pizza",
"Profile": "Profiel",
"Registration": "Registratie",
"Settings": "Instellingen"
"Settings": "Instellingen",
"Registrations": "Inschrijvingen"
}
{
"Disabled filter": "Filter uitgeschakeld",
"Filtering on presence": "Filteren op aanwezigheid",
"Filtering on payment": "Filteren op betaling",
"Present": "Aanwezig",
"Not paid": "Niet betaald",
"Cash": "Cash",
"Card": "Pin",
"No registrations found...": "Geen inschrijvingen gevonden...",
"Registrations": "Inschrijvingen",
"Find a member": "Zoek een lid",
"No registrations found with this filter.": "Geen inschrijvingen gevonden met dit filter.",
"Could not load the event...": "Kon het evenement niet laden..."
}
......@@ -16,6 +16,7 @@ import Registration from './ui/screens/events/RegistrationScreenContainer';
import MemberList from './ui/screens/memberList/MemberListScreenContainer';
import SplashScreen from './ui/screens/splash/SplashScreen';
import Settings from './ui/screens/settings/SettingsScreenContainer';
import EventAdmin from './ui/screens/events/EventAdminScreenContainer';
import Sidebar from './ui/components/sidebar/SidebarContainer';
const MainNavigator = createDrawerNavigator({
......@@ -33,6 +34,7 @@ const SignedInNavigator = createStackNavigator({
Profile,
Pizza,
Registration,
EventAdmin,
}, {
headerMode: 'none',
});
......
......@@ -15,6 +15,7 @@ const initialState = {
has_fields: false,
is_pizza_event: false,
google_maps_url: '',
is_admin: false,
},
registrations: [],
status: 'initial',
......@@ -23,9 +24,12 @@ const initialState = {
export default function loadEvent(state = initialState, action = {}) {
switch (action.type) {
case eventActions.OPEN: {
return initialState;
}
case eventActions.FETCHING: {
return {
...initialState,
...state,
loading: true,
};
}
......
......@@ -7,12 +7,16 @@ import { apiRequest } from '../utils/url';
import * as eventActions from '../actions/event';
import { tokenSelector } from '../selectors/session';
const event = function* event(action) {
const { pk } = action.payload;
function* event(action) {
const { pk, navigateToEventScreen } = action.payload;
const token = yield select(tokenSelector);
yield put(eventActions.fetching());
if (navigateToEventScreen) {
yield put(eventActions.open());
}
const data = {
method: 'GET',
headers: {
......@@ -39,10 +43,40 @@ const event = function* event(action) {
Sentry.captureException(error);
yield put(eventActions.failure());
}
};
}
function* updateRegistration(action) {
const { pk, present, payment } = action.payload;
const token = yield select(tokenSelector);
yield put(eventActions.fetching());
const data = {
method: 'PATCH',
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
Authorization: `Token ${token}`,
},
body: JSON.stringify({
present,
payment,
}),
};
try {
yield call(apiRequest, `registrations/${pk}`, data);
yield put(eventActions.done());
} catch (error) {
Sentry.captureException(error);
yield put(eventActions.failure());
}
}
const eventSaga = function* eventSaga() {
function* eventSaga() {
yield takeEvery(eventActions.EVENT, event);
};
yield takeEvery(eventActions.UPDATE_REGISTRATION, updateRegistration);
}
export default eventSaga;
......@@ -30,7 +30,8 @@ const routerSaga = function* eventSaga() {
yield takeEvery(settingsActions.OPEN, navigate, 'Settings');
yield takeEvery(calendarActions.OPEN, navigate, 'Calendar');
yield takeEvery(membersActions.MEMBERS, navigate, 'MemberList');
yield takeEvery(eventActions.EVENT, navigate, 'Event');
yield takeEvery(eventActions.OPEN, navigate, 'Event');
yield takeEvery(eventActions.ADMIN, navigate, 'EventAdmin');
yield takeEvery(profileActions.PROFILE, navigate, 'Profile');
yield takeEvery(registrationActions.FIELDS, navigate, 'Registration');
yield takeEvery(registrationActions.SUCCESS, back);
......
......@@ -26,6 +26,8 @@ const sceneToTitle = (routeName, t) => {
return t('Registration');
case 'Settings':
return t('Settings');
case 'EventAdmin':
return t('Registrations');
default:
return 'ThaliApp';
}
......
......@@ -63,13 +63,8 @@ const styles = StyleSheet.create({
color: Colors.white,
},
rightView: {
ios: {
width: 24 + 16 + 10,
},
android: {
position: 'absolute',
right: 0,
},
position: 'absolute',
right: 0,
},
});
......
import React, { Component } from 'react';
import {
View, Text, Switch, RefreshControl, ScrollView, FlatList, TouchableOpacity,
} from 'react-native';
import { translate } from 'react-i18next';
import PropTypes from 'prop-types';
import Icon from 'react-native-vector-icons/MaterialIcons';
import Snackbar from 'react-native-snackbar';
import styles from './style/EventAdminScreen';
import Colors from '../../style/Colors';
import StandardHeader from '../../components/standardHeader/StandardHeader';
import LoadingScreen from '../../components/loadingScreen/LoadingScreen';
import ErrorScreen from '../../components/errorScreen/ErrorScreen';
import SearchHeader from '../memberList/SearchHeaderContainer';
const PAYMENT_TYPES = {
NONE: 'no_payment',
CARD: 'card_payment',
CASH: 'cash_payment',
};