Commit 5a29f52f authored by Gijs Hendriksen's avatar Gijs Hendriksen

Create admin screen for the pizza orders

parent 7f525dc5
......@@ -2,6 +2,11 @@
exports[`pizza reducer initially should return the initial state 1`] = `
Object {
"admin": Object {
"loading": false,
"orders": Array [],
"status": "initial",
},
"event": null,
"hasLoaded": false,
"loading": false,
......
......@@ -8,6 +8,7 @@ import eventSaga from '../../app/sagas/event';
import * as eventActions from '../../app/actions/event';
import { tokenSelector } from '../../app/selectors/session';
import { currentEventSelector } from '../../app/selectors/events';
jest.mock('../../app/utils/url', () => ({
apiRequest: jest.fn(() => {}),
......@@ -98,6 +99,7 @@ describe('event saga', () => {
it('should start fetching', () => expectSaga(eventSaga)
.provide([
[select(tokenSelector), 'token'],
[select(currentEventSelector), 1],
])
.dispatch(eventActions.updateRegistration(1, true, 'payment'))
.put(eventActions.fetching())
......@@ -106,6 +108,7 @@ describe('event saga', () => {
it('should do a PATCH request', () => expectSaga(eventSaga)
.provide([
[select(tokenSelector), 'token'],
[select(currentEventSelector), 1],
])
.dispatch(eventActions.updateRegistration(1, true, 'payment'))
.silentRun()
......@@ -121,18 +124,20 @@ describe('event saga', () => {
});
}));
it('should be done when the request is successful', () => expectSaga(eventSaga)
it('should refresh the event when the request is successful', () => expectSaga(eventSaga)
.provide([
[select(tokenSelector), 'token'],
[select(currentEventSelector), 1],
[matchers.call.like({ fn: apiRequest, args: ['registrations/1'] }), 'response'],
])
.dispatch(eventActions.updateRegistration(1, true, 'payment'))
.put(eventActions.done())
.put(eventActions.event(1, false))
.silentRun());
it('should put an error when the request fails', () => expectSaga(eventSaga)
.provide([
[select(tokenSelector), 'token'],
[select(currentEventSelector), 1],
[matchers.call.like({ fn: apiRequest, args: ['registrations/1'] }), throwError(error)],
])
.dispatch(eventActions.updateRegistration(1, true, 'payment'))
......
......@@ -6,6 +6,12 @@ export const CANCEL = 'PIZZA_CANCEL';
export const CANCEL_SUCCESS = 'PIZZA_CANCEL_SUCCESS';
export const ORDER = 'PIZZA_ORDER';
export const ORDER_SUCCESS = 'PIZZA_ORDER_SUCCESS';
export const ADMIN = 'PIZZA_ADMIN';
export const ADMIN_ORDERS = 'PIZZA_ADMIN_ORDERS';
export const ADMIN_UPDATE_ORDER = 'PIZZA_ADMIN_UPDATE_ORDER';
export const ADMIN_LOADING = 'PIZZA_ADMIN_LOADING';
export const ADMIN_SUCCESS = 'PIZZA_ADMIN_SUCCESS';
export const ADMIN_FAILURE = 'PIZZA_ADMIN_FAILURE';
export function retrievePizzaInfo() {
return {
......@@ -57,3 +63,41 @@ export function orderSuccess(order) {
payload: { order },
};
}
export function openAdmin() {
return {
type: ADMIN,
};
}
export function retrieveOrders() {
return {
type: ADMIN_ORDERS,
};
}
export function updateOrder(pk, payment) {
return {
type: ADMIN_UPDATE_ORDER,
payload: { pk, payment },
};
}
export function adminLoading() {
return {
type: ADMIN_LOADING,
};
}
export function adminSuccess(orders) {
return {
type: ADMIN_SUCCESS,
payload: { orders },
};
}
export function adminFailure() {
return {
type: ADMIN_FAILURE,
};
}
......@@ -21,6 +21,7 @@ import PhotoGallery from './ui/screens/photos/AlbumGalleryScreenConnector';
import SplashScreen from './ui/screens/splash/SplashScreen';
import Settings from './ui/screens/settings/SettingsScreenConnector';
import EventAdmin from './ui/screens/events/EventAdminScreenConnector';
import PizzaAdmin from './ui/screens/pizza/PizzaAdminScreenConnector';
import Sidebar from './ui/components/sidebar/SidebarConnector';
const MainNavigator = createDrawerNavigator({
......@@ -42,6 +43,7 @@ const SignedInNavigator = createStackNavigator({
PhotoGallery,
Registration,
EventAdmin,
PizzaAdmin,
}, {
headerMode: 'none',
});
......
......@@ -7,12 +7,18 @@ const initialState = {
event: null,
order: null,
pizzaList: [],
admin: {
loading: false,
status: 'initial',
orders: [],
},
};
export default function pizza(state = initialState, action = {}) {
switch (action.type) {
case pizzaActions.SUCCESS:
return {
...state,
success: true,
loading: false,
hasLoaded: true,
......@@ -42,6 +48,31 @@ export default function pizza(state = initialState, action = {}) {
...state,
order: action.payload.order,
};
case pizzaActions.ADMIN_LOADING:
return {
...state,
admin: {
...state.admin,
loading: true,
},
};
case pizzaActions.ADMIN_SUCCESS:
return {
...state,
admin: {
loading: false,
status: 'success',
orders: action.payload.orders,
},
};
case pizzaActions.ADMIN_FAILURE:
return {
...state,
admin: {
...initialState.admin,
status: 'failure',
},
};
default:
return state;
}
......
......@@ -5,6 +5,7 @@ import {
import { apiRequest } from '../utils/url';
import * as eventActions from '../actions/event';
import { tokenSelector } from '../selectors/session';
import { currentEventSelector } from '../selectors/events';
import reportError from '../utils/errorReporting';
function* event(action) {
......@@ -48,6 +49,7 @@ function* event(action) {
function* updateRegistration(action) {
const { pk, present, payment } = action.payload;
const token = yield select(tokenSelector);
const currentEvent = yield select(currentEventSelector);
yield put(eventActions.fetching());
......@@ -66,8 +68,7 @@ function* updateRegistration(action) {
try {
yield call(apiRequest, `registrations/${pk}`, data);
yield put(eventActions.done());
yield put(eventActions.event(currentEvent, false));
} catch (error) {
yield call(reportError, error);
yield put(eventActions.failure());
......
......@@ -43,6 +43,7 @@ export default function* () {
yield takeEvery(registrationActions.FIELDS, navigate, 'Registration');
yield takeEvery(registrationActions.SUCCESS, back);
yield takeEvery(pizzaActions.PIZZA, navigate, 'Pizza');
yield takeEvery(pizzaActions.ADMIN, navigate, 'PizzaAdmin');
yield takeEvery(photosActions.PHOTOS_ALBUMS_OPEN, navigate, 'Photos');
yield takeEvery(photosActions.PHOTOS_ALBUM_OPEN, navigate, 'PhotoAlbum');
yield takeEvery(photosActions.PHOTOS_GALLERY_OPEN, navigate, 'PhotoGallery');
......
......@@ -15,7 +15,7 @@ export const Payment = {
const NOT_FOUND = 404;
const retrievePizzaInfo = function* retrievePizzaInfo() {
function* retrievePizzaInfo() {
const token = yield select(tokenSelector);
yield put(pizzaActions.fetching());
......@@ -33,8 +33,8 @@ const retrievePizzaInfo = function* retrievePizzaInfo() {
const event = yield call(apiRequest, 'pizzas/event', data);
const pizzaList = yield call(apiRequest, 'pizzas', data);
try {
const order = yield call(apiRequest, 'pizzas/orders/me', data);
yield put(pizzaActions.success(event, order, pizzaList));
const currentOrder = yield call(apiRequest, 'pizzas/orders/me', data);
yield put(pizzaActions.success(event, currentOrder, pizzaList));
} catch (error) {
if (error.response !== null && error.response.status === NOT_FOUND) {
yield put(pizzaActions.success(event, null, pizzaList));
......@@ -51,9 +51,9 @@ const retrievePizzaInfo = function* retrievePizzaInfo() {
yield put(pizzaActions.failure());
}
}
};
}
const cancel = function* cancel() {
function* cancel() {
const token = yield select(tokenSelector);
const data = {
method: 'DELETE',
......@@ -71,9 +71,9 @@ const cancel = function* cancel() {
yield call(reportError, error);
yield put(pizzaActions.failure());
}
};
}
const order = function* order(action) {
function* order(action) {
const { pk, hasOrder } = action.payload;
const token = yield select(tokenSelector);
const data = {
......@@ -96,10 +96,63 @@ const order = function* order(action) {
yield call(reportError, error);
yield put(pizzaActions.failure());
}
};
}
function* retrieveOrders() {
const token = yield select(tokenSelector);
yield put(pizzaActions.adminLoading());
const data = {
method: 'GET',
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
Authorization: `Token ${token}`,
},
};
try {
const orders = yield call(apiRequest, 'pizzas/orders', data);
yield put(pizzaActions.adminSuccess(orders));
} catch (error) {
yield call(reportError, error);
yield put(pizzaActions.adminFailure());
}
}
function* updateOrder(action) {
const { pk, payment } = action.payload;
const token = yield select(tokenSelector);
yield put(pizzaActions.adminLoading());
const data = {
method: 'PATCH',
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
Authorization: `Token ${token}`,
},
body: JSON.stringify({
payment,
}),
};
try {
yield call(apiRequest, `pizzas/orders/${pk}`, data);
yield put(pizzaActions.retrieveOrders());
} catch (error) {
yield call(reportError, error);
yield put(pizzaActions.adminFailure());
}
}
export default function* () {
yield takeEvery(pizzaActions.PIZZA, retrievePizzaInfo);
yield takeEvery(pizzaActions.CANCEL, cancel);
yield takeEvery(pizzaActions.ORDER, order);
yield takeEvery([pizzaActions.ADMIN, pizzaActions.ADMIN_ORDERS], retrieveOrders);
yield takeEvery(pizzaActions.ADMIN_UPDATE_ORDER, updateOrder);
}
......@@ -31,6 +31,8 @@ const sceneToTitle = (routeName, t) => {
return t('Settings');
case 'EventAdmin':
return t('Registrations');
case 'PizzaAdmin':
return t('Orders');
default:
return 'ThaliApp';
}
......
......@@ -130,7 +130,7 @@ class AdminScreen extends Component {
{name}
</Text>
<View style={styles.itemControls}>
{checkboxLabel && (
{checkboxLabel ? (
<View style={styles.checkboxContainer}>
<Text style={[styles.text, styles.label]}>
{checkboxLabel}
......@@ -145,7 +145,7 @@ class AdminScreen extends Component {
thumbColor={checkbox ? Colors.darkMagenta : Colors.grey}
/>
</View>
)}
) : null}
<View style={styles.selectContainer}>
{
select.options.map(({ key, label }, buttonIndex) => (
......
import React, { Component } from 'react';
import {
RefreshControl, ScrollView, View,
} from 'react-native';
import { withTranslation } from 'react-i18next';
import PropTypes from 'prop-types';
import styles from './style/PizzaAdminScreen';
import Colors from '../../style/Colors';
import AdminScreen from '../admin/AdminScreenConnector';
import StandardHeader from '../../components/standardHeader/StandardHeader';
import LoadingScreen from '../../components/loadingScreen/LoadingScreen';
import ErrorScreen from '../../components/errorScreen/ErrorScreen';
const PAYMENT_TYPES = {
NONE: 'no_payment',
CARD: 'card_payment',
CASH: 'cash_payment',
};
class PizzaAdminScreen extends Component {
handleRefresh = () => {
this.props.retrieveOrders();
};
render() {
const {
status, loading, orders, t,
} = this.props;
const items = orders.map(item => ({
pk: item.pk,
name: item.display_name,
select: {
options: [
{
key: PAYMENT_TYPES.NONE,
label: t('NOT PAID'),
},
{
key: PAYMENT_TYPES.CARD,
label: t('CARD'),
},
{
key: PAYMENT_TYPES.CASH,
label: t('CASH'),
},
],
value: item.payment,
},
}));
const filterTypes = [
{
label: t('Disabled filter'),
checkItem: () => true,
},
{
label: t('Filtering on payment'),
checkItem: item => item.select.value === PAYMENT_TYPES.NONE,
},
];
if (status === 'initial') {
return (
<View style={styles.rootWrapper}>
<StandardHeader />
<LoadingScreen />
</View>
);
}
if (status === 'success') {
if (items.length === 0) {
return (
<View style={styles.rootWrapper}>
<StandardHeader />
<ScrollView
backgroundColor={Colors.background}
contentContainerStyle={styles.rootWrapper}
refreshControl={(
<RefreshControl
refreshing={loading}
onRefresh={this.handleRefresh}
/>
)}
>
<ErrorScreen message={t('No registrations found...')} />
</ScrollView>
</View>
);
}
return (
<View style={styles.rootWrapper}>
<AdminScreen
items={items}
filterTypes={filterTypes}
handleRefresh={this.handleRefresh}
title={t('Orders')}
updateItem={(pk, checkbox, select) => this.props.updateOrder(pk, select)}
loading={loading}
/>
</View>
);
}
return (
<View style={styles.rootWrapper}>
<StandardHeader />
<ScrollView
backgroundColor={Colors.background}
contentContainerStyle={styles.rootWrapper}
refreshControl={(
<RefreshControl
refreshing={loading}
onRefresh={this.handleRefresh}
/>
)}
>
<ErrorScreen message={t('Could not load the event...')} />
</ScrollView>
</View>
);
}
}
PizzaAdminScreen.propTypes = {
orders: PropTypes.arrayOf(PropTypes.shape({
pk: PropTypes.number.isRequired,
display_name: PropTypes.string.isRequired,
payment: PropTypes.string.isRequired,
})).isRequired,
status: PropTypes.string.isRequired,
loading: PropTypes.bool.isRequired,
retrieveOrders: PropTypes.func.isRequired,
updateOrder: PropTypes.func.isRequired,
t: PropTypes.func.isRequired,
};
export default withTranslation('ui/screens/pizza/PizzaAdminScreen')(PizzaAdminScreen);
import { connect } from 'react-redux';
import * as pizzaActions from '../../../actions/pizza';
import PizzaAdminScreen from './PizzaAdminScreen';
const mapStateToProps = state => ({
orders: state.pizza.admin.orders,
status: state.pizza.admin.status,
loading: state.pizza.admin.loading,
});
const mapDispatchToProps = {
retrieveOrders: pizzaActions.retrieveOrders,
updateOrder: pizzaActions.updateOrder,
};
export default connect(mapStateToProps, mapDispatchToProps)(PizzaAdminScreen);
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import {
RefreshControl, ScrollView, Text, TouchableHighlight, View,
RefreshControl, ScrollView, Text, TouchableHighlight, TouchableOpacity, View,
} from 'react-native';
import { withTranslation } from 'react-i18next';
import Icon from 'react-native-vector-icons/MaterialIcons';
......@@ -10,7 +10,7 @@ import LoadingScreen from '../../components/loadingScreen/LoadingScreen';
import ErrorScreen from '../../components/errorScreen/ErrorScreen';
import styles from './style/PizzaScreen';
import Colors from '../../style/Colors';
import { withStandardHeader } from '../../components/standardHeader/StandardHeader';
import StandardHeader from '../../components/standardHeader/StandardHeader';
import CardSection from '../../components/cardSection/CardSection';
class PizzaScreen extends Component {
......@@ -165,44 +165,50 @@ class PizzaScreen extends Component {
render() {
const {
hasLoaded, success, event, loading, t, pizzaList, order,
hasLoaded, success, event, loading, t, pizzaList, order, openAdmin,
} = this.props;
if (!hasLoaded) {
return <LoadingScreen />;
} if (!success) {
return (
<ScrollView
refreshControl={(
<RefreshControl
refreshing={loading}
onRefresh={this.handleRefresh}
/>
)}
style={styles.scrollView}
contentContainerStyle={styles.content}
>
<ErrorScreen message={this.props.t('Sorry! We couldn\'t load any data.')} />
</ScrollView>
<View style={styles.rootWrapper}>
<StandardHeader />
<ScrollView
refreshControl={(
<RefreshControl
refreshing={loading}
onRefresh={this.handleRefresh}
/>
)}
style={styles.scrollView}
contentContainerStyle={styles.content}
>
<ErrorScreen message={this.props.t('Sorry! We couldn\'t load any data.')} />
</ScrollView>
</View>
);
} if (!event) {
return (
<ScrollView
refreshControl={(
<RefreshControl
refreshing={loading}
onRefresh={this.handleRefresh}
/>
)}
style={styles.scrollView}
contentContainerStyle={styles.content}
>
<Text
style={styles.title}
<View style={styles.rootWrapper}>
<StandardHeader />
<ScrollView
refreshControl={(
<RefreshControl
refreshing={loading}
onRefresh={this.handleRefresh}
/>
)}
style={styles.scrollView}
contentContainerStyle={styles.content}
>
{t('There is currently no event for which you can order food.')}
</Text>
</ScrollView>
<Text
style={styles.title}
>
{t('There is currently no event for which you can order food.')}
</Text>
</ScrollView>
</View>
);
}
......@@ -222,25 +228,45 @@ class PizzaScreen extends Component {
subtitle = t(`You can order until ${end.format('HH:mm')}`);
}
return (
<ScrollView
refreshControl={(
<RefreshControl
refreshing={loading}
onRefresh={this.handleRefresh}
const hasAdminView = event.is_admin && !inFuture;
const adminButton = (
<View style={styles.rightView}>
<TouchableOpacity
onPress={() => openAdmin()}
>
<Icon
name="settings"
style={styles.adminIcon}
size={24}
/>
)}
ref={(ref) => { this.pizzaScroll = ref; }}
style={styles.scrollView}
>
<View style={styles.content}>
{this.getEventInfo(subtitle)}
{hasEnded && this.getOverview()}
{this.getOrder(hasEnded)}
{inFuture || hasEnded || (order && order.paid)
|| this.getPizzaList(pizzaList, !!order)}
</View>
</ScrollView>
</TouchableOpacity>
</View>
);
const header = <StandardHeader rightView={hasAdminView && adminButton} />;
return (
<View style={styles.rootWrapper}>
{header}
<ScrollView
refreshControl={(
<RefreshControl
refreshing={loading}
onRefresh={this.handleRefresh}
/>
)}
ref={(ref) => { this.pizzaScroll = ref; }}
style={styles.scrollView}
>
<View style={styles.content}>
{this.getEventInfo(subtitle)}
{hasEnded && this.getOverview()}
{this.getOrder(hasEnded)}
{inFuture || hasEnded || (order && order.paid)
|| this.getPizzaList(pizzaList, !!order)}
</View>
</ScrollView>
</View>
);
}
}
......@@ -254,6 +280,7 @@ PizzaScreen.propTypes = {
end: PropTypes.string.isRequired,
event: PropTypes.number.isRequired,
title: PropTypes.string.isRequired,
is_admin: PropTypes.bool.isRequired,
}),
order: PropTypes.shape({
pk: PropTypes.number.isRequired,
......@@ -273,6 +300,7 @@ PizzaScreen.propTypes = {
loadPizzas: PropTypes.func.isRequired,
cancelPizza: PropTypes.func.isRequired,
orderPizza: PropTypes.func.isRequired,
openAdmin: PropTypes.func.isRequired,
t: PropTypes.func.isRequired,
};
......@@ -281,4 +309,4 @@ PizzaScreen.defaultProps = {
order: null,
};
export default withTranslation('ui/screens/pizza/PizzaScreen')(withStandardHeader(PizzaScreen));
export default withTranslation('ui/screens/pizza/PizzaScreen')(PizzaScreen);
......@@ -20,6 +20,7 @@ const mapDispatchToProps = {
loadPizzas: pizzaActions.retrievePizzaInfo,
cancelPizza: pizzaActions.cancelOrder,
orderPizza: pizzaActions.orderPizza,
openAdmin: pizzaActions.openAdmin,
};
export default connect(mapStateToProps, mapDispatchToProps)(PizzaScreen);
import StyleSheet from '../../../style/StyleSheet';
const styles = StyleSheet.create({
rootWrapper: {
flex: 1,
},
});
export default styles;
......@@ -2,6 +2,9 @@ import Colors from '../../../style/Colors';
import StyleSheet from '../../../style/StyleSheet';
const styles = StyleSheet.create({
rootWrapper: {
flex: 1,
},
scrollView: {
flex: 1,
backgroundColor: Colors.background,
......@@ -190,6 +193,20 @@ const styles = StyleSheet.create({