Debug issues with work item CRUD
This commit is contained in:
@@ -1,3 +1,6 @@
|
||||
{
|
||||
"semi": false,
|
||||
"arrowParens": "always",
|
||||
"trailingComma": "es5",
|
||||
"jsxBracketSameLine": true,
|
||||
}
|
||||
5
mobile/package-lock.json
generated
5
mobile/package-lock.json
generated
@@ -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",
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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"} />} />
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 />
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
|
||||
68
mobile/src/ui/BoundText.js
Normal file
68
mobile/src/ui/BoundText.js
Normal 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>
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -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
46
mobile/src/util.js
Normal 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" },
|
||||
]
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user