Hooked up Work Item screen to API

This commit is contained in:
John Lyon-Smith
2018-04-05 15:28:57 -07:00
parent e6bd1f8fed
commit aec39ae17d
7 changed files with 195 additions and 103 deletions

Binary file not shown.

View File

@@ -248,9 +248,6 @@ class API extends EventEmitter {
deleteUser(_id) { deleteUser(_id) {
return this.delete('/users/' + _id) return this.delete('/users/' + _id)
} }
setUserImage(details) {
return this.put('/users/set-image', details)
}
enterRoom(roomName) { enterRoom(roomName) {
return this.put('/users/enter-room/' + (roomName || '')) return this.put('/users/enter-room/' + (roomName || ''))
} }
@@ -258,6 +255,22 @@ class API extends EventEmitter {
return this.put('/users/leave-room') return this.put('/users/leave-room')
} }
getWorkItem(_id) {
return this.get('/workitems/' + _id)
}
listWorkItems() {
return this.get('/workitems')
}
createWorkItem(workItem) {
return this.post('/workitems', workItem)
}
updateWorkItem(workItem) {
return this.put('/workitems', workItem)
}
deleteWorkItem(_id) {
return this.delete('/workitems/' + _id)
}
upload(file, progressCallback) { upload(file, progressCallback) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const chunkSize = 32 * 1024 const chunkSize = 32 * 1024

View File

@@ -86,7 +86,10 @@ export class Home extends React.Component {
render() { render() {
return ( return (
<View style={styles.container}> <View style={styles.container}>
<Header title='Work Item Map' leftButton={{ icon: 'logout', onPress: this.handleLogoutPress }} rightButton={{ icon: 'glasses', onPress: this.handleGlassesPress }} /> <Header
title='Work Item Map'
leftButton={{ icon: 'logout', onPress: this.handleLogoutPress }}
rightButton={{ icon: 'glasses', onPress: this.handleGlassesPress }} />
<MapView <MapView
ref={ref => { this.map = ref }} ref={ref => { this.map = ref }}
style={{ style={{

View File

@@ -35,14 +35,14 @@ export class MessageModal extends Component {
return ( return (
<Modal isVisible={open}> <Modal isVisible={open}>
<View style={{ alignSelf: 'center', padding: 5, backgroundColor: '#FFFFFF', flexDirection: 'row' }}> <View style={{ flexDirection: 'row', width: '100%', padding: 5, backgroundColor: '#FFFFFF' }}>
<Icon name={icon} size={130} margin={3} /> <Icon name={icon} size={100} margin={3} />
<View style={{ marginLeft: 20, marginRight: 20, flexGrow: 1, flexDirection: 'column' }}> <View style={{ flexGrow: 1, flexBasis: 0, flexDirection: 'column', flexWrap: 'wrap', marginLeft: 20, marginRight: 20}}>
<Text style={{ marginTop: 5, fontSize: 18, alignSelf: 'center' }}>{message}</Text> <Text style={{ marginTop: 5, fontSize: 18 }}>{message}</Text>
<Text style={{ marginTop: 20, alignSelf: 'center' }}>{detail}</Text> <Text style={{ marginTop: 20 }}>{detail}</Text>
<TouchableOpacity onPress={this.handleButtonPress} <TouchableOpacity onPress={this.handleButtonPress}
style={{ style={{
alignSelf: 'center', alignSelf: 'flex-end',
backgroundColor: 'blue', backgroundColor: 'blue',
marginTop: 20, marginTop: 20,
marginBottom: 10, marginBottom: 10,

View File

@@ -21,11 +21,15 @@ import {
PhotoButton, PhotoButton,
BoundOptionStrip, BoundOptionStrip,
} from '../ui' } from '../ui'
import { MessageModal } from '../Modal'
import autobind from 'autobind-decorator' import autobind from 'autobind-decorator'
import { ifIphoneX, isIphoneX } from 'react-native-iphone-x-helper' import { ifIphoneX, isIphoneX } from 'react-native-iphone-x-helper'
import KeyboardSpacer from 'react-native-keyboard-spacer'
import { api } from '../API'
const styles = StyleSheet.create({ const styles = StyleSheet.create({
container: { container: {
flexDirection: 'column',
flexGrow: 1, flexGrow: 1,
backgroundColor: '#DDDDDD', backgroundColor: '#DDDDDD',
}, },
@@ -47,11 +51,23 @@ const styles = StyleSheet.create({
}) })
const workItemOptions = [ const workItemOptions = [
{ value: 'workOrder', text: 'Work Order' }, { value: 'order', text: 'Work Order' },
{ value: 'inspection', text: 'Inspection' }, { value: 'inspection', text: 'Inspection' },
{ value: 'complaint', text: 'Complaint' } { value: 'complaint', text: 'Complaint' }
] ]
const latLngToString = (lat, lng) => (`${Math.abs(lng).toFixed(4)}°${lng > 0 ? 'S' : 'N'}, ${Math.abs(lat).toFixed(4)}°${lat > 0 ? 'W' : 'E'}`)
const latLngStringToPoint = (str) => {
const parts = str.split(', ')
return {
type: 'Point',
coordinates: [
new Number(parts[0].substring(0, parts[0].length - 2)),
new Number(parts[1].substring(0, parts[1].length - 2)),
]
}
}
export class WorkItem extends React.Component { export class WorkItem extends React.Component {
static bindings = { static bindings = {
header: { header: {
@@ -67,6 +83,8 @@ export class WorkItem extends React.Component {
}, },
workItemType: { workItemType: {
isValid: true, isValid: true,
initValue: 'order',
alwaysGet: true,
} }
} }
@@ -91,28 +109,51 @@ export class WorkItem extends React.Component {
@autobind @autobind
handleDonePress() { handleDonePress() {
const { binder } = this.state
let obj = binder.getModifiedFieldValues()
obj.location = latLngStringToPoint(obj.location)
if (!obj._id) {
api.createWorkItem(obj).then((workItem) => {
this.handleBackPress()
}).catch((error) => {
this.setState({ messageModal: {
icon: 'hand',
message: 'Unable to create work item',
detail: error.message,
}})
})
} else {
api.updateWorkItem(obj).then((workItem) => {
this.handleBackPress()
}).catch((error) => {
this.setState({ messageModal: {
icon: 'hand',
message: 'Unable to update work item',
detail: error.message,
}})
})
}
}
@autobind
handleMessageDismiss() {
this.setState({ messageModal: null })
} }
@autobind @autobind
handleRegionChange(region) { handleRegionChange(region) {
const { binder } = this.state const { binder } = this.state
// NOTE: For some reason, object destructuring does not work here :(
const lon = region.longitude
const lat = region.latitude
this.setState(binder.updateFieldValue('location', this.setState(binder.updateFieldValue('location', latLngToString(region.latitude, region.longitude)))
`${Math.abs(lon).toFixed(4)}°${lon > 0 ? 'S' : 'N'}, ${Math.abs(lat).toFixed(4)}°${lat > 0 ? 'W' : 'E'}`))
} }
render() { render() {
const { binder } = this.state const { binder, messageModal } = this.state
return ( return (
<KeyboardAvoidingView <View style={{ flex: 1 }}>
style={{ width: '100%', height: '100%' }}
behavior='padding'
keyboardVerticalOffset={Platform.select({ios: 0, android: -220})}>
<BoundHeader <BoundHeader
binder={binder} binder={binder}
name='header' name='header'
@@ -123,6 +164,9 @@ export class WorkItem extends React.Component {
<View style={styles.panel}> <View style={styles.panel}>
<BoundOptionStrip binder={binder} name='workItemType' label='Work Item Type:' options={workItemOptions} /> <BoundOptionStrip binder={binder} name='workItemType' label='Work Item Type:' options={workItemOptions} />
</View> </View>
<View style={styles.panel}>
<BoundInput binder={binder} name='details' lines={4} label='Details:' message='You must supply details for the work item' />
</View>
<View style={styles.panel}> <View style={styles.panel}>
<MapView <MapView
style={{ style={{
@@ -152,12 +196,16 @@ export class WorkItem extends React.Component {
<PhotoButton /> <PhotoButton />
</View> </View>
</View> </View>
<View style={styles.panel}>
<BoundInput binder={binder} name='details' lines={4} label='Details:' message='You must supply details for the work item' />
</View>
{ isIphoneX ? <View style={{ height: 30, width: '100%' }} /> : null } { isIphoneX ? <View style={{ height: 30, width: '100%' }} /> : null }
</ScrollView> </ScrollView>
</KeyboardAvoidingView> <MessageModal
open={!!messageModal}
icon={messageModal ? messageModal.icon : ''}
message={messageModal ? messageModal.message : ''}
detail={messageModal ? messageModal.detail : ''}
onDismiss={messageModal && this.handleMessageDismiss} />
<KeyboardSpacer />
</View>
) )
} }
} }

View File

@@ -22,14 +22,16 @@ export class WorkItemRoutes {
.delete(passport.authenticate('bearer', { session: false }), this.deleteWorkItem) .delete(passport.authenticate('bearer', { session: false }), this.deleteWorkItem)
} }
listWorkItems(req, res, next) { async listWorkItems(req, res, next) {
const WorkItem = this.db.WorkItem try {
const limit = req.query.limit || 20 const WorkItem = this.db.WorkItem
const skip = req.query.skip || 0 const limit = req.query.limit || 20
const partial = !!req.query.partial const skip = req.query.skip || 0
let query = {} const partial = !!req.query.partial
let query = {}
const total = await WorkItem.count({})
WorkItem.count({}).then((total) => {
let workItems = [] let workItems = []
let cursor = WorkItem.find(query).limit(limit).skip(skip).cursor().map((doc) => { let cursor = WorkItem.find(query).limit(limit).skip(skip).cursor().map((doc) => {
return doc.toClient(partial) return doc.toClient(partial)
@@ -47,102 +49,128 @@ export class WorkItemRoutes {
}) })
}) })
cursor.on('error', (err) => { cursor.on('error', (err) => {
next(createError.InternalServerError(err.message)) throw createError.InternalServerError(err.message)
}) })
}).catch((err) => { } catch(err) {
next(createError.InternalServerError(err.message)) if (err instanceof createError.HttpError) {
}) next(err)
} else {
next(createError.InternalServerError(err.message))
}
}
} }
createWorkItem(req, res, next) { async createWorkItem(req, res, next) {
const role = req.user.role
// If user's role is not Executive or Administrator, return an error
if (role !== 'executive' && role !== 'administrator') {
return next(new createError.Forbidden())
}
// Create a new WorkItem template then assign it to a value in the req.body
const WorkItem = this.db.WorkItem
let workItem = new WorkItem(req.body)
// Save the workItem (with promise) - If it doesnt, catch and throw error
workItem.save().then((newWorkItem) => {
res.json(newWorkItem.toClient())
}).catch((err) => {
next(createError.InternalServerError(err.message))
})
}
updateWorkItem(req, res, next) {
const role = req.user.role
// If user's role is not Executive or Administrator, return an error
if (role !== 'executive' && role !== 'administrator') {
return new createError.Forbidden()
}
// Do this here because Mongoose will add it automatically otherwise
if (!req.body._id) {
return next(createError.BadRequest('No _id given in body'))
}
let WorkItem = this.db.WorkItem
let workItemUpdates = null
try { try {
workItemUpdates = new WorkItem(req.body) const isAdmin = req.user.administrator
} catch (err) {
return next(createError.BadRequest('Invalid data')) if (!isAdmin) {
} return new createError.Forbidden()
}
// Create a new WorkItem template then assign it to a value in the req.body
const WorkItem = this.db.WorkItem
let workItem = new WorkItem(req.body)
// Save the workItem (with promise) - If it doesnt, catch and throw error
const newWorkItem = await workItem.save()
res.json(newWorkItem.toClient())
} catch(err) {
if (err instanceof createError.HttpError) {
next(err)
} else {
next(createError.InternalServerError(err.message))
}
}
}
async updateWorkItem(req, res, next) {
try {
const isAdmin = req.user.administrator
if (!isAdmin) {
return new createError.Forbidden()
}
// Do this here because Mongoose will add it automatically otherwise
if (!req.body._id) {
throw createError.BadRequest('No _id given in body')
}
let WorkItem = this.db.WorkItem
let workItemUpdates = null
try {
workItemUpdates = new WorkItem(req.body)
} catch (err) {
throw createError.BadRequest('Invalid data')
}
const foundWorkItem = await WorkItem.findById(workItemUpdates._id)
WorkItem.findById(workItemUpdates._id).then((foundWorkItem) => {
if (!foundWorkItem) { if (!foundWorkItem) {
return next(createError.NotFound(`WorkItem with _id ${_id} was not found`)) return next(createError.NotFound(`WorkItem with _id ${_id} was not found`))
} }
foundWorkItem.merge(workItemUpdates) foundWorkItem.merge(workItemUpdates)
return foundWorkItem.save()
}).then((savedWorkItem) => { const savedWorkItem = await foundWorkItem.save()
res.json(savedWorkItem.toClient()) res.json(savedWorkItem.toClient())
}).catch((err) => { } catch(err) {
next(createError.InternalServerError(err.message)) if (err instanceof createError.HttpError) {
}) next(err)
} else {
next(createError.InternalServerError(err.message))
}
}
} }
getWorkItem(req, res, next) { async getWorkItem(req, res, next) {
const WorkItem = this.db.WorkItem try {
const _id = req.params._id const WorkItem = this.db.WorkItem
const _id = req.params._id
const workItem = await WorkItem.findById(_id)
WorkItem.findById(_id).then((workItem) => {
if (!workItem) { if (!workItem) {
return next(createError.NotFound(`WorkItem with _id ${_id} not found`)) throw createError.NotFound(`WorkItem with _id ${_id} not found`)
} }
res.json(workItem.toClient()) res.json(workItem.toClient())
}).catch((err) => { } catch(err) {
next(createError.InternalServerError(err.message)) if (err instanceof createError.HttpError) {
}) next(err)
} else {
next(createError.InternalServerError(err.message))
}
}
} }
deleteWorkItem(req, res, next) { async deleteWorkItem(req, res, next) {
const role = req.user.role try {
const isAdmin = req.user.administrator
// If user's role is not Executive or Administrator, return an error if (!isAdmin) {
if (role !== 'executive' && role !== 'administrator') { return new createError.Forbidden()
return new createError.Forbidden() }
}
const WorkItem = this.db.WorkItem const WorkItem = this.db.WorkItem
const _id = req.params._id const _id = req.params._id
const workItem = await WorkItem.remove({ _id })
WorkItem.remove({ _id }).then((workItem) => {
if (!workItem) { if (!workItem) {
return next(createError.NotFound(`WorkItem with _id ${_id} not found`)) throw createError.NotFound(`WorkItem with _id ${_id} not found`)
} }
res.json({}) res.json({})
}).catch((err) => { } catch(err) {
next(createError.InternalServerError(err.message)) if (err instanceof createError.HttpError) {
}) next(err)
} else {
next(createError.InternalServerError(err.message))
}
}
} }
} }

View File

@@ -8,7 +8,7 @@ export let workItemSchema = new Schema({
message: 'enum validator failed for path `{PATH}` with value `{VALUE}`' message: 'enum validator failed for path `{PATH}` with value `{VALUE}`'
}}, }},
number: Number, number: Number,
loc: { location: {
type: { type: String }, type: { type: String },
coordinates: [Number], coordinates: [Number],
}, },