Debug issues with work item CRUD

This commit is contained in:
John Lyon-Smith
2018-04-06 14:59:18 -07:00
parent d646b9477b
commit 57f98ad398
14 changed files with 684 additions and 418 deletions

View File

@@ -1,3 +1,6 @@
{ {
"semi": false, "semi": false,
"arrowParens": "always",
"trailingComma": "es5",
"jsxBracketSameLine": true,
} }

View File

@@ -7038,6 +7038,11 @@
"resolved": "https://registry.npmjs.org/urix/-/urix-0.1.0.tgz", "resolved": "https://registry.npmjs.org/urix/-/urix-0.1.0.tgz",
"integrity": "sha1-2pN/emLiH+wf0Y1Js1wpNQZ6bHI=" "integrity": "sha1-2pN/emLiH+wf0Y1Js1wpNQZ6bHI="
}, },
"url-search-params-polyfill": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/url-search-params-polyfill/-/url-search-params-polyfill-3.0.0.tgz",
"integrity": "sha512-oRNWuBkJ/zKKK1aiBaTBZTf07zOKd0g+nJYB+vFNPO14gFjA75BaHgIJLtveWBRxI/2qff7xcTb9H6wkpTmqjg=="
},
"use": { "use": {
"version": "3.1.0", "version": "3.1.0",
"resolved": "https://registry.npmjs.org/use/-/use-3.1.0.tgz", "resolved": "https://registry.npmjs.org/use/-/use-3.1.0.tgz",

View File

@@ -32,6 +32,7 @@
"react-native-swipe-list-view": "^1.0.7", "react-native-swipe-list-view": "^1.0.7",
"react-router-native": "^4.2.0", "react-router-native": "^4.2.0",
"react-viro": "^2.4.0", "react-viro": "^2.4.0",
"socket.io-client": "^2.0.4" "socket.io-client": "^2.0.4",
"url-search-params-polyfill": "^3.0.0"
} }
} }

View File

@@ -1,42 +1,42 @@
import EventEmitter from 'eventemitter3' import EventEmitter from "eventemitter3"
import io from 'socket.io-client' import io from "socket.io-client"
import { AsyncStorage } from 'react-native' import { AsyncStorage } from "react-native"
const authTokenName = 'AuthToken' const authTokenName = "AuthToken"
let baseURL = null let baseURL = null
let apiPath = null let apiPath = null
// //
if (__DEV__) { if (__DEV__) {
const localIPAddr = process.env.LOCAL_IP_ADDR const localIPAddr = process.env.LOCAL_IP_ADDR
baseURL = `http://${localIPAddr || 'localhost'}:3001` baseURL = `http://${localIPAddr || "localhost"}:3001`
apiPath = '' apiPath = ""
} else { } else {
baseURL = 'https://dar.kss.us.com' baseURL = "https://dar.kss.us.com"
apiPath = '/api' apiPath = "/api"
} }
class NetworkError extends Error { class NetworkError extends Error {
constructor(message) { constructor(message) {
super(message) super(message)
this.name = this.constructor.name this.name = this.constructor.name
if (typeof Error.captureStackTrace === 'function') { if (typeof Error.captureStackTrace === "function") {
Error.captureStackTrace(this, this.constructor) Error.captureStackTrace(this, this.constructor)
} else { } else {
this.stack = (new Error(message)).stack this.stack = new Error(message).stack
} }
} }
} }
class APIError extends Error { class APIError extends Error {
constructor(status, message) { constructor(status, message) {
super(message || '') super(message || "")
this.status = status || 500 this.status = status || 500
this.name = this.constructor.name this.name = this.constructor.name
if (typeof Error.captureStackTrace === 'function') { if (typeof Error.captureStackTrace === "function") {
Error.captureStackTrace(this, this.constructor) Error.captureStackTrace(this, this.constructor)
} else { } else {
this.stack = (new Error(message)).stack this.stack = new Error(message).stack
} }
} }
} }
@@ -46,39 +46,42 @@ class API extends EventEmitter {
super() super()
this.user = { pending: true } this.user = { pending: true }
AsyncStorage.getItem(authTokenName).then((token) => { AsyncStorage.getItem(authTokenName)
if (!token) { .then((token) => {
return Promise.reject() if (!token) {
} return Promise.reject()
}
this.token = token this.token = token
return this.who() return this.who()
}).then((user) => { })
this.user = user .then((user) => {
this.connectSocket() this.user = user
this.emit('login') this.connectSocket()
}).catch((err) => { this.emit("login")
console.error(err) })
AsyncStorage.removeItem(authTokenName) .catch((err) => {
this.token = null console.error(err)
this.user = {} AsyncStorage.removeItem(authTokenName)
this.socket = null this.token = null
this.emit('logout') this.user = {}
}) this.socket = null
this.emit("logout")
})
} }
connectSocket() { connectSocket() {
this.socket = io(baseURL, { this.socket = io(baseURL, {
path: apiPath + '/socketio', path: apiPath + "/socketio",
query: { query: {
auth_token: this.token auth_token: this.token,
} },
}) })
this.socket.on('disconnect', (reason) => { this.socket.on("disconnect", (reason) => {
// Could happen if the auth_token is bad // Could happen if the auth_token is bad
this.socket = null this.socket = null
}) })
this.socket.on('notify', (message) => { this.socket.on("notify", (message) => {
const { eventName, eventData } = message const { eventName, eventData } = message
// Filter the few massages that affect our cached user data to avoid a server round trip // Filter the few massages that affect our cached user data to avoid a server round trip
@@ -109,22 +112,27 @@ class API extends EventEmitter {
makeImageUrl(id, size) { makeImageUrl(id, size) {
if (id) { if (id) {
return apiPath + '/assets/' + id + '?access_token=' + this.token return apiPath + "/assets/" + id + "?access_token=" + this.token
} else if (size && size.width && size.height) { } else if (size && size.width && size.height) {
return `${apiPath}/placeholders/${size.width}x${size.height}?access_token=${this.token}` return `${apiPath}/placeholders/${size.width}x${
size.height
}?access_token=${this.token}`
} else { } else {
return null return null
} }
} }
makeAssetUrl(id) { makeAssetUrl(id) {
return id ? apiPath + '/assets/' + id + '?access_token=' + this.token : null return id ? apiPath + "/assets/" + id + "?access_token=" + this.token : null
} }
static makeParams(params) { static makeParams(params) {
return params ? '?' + Object.keys(params).map((key) => ( return params
[key, params[key]].map(encodeURIComponent).join('=') ? "?" +
)).join('&') : '' Object.keys(params)
.map((key) => [key, params[key]].map(encodeURIComponent).join("="))
.join("&")
: ""
} }
request(method, path, requestBody, requestOptions) { request(method, path, requestBody, requestOptions) {
@@ -132,80 +140,88 @@ class API extends EventEmitter {
var promise = new Promise((resolve, reject) => { var promise = new Promise((resolve, reject) => {
let fetchOptions = { let fetchOptions = {
method: method, method: method,
mode: 'cors', mode: "cors",
cache: 'no-store' cache: "no-store",
} }
let headers = new Headers() let headers = new Headers()
if (this.token) { if (this.token) {
headers.set('Authorization', 'Bearer ' + this.token) headers.set("Authorization", "Bearer " + this.token)
} }
if (method === 'POST' || method === 'PUT') { if (method === "POST" || method === "PUT") {
if (requestOptions.binary) { if (requestOptions.binary) {
headers.set('Content-Type', 'application/octet-stream') headers.set("Content-Type", "application/octet-stream")
headers.set('Content-Length', requestOptions.binary.length) headers.set("Content-Length", requestOptions.binary.length)
headers.set('Range', 'byte ' + requestOptions.binary.offset) headers.set("Range", "byte " + requestOptions.binary.offset)
fetchOptions.body = requestBody fetchOptions.body = requestBody
} else { } else {
headers.set('Content-Type', 'application/json') headers.set("Content-Type", "application/json")
fetchOptions.body = JSON.stringify(requestBody) fetchOptions.body = JSON.stringify(requestBody)
} }
} }
fetchOptions.headers = headers fetchOptions.headers = headers
fetch(this.apiURL + path, fetchOptions).then((res) => { fetch(this.apiURL + path, fetchOptions)
return Promise.all([ Promise.resolve(res), (requestOptions.binary && method === 'GET') ? res.blob() : res.json() ]) .then((res) => {
}).then((arr) => { return Promise.all([
let [ res, responseBody ] = arr Promise.resolve(res),
if (res.ok) { requestOptions.binary && method === "GET" ? res.blob() : res.json(),
if (requestOptions.wantHeaders) { ])
resolve({ body: responseBody, headers: res.headers }) })
.then((arr) => {
let [res, responseBody] = arr
if (res.ok) {
if (requestOptions.wantHeaders) {
resolve({ body: responseBody, headers: res.headers })
} else {
resolve(responseBody)
}
} else { } else {
resolve(responseBody) reject(new APIError(res.status, responseBody.message))
} }
} else { })
reject(new APIError(res.status, responseBody.message)) .catch((error) => {
} reject(new NetworkError(error.message))
}).catch((error) => { })
reject(new NetworkError(error.message))
})
}) })
return promise return promise
} }
post(path, requestBody, options) { post(path, requestBody, options) {
return this.request('POST', path, requestBody, options) return this.request("POST", path, requestBody, options)
} }
put(path, requestBody, options) { put(path, requestBody, options) {
return this.request('PUT', path, requestBody, options) return this.request("PUT", path, requestBody, options)
} }
get(path, options) { get(path, options) {
return this.request('GET', path, options) return this.request("GET", path, options)
} }
delete(path, options) { delete(path, options) {
return this.request('DELETE', path, options) return this.request("DELETE", path, options)
} }
login(email, password, remember) { login(email, password, remember) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
this.post('/auth/login', { email, password }, { wantHeaders: true }).then((response) => { this.post("/auth/login", { email, password }, { wantHeaders: true })
// Save bearer token for later use .then((response) => {
const authValue = response.headers.get('Authorization') // Save bearer token for later use
const [ scheme, token ] = authValue.split(' ') const authValue = response.headers.get("Authorization")
const [scheme, token] = authValue.split(" ")
if (scheme !== 'Bearer' || !token) { if (scheme !== "Bearer" || !token) {
reject(new APIError('Unexpected Authorization scheme or token')) reject(new APIError("Unexpected Authorization scheme or token"))
} }
if (remember) { if (remember) {
AsyncStorage.setItem(authTokenName, token) AsyncStorage.setItem(authTokenName, token)
} }
this.token = token this.token = token
this.user = response.body this.user = response.body
this.connectSocket() this.connectSocket()
this.emit('login') this.emit("login")
resolve(response.body) resolve(response.body)
}).catch((err) => { })
reject(err) .catch((err) => {
}) reject(err)
})
}) })
} }
logout() { logout() {
@@ -215,61 +231,63 @@ class API extends EventEmitter {
this.token = null this.token = null
this.user = {} this.user = {}
this.disconnectSocket() this.disconnectSocket()
this.emit('logout') this.emit("logout")
} }
return this.delete('/auth/login').then(cb, cb) return this.delete("/auth/login").then(cb, cb)
} }
who() { who() {
return this.get('/auth/who') return this.get("/auth/who")
} }
getUser(_id) { getUser(_id) {
return this.get('/users/' + _id) return this.get("/users/" + _id)
} }
listUsers() { listUsers() {
return this.get('/users') return this.get("/users")
} }
createUser(user) { createUser(user) {
return this.post('/users', user) return this.post("/users", user)
} }
updateUser(user) { updateUser(user) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
this.put('/users', user).then((user) => { this.put("/users", user)
// If we just updated ourselves, update the internal cached copy .then((user) => {
if (user._id === this.user._id) { // If we just updated ourselves, update the internal cached copy
this.user = user if (user._id === this.user._id) {
this.emit('login') this.user = user
} this.emit("login")
resolve(user) }
}).catch((reason) => { resolve(user)
reject(reason) })
}) .catch((reason) => {
reject(reason)
})
}) })
} }
deleteUser(_id) { deleteUser(_id) {
return this.delete('/users/' + _id) return this.delete("/users/" + _id)
} }
enterRoom(roomName) { enterRoom(roomName) {
return this.put('/users/enter-room/' + (roomName || '')) return this.put("/users/enter-room/" + (roomName || ""))
} }
leaveRoom() { leaveRoom() {
return this.put('/users/leave-room') return this.put("/users/leave-room")
} }
getWorkItem(_id) { getWorkItem(_id) {
return this.get('/workitems/' + _id) return this.get("/workitems/" + _id)
} }
listWorkItems() { listWorkItems() {
return this.get('/workitems') return this.get("/workitems")
} }
createWorkItem(workItem) { createWorkItem(workItem) {
return this.post('/workitems', workItem) return this.post("/workitems", workItem)
} }
updateWorkItem(workItem) { updateWorkItem(workItem) {
return this.put('/workitems', workItem) return this.put("/workitems", workItem)
} }
deleteWorkItem(_id) { deleteWorkItem(_id) {
return this.delete('/workitems/' + _id) return this.delete("/workitems/" + _id)
} }
upload(file, progressCallback) { upload(file, progressCallback) {
@@ -285,36 +303,40 @@ class API extends EventEmitter {
const buffer = e.target.result const buffer = e.target.result
const bytesRead = buffer.byteLength const bytesRead = buffer.byteLength
this.post('/assets/upload/' + uploadId, buffer, { this.post("/assets/upload/" + uploadId, buffer, {
binary: { offset: chunk * chunkSize, length: bytesRead } binary: { offset: chunk * chunkSize, length: bytesRead },
}).then((uploadData) => {
chunk++
if (!progressCallback(uploadData)) {
return Promise.reject(new Error('Upload was canceled'))
}
if (chunk < numberOfChunks) {
let start = chunk * chunkSize
let end = Math.min(fileSize, start + chunkSize)
reader.readAsArrayBuffer(file.slice(start, end))
} else {
resolve(uploadData)
}
}).catch((err) => {
reject(err)
}) })
.then((uploadData) => {
chunk++
if (!progressCallback(uploadData)) {
return Promise.reject(new Error("Upload was canceled"))
}
if (chunk < numberOfChunks) {
let start = chunk * chunkSize
let end = Math.min(fileSize, start + chunkSize)
reader.readAsArrayBuffer(file.slice(start, end))
} else {
resolve(uploadData)
}
})
.catch((err) => {
reject(err)
})
} }
this.post('/assets/upload', { this.post("/assets/upload", {
fileName: file.name, fileName: file.name,
fileSize, fileSize,
contentType: file.type, contentType: file.type,
numberOfChunks numberOfChunks,
}).then((uploadData) => {
uploadId = uploadData.uploadId
reader.readAsArrayBuffer(file.slice(0, chunkSize))
}).catch((err) => {
reject(err)
}) })
.then((uploadData) => {
uploadId = uploadData.uploadId
reader.readAsArrayBuffer(file.slice(0, chunkSize))
})
.catch((err) => {
reject(err)
})
}) })
} }
} }

View File

@@ -1,7 +1,7 @@
import React, { Fragment, Component } from 'react' import React, { Fragment, Component } from "react"
import { Route, Redirect } from 'react-router-native' import { Route, Redirect } from "react-router-native"
export const DefaultRoute = () => { export const DefaultRoute = () => {
// NOTE: When working on the app, change this to the page you are working on // NOTE: When working on the app, change this to the page you are working on
return <Route render={() => (<Redirect to={'/workitemlist'} />)} /> return <Route render={() => <Redirect to={"/workitemlist"} />} />
} }

View File

@@ -1,4 +1,4 @@
import React from 'react' import React from "react"
import { import {
StyleSheet, StyleSheet,
Text, Text,
@@ -7,32 +7,80 @@ import {
Image, Image,
View, View,
TouchableOpacity, TouchableOpacity,
} from 'react-native' } from "react-native"
import MapView, { Marker } from 'react-native-maps' import MapView, { Marker } from "react-native-maps"
import { Icon, Header } from '../ui' import { Icon, Header } from "../ui"
import { api } from '../API' import { api } from "../API"
import autobind from 'autobind-decorator' import autobind from "autobind-decorator"
import pinImage from './images/pin.png' import pinImage from "./images/pin.png"
import { ifIphoneX } from 'react-native-iphone-x-helper' import { ifIphoneX } from "react-native-iphone-x-helper"
const styles = StyleSheet.create({ const styles = StyleSheet.create({
container: { container: {
flex: 1, flex: 1,
backgroundColor: '#FFFFFF', backgroundColor: "#FFFFFF",
alignItems: 'flex-start', alignItems: "flex-start",
justifyContent: 'flex-start', justifyContent: "flex-start",
} },
}) })
const data = [ const data = [
{key: '1', title: 'Remove Animal Carcass', location: 'Ossington Ave. | 0.2 mi.', state: 'planned', latlng: { latitude: 43.653226, longitude: -79.383184 } }, {
{key: '2', title: 'Fix sign post', location: 'Alexandre St. | 0.7 mi.', state: 'open', latlng: { latitude: 43.648118, longitude: 79.392636 }}, key: "1",
{key: '3', title: 'Overflowing trash', location: 'Bay St. | 0.8 mi.', state: 'open', latlng: { latitude: 43.640168, longitude: -79.409373 }}, title: "Remove Animal Carcass",
{key: '4', title: 'Leaking water pipe', location: 'Bloor St. | 1.2 mi.', state: 'planned', latlng: { latitude: 43.633110, longitude: -79.415880 }}, location: "Ossington Ave. | 0.2 mi.",
{key: '5', title: 'Tree branch in road', location: 'Blue Jays Way | 2.2 mi.', state: 'open', latlng: { latitude: 43.653526, longitude: -79.361385 }}, state: "planned",
{key: '6', title: 'Washing machine on sidewalk', location: 'Christie St. | 3.0 mi.', state: 'open', latlng: { latitude: 43.663870, longitude: -79.383705 }}, latlng: { latitude: 43.653226, longitude: -79.383184 },
{key: '7', title: 'Dead moose', location: 'Cummer Ave. | 4.2 mi.', state: 'open', latlng: { latitude: 43.659166, longitude: -79.391350 }}, },
{key: '8', title: 'Glass in street', location: 'Danforth Ave. | 4.7 mi.', state: 'open', latlng: { latitude: 43.663538, longitude: -79.423212 }}, {
key: "2",
title: "Fix sign post",
location: "Alexandre St. | 0.7 mi.",
state: "open",
latlng: { latitude: 43.648118, longitude: 79.392636 },
},
{
key: "3",
title: "Overflowing trash",
location: "Bay St. | 0.8 mi.",
state: "open",
latlng: { latitude: 43.640168, longitude: -79.409373 },
},
{
key: "4",
title: "Leaking water pipe",
location: "Bloor St. | 1.2 mi.",
state: "planned",
latlng: { latitude: 43.63311, longitude: -79.41588 },
},
{
key: "5",
title: "Tree branch in road",
location: "Blue Jays Way | 2.2 mi.",
state: "open",
latlng: { latitude: 43.653526, longitude: -79.361385 },
},
{
key: "6",
title: "Washing machine on sidewalk",
location: "Christie St. | 3.0 mi.",
state: "open",
latlng: { latitude: 43.66387, longitude: -79.383705 },
},
{
key: "7",
title: "Dead moose",
location: "Cummer Ave. | 4.2 mi.",
state: "open",
latlng: { latitude: 43.659166, longitude: -79.39135 },
},
{
key: "8",
title: "Glass in street",
location: "Danforth Ave. | 4.7 mi.",
state: "open",
latlng: { latitude: 43.663538, longitude: -79.423212 },
},
] ]
export class Home extends React.Component { export class Home extends React.Component {
@@ -43,42 +91,45 @@ export class Home extends React.Component {
@autobind @autobind
_handleNavigatorEvent(event) { _handleNavigatorEvent(event) {
switch (event.id) { switch (event.id) {
case 'logout': case "logout":
api.logout().then(() => { api.logout().then(() => {
this.props.history.replace('/login') this.props.history.replace("/login")
}) })
break break
case 'viewer': case "viewer":
this.props.push('/viewer') this.props.push("/viewer")
break break
} }
} }
@autobind @autobind
handleWorkItemsListPress() { handleWorkItemsListPress() {
this.props.history.push('/workitemlist') this.props.history.push("/workitemlist")
} }
@autobind @autobind
handleItemSelect(item, index) { handleItemSelect(item, index) {
this.props.history.push('/activity') this.props.history.push("/activity")
} }
@autobind @autobind
handleLogoutPress() { handleLogoutPress() {
this.props.history.replace('/logout') this.props.history.replace("/logout")
} }
@autobind @autobind
handleGlassesPress() { handleGlassesPress() {
this.props.history.push('/arviewer') this.props.history.push("/arviewer")
} }
@autobind @autobind
handleMyLocationPress() { handleMyLocationPress() {
navigator.geolocation.getCurrentPosition((info) => { navigator.geolocation.getCurrentPosition((info) => {
if (this.map) { if (this.map) {
this.map.animateToCoordinate({latitude: info.coords.latitude, longitude: info.coords.longitude}) this.map.animateToCoordinate({
latitude: info.coords.latitude,
longitude: info.coords.longitude,
})
} }
}) })
} }
@@ -87,13 +138,17 @@ export class Home extends React.Component {
return ( return (
<View style={styles.container}> <View style={styles.container}>
<Header <Header
title='Work Item Map' title="Work Item Map"
leftButton={{ icon: 'logout', onPress: this.handleLogoutPress }} leftButton={{ icon: "logout", onPress: this.handleLogoutPress }}
rightButton={{ icon: 'glasses', onPress: this.handleGlassesPress }} /> rightButton={{ icon: "glasses", onPress: this.handleGlassesPress }}
/>
<MapView <MapView
ref={ref => { this.map = ref }} ref={(ref) => {
this.map = ref
}}
style={{ style={{
width: '100%', height: '50%', width: "100%",
height: "50%",
}} }}
showsUserLocation showsUserLocation
showsBuildings={false} showsBuildings={false}
@@ -106,58 +161,97 @@ export class Home extends React.Component {
latitudeDelta: 0.0922, latitudeDelta: 0.0922,
longitudeDelta: 0.0922, longitudeDelta: 0.0922,
}}> }}>
{ {data.map((marker) => (
data.map(marker => ( <Marker
<Marker key={marker.key}
key={marker.key} coordinate={marker.latlng}
coordinate={marker.latlng} title={marker.title}
title={marker.title} description={marker.location}
description={marker.location} image={pinImage}
image={pinImage} anchor={{ x: 0.5, y: 1.0 }}
anchor={{x: 0.5, y: 1.0}} /> />
)) ))}
}
</MapView> </MapView>
<View style={{ flexDirection: 'row', alignItems: 'center', width: '100%', height: 40, backgroundColor: '#F4F4F4' }}> <View
<Icon name='search' size={16} style={{marginLeft: 10, marginRight: 5, tintColor: 'gray' }} /> style={{
<TextInput style={{ flexGrow: 1, height: '100%' }} placeholder='Search' /> flexDirection: "row",
<Icon name='cancel' size={16} style={{marginLeft: 5, marginRight: 10, tintColor: 'gray' }} /> alignItems: "center",
width: "100%",
height: 40,
backgroundColor: "#F4F4F4",
}}>
<Icon
name="search"
size={16}
style={{ marginLeft: 10, marginRight: 5, tintColor: "gray" }}
/>
<TextInput
style={{ flexGrow: 1, height: "100%" }}
placeholder="Search"
/>
<Icon
name="cancel"
size={16}
style={{ marginLeft: 5, marginRight: 10, tintColor: "gray" }}
/>
</View> </View>
<FlatList <FlatList
style={{ width: '100%', flexGrow: 1 }} style={{ width: "100%", flexGrow: 1 }}
data={data} data={data}
renderItem={({item, index}) => { renderItem={({ item, index }) => {
return ( return (
<View style={{ flexDirection: 'row', height: 50 }}> <View style={{ flexDirection: "row", height: 50 }}>
<Text style={{ fontSize: 8, width: 45, marginLeft: 5, alignSelf: 'center' }}>{item.state.toUpperCase()}</Text> <Text
<View style={{ width: '75%', flexDirection: 'column' }}> style={{
fontSize: 8,
width: 45,
marginLeft: 5,
alignSelf: "center",
}}>
{item.state.toUpperCase()}
</Text>
<View style={{ width: "75%", flexDirection: "column" }}>
<Text style={{ fontSize: 20 }}>{item.title}</Text> <Text style={{ fontSize: 20 }}>{item.title}</Text>
<Text style={{ fontSize: 14, color: 'gray' }}>{item.location}</Text> <Text style={{ fontSize: 14, color: "gray" }}>
{item.location}
</Text>
</View> </View>
<TouchableOpacity style={{ alignSelf: 'center' }} onPress={() => (this.handleItemSelect(item, index))} > <TouchableOpacity
<Icon name='rightArrow' size={16} /> style={{ alignSelf: "center" }}
onPress={() => this.handleItemSelect(item, index)}>
<Icon name="rightArrow" size={16} />
</TouchableOpacity> </TouchableOpacity>
</View> </View>
) )
}} /> }}
<View style={{ />
flexDirection: 'row', <View
justifyContent: 'space-between', style={{
alignItems: 'center', flexDirection: "row",
width: '100%', justifyContent: "space-between",
height: 45, alignItems: "center",
backgroundColor: '#F4F4F4', width: "100%",
...ifIphoneX({ marginBottom: 22 }, {}), height: 45,
}}> backgroundColor: "#F4F4F4",
...ifIphoneX({ marginBottom: 22 }, {}),
}}>
<TouchableOpacity onPress={this.handleMyLocationPress}> <TouchableOpacity onPress={this.handleMyLocationPress}>
<Icon name='center' size={24} style={{ marginLeft: 15, tintColor: 'gray' }} /> <Icon
name="center"
size={24}
style={{ marginLeft: 15, tintColor: "gray" }}
/>
</TouchableOpacity> </TouchableOpacity>
<Text style={{ color: 'gray', fontSize: 20, }}>Hide List</Text> <Text style={{ color: "gray", fontSize: 20 }}>Hide List</Text>
<TouchableOpacity onPress={this.handleWorkItemsListPress}> <TouchableOpacity onPress={this.handleWorkItemsListPress}>
<Icon name='settings' size={24} style={{ marginRight: 15, tintColor: 'gray' }} /> <Icon
name="settings"
size={24}
style={{ marginRight: 15, tintColor: "gray" }}
/>
</TouchableOpacity> </TouchableOpacity>
</View> </View>
</View> </View>
); )
} }
} }

View File

@@ -8,7 +8,7 @@ import {
TextInput, TextInput,
KeyboardAvoidingView, KeyboardAvoidingView,
Platform, Platform,
TouchableOpacity TouchableOpacity,
} from "react-native" } from "react-native"
import MapView, { Marker } from "react-native-maps" import MapView, { Marker } from "react-native-maps"
import { FormBinder } from "react-form-binder" import { FormBinder } from "react-form-binder"
@@ -19,19 +19,21 @@ import {
Icon, Icon,
Header, Header,
PhotoButton, PhotoButton,
BoundOptionStrip BoundOptionStrip,
} from "../ui" } from "../ui"
import { MessageModal } from "../Modal" 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 KeyboardSpacer from "react-native-keyboard-spacer"
import { api } from "../API" import { api } from "../API"
import "url-search-params-polyfill"
import { workItemTypeEnum, formatLatLng, parseLatLng } from "../util"
const styles = StyleSheet.create({ const styles = StyleSheet.create({
container: { container: {
flexDirection: "column", flexDirection: "column",
flexGrow: 1, flexGrow: 1,
backgroundColor: "#DDDDDD" backgroundColor: "#DDDDDD",
}, },
panel: { panel: {
width: "94%", width: "94%",
@@ -42,60 +44,68 @@ const styles = StyleSheet.create({
shadowOffset: { width: 2, height: 2 }, shadowOffset: { width: 2, height: 2 },
shadowRadius: 2, shadowRadius: 2,
shadowOpacity: 0.5, shadowOpacity: 0.5,
padding: 10 padding: 10,
}, },
label: { label: {
fontSize: 14, fontSize: 14,
marginBottom: 4 marginBottom: 4,
} },
}) })
const workItemOptions = [
{ value: "order", text: "Work Order" },
{ value: "inspection", text: "Inspection" },
{ 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: {
noValue: true, noValue: true,
isDisabled: r => !(r.anyModified && r.allValid) isDisabled: (r) => !(r.anyModified && r.allValid),
}, },
location: { location: {
isValid: true, isValid: true,
isDisabled: true isDisabled: true,
}, },
details: { details: {
isValid: (r, v) => v !== "" isValid: (r, v) => v !== "",
}, },
workItemType: { workItemType: {
isValid: true, isValid: true,
initValue: "order", alwaysGet: true,
alwaysGet: true },
}
} }
constructor(props) { constructor(props) {
super(props) super(props)
this.state = { this.state = {
binder: new FormBinder({}, WorkItem.bindings), binder: new FormBinder({}, WorkItem.bindings),
messageModal: null messageModal: null,
}
const { search } = this.props.location
if (search) {
const id = new URLSearchParams(search).get("id")
if (id) {
api
.getWorkItem(id)
.then((workItem) => {
if (workItem) {
const [lng, lat] = workItem.location.coordinates
workItem.location = formatLatLng(lat, lng)
this.setState({
binder: new FormBinder(workItem, WorkItem.bindings),
})
}
})
.catch((err) => {
this.setState({
messageModal: {
icon: "hand",
message: "Unable to get work item details",
detail: err.message,
back: true,
},
})
})
}
} }
} }
@@ -115,36 +125,36 @@ export class WorkItem extends React.Component {
const { binder } = this.state const { binder } = this.state
let obj = binder.getModifiedFieldValues() let obj = binder.getModifiedFieldValues()
obj.location = latLngStringToPoint(obj.location) obj.location = parseLatLng(obj.location)
if (!obj._id) { if (!obj._id) {
api api
.createWorkItem(obj) .createWorkItem(obj)
.then(workItem => { .then((workItem) => {
this.handleBackPress() this.handleBackPress()
}) })
.catch(error => { .catch((error) => {
this.setState({ this.setState({
messageModal: { messageModal: {
icon: "hand", icon: "hand",
message: "Unable to create work item", message: "Unable to create work item",
detail: error.message detail: error.message,
} },
}) })
}) })
} else { } else {
api api
.updateWorkItem(obj) .updateWorkItem(obj)
.then(workItem => { .then((workItem) => {
this.handleBackPress() this.handleBackPress()
}) })
.catch(error => { .catch((error) => {
this.setState({ this.setState({
messageModal: { messageModal: {
icon: "hand", icon: "hand",
message: "Unable to update work item", message: "Unable to update work item",
detail: error.message detail: error.message,
} },
}) })
}) })
} }
@@ -152,7 +162,11 @@ export class WorkItem extends React.Component {
@autobind @autobind
handleMessageDismiss() { handleMessageDismiss() {
const back = this.state.messageModal.back
this.setState({ messageModal: null }) this.setState({ messageModal: null })
if (back) {
this.handleBackPress()
}
} }
@autobind @autobind
@@ -162,7 +176,7 @@ export class WorkItem extends React.Component {
this.setState( this.setState(
binder.updateFieldValue( binder.updateFieldValue(
"location", "location",
latLngToString(region.latitude, region.longitude) formatLatLng(region.latitude, region.longitude)
) )
) )
} }
@@ -185,7 +199,7 @@ export class WorkItem extends React.Component {
binder={binder} binder={binder}
name="workItemType" name="workItemType"
label="Work Item Type:" label="Work Item Type:"
options={workItemOptions} options={workItemTypeEnum}
/> />
</View> </View>
<View style={styles.panel}> <View style={styles.panel}>
@@ -204,17 +218,16 @@ export class WorkItem extends React.Component {
justifyContent: "center", justifyContent: "center",
width: "100%", width: "100%",
height: 400, height: 400,
marginBottom: 10 marginBottom: 10,
}} }}
zoomControlEnabled zoomControlEnabled
initialRegion={{ initialRegion={{
latitude: 43.653908, latitude: 43.653908,
longitude: -79.384293, longitude: -79.384293,
latitudeDelta: 0.0922, latitudeDelta: 0.0922,
longitudeDelta: 0.0421 longitudeDelta: 0.0421,
}} }}
onRegionChange={this.handleRegionChange} onRegionChange={this.handleRegionChange}>
>
<Icon <Icon
name="target" name="target"
size={24} size={24}
@@ -227,8 +240,7 @@ export class WorkItem extends React.Component {
<View style={styles.panel}> <View style={styles.panel}>
<Text style={styles.label}>Pictures:</Text> <Text style={styles.label}>Pictures:</Text>
<View <View
style={{ flexDirection: "row", justifyContent: "space-between" }} style={{ flexDirection: "row", justifyContent: "space-between" }}>
>
<PhotoButton /> <PhotoButton />
<PhotoButton /> <PhotoButton />
<PhotoButton /> <PhotoButton />

View File

@@ -6,122 +6,58 @@ import {
TouchableOpacity, TouchableOpacity,
Image, Image,
FlatList, FlatList,
Text Text,
} from "react-native" } from "react-native"
import { Icon, Header } from "../ui" import { Icon, Header } from "../ui"
import { MessageModal } from "../Modal" import { MessageModal } from "../Modal"
import autobind from "autobind-decorator" import autobind from "autobind-decorator"
import { SwipeListView } from "react-native-swipe-list-view" import { SwipeListView } from "react-native-swipe-list-view"
import { api } from "../API" import { api } from "../API"
import { workItemTypeEnum, formatLatLng, parseLatLng } from "../util"
const styles = StyleSheet.create({ const styles = StyleSheet.create({
container: { container: {
height: "100%", height: "100%",
width: "100%", width: "100%",
justifyContent: "flex-start", justifyContent: "flex-start",
backgroundColor: "#FFFFFF" backgroundColor: "#FFFFFF",
} },
}) })
const data = [ const workItemTypeText = workItemTypeEnum.reduce((result, item) => {
{ result[item.value] = item.text
key: "1", return result
type: "work", }, {})
location: "Ossington Ave. | 0.2 mi.",
state: "open",
latlng: { latitude: 43.653226, longitude: -79.383184 }
},
{
key: "2",
type: "inspection",
location: "Alexandre St. | 0.7 mi.",
state: "open",
latlng: { latitude: 43.648118, longitude: 79.392636 }
},
{
key: "3",
type: "complaint",
location: "Bay St. | 0.8 mi.",
state: "open",
latlng: { latitude: 43.640168, longitude: -79.409373 }
},
{
key: "4",
type: "work",
location: "Bloor St. | 1.2 mi.",
state: "open",
latlng: { latitude: 43.63311, longitude: -79.41588 }
},
{
key: "5",
type: "inspection",
location: "Blue Jays Way | 2.2 mi.",
state: "open",
latlng: { latitude: 43.653526, longitude: -79.361385 }
},
{
key: "6",
type: "complaint",
location: "Christie St. | 3.0 mi.",
state: "open",
latlng: { latitude: 43.66387, longitude: -79.383705 }
},
{
key: "7",
type: "work",
location: "Cummer Ave. | 4.2 mi.",
state: "open",
latlng: { latitude: 43.659166, longitude: -79.39135 }
},
{
key: "8",
type: "complaint",
location: "Danforth Ave. | 4.7 mi.",
state: "open",
latlng: { latitude: 43.663538, longitude: -79.423212 }
}
]
const inspectionTypes = {
work: {
title: "Work Order"
},
inspection: {
title: "Inspection"
},
complaint: {
title: "Complaint"
}
}
export class WorkItemList extends React.Component { export class WorkItemList extends React.Component {
constructor(props) { constructor(props) {
super(props) super(props)
this.state = { this.state = {
messageModal: null messageModal: null,
} }
api api
.listWorkItems() .listWorkItems()
.then(list => {}) .then((list) => {
this.setState({ listItems: list.items })
})
.catch(() => { .catch(() => {
this.setState({ this.setState({
messageModal: { messageModal: {
icon: "hand", icon: "hand",
message: "Unable to get list of work items", message: "Unable to get list of work items",
detail: error.message detail: error.message,
} },
}) })
}) })
} }
@autobind @autobind
handleItemSelect(item, index) { handleItemSelect(item, index) {
this.props.history.push("/workitem") this.props.history.push(`/workitem?id=${item._id}`)
} }
@autobind @autobind
handleItemDelete(item, index) { handleItemDelete(item, index) {}
}
@autobind @autobind
handleMessageDismiss() { handleMessageDismiss() {
@@ -145,7 +81,7 @@ export class WorkItemList extends React.Component {
} }
render() { render() {
const { messageModal } = this.state const { listItems, messageModal } = this.state
return ( return (
<View style={styles.container}> <View style={styles.container}>
@@ -160,9 +96,10 @@ export class WorkItemList extends React.Component {
width: "100%", width: "100%",
flexGrow: 1, flexGrow: 1,
paddingTop: 20, paddingTop: 20,
paddingBottom: 20 paddingBottom: 20,
}} }}
data={data} data={listItems}
keyExtractor={(item) => (item._id)}
renderItem={({ item, index }) => ( renderItem={({ item, index }) => (
<TouchableHighlight <TouchableHighlight
style={{ style={{
@@ -171,16 +108,21 @@ export class WorkItemList extends React.Component {
paddingRight: 20, paddingRight: 20,
backgroundColor: "white", backgroundColor: "white",
}} }}
underlayColor='#EEEEEE' underlayColor="#EEEEEE"
onPress={() => this.handleItemSelect(item, index)} onPress={() => this.handleItemSelect(item, index)}>
> <View
<View style={{ height: '100%', width: '100%', flexDirection: 'row' }}> style={{ height: "100%", width: "100%", flexDirection: "row" }}>
<View style={{ flexGrow: 1, flexDirection: "column", justifyContent: 'center' }}> <View
style={{
flexGrow: 1,
flexDirection: "column",
justifyContent: "center",
}}>
<Text style={{ fontSize: 20 }}> <Text style={{ fontSize: 20 }}>
{inspectionTypes[item.type].title} {workItemTypeText[item.workItemType]}
</Text> </Text>
<Text style={{ fontSize: 14, color: "gray" }}> <Text style={{ fontSize: 14, color: "gray" }}>
{item.location} {`${item.address || "..."} | ??? mi`}
</Text> </Text>
</View> </View>
<Icon <Icon
@@ -199,9 +141,14 @@ export class WorkItemList extends React.Component {
height: 50, height: 50,
backgroundColor: "red", backgroundColor: "red",
}} }}
onPress={() => this.handleItemDelete(item, index)} onPress={() => this.handleItemDelete(item, index)}>
> <View
<View style={{ flexDirection: 'column', justifyContent: 'center', backgroundColor: "red", width: 75 }}> style={{
flexDirection: "column",
justifyContent: "center",
backgroundColor: "red",
width: 75,
}}>
<Text style={{ fontSize: 20, alignSelf: "center" }}> <Text style={{ fontSize: 20, alignSelf: "center" }}>
Delete Delete
</Text> </Text>
@@ -209,7 +156,7 @@ export class WorkItemList extends React.Component {
</TouchableOpacity> </TouchableOpacity>
)} )}
rightOpenValue={-80} rightOpenValue={-80}
stopLeftSwipe={100} stopRightSwipe={-120}
disableRightSwipe disableRightSwipe
/> />
<MessageModal <MessageModal

View File

@@ -20,7 +20,16 @@ export class BoundInput extends React.Component {
constructor(props) { constructor(props) {
super(props) super(props)
this.state = props.binder.getFieldState(props.name)
const { name, binder } = this.props
this.state = binder.getFieldState(name)
}
componentWillReceiveProps(nextProps) {
if (nextProps.binder !== this.props.binder) {
this.setState(nextProps.binder.getFieldState(nextProps.name))
}
} }
@autobind @autobind

View File

@@ -1,15 +1,17 @@
import React from 'react' import React from "react"
import PropTypes from 'prop-types' import PropTypes from "prop-types"
import { View, Text } from 'react-native' import { View, Text } from "react-native"
import { OptionStrip } from '.' import { OptionStrip } from "."
import autobind from 'autobind-decorator' import autobind from "autobind-decorator"
export class BoundOptionStrip extends React.Component { export class BoundOptionStrip extends React.Component {
static propTypes = { static propTypes = {
name: PropTypes.string.isRequired, name: PropTypes.string.isRequired,
label: PropTypes.string, label: PropTypes.string,
binder: PropTypes.object.isRequired, binder: PropTypes.object.isRequired,
options: PropTypes.arrayOf(PropTypes.shape({ value: PropTypes.string, text: PropTypes.string })).isRequired, options: PropTypes.arrayOf(
PropTypes.shape({ value: PropTypes.string, text: PropTypes.string })
).isRequired,
} }
constructor(props) { constructor(props) {
@@ -18,13 +20,8 @@ export class BoundOptionStrip extends React.Component {
} }
@autobind @autobind
handleValueChange() { handleValueChanged(newValue) {
const { binder, name } = this.props this.setState(this.props.binder.updateFieldValue(this.props.name, newValue))
const state = binder.getFieldState(name)
if (!state.readOnly && !state.disabled) {
this.setState(binder.updateFieldValue(name, !state.value))
}
} }
componentWillReceiveProps(nextProps) { componentWillReceiveProps(nextProps) {
@@ -38,14 +35,19 @@ export class BoundOptionStrip extends React.Component {
const { visible, disabled, value } = this.state const { visible, disabled, value } = this.state
return ( return (
<View style={{ <View
display: visible ? 'flex' : 'none', style={{
flexDirection: 'column', display: visible ? "flex" : "none",
flexDirection: "column",
}}> }}>
<Text style={{ color: 'black', fontSize: 14, marginBottom: 5 }}>{label}</Text> <Text style={{ color: "black", fontSize: 14, marginBottom: 5 }}>
{label}
</Text>
{/* TODO: Handle visible, disabled & read-only */}
<OptionStrip <OptionStrip
value={value} value={value}
options={options} options={options}
onValueChanged={this.handleValueChanged}
/> />
</View> </View>
) )

View File

@@ -0,0 +1,68 @@
import React from "react"
import PropTypes from "prop-types"
import { View, Text, TouchableHighlight } from "react-native"
import autobind from "autobind-decorator"
export class BoundText extends React.Component {
static propTypes = {
name: PropTypes.string.isRequired,
label: PropTypes.string,
value: PropTypes.string,
binder: PropTypes.object.isRequired,
}
constructor(props) {
super(props)
const { name, binder } = this.props
binder.addListener(name, this.updateValue)
this.state = binder.getFieldState(name)
}
@autobind
updateValue(e) {
this.setState(e.state)
}
componentWillUnmount() {
this.props.binder.removeListener(this.props.name, this.updateValue)
}
componentWillReceiveProps(nextProps) {
if (nextProps.binder !== this.props.binder) {
this.props.binder.removeListener(this.props.name, this.updateValue)
nextProps.binder.addListener(nextProps.name, this.updateValue)
this.setState(nextProps.binder.getFieldState(nextProps.name))
}
}
render() {
const { name, label, value } = this.props
const { visible, disabled } = this.state
if (!visible) {
return null
}
return (
<View>
<Text style={{ color: "black", fontSize: 14, marginBottom: 5 }}>
{label}
</Text>
<Text
style={{
paddingLeft: 5,
paddingRight: 5,
borderColor: "gray",
borderWidth: 1,
fontSize: 16,
paddingTop: 7,
paddingBottom: 7,
}}>
{value}
</Text>
</View>
)
}
}

View File

@@ -1,30 +1,40 @@
import React, { Component } from 'react' import React, { Component } from "react"
import { View, Text, TouchableHighlight } from 'react-native' import { View, Text, TouchableHighlight } from "react-native"
import PropTypes from 'prop-types' import PropTypes from "prop-types"
import autobind from 'autobind-decorator'; import autobind from "autobind-decorator"
export class OptionStrip extends Component { export class OptionStrip extends Component {
static propTypes = { static propTypes = {
options: PropTypes.arrayOf(PropTypes.shape({ value: PropTypes.string, text: PropTypes.string })).isRequired, options: PropTypes.arrayOf(
value: PropTypes.string.isRequired, PropTypes.shape({ value: PropTypes.string, text: PropTypes.string })
).isRequired,
value: PropTypes.string,
onValueChanged: PropTypes.func, onValueChanged: PropTypes.func,
} }
constructor(props) { constructor(props) {
super(props) super(props)
this.state = { this.state = {
selectedOption: this.getSelectedOption(props.options, props.value) selectedOption: this.getSelectedOption(props.options, props.value),
} }
} }
@autobind @autobind
getSelectedOption(options, value) { getSelectedOption(options, value) {
return options.find((option) => (value === option.value)) || options[0] return options.find((option) => value === option.value) || null
} }
componentWillReceiveProps(newProps) { componentWillReceiveProps(newProps) {
if (newProps.options !== this.props.options || newProps.value !== this.props.value) { if (
this.setState({ selectedIndex: this.getSelectedIndex(newProps.options, newProps.value)}) newProps.options !== this.props.options ||
newProps.value !== this.props.value
) {
this.setState({
selectedOption: this.getSelectedOption(
newProps.options,
newProps.value
),
})
} }
} }
@@ -42,25 +52,50 @@ export class OptionStrip extends Component {
const { style, options, value } = this.props const { style, options, value } = this.props
const { selectedOption } = this.state const { selectedOption } = this.state
// TODO: Handle visible, disabled & read-only
return ( return (
<View style={{ flexDirection: 'row' }}> <View style={{ flexDirection: "row" }}>
{options.map((option, index) => ( {options.map((option, index) => (
<TouchableHighlight <TouchableHighlight
key={index} key={index}
underlayColor='#3BB0FD' underlayColor="#3BB0FD"
style={[ style={[
{ flexGrow: 1, flexBasis: 0, height: 40 }, { flexGrow: 1, flexBasis: 0, height: 40 },
option === selectedOption && { backgroundColor: '#3BB0FD' }, option === selectedOption && { backgroundColor: "#3BB0FD" },
index === 0 && { borderTopLeftRadius: 6, borderBottomLeftRadius: 6 }, index === 0 && {
index === options.length - 1 && { borderTopRightRadius: 6, borderBottomRightRadius: 6 } borderTopLeftRadius: 6,
borderBottomLeftRadius: 6,
},
index === options.length - 1 && {
borderTopRightRadius: 6,
borderBottomRightRadius: 6,
},
]} ]}
onPress={() => this.handlePress(option)}> onPress={() => this.handlePress(option)}>
<View style={[ <View
{ flex: 1, justifyContent: 'center', borderTopWidth: 1, borderBottomWidth: 1, borderLeftWidth: 1, borderColor: 'black' }, style={[
index === 0 && { borderTopLeftRadius: 6, borderBottomLeftRadius: 6 }, {
index === options.length - 1 && { borderRightWidth: 1, borderTopRightRadius: 6, borderBottomRightRadius: 6 } flex: 1,
]}> justifyContent: "center",
<Text style={{ alignSelf: 'center', color: 'black' }}>{option.text}</Text> borderTopWidth: 1,
borderBottomWidth: 1,
borderLeftWidth: 1,
borderColor: "black",
},
index === 0 && {
borderTopLeftRadius: 6,
borderBottomLeftRadius: 6,
},
index === options.length - 1 && {
borderRightWidth: 1,
borderTopRightRadius: 6,
borderBottomRightRadius: 6,
},
]}>
<Text style={{ alignSelf: "center", color: "black" }}>
{option.text}
</Text>
</View> </View>
</TouchableHighlight> </TouchableHighlight>
))} ))}

46
mobile/src/util.js Normal file
View File

@@ -0,0 +1,46 @@
export const geoDistance = (lat1, lng1, lat2, lng2, unit) => {
var radlat1 = Math.PI * lat1 / 180
var radlat2 = Math.PI * lat2 / 180
var theta = lng1 - lng2
var radtheta = Math.PI * theta / 180
var dist =
Math.sin(radlat1) * Math.sin(radlat2) +
Math.cos(radlat1) * Math.cos(radlat2) * Math.cos(radtheta)
dist = Math.acos(dist)
dist = dist * 180 / Math.PI
dist = dist * 60 * 1.1515
if (unit == "K") {
dist = dist * 1.609344
} else if (unit == "N") {
dist = dist * 0.8684
}
return dist
}
export const formatLatLng = (lat, lng) =>
`${Math.abs(lat).toFixed(4)}°${lat >= 0 ? "N" : "S"}, ${Math.abs(lng).toFixed(
4
)}°${lng >= 0 ? "E" : "W"}`
export const parseLatLng = (str) => {
const [lat, lng] = str.split(", ")
return {
type: "Point",
coordinates: [
parseFloat(
(lng[lng.length - 1] === "W" ? "-" : "") +
lng.substring(0, lng.length - 2)
),
parseFloat(
(lat[lat.length - 1] === "S" ? "-" : "") +
lat.substring(0, lat.length - 2)
),
],
}
}
export const workItemTypeEnum = [
{ value: "order", text: "Work Order" },
{ value: "inspection", text: "Inspection" },
{ value: "complaint", text: "Complaint" },
]

View File

@@ -1,6 +1,6 @@
import passport from 'passport' import passport from "passport"
import createError from 'http-errors' import createError from "http-errors"
import autobind from 'autobind-decorator' import autobind from "autobind-decorator"
@autobind @autobind
export class WorkItemRoutes { export class WorkItemRoutes {
@@ -12,14 +12,31 @@ export class WorkItemRoutes {
this.mq = container.mq this.mq = container.mq
this.ws = container.ws this.ws = container.ws
app.route('/workitems') app
.get(passport.authenticate('bearer', { session: false }), this.listWorkItems) .route("/workitems")
.post(passport.authenticate('bearer', { session: false }), this.createWorkItem) .get(
.put(passport.authenticate('bearer', { session: false }), this.updateWorkItem) passport.authenticate("bearer", { session: false }),
this.listWorkItems
)
.post(
passport.authenticate("bearer", { session: false }),
this.createWorkItem
)
.put(
passport.authenticate("bearer", { session: false }),
this.updateWorkItem
)
app.route('/workitems/:_id([a-f0-9]{24})') app
.get(passport.authenticate('bearer', { session: false }), this.getWorkItem) .route("/workitems/:_id([a-f0-9]{24})")
.delete(passport.authenticate('bearer', { session: false }), this.deleteWorkItem) .get(
passport.authenticate("bearer", { session: false }),
this.getWorkItem
)
.delete(
passport.authenticate("bearer", { session: false }),
this.deleteWorkItem
)
} }
async listWorkItems(req, res, next) { async listWorkItems(req, res, next) {
@@ -33,25 +50,29 @@ export class WorkItemRoutes {
const total = await WorkItem.count({}) const total = await WorkItem.count({})
let workItems = [] let workItems = []
let cursor = WorkItem.find(query).limit(limit).skip(skip).cursor().map((doc) => { let cursor = WorkItem.find(query)
return doc.toClient(partial) .limit(limit)
}) .skip(skip)
.cursor()
.map((doc) => {
return doc.toClient(partial)
})
cursor.on('data', (doc) => { cursor.on("data", (doc) => {
workItems.push(doc) workItems.push(doc)
}) })
cursor.on('end', () => { cursor.on("end", () => {
res.json({ res.json({
total: total, total: total,
offset: skip, offset: skip,
count: workItems.length, count: workItems.length,
items: workItems items: workItems,
}) })
}) })
cursor.on('error', (err) => { cursor.on("error", (err) => {
throw createError.InternalServerError(err.message) throw createError.InternalServerError(err.message)
}) })
} catch(err) { } catch (err) {
if (err instanceof createError.HttpError) { if (err instanceof createError.HttpError) {
next(err) next(err)
} else { } else {
@@ -61,7 +82,6 @@ export class WorkItemRoutes {
} }
async createWorkItem(req, res, next) { async createWorkItem(req, res, next) {
try { try {
const isAdmin = req.user.administrator const isAdmin = req.user.administrator
@@ -77,7 +97,7 @@ export class WorkItemRoutes {
const newWorkItem = await workItem.save() const newWorkItem = await workItem.save()
res.json(newWorkItem.toClient()) res.json(newWorkItem.toClient())
} catch(err) { } catch (err) {
if (err instanceof createError.HttpError) { if (err instanceof createError.HttpError) {
next(err) next(err)
} else { } else {
@@ -96,7 +116,7 @@ export class WorkItemRoutes {
// Do this here because Mongoose will add it automatically otherwise // Do this here because Mongoose will add it automatically otherwise
if (!req.body._id) { if (!req.body._id) {
throw createError.BadRequest('No _id given in body') throw createError.BadRequest("No _id given in body")
} }
let WorkItem = this.db.WorkItem let WorkItem = this.db.WorkItem
@@ -105,13 +125,15 @@ export class WorkItemRoutes {
try { try {
workItemUpdates = new WorkItem(req.body) workItemUpdates = new WorkItem(req.body)
} catch (err) { } catch (err) {
throw createError.BadRequest('Invalid data') throw createError.BadRequest("Invalid data")
} }
const foundWorkItem = await WorkItem.findById(workItemUpdates._id) const foundWorkItem = await WorkItem.findById(workItemUpdates._id)
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)
@@ -119,7 +141,7 @@ export class WorkItemRoutes {
const savedWorkItem = await foundWorkItem.save() const savedWorkItem = await foundWorkItem.save()
res.json(savedWorkItem.toClient()) res.json(savedWorkItem.toClient())
} catch(err) { } catch (err) {
if (err instanceof createError.HttpError) { if (err instanceof createError.HttpError) {
next(err) next(err)
} else { } else {
@@ -139,7 +161,7 @@ export class WorkItemRoutes {
} }
res.json(workItem.toClient()) res.json(workItem.toClient())
} catch(err) { } catch (err) {
if (err instanceof createError.HttpError) { if (err instanceof createError.HttpError) {
next(err) next(err)
} else { } else {
@@ -165,7 +187,7 @@ export class WorkItemRoutes {
} }
res.json({}) res.json({})
} catch(err) { } catch (err) {
if (err instanceof createError.HttpError) { if (err instanceof createError.HttpError) {
next(err) next(err)
} else { } else {