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,
"arrowParens": "always",
"trailingComma": "es5",
"jsxBracketSameLine": true,
}

View File

@@ -7038,6 +7038,11 @@
"resolved": "https://registry.npmjs.org/urix/-/urix-0.1.0.tgz",
"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": {
"version": "3.1.0",
"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-router-native": "^4.2.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 io from 'socket.io-client'
import { AsyncStorage } from 'react-native'
import EventEmitter from "eventemitter3"
import io from "socket.io-client"
import { AsyncStorage } from "react-native"
const authTokenName = 'AuthToken'
const authTokenName = "AuthToken"
let baseURL = null
let apiPath = null
//
if (__DEV__) {
const localIPAddr = process.env.LOCAL_IP_ADDR
baseURL = `http://${localIPAddr || 'localhost'}:3001`
apiPath = ''
baseURL = `http://${localIPAddr || "localhost"}:3001`
apiPath = ""
} else {
baseURL = 'https://dar.kss.us.com'
apiPath = '/api'
baseURL = "https://dar.kss.us.com"
apiPath = "/api"
}
class NetworkError extends Error {
constructor(message) {
super(message)
this.name = this.constructor.name
if (typeof Error.captureStackTrace === 'function') {
if (typeof Error.captureStackTrace === "function") {
Error.captureStackTrace(this, this.constructor)
} else {
this.stack = (new Error(message)).stack
this.stack = new Error(message).stack
}
}
}
class APIError extends Error {
constructor(status, message) {
super(message || '')
super(message || "")
this.status = status || 500
this.name = this.constructor.name
if (typeof Error.captureStackTrace === 'function') {
if (typeof Error.captureStackTrace === "function") {
Error.captureStackTrace(this, this.constructor)
} else {
this.stack = (new Error(message)).stack
this.stack = new Error(message).stack
}
}
}
@@ -46,39 +46,42 @@ class API extends EventEmitter {
super()
this.user = { pending: true }
AsyncStorage.getItem(authTokenName).then((token) => {
AsyncStorage.getItem(authTokenName)
.then((token) => {
if (!token) {
return Promise.reject()
}
this.token = token
return this.who()
}).then((user) => {
})
.then((user) => {
this.user = user
this.connectSocket()
this.emit('login')
}).catch((err) => {
this.emit("login")
})
.catch((err) => {
console.error(err)
AsyncStorage.removeItem(authTokenName)
this.token = null
this.user = {}
this.socket = null
this.emit('logout')
this.emit("logout")
})
}
connectSocket() {
this.socket = io(baseURL, {
path: apiPath + '/socketio',
path: apiPath + "/socketio",
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
this.socket = null
})
this.socket.on('notify', (message) => {
this.socket.on("notify", (message) => {
const { eventName, eventData } = message
// 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) {
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) {
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 {
return null
}
}
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) {
return params ? '?' + Object.keys(params).map((key) => (
[key, params[key]].map(encodeURIComponent).join('=')
)).join('&') : ''
return params
? "?" +
Object.keys(params)
.map((key) => [key, params[key]].map(encodeURIComponent).join("="))
.join("&")
: ""
}
request(method, path, requestBody, requestOptions) {
@@ -132,29 +140,34 @@ class API extends EventEmitter {
var promise = new Promise((resolve, reject) => {
let fetchOptions = {
method: method,
mode: 'cors',
cache: 'no-store'
mode: "cors",
cache: "no-store",
}
let headers = new Headers()
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) {
headers.set('Content-Type', 'application/octet-stream')
headers.set('Content-Length', requestOptions.binary.length)
headers.set('Range', 'byte ' + requestOptions.binary.offset)
headers.set("Content-Type", "application/octet-stream")
headers.set("Content-Length", requestOptions.binary.length)
headers.set("Range", "byte " + requestOptions.binary.offset)
fetchOptions.body = requestBody
} else {
headers.set('Content-Type', 'application/json')
headers.set("Content-Type", "application/json")
fetchOptions.body = JSON.stringify(requestBody)
}
}
fetchOptions.headers = headers
fetch(this.apiURL + path, fetchOptions).then((res) => {
return Promise.all([ Promise.resolve(res), (requestOptions.binary && method === 'GET') ? res.blob() : res.json() ])
}).then((arr) => {
let [ res, responseBody ] = arr
fetch(this.apiURL + path, fetchOptions)
.then((res) => {
return Promise.all([
Promise.resolve(res),
requestOptions.binary && method === "GET" ? res.blob() : res.json(),
])
})
.then((arr) => {
let [res, responseBody] = arr
if (res.ok) {
if (requestOptions.wantHeaders) {
resolve({ body: responseBody, headers: res.headers })
@@ -164,7 +177,8 @@ class API extends EventEmitter {
} else {
reject(new APIError(res.status, responseBody.message))
}
}).catch((error) => {
})
.catch((error) => {
reject(new NetworkError(error.message))
})
})
@@ -172,27 +186,28 @@ class API extends EventEmitter {
}
post(path, requestBody, options) {
return this.request('POST', path, requestBody, options)
return this.request("POST", path, requestBody, options)
}
put(path, requestBody, options) {
return this.request('PUT', path, requestBody, options)
return this.request("PUT", path, requestBody, options)
}
get(path, options) {
return this.request('GET', path, options)
return this.request("GET", path, options)
}
delete(path, options) {
return this.request('DELETE', path, options)
return this.request("DELETE", path, options)
}
login(email, password, remember) {
return new Promise((resolve, reject) => {
this.post('/auth/login', { email, password }, { wantHeaders: true }).then((response) => {
this.post("/auth/login", { email, password }, { wantHeaders: true })
.then((response) => {
// Save bearer token for later use
const authValue = response.headers.get('Authorization')
const [ scheme, token ] = authValue.split(' ')
const authValue = response.headers.get("Authorization")
const [scheme, token] = authValue.split(" ")
if (scheme !== 'Bearer' || !token) {
reject(new APIError('Unexpected Authorization scheme or token'))
if (scheme !== "Bearer" || !token) {
reject(new APIError("Unexpected Authorization scheme or token"))
}
if (remember) {
@@ -201,9 +216,10 @@ class API extends EventEmitter {
this.token = token
this.user = response.body
this.connectSocket()
this.emit('login')
this.emit("login")
resolve(response.body)
}).catch((err) => {
})
.catch((err) => {
reject(err)
})
})
@@ -215,61 +231,63 @@ class API extends EventEmitter {
this.token = null
this.user = {}
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() {
return this.get('/auth/who')
return this.get("/auth/who")
}
getUser(_id) {
return this.get('/users/' + _id)
return this.get("/users/" + _id)
}
listUsers() {
return this.get('/users')
return this.get("/users")
}
createUser(user) {
return this.post('/users', user)
return this.post("/users", user)
}
updateUser(user) {
return new Promise((resolve, reject) => {
this.put('/users', user).then((user) => {
this.put("/users", user)
.then((user) => {
// If we just updated ourselves, update the internal cached copy
if (user._id === this.user._id) {
this.user = user
this.emit('login')
this.emit("login")
}
resolve(user)
}).catch((reason) => {
})
.catch((reason) => {
reject(reason)
})
})
}
deleteUser(_id) {
return this.delete('/users/' + _id)
return this.delete("/users/" + _id)
}
enterRoom(roomName) {
return this.put('/users/enter-room/' + (roomName || ''))
return this.put("/users/enter-room/" + (roomName || ""))
}
leaveRoom() {
return this.put('/users/leave-room')
return this.put("/users/leave-room")
}
getWorkItem(_id) {
return this.get('/workitems/' + _id)
return this.get("/workitems/" + _id)
}
listWorkItems() {
return this.get('/workitems')
return this.get("/workitems")
}
createWorkItem(workItem) {
return this.post('/workitems', workItem)
return this.post("/workitems", workItem)
}
updateWorkItem(workItem) {
return this.put('/workitems', workItem)
return this.put("/workitems", workItem)
}
deleteWorkItem(_id) {
return this.delete('/workitems/' + _id)
return this.delete("/workitems/" + _id)
}
upload(file, progressCallback) {
@@ -285,12 +303,13 @@ class API extends EventEmitter {
const buffer = e.target.result
const bytesRead = buffer.byteLength
this.post('/assets/upload/' + uploadId, buffer, {
binary: { offset: chunk * chunkSize, length: bytesRead }
}).then((uploadData) => {
this.post("/assets/upload/" + uploadId, buffer, {
binary: { offset: chunk * chunkSize, length: bytesRead },
})
.then((uploadData) => {
chunk++
if (!progressCallback(uploadData)) {
return Promise.reject(new Error('Upload was canceled'))
return Promise.reject(new Error("Upload was canceled"))
}
if (chunk < numberOfChunks) {
let start = chunk * chunkSize
@@ -299,20 +318,23 @@ class API extends EventEmitter {
} else {
resolve(uploadData)
}
}).catch((err) => {
})
.catch((err) => {
reject(err)
})
}
this.post('/assets/upload', {
this.post("/assets/upload", {
fileName: file.name,
fileSize,
contentType: file.type,
numberOfChunks
}).then((uploadData) => {
numberOfChunks,
})
.then((uploadData) => {
uploadId = uploadData.uploadId
reader.readAsArrayBuffer(file.slice(0, chunkSize))
}).catch((err) => {
})
.catch((err) => {
reject(err)
})
})

View File

@@ -1,7 +1,7 @@
import React, { Fragment, Component } from 'react'
import { Route, Redirect } from 'react-router-native'
import React, { Fragment, Component } from "react"
import { Route, Redirect } from "react-router-native"
export const DefaultRoute = () => {
// 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 {
StyleSheet,
Text,
@@ -7,32 +7,80 @@ import {
Image,
View,
TouchableOpacity,
} from 'react-native'
import MapView, { Marker } from 'react-native-maps'
import { Icon, Header } from '../ui'
import { api } from '../API'
import autobind from 'autobind-decorator'
import pinImage from './images/pin.png'
import { ifIphoneX } from 'react-native-iphone-x-helper'
} from "react-native"
import MapView, { Marker } from "react-native-maps"
import { Icon, Header } from "../ui"
import { api } from "../API"
import autobind from "autobind-decorator"
import pinImage from "./images/pin.png"
import { ifIphoneX } from "react-native-iphone-x-helper"
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#FFFFFF',
alignItems: 'flex-start',
justifyContent: 'flex-start',
}
backgroundColor: "#FFFFFF",
alignItems: "flex-start",
justifyContent: "flex-start",
},
})
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: '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.633110, longitude: -79.415880 }},
{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.663870, longitude: -79.383705 }},
{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: "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: "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 {
@@ -43,42 +91,45 @@ export class Home extends React.Component {
@autobind
_handleNavigatorEvent(event) {
switch (event.id) {
case 'logout':
case "logout":
api.logout().then(() => {
this.props.history.replace('/login')
this.props.history.replace("/login")
})
break
case 'viewer':
this.props.push('/viewer')
case "viewer":
this.props.push("/viewer")
break
}
}
@autobind
handleWorkItemsListPress() {
this.props.history.push('/workitemlist')
this.props.history.push("/workitemlist")
}
@autobind
handleItemSelect(item, index) {
this.props.history.push('/activity')
this.props.history.push("/activity")
}
@autobind
handleLogoutPress() {
this.props.history.replace('/logout')
this.props.history.replace("/logout")
}
@autobind
handleGlassesPress() {
this.props.history.push('/arviewer')
this.props.history.push("/arviewer")
}
@autobind
handleMyLocationPress() {
navigator.geolocation.getCurrentPosition((info) => {
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 (
<View style={styles.container}>
<Header
title='Work Item Map'
leftButton={{ icon: 'logout', onPress: this.handleLogoutPress }}
rightButton={{ icon: 'glasses', onPress: this.handleGlassesPress }} />
title="Work Item Map"
leftButton={{ icon: "logout", onPress: this.handleLogoutPress }}
rightButton={{ icon: "glasses", onPress: this.handleGlassesPress }}
/>
<MapView
ref={ref => { this.map = ref }}
ref={(ref) => {
this.map = ref
}}
style={{
width: '100%', height: '50%',
width: "100%",
height: "50%",
}}
showsUserLocation
showsBuildings={false}
@@ -106,58 +161,97 @@ export class Home extends React.Component {
latitudeDelta: 0.0922,
longitudeDelta: 0.0922,
}}>
{
data.map(marker => (
{data.map((marker) => (
<Marker
key={marker.key}
coordinate={marker.latlng}
title={marker.title}
description={marker.location}
image={pinImage}
anchor={{x: 0.5, y: 1.0}} />
))
}
anchor={{ x: 0.5, y: 1.0 }}
/>
))}
</MapView>
<View style={{ flexDirection: 'row', 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
style={{
flexDirection: "row",
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>
<FlatList
style={{ width: '100%', flexGrow: 1 }}
style={{ width: "100%", flexGrow: 1 }}
data={data}
renderItem={({item, index}) => {
renderItem={({ item, index }) => {
return (
<View style={{ flexDirection: 'row', height: 50 }}>
<Text style={{ fontSize: 8, width: 45, marginLeft: 5, alignSelf: 'center' }}>{item.state.toUpperCase()}</Text>
<View style={{ width: '75%', flexDirection: 'column' }}>
<View style={{ flexDirection: "row", height: 50 }}>
<Text
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: 14, color: 'gray' }}>{item.location}</Text>
<Text style={{ fontSize: 14, color: "gray" }}>
{item.location}
</Text>
</View>
<TouchableOpacity style={{ alignSelf: 'center' }} onPress={() => (this.handleItemSelect(item, index))} >
<Icon name='rightArrow' size={16} />
<TouchableOpacity
style={{ alignSelf: "center" }}
onPress={() => this.handleItemSelect(item, index)}>
<Icon name="rightArrow" size={16} />
</TouchableOpacity>
</View>
)
}} />
<View style={{
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
width: '100%',
}}
/>
<View
style={{
flexDirection: "row",
justifyContent: "space-between",
alignItems: "center",
width: "100%",
height: 45,
backgroundColor: '#F4F4F4',
backgroundColor: "#F4F4F4",
...ifIphoneX({ marginBottom: 22 }, {}),
}}>
<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>
<Text style={{ color: 'gray', fontSize: 20, }}>Hide List</Text>
<Text style={{ color: "gray", fontSize: 20 }}>Hide List</Text>
<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>
</View>
</View>
);
)
}
}

View File

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

View File

@@ -6,122 +6,58 @@ import {
TouchableOpacity,
Image,
FlatList,
Text
Text,
} from "react-native"
import { Icon, Header } from "../ui"
import { MessageModal } from "../Modal"
import autobind from "autobind-decorator"
import { SwipeListView } from "react-native-swipe-list-view"
import { api } from "../API"
import { workItemTypeEnum, formatLatLng, parseLatLng } from "../util"
const styles = StyleSheet.create({
container: {
height: "100%",
width: "100%",
justifyContent: "flex-start",
backgroundColor: "#FFFFFF"
}
backgroundColor: "#FFFFFF",
},
})
const data = [
{
key: "1",
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"
}
}
const workItemTypeText = workItemTypeEnum.reduce((result, item) => {
result[item.value] = item.text
return result
}, {})
export class WorkItemList extends React.Component {
constructor(props) {
super(props)
this.state = {
messageModal: null
messageModal: null,
}
api
.listWorkItems()
.then(list => {})
.then((list) => {
this.setState({ listItems: list.items })
})
.catch(() => {
this.setState({
messageModal: {
icon: "hand",
message: "Unable to get list of work items",
detail: error.message
}
detail: error.message,
},
})
})
}
@autobind
handleItemSelect(item, index) {
this.props.history.push("/workitem")
this.props.history.push(`/workitem?id=${item._id}`)
}
@autobind
handleItemDelete(item, index) {
}
handleItemDelete(item, index) {}
@autobind
handleMessageDismiss() {
@@ -145,7 +81,7 @@ export class WorkItemList extends React.Component {
}
render() {
const { messageModal } = this.state
const { listItems, messageModal } = this.state
return (
<View style={styles.container}>
@@ -160,9 +96,10 @@ export class WorkItemList extends React.Component {
width: "100%",
flexGrow: 1,
paddingTop: 20,
paddingBottom: 20
paddingBottom: 20,
}}
data={data}
data={listItems}
keyExtractor={(item) => (item._id)}
renderItem={({ item, index }) => (
<TouchableHighlight
style={{
@@ -171,16 +108,21 @@ export class WorkItemList extends React.Component {
paddingRight: 20,
backgroundColor: "white",
}}
underlayColor='#EEEEEE'
onPress={() => this.handleItemSelect(item, index)}
>
<View style={{ height: '100%', width: '100%', flexDirection: 'row' }}>
<View style={{ flexGrow: 1, flexDirection: "column", justifyContent: 'center' }}>
underlayColor="#EEEEEE"
onPress={() => this.handleItemSelect(item, index)}>
<View
style={{ height: "100%", width: "100%", flexDirection: "row" }}>
<View
style={{
flexGrow: 1,
flexDirection: "column",
justifyContent: "center",
}}>
<Text style={{ fontSize: 20 }}>
{inspectionTypes[item.type].title}
{workItemTypeText[item.workItemType]}
</Text>
<Text style={{ fontSize: 14, color: "gray" }}>
{item.location}
{`${item.address || "..."} | ??? mi`}
</Text>
</View>
<Icon
@@ -199,9 +141,14 @@ export class WorkItemList extends React.Component {
height: 50,
backgroundColor: "red",
}}
onPress={() => this.handleItemDelete(item, index)}
>
<View style={{ flexDirection: 'column', justifyContent: 'center', backgroundColor: "red", width: 75 }}>
onPress={() => this.handleItemDelete(item, index)}>
<View
style={{
flexDirection: "column",
justifyContent: "center",
backgroundColor: "red",
width: 75,
}}>
<Text style={{ fontSize: 20, alignSelf: "center" }}>
Delete
</Text>
@@ -209,7 +156,7 @@ export class WorkItemList extends React.Component {
</TouchableOpacity>
)}
rightOpenValue={-80}
stopLeftSwipe={100}
stopRightSwipe={-120}
disableRightSwipe
/>
<MessageModal

View File

@@ -20,7 +20,16 @@ export class BoundInput extends React.Component {
constructor(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

View File

@@ -1,15 +1,17 @@
import React from 'react'
import PropTypes from 'prop-types'
import { View, Text } from 'react-native'
import { OptionStrip } from '.'
import autobind from 'autobind-decorator'
import React from "react"
import PropTypes from "prop-types"
import { View, Text } from "react-native"
import { OptionStrip } from "."
import autobind from "autobind-decorator"
export class BoundOptionStrip extends React.Component {
static propTypes = {
name: PropTypes.string.isRequired,
label: PropTypes.string,
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) {
@@ -18,13 +20,8 @@ export class BoundOptionStrip extends React.Component {
}
@autobind
handleValueChange() {
const { binder, name } = this.props
const state = binder.getFieldState(name)
if (!state.readOnly && !state.disabled) {
this.setState(binder.updateFieldValue(name, !state.value))
}
handleValueChanged(newValue) {
this.setState(this.props.binder.updateFieldValue(this.props.name, newValue))
}
componentWillReceiveProps(nextProps) {
@@ -38,14 +35,19 @@ export class BoundOptionStrip extends React.Component {
const { visible, disabled, value } = this.state
return (
<View style={{
display: visible ? 'flex' : 'none',
flexDirection: 'column',
<View
style={{
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
value={value}
options={options}
onValueChanged={this.handleValueChanged}
/>
</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 { View, Text, TouchableHighlight } from 'react-native'
import PropTypes from 'prop-types'
import autobind from 'autobind-decorator';
import React, { Component } from "react"
import { View, Text, TouchableHighlight } from "react-native"
import PropTypes from "prop-types"
import autobind from "autobind-decorator"
export class OptionStrip extends Component {
static propTypes = {
options: PropTypes.arrayOf(PropTypes.shape({ value: PropTypes.string, text: PropTypes.string })).isRequired,
value: PropTypes.string.isRequired,
options: PropTypes.arrayOf(
PropTypes.shape({ value: PropTypes.string, text: PropTypes.string })
).isRequired,
value: PropTypes.string,
onValueChanged: PropTypes.func,
}
constructor(props) {
super(props)
this.state = {
selectedOption: this.getSelectedOption(props.options, props.value)
selectedOption: this.getSelectedOption(props.options, props.value),
}
}
@autobind
getSelectedOption(options, value) {
return options.find((option) => (value === option.value)) || options[0]
return options.find((option) => value === option.value) || null
}
componentWillReceiveProps(newProps) {
if (newProps.options !== this.props.options || newProps.value !== this.props.value) {
this.setState({ selectedIndex: this.getSelectedIndex(newProps.options, newProps.value)})
if (
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 { selectedOption } = this.state
// TODO: Handle visible, disabled & read-only
return (
<View style={{ flexDirection: 'row' }}>
<View style={{ flexDirection: "row" }}>
{options.map((option, index) => (
<TouchableHighlight
key={index}
underlayColor='#3BB0FD'
underlayColor="#3BB0FD"
style={[
{ flexGrow: 1, flexBasis: 0, height: 40 },
option === selectedOption && { backgroundColor: '#3BB0FD' },
index === 0 && { borderTopLeftRadius: 6, borderBottomLeftRadius: 6 },
index === options.length - 1 && { borderTopRightRadius: 6, borderBottomRightRadius: 6 }
option === selectedOption && { backgroundColor: "#3BB0FD" },
index === 0 && {
borderTopLeftRadius: 6,
borderBottomLeftRadius: 6,
},
index === options.length - 1 && {
borderTopRightRadius: 6,
borderBottomRightRadius: 6,
},
]}
onPress={() => this.handlePress(option)}>
<View style={[
{ flex: 1, justifyContent: 'center', borderTopWidth: 1, borderBottomWidth: 1, borderLeftWidth: 1, borderColor: 'black' },
index === 0 && { borderTopLeftRadius: 6, borderBottomLeftRadius: 6 },
index === options.length - 1 && { borderRightWidth: 1, borderTopRightRadius: 6, borderBottomRightRadius: 6 }
<View
style={[
{
flex: 1,
justifyContent: "center",
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>
<Text style={{ alignSelf: "center", color: "black" }}>
{option.text}
</Text>
</View>
</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 createError from 'http-errors'
import autobind from 'autobind-decorator'
import passport from "passport"
import createError from "http-errors"
import autobind from "autobind-decorator"
@autobind
export class WorkItemRoutes {
@@ -12,14 +12,31 @@ export class WorkItemRoutes {
this.mq = container.mq
this.ws = container.ws
app.route('/workitems')
.get(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")
.get(
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})')
.get(passport.authenticate('bearer', { session: false }), this.getWorkItem)
.delete(passport.authenticate('bearer', { session: false }), this.deleteWorkItem)
app
.route("/workitems/:_id([a-f0-9]{24})")
.get(
passport.authenticate("bearer", { session: false }),
this.getWorkItem
)
.delete(
passport.authenticate("bearer", { session: false }),
this.deleteWorkItem
)
}
async listWorkItems(req, res, next) {
@@ -33,25 +50,29 @@ export class WorkItemRoutes {
const total = await WorkItem.count({})
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)
})
cursor.on('data', (doc) => {
cursor.on("data", (doc) => {
workItems.push(doc)
})
cursor.on('end', () => {
cursor.on("end", () => {
res.json({
total: total,
offset: skip,
count: workItems.length,
items: workItems
items: workItems,
})
})
cursor.on('error', (err) => {
cursor.on("error", (err) => {
throw createError.InternalServerError(err.message)
})
} catch(err) {
} catch (err) {
if (err instanceof createError.HttpError) {
next(err)
} else {
@@ -61,7 +82,6 @@ export class WorkItemRoutes {
}
async createWorkItem(req, res, next) {
try {
const isAdmin = req.user.administrator
@@ -77,7 +97,7 @@ export class WorkItemRoutes {
const newWorkItem = await workItem.save()
res.json(newWorkItem.toClient())
} catch(err) {
} catch (err) {
if (err instanceof createError.HttpError) {
next(err)
} else {
@@ -96,7 +116,7 @@ export class WorkItemRoutes {
// Do this here because Mongoose will add it automatically otherwise
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
@@ -105,13 +125,15 @@ export class WorkItemRoutes {
try {
workItemUpdates = new WorkItem(req.body)
} catch (err) {
throw createError.BadRequest('Invalid data')
throw createError.BadRequest("Invalid data")
}
const foundWorkItem = await WorkItem.findById(workItemUpdates._id)
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)
@@ -119,7 +141,7 @@ export class WorkItemRoutes {
const savedWorkItem = await foundWorkItem.save()
res.json(savedWorkItem.toClient())
} catch(err) {
} catch (err) {
if (err instanceof createError.HttpError) {
next(err)
} else {
@@ -139,7 +161,7 @@ export class WorkItemRoutes {
}
res.json(workItem.toClient())
} catch(err) {
} catch (err) {
if (err instanceof createError.HttpError) {
next(err)
} else {
@@ -165,7 +187,7 @@ export class WorkItemRoutes {
}
res.json({})
} catch(err) {
} catch (err) {
if (err instanceof createError.HttpError) {
next(err)
} else {