Debug issues with work item CRUD
This commit is contained in:
@@ -1,3 +1,6 @@
|
|||||||
{
|
{
|
||||||
"semi": false,
|
"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",
|
"resolved": "https://registry.npmjs.org/urix/-/urix-0.1.0.tgz",
|
||||||
"integrity": "sha1-2pN/emLiH+wf0Y1Js1wpNQZ6bHI="
|
"integrity": "sha1-2pN/emLiH+wf0Y1Js1wpNQZ6bHI="
|
||||||
},
|
},
|
||||||
|
"url-search-params-polyfill": {
|
||||||
|
"version": "3.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/url-search-params-polyfill/-/url-search-params-polyfill-3.0.0.tgz",
|
||||||
|
"integrity": "sha512-oRNWuBkJ/zKKK1aiBaTBZTf07zOKd0g+nJYB+vFNPO14gFjA75BaHgIJLtveWBRxI/2qff7xcTb9H6wkpTmqjg=="
|
||||||
|
},
|
||||||
"use": {
|
"use": {
|
||||||
"version": "3.1.0",
|
"version": "3.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/use/-/use-3.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/use/-/use-3.1.0.tgz",
|
||||||
|
|||||||
@@ -32,6 +32,7 @@
|
|||||||
"react-native-swipe-list-view": "^1.0.7",
|
"react-native-swipe-list-view": "^1.0.7",
|
||||||
"react-router-native": "^4.2.0",
|
"react-router-native": "^4.2.0",
|
||||||
"react-viro": "^2.4.0",
|
"react-viro": "^2.4.0",
|
||||||
"socket.io-client": "^2.0.4"
|
"socket.io-client": "^2.0.4",
|
||||||
|
"url-search-params-polyfill": "^3.0.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,42 +1,42 @@
|
|||||||
import EventEmitter from 'eventemitter3'
|
import EventEmitter from "eventemitter3"
|
||||||
import io from 'socket.io-client'
|
import io from "socket.io-client"
|
||||||
import { AsyncStorage } from 'react-native'
|
import { AsyncStorage } from "react-native"
|
||||||
|
|
||||||
const authTokenName = 'AuthToken'
|
const authTokenName = "AuthToken"
|
||||||
let baseURL = null
|
let baseURL = null
|
||||||
let apiPath = null
|
let apiPath = null
|
||||||
//
|
//
|
||||||
if (__DEV__) {
|
if (__DEV__) {
|
||||||
const localIPAddr = process.env.LOCAL_IP_ADDR
|
const localIPAddr = process.env.LOCAL_IP_ADDR
|
||||||
|
|
||||||
baseURL = `http://${localIPAddr || 'localhost'}:3001`
|
baseURL = `http://${localIPAddr || "localhost"}:3001`
|
||||||
apiPath = ''
|
apiPath = ""
|
||||||
} else {
|
} else {
|
||||||
baseURL = 'https://dar.kss.us.com'
|
baseURL = "https://dar.kss.us.com"
|
||||||
apiPath = '/api'
|
apiPath = "/api"
|
||||||
}
|
}
|
||||||
|
|
||||||
class NetworkError extends Error {
|
class NetworkError extends Error {
|
||||||
constructor(message) {
|
constructor(message) {
|
||||||
super(message)
|
super(message)
|
||||||
this.name = this.constructor.name
|
this.name = this.constructor.name
|
||||||
if (typeof Error.captureStackTrace === 'function') {
|
if (typeof Error.captureStackTrace === "function") {
|
||||||
Error.captureStackTrace(this, this.constructor)
|
Error.captureStackTrace(this, this.constructor)
|
||||||
} else {
|
} else {
|
||||||
this.stack = (new Error(message)).stack
|
this.stack = new Error(message).stack
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class APIError extends Error {
|
class APIError extends Error {
|
||||||
constructor(status, message) {
|
constructor(status, message) {
|
||||||
super(message || '')
|
super(message || "")
|
||||||
this.status = status || 500
|
this.status = status || 500
|
||||||
this.name = this.constructor.name
|
this.name = this.constructor.name
|
||||||
if (typeof Error.captureStackTrace === 'function') {
|
if (typeof Error.captureStackTrace === "function") {
|
||||||
Error.captureStackTrace(this, this.constructor)
|
Error.captureStackTrace(this, this.constructor)
|
||||||
} else {
|
} else {
|
||||||
this.stack = (new Error(message)).stack
|
this.stack = new Error(message).stack
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -46,39 +46,42 @@ class API extends EventEmitter {
|
|||||||
super()
|
super()
|
||||||
this.user = { pending: true }
|
this.user = { pending: true }
|
||||||
|
|
||||||
AsyncStorage.getItem(authTokenName).then((token) => {
|
AsyncStorage.getItem(authTokenName)
|
||||||
if (!token) {
|
.then((token) => {
|
||||||
return Promise.reject()
|
if (!token) {
|
||||||
}
|
return Promise.reject()
|
||||||
|
}
|
||||||
|
|
||||||
this.token = token
|
this.token = token
|
||||||
return this.who()
|
return this.who()
|
||||||
}).then((user) => {
|
})
|
||||||
this.user = user
|
.then((user) => {
|
||||||
this.connectSocket()
|
this.user = user
|
||||||
this.emit('login')
|
this.connectSocket()
|
||||||
}).catch((err) => {
|
this.emit("login")
|
||||||
console.error(err)
|
})
|
||||||
AsyncStorage.removeItem(authTokenName)
|
.catch((err) => {
|
||||||
this.token = null
|
console.error(err)
|
||||||
this.user = {}
|
AsyncStorage.removeItem(authTokenName)
|
||||||
this.socket = null
|
this.token = null
|
||||||
this.emit('logout')
|
this.user = {}
|
||||||
})
|
this.socket = null
|
||||||
|
this.emit("logout")
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
connectSocket() {
|
connectSocket() {
|
||||||
this.socket = io(baseURL, {
|
this.socket = io(baseURL, {
|
||||||
path: apiPath + '/socketio',
|
path: apiPath + "/socketio",
|
||||||
query: {
|
query: {
|
||||||
auth_token: this.token
|
auth_token: this.token,
|
||||||
}
|
},
|
||||||
})
|
})
|
||||||
this.socket.on('disconnect', (reason) => {
|
this.socket.on("disconnect", (reason) => {
|
||||||
// Could happen if the auth_token is bad
|
// Could happen if the auth_token is bad
|
||||||
this.socket = null
|
this.socket = null
|
||||||
})
|
})
|
||||||
this.socket.on('notify', (message) => {
|
this.socket.on("notify", (message) => {
|
||||||
const { eventName, eventData } = message
|
const { eventName, eventData } = message
|
||||||
|
|
||||||
// Filter the few massages that affect our cached user data to avoid a server round trip
|
// Filter the few massages that affect our cached user data to avoid a server round trip
|
||||||
@@ -109,22 +112,27 @@ class API extends EventEmitter {
|
|||||||
|
|
||||||
makeImageUrl(id, size) {
|
makeImageUrl(id, size) {
|
||||||
if (id) {
|
if (id) {
|
||||||
return apiPath + '/assets/' + id + '?access_token=' + this.token
|
return apiPath + "/assets/" + id + "?access_token=" + this.token
|
||||||
} else if (size && size.width && size.height) {
|
} else if (size && size.width && size.height) {
|
||||||
return `${apiPath}/placeholders/${size.width}x${size.height}?access_token=${this.token}`
|
return `${apiPath}/placeholders/${size.width}x${
|
||||||
|
size.height
|
||||||
|
}?access_token=${this.token}`
|
||||||
} else {
|
} else {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
makeAssetUrl(id) {
|
makeAssetUrl(id) {
|
||||||
return id ? apiPath + '/assets/' + id + '?access_token=' + this.token : null
|
return id ? apiPath + "/assets/" + id + "?access_token=" + this.token : null
|
||||||
}
|
}
|
||||||
|
|
||||||
static makeParams(params) {
|
static makeParams(params) {
|
||||||
return params ? '?' + Object.keys(params).map((key) => (
|
return params
|
||||||
[key, params[key]].map(encodeURIComponent).join('=')
|
? "?" +
|
||||||
)).join('&') : ''
|
Object.keys(params)
|
||||||
|
.map((key) => [key, params[key]].map(encodeURIComponent).join("="))
|
||||||
|
.join("&")
|
||||||
|
: ""
|
||||||
}
|
}
|
||||||
|
|
||||||
request(method, path, requestBody, requestOptions) {
|
request(method, path, requestBody, requestOptions) {
|
||||||
@@ -132,80 +140,88 @@ class API extends EventEmitter {
|
|||||||
var promise = new Promise((resolve, reject) => {
|
var promise = new Promise((resolve, reject) => {
|
||||||
let fetchOptions = {
|
let fetchOptions = {
|
||||||
method: method,
|
method: method,
|
||||||
mode: 'cors',
|
mode: "cors",
|
||||||
cache: 'no-store'
|
cache: "no-store",
|
||||||
}
|
}
|
||||||
let headers = new Headers()
|
let headers = new Headers()
|
||||||
if (this.token) {
|
if (this.token) {
|
||||||
headers.set('Authorization', 'Bearer ' + this.token)
|
headers.set("Authorization", "Bearer " + this.token)
|
||||||
}
|
}
|
||||||
if (method === 'POST' || method === 'PUT') {
|
if (method === "POST" || method === "PUT") {
|
||||||
if (requestOptions.binary) {
|
if (requestOptions.binary) {
|
||||||
headers.set('Content-Type', 'application/octet-stream')
|
headers.set("Content-Type", "application/octet-stream")
|
||||||
headers.set('Content-Length', requestOptions.binary.length)
|
headers.set("Content-Length", requestOptions.binary.length)
|
||||||
headers.set('Range', 'byte ' + requestOptions.binary.offset)
|
headers.set("Range", "byte " + requestOptions.binary.offset)
|
||||||
fetchOptions.body = requestBody
|
fetchOptions.body = requestBody
|
||||||
} else {
|
} else {
|
||||||
headers.set('Content-Type', 'application/json')
|
headers.set("Content-Type", "application/json")
|
||||||
fetchOptions.body = JSON.stringify(requestBody)
|
fetchOptions.body = JSON.stringify(requestBody)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
fetchOptions.headers = headers
|
fetchOptions.headers = headers
|
||||||
fetch(this.apiURL + path, fetchOptions).then((res) => {
|
fetch(this.apiURL + path, fetchOptions)
|
||||||
return Promise.all([ Promise.resolve(res), (requestOptions.binary && method === 'GET') ? res.blob() : res.json() ])
|
.then((res) => {
|
||||||
}).then((arr) => {
|
return Promise.all([
|
||||||
let [ res, responseBody ] = arr
|
Promise.resolve(res),
|
||||||
if (res.ok) {
|
requestOptions.binary && method === "GET" ? res.blob() : res.json(),
|
||||||
if (requestOptions.wantHeaders) {
|
])
|
||||||
resolve({ body: responseBody, headers: res.headers })
|
})
|
||||||
|
.then((arr) => {
|
||||||
|
let [res, responseBody] = arr
|
||||||
|
if (res.ok) {
|
||||||
|
if (requestOptions.wantHeaders) {
|
||||||
|
resolve({ body: responseBody, headers: res.headers })
|
||||||
|
} else {
|
||||||
|
resolve(responseBody)
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
resolve(responseBody)
|
reject(new APIError(res.status, responseBody.message))
|
||||||
}
|
}
|
||||||
} else {
|
})
|
||||||
reject(new APIError(res.status, responseBody.message))
|
.catch((error) => {
|
||||||
}
|
reject(new NetworkError(error.message))
|
||||||
}).catch((error) => {
|
})
|
||||||
reject(new NetworkError(error.message))
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
return promise
|
return promise
|
||||||
}
|
}
|
||||||
|
|
||||||
post(path, requestBody, options) {
|
post(path, requestBody, options) {
|
||||||
return this.request('POST', path, requestBody, options)
|
return this.request("POST", path, requestBody, options)
|
||||||
}
|
}
|
||||||
put(path, requestBody, options) {
|
put(path, requestBody, options) {
|
||||||
return this.request('PUT', path, requestBody, options)
|
return this.request("PUT", path, requestBody, options)
|
||||||
}
|
}
|
||||||
get(path, options) {
|
get(path, options) {
|
||||||
return this.request('GET', path, options)
|
return this.request("GET", path, options)
|
||||||
}
|
}
|
||||||
delete(path, options) {
|
delete(path, options) {
|
||||||
return this.request('DELETE', path, options)
|
return this.request("DELETE", path, options)
|
||||||
}
|
}
|
||||||
|
|
||||||
login(email, password, remember) {
|
login(email, password, remember) {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
this.post('/auth/login', { email, password }, { wantHeaders: true }).then((response) => {
|
this.post("/auth/login", { email, password }, { wantHeaders: true })
|
||||||
// Save bearer token for later use
|
.then((response) => {
|
||||||
const authValue = response.headers.get('Authorization')
|
// Save bearer token for later use
|
||||||
const [ scheme, token ] = authValue.split(' ')
|
const authValue = response.headers.get("Authorization")
|
||||||
|
const [scheme, token] = authValue.split(" ")
|
||||||
|
|
||||||
if (scheme !== 'Bearer' || !token) {
|
if (scheme !== "Bearer" || !token) {
|
||||||
reject(new APIError('Unexpected Authorization scheme or token'))
|
reject(new APIError("Unexpected Authorization scheme or token"))
|
||||||
}
|
}
|
||||||
|
|
||||||
if (remember) {
|
if (remember) {
|
||||||
AsyncStorage.setItem(authTokenName, token)
|
AsyncStorage.setItem(authTokenName, token)
|
||||||
}
|
}
|
||||||
this.token = token
|
this.token = token
|
||||||
this.user = response.body
|
this.user = response.body
|
||||||
this.connectSocket()
|
this.connectSocket()
|
||||||
this.emit('login')
|
this.emit("login")
|
||||||
resolve(response.body)
|
resolve(response.body)
|
||||||
}).catch((err) => {
|
})
|
||||||
reject(err)
|
.catch((err) => {
|
||||||
})
|
reject(err)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
logout() {
|
logout() {
|
||||||
@@ -215,61 +231,63 @@ class API extends EventEmitter {
|
|||||||
this.token = null
|
this.token = null
|
||||||
this.user = {}
|
this.user = {}
|
||||||
this.disconnectSocket()
|
this.disconnectSocket()
|
||||||
this.emit('logout')
|
this.emit("logout")
|
||||||
}
|
}
|
||||||
return this.delete('/auth/login').then(cb, cb)
|
return this.delete("/auth/login").then(cb, cb)
|
||||||
}
|
}
|
||||||
who() {
|
who() {
|
||||||
return this.get('/auth/who')
|
return this.get("/auth/who")
|
||||||
}
|
}
|
||||||
|
|
||||||
getUser(_id) {
|
getUser(_id) {
|
||||||
return this.get('/users/' + _id)
|
return this.get("/users/" + _id)
|
||||||
}
|
}
|
||||||
listUsers() {
|
listUsers() {
|
||||||
return this.get('/users')
|
return this.get("/users")
|
||||||
}
|
}
|
||||||
createUser(user) {
|
createUser(user) {
|
||||||
return this.post('/users', user)
|
return this.post("/users", user)
|
||||||
}
|
}
|
||||||
updateUser(user) {
|
updateUser(user) {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
this.put('/users', user).then((user) => {
|
this.put("/users", user)
|
||||||
// If we just updated ourselves, update the internal cached copy
|
.then((user) => {
|
||||||
if (user._id === this.user._id) {
|
// If we just updated ourselves, update the internal cached copy
|
||||||
this.user = user
|
if (user._id === this.user._id) {
|
||||||
this.emit('login')
|
this.user = user
|
||||||
}
|
this.emit("login")
|
||||||
resolve(user)
|
}
|
||||||
}).catch((reason) => {
|
resolve(user)
|
||||||
reject(reason)
|
})
|
||||||
})
|
.catch((reason) => {
|
||||||
|
reject(reason)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
deleteUser(_id) {
|
deleteUser(_id) {
|
||||||
return this.delete('/users/' + _id)
|
return this.delete("/users/" + _id)
|
||||||
}
|
}
|
||||||
enterRoom(roomName) {
|
enterRoom(roomName) {
|
||||||
return this.put('/users/enter-room/' + (roomName || ''))
|
return this.put("/users/enter-room/" + (roomName || ""))
|
||||||
}
|
}
|
||||||
leaveRoom() {
|
leaveRoom() {
|
||||||
return this.put('/users/leave-room')
|
return this.put("/users/leave-room")
|
||||||
}
|
}
|
||||||
|
|
||||||
getWorkItem(_id) {
|
getWorkItem(_id) {
|
||||||
return this.get('/workitems/' + _id)
|
return this.get("/workitems/" + _id)
|
||||||
}
|
}
|
||||||
listWorkItems() {
|
listWorkItems() {
|
||||||
return this.get('/workitems')
|
return this.get("/workitems")
|
||||||
}
|
}
|
||||||
createWorkItem(workItem) {
|
createWorkItem(workItem) {
|
||||||
return this.post('/workitems', workItem)
|
return this.post("/workitems", workItem)
|
||||||
}
|
}
|
||||||
updateWorkItem(workItem) {
|
updateWorkItem(workItem) {
|
||||||
return this.put('/workitems', workItem)
|
return this.put("/workitems", workItem)
|
||||||
}
|
}
|
||||||
deleteWorkItem(_id) {
|
deleteWorkItem(_id) {
|
||||||
return this.delete('/workitems/' + _id)
|
return this.delete("/workitems/" + _id)
|
||||||
}
|
}
|
||||||
|
|
||||||
upload(file, progressCallback) {
|
upload(file, progressCallback) {
|
||||||
@@ -285,36 +303,40 @@ class API extends EventEmitter {
|
|||||||
const buffer = e.target.result
|
const buffer = e.target.result
|
||||||
const bytesRead = buffer.byteLength
|
const bytesRead = buffer.byteLength
|
||||||
|
|
||||||
this.post('/assets/upload/' + uploadId, buffer, {
|
this.post("/assets/upload/" + uploadId, buffer, {
|
||||||
binary: { offset: chunk * chunkSize, length: bytesRead }
|
binary: { offset: chunk * chunkSize, length: bytesRead },
|
||||||
}).then((uploadData) => {
|
|
||||||
chunk++
|
|
||||||
if (!progressCallback(uploadData)) {
|
|
||||||
return Promise.reject(new Error('Upload was canceled'))
|
|
||||||
}
|
|
||||||
if (chunk < numberOfChunks) {
|
|
||||||
let start = chunk * chunkSize
|
|
||||||
let end = Math.min(fileSize, start + chunkSize)
|
|
||||||
reader.readAsArrayBuffer(file.slice(start, end))
|
|
||||||
} else {
|
|
||||||
resolve(uploadData)
|
|
||||||
}
|
|
||||||
}).catch((err) => {
|
|
||||||
reject(err)
|
|
||||||
})
|
})
|
||||||
|
.then((uploadData) => {
|
||||||
|
chunk++
|
||||||
|
if (!progressCallback(uploadData)) {
|
||||||
|
return Promise.reject(new Error("Upload was canceled"))
|
||||||
|
}
|
||||||
|
if (chunk < numberOfChunks) {
|
||||||
|
let start = chunk * chunkSize
|
||||||
|
let end = Math.min(fileSize, start + chunkSize)
|
||||||
|
reader.readAsArrayBuffer(file.slice(start, end))
|
||||||
|
} else {
|
||||||
|
resolve(uploadData)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
reject(err)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
this.post('/assets/upload', {
|
this.post("/assets/upload", {
|
||||||
fileName: file.name,
|
fileName: file.name,
|
||||||
fileSize,
|
fileSize,
|
||||||
contentType: file.type,
|
contentType: file.type,
|
||||||
numberOfChunks
|
numberOfChunks,
|
||||||
}).then((uploadData) => {
|
|
||||||
uploadId = uploadData.uploadId
|
|
||||||
reader.readAsArrayBuffer(file.slice(0, chunkSize))
|
|
||||||
}).catch((err) => {
|
|
||||||
reject(err)
|
|
||||||
})
|
})
|
||||||
|
.then((uploadData) => {
|
||||||
|
uploadId = uploadData.uploadId
|
||||||
|
reader.readAsArrayBuffer(file.slice(0, chunkSize))
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
reject(err)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import React, { Fragment, Component } from 'react'
|
import React, { Fragment, Component } from "react"
|
||||||
import { Route, Redirect } from 'react-router-native'
|
import { Route, Redirect } from "react-router-native"
|
||||||
|
|
||||||
export const DefaultRoute = () => {
|
export const DefaultRoute = () => {
|
||||||
// NOTE: When working on the app, change this to the page you are working on
|
// NOTE: When working on the app, change this to the page you are working on
|
||||||
return <Route render={() => (<Redirect to={'/workitemlist'} />)} />
|
return <Route render={() => <Redirect to={"/workitemlist"} />} />
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import React from 'react'
|
import React from "react"
|
||||||
import {
|
import {
|
||||||
StyleSheet,
|
StyleSheet,
|
||||||
Text,
|
Text,
|
||||||
@@ -7,32 +7,80 @@ import {
|
|||||||
Image,
|
Image,
|
||||||
View,
|
View,
|
||||||
TouchableOpacity,
|
TouchableOpacity,
|
||||||
} from 'react-native'
|
} from "react-native"
|
||||||
import MapView, { Marker } from 'react-native-maps'
|
import MapView, { Marker } from "react-native-maps"
|
||||||
import { Icon, Header } from '../ui'
|
import { Icon, Header } from "../ui"
|
||||||
import { api } from '../API'
|
import { api } from "../API"
|
||||||
import autobind from 'autobind-decorator'
|
import autobind from "autobind-decorator"
|
||||||
import pinImage from './images/pin.png'
|
import pinImage from "./images/pin.png"
|
||||||
import { ifIphoneX } from 'react-native-iphone-x-helper'
|
import { ifIphoneX } from "react-native-iphone-x-helper"
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
const styles = StyleSheet.create({
|
||||||
container: {
|
container: {
|
||||||
flex: 1,
|
flex: 1,
|
||||||
backgroundColor: '#FFFFFF',
|
backgroundColor: "#FFFFFF",
|
||||||
alignItems: 'flex-start',
|
alignItems: "flex-start",
|
||||||
justifyContent: 'flex-start',
|
justifyContent: "flex-start",
|
||||||
}
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
const data = [
|
const data = [
|
||||||
{key: '1', title: 'Remove Animal Carcass', location: 'Ossington Ave. | 0.2 mi.', state: 'planned', latlng: { latitude: 43.653226, longitude: -79.383184 } },
|
{
|
||||||
{key: '2', title: 'Fix sign post', location: 'Alexandre St. | 0.7 mi.', state: 'open', latlng: { latitude: 43.648118, longitude: 79.392636 }},
|
key: "1",
|
||||||
{key: '3', title: 'Overflowing trash', location: 'Bay St. | 0.8 mi.', state: 'open', latlng: { latitude: 43.640168, longitude: -79.409373 }},
|
title: "Remove Animal Carcass",
|
||||||
{key: '4', title: 'Leaking water pipe', location: 'Bloor St. | 1.2 mi.', state: 'planned', latlng: { latitude: 43.633110, longitude: -79.415880 }},
|
location: "Ossington Ave. | 0.2 mi.",
|
||||||
{key: '5', title: 'Tree branch in road', location: 'Blue Jays Way | 2.2 mi.', state: 'open', latlng: { latitude: 43.653526, longitude: -79.361385 }},
|
state: "planned",
|
||||||
{key: '6', title: 'Washing machine on sidewalk', location: 'Christie St. | 3.0 mi.', state: 'open', latlng: { latitude: 43.663870, longitude: -79.383705 }},
|
latlng: { latitude: 43.653226, longitude: -79.383184 },
|
||||||
{key: '7', title: 'Dead moose', location: 'Cummer Ave. | 4.2 mi.', state: 'open', latlng: { latitude: 43.659166, longitude: -79.391350 }},
|
},
|
||||||
{key: '8', title: 'Glass in street', location: 'Danforth Ave. | 4.7 mi.', state: 'open', latlng: { latitude: 43.663538, longitude: -79.423212 }},
|
{
|
||||||
|
key: "2",
|
||||||
|
title: "Fix sign post",
|
||||||
|
location: "Alexandre St. | 0.7 mi.",
|
||||||
|
state: "open",
|
||||||
|
latlng: { latitude: 43.648118, longitude: 79.392636 },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "3",
|
||||||
|
title: "Overflowing trash",
|
||||||
|
location: "Bay St. | 0.8 mi.",
|
||||||
|
state: "open",
|
||||||
|
latlng: { latitude: 43.640168, longitude: -79.409373 },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "4",
|
||||||
|
title: "Leaking water pipe",
|
||||||
|
location: "Bloor St. | 1.2 mi.",
|
||||||
|
state: "planned",
|
||||||
|
latlng: { latitude: 43.63311, longitude: -79.41588 },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "5",
|
||||||
|
title: "Tree branch in road",
|
||||||
|
location: "Blue Jays Way | 2.2 mi.",
|
||||||
|
state: "open",
|
||||||
|
latlng: { latitude: 43.653526, longitude: -79.361385 },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "6",
|
||||||
|
title: "Washing machine on sidewalk",
|
||||||
|
location: "Christie St. | 3.0 mi.",
|
||||||
|
state: "open",
|
||||||
|
latlng: { latitude: 43.66387, longitude: -79.383705 },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "7",
|
||||||
|
title: "Dead moose",
|
||||||
|
location: "Cummer Ave. | 4.2 mi.",
|
||||||
|
state: "open",
|
||||||
|
latlng: { latitude: 43.659166, longitude: -79.39135 },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "8",
|
||||||
|
title: "Glass in street",
|
||||||
|
location: "Danforth Ave. | 4.7 mi.",
|
||||||
|
state: "open",
|
||||||
|
latlng: { latitude: 43.663538, longitude: -79.423212 },
|
||||||
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
export class Home extends React.Component {
|
export class Home extends React.Component {
|
||||||
@@ -43,42 +91,45 @@ export class Home extends React.Component {
|
|||||||
@autobind
|
@autobind
|
||||||
_handleNavigatorEvent(event) {
|
_handleNavigatorEvent(event) {
|
||||||
switch (event.id) {
|
switch (event.id) {
|
||||||
case 'logout':
|
case "logout":
|
||||||
api.logout().then(() => {
|
api.logout().then(() => {
|
||||||
this.props.history.replace('/login')
|
this.props.history.replace("/login")
|
||||||
})
|
})
|
||||||
break
|
break
|
||||||
case 'viewer':
|
case "viewer":
|
||||||
this.props.push('/viewer')
|
this.props.push("/viewer")
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@autobind
|
@autobind
|
||||||
handleWorkItemsListPress() {
|
handleWorkItemsListPress() {
|
||||||
this.props.history.push('/workitemlist')
|
this.props.history.push("/workitemlist")
|
||||||
}
|
}
|
||||||
|
|
||||||
@autobind
|
@autobind
|
||||||
handleItemSelect(item, index) {
|
handleItemSelect(item, index) {
|
||||||
this.props.history.push('/activity')
|
this.props.history.push("/activity")
|
||||||
}
|
}
|
||||||
|
|
||||||
@autobind
|
@autobind
|
||||||
handleLogoutPress() {
|
handleLogoutPress() {
|
||||||
this.props.history.replace('/logout')
|
this.props.history.replace("/logout")
|
||||||
}
|
}
|
||||||
|
|
||||||
@autobind
|
@autobind
|
||||||
handleGlassesPress() {
|
handleGlassesPress() {
|
||||||
this.props.history.push('/arviewer')
|
this.props.history.push("/arviewer")
|
||||||
}
|
}
|
||||||
|
|
||||||
@autobind
|
@autobind
|
||||||
handleMyLocationPress() {
|
handleMyLocationPress() {
|
||||||
navigator.geolocation.getCurrentPosition((info) => {
|
navigator.geolocation.getCurrentPosition((info) => {
|
||||||
if (this.map) {
|
if (this.map) {
|
||||||
this.map.animateToCoordinate({latitude: info.coords.latitude, longitude: info.coords.longitude})
|
this.map.animateToCoordinate({
|
||||||
|
latitude: info.coords.latitude,
|
||||||
|
longitude: info.coords.longitude,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -87,13 +138,17 @@ export class Home extends React.Component {
|
|||||||
return (
|
return (
|
||||||
<View style={styles.container}>
|
<View style={styles.container}>
|
||||||
<Header
|
<Header
|
||||||
title='Work Item Map'
|
title="Work Item Map"
|
||||||
leftButton={{ icon: 'logout', onPress: this.handleLogoutPress }}
|
leftButton={{ icon: "logout", onPress: this.handleLogoutPress }}
|
||||||
rightButton={{ icon: 'glasses', onPress: this.handleGlassesPress }} />
|
rightButton={{ icon: "glasses", onPress: this.handleGlassesPress }}
|
||||||
|
/>
|
||||||
<MapView
|
<MapView
|
||||||
ref={ref => { this.map = ref }}
|
ref={(ref) => {
|
||||||
|
this.map = ref
|
||||||
|
}}
|
||||||
style={{
|
style={{
|
||||||
width: '100%', height: '50%',
|
width: "100%",
|
||||||
|
height: "50%",
|
||||||
}}
|
}}
|
||||||
showsUserLocation
|
showsUserLocation
|
||||||
showsBuildings={false}
|
showsBuildings={false}
|
||||||
@@ -106,58 +161,97 @@ export class Home extends React.Component {
|
|||||||
latitudeDelta: 0.0922,
|
latitudeDelta: 0.0922,
|
||||||
longitudeDelta: 0.0922,
|
longitudeDelta: 0.0922,
|
||||||
}}>
|
}}>
|
||||||
{
|
{data.map((marker) => (
|
||||||
data.map(marker => (
|
<Marker
|
||||||
<Marker
|
key={marker.key}
|
||||||
key={marker.key}
|
coordinate={marker.latlng}
|
||||||
coordinate={marker.latlng}
|
title={marker.title}
|
||||||
title={marker.title}
|
description={marker.location}
|
||||||
description={marker.location}
|
image={pinImage}
|
||||||
image={pinImage}
|
anchor={{ x: 0.5, y: 1.0 }}
|
||||||
anchor={{x: 0.5, y: 1.0}} />
|
/>
|
||||||
))
|
))}
|
||||||
}
|
|
||||||
</MapView>
|
</MapView>
|
||||||
<View style={{ flexDirection: 'row', alignItems: 'center', width: '100%', height: 40, backgroundColor: '#F4F4F4' }}>
|
<View
|
||||||
<Icon name='search' size={16} style={{marginLeft: 10, marginRight: 5, tintColor: 'gray' }} />
|
style={{
|
||||||
<TextInput style={{ flexGrow: 1, height: '100%' }} placeholder='Search' />
|
flexDirection: "row",
|
||||||
<Icon name='cancel' size={16} style={{marginLeft: 5, marginRight: 10, tintColor: 'gray' }} />
|
alignItems: "center",
|
||||||
|
width: "100%",
|
||||||
|
height: 40,
|
||||||
|
backgroundColor: "#F4F4F4",
|
||||||
|
}}>
|
||||||
|
<Icon
|
||||||
|
name="search"
|
||||||
|
size={16}
|
||||||
|
style={{ marginLeft: 10, marginRight: 5, tintColor: "gray" }}
|
||||||
|
/>
|
||||||
|
<TextInput
|
||||||
|
style={{ flexGrow: 1, height: "100%" }}
|
||||||
|
placeholder="Search"
|
||||||
|
/>
|
||||||
|
<Icon
|
||||||
|
name="cancel"
|
||||||
|
size={16}
|
||||||
|
style={{ marginLeft: 5, marginRight: 10, tintColor: "gray" }}
|
||||||
|
/>
|
||||||
</View>
|
</View>
|
||||||
<FlatList
|
<FlatList
|
||||||
style={{ width: '100%', flexGrow: 1 }}
|
style={{ width: "100%", flexGrow: 1 }}
|
||||||
data={data}
|
data={data}
|
||||||
renderItem={({item, index}) => {
|
renderItem={({ item, index }) => {
|
||||||
return (
|
return (
|
||||||
<View style={{ flexDirection: 'row', height: 50 }}>
|
<View style={{ flexDirection: "row", height: 50 }}>
|
||||||
<Text style={{ fontSize: 8, width: 45, marginLeft: 5, alignSelf: 'center' }}>{item.state.toUpperCase()}</Text>
|
<Text
|
||||||
<View style={{ width: '75%', flexDirection: 'column' }}>
|
style={{
|
||||||
|
fontSize: 8,
|
||||||
|
width: 45,
|
||||||
|
marginLeft: 5,
|
||||||
|
alignSelf: "center",
|
||||||
|
}}>
|
||||||
|
{item.state.toUpperCase()}
|
||||||
|
</Text>
|
||||||
|
<View style={{ width: "75%", flexDirection: "column" }}>
|
||||||
<Text style={{ fontSize: 20 }}>{item.title}</Text>
|
<Text style={{ fontSize: 20 }}>{item.title}</Text>
|
||||||
<Text style={{ fontSize: 14, color: 'gray' }}>{item.location}</Text>
|
<Text style={{ fontSize: 14, color: "gray" }}>
|
||||||
|
{item.location}
|
||||||
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
<TouchableOpacity style={{ alignSelf: 'center' }} onPress={() => (this.handleItemSelect(item, index))} >
|
<TouchableOpacity
|
||||||
<Icon name='rightArrow' size={16} />
|
style={{ alignSelf: "center" }}
|
||||||
|
onPress={() => this.handleItemSelect(item, index)}>
|
||||||
|
<Icon name="rightArrow" size={16} />
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
</View>
|
</View>
|
||||||
)
|
)
|
||||||
}} />
|
}}
|
||||||
<View style={{
|
/>
|
||||||
flexDirection: 'row',
|
<View
|
||||||
justifyContent: 'space-between',
|
style={{
|
||||||
alignItems: 'center',
|
flexDirection: "row",
|
||||||
width: '100%',
|
justifyContent: "space-between",
|
||||||
height: 45,
|
alignItems: "center",
|
||||||
backgroundColor: '#F4F4F4',
|
width: "100%",
|
||||||
...ifIphoneX({ marginBottom: 22 }, {}),
|
height: 45,
|
||||||
}}>
|
backgroundColor: "#F4F4F4",
|
||||||
|
...ifIphoneX({ marginBottom: 22 }, {}),
|
||||||
|
}}>
|
||||||
<TouchableOpacity onPress={this.handleMyLocationPress}>
|
<TouchableOpacity onPress={this.handleMyLocationPress}>
|
||||||
<Icon name='center' size={24} style={{ marginLeft: 15, tintColor: 'gray' }} />
|
<Icon
|
||||||
|
name="center"
|
||||||
|
size={24}
|
||||||
|
style={{ marginLeft: 15, tintColor: "gray" }}
|
||||||
|
/>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
<Text style={{ color: 'gray', fontSize: 20, }}>Hide List</Text>
|
<Text style={{ color: "gray", fontSize: 20 }}>Hide List</Text>
|
||||||
<TouchableOpacity onPress={this.handleWorkItemsListPress}>
|
<TouchableOpacity onPress={this.handleWorkItemsListPress}>
|
||||||
<Icon name='settings' size={24} style={{ marginRight: 15, tintColor: 'gray' }} />
|
<Icon
|
||||||
|
name="settings"
|
||||||
|
size={24}
|
||||||
|
style={{ marginRight: 15, tintColor: "gray" }}
|
||||||
|
/>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import {
|
|||||||
TextInput,
|
TextInput,
|
||||||
KeyboardAvoidingView,
|
KeyboardAvoidingView,
|
||||||
Platform,
|
Platform,
|
||||||
TouchableOpacity
|
TouchableOpacity,
|
||||||
} from "react-native"
|
} from "react-native"
|
||||||
import MapView, { Marker } from "react-native-maps"
|
import MapView, { Marker } from "react-native-maps"
|
||||||
import { FormBinder } from "react-form-binder"
|
import { FormBinder } from "react-form-binder"
|
||||||
@@ -19,19 +19,21 @@ import {
|
|||||||
Icon,
|
Icon,
|
||||||
Header,
|
Header,
|
||||||
PhotoButton,
|
PhotoButton,
|
||||||
BoundOptionStrip
|
BoundOptionStrip,
|
||||||
} from "../ui"
|
} from "../ui"
|
||||||
import { MessageModal } from "../Modal"
|
import { MessageModal } from "../Modal"
|
||||||
import autobind from "autobind-decorator"
|
import autobind from "autobind-decorator"
|
||||||
import { ifIphoneX, isIphoneX } from "react-native-iphone-x-helper"
|
import { ifIphoneX, isIphoneX } from "react-native-iphone-x-helper"
|
||||||
import KeyboardSpacer from "react-native-keyboard-spacer"
|
import KeyboardSpacer from "react-native-keyboard-spacer"
|
||||||
import { api } from "../API"
|
import { api } from "../API"
|
||||||
|
import "url-search-params-polyfill"
|
||||||
|
import { workItemTypeEnum, formatLatLng, parseLatLng } from "../util"
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
const styles = StyleSheet.create({
|
||||||
container: {
|
container: {
|
||||||
flexDirection: "column",
|
flexDirection: "column",
|
||||||
flexGrow: 1,
|
flexGrow: 1,
|
||||||
backgroundColor: "#DDDDDD"
|
backgroundColor: "#DDDDDD",
|
||||||
},
|
},
|
||||||
panel: {
|
panel: {
|
||||||
width: "94%",
|
width: "94%",
|
||||||
@@ -42,60 +44,68 @@ const styles = StyleSheet.create({
|
|||||||
shadowOffset: { width: 2, height: 2 },
|
shadowOffset: { width: 2, height: 2 },
|
||||||
shadowRadius: 2,
|
shadowRadius: 2,
|
||||||
shadowOpacity: 0.5,
|
shadowOpacity: 0.5,
|
||||||
padding: 10
|
padding: 10,
|
||||||
},
|
},
|
||||||
label: {
|
label: {
|
||||||
fontSize: 14,
|
fontSize: 14,
|
||||||
marginBottom: 4
|
marginBottom: 4,
|
||||||
}
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
const workItemOptions = [
|
|
||||||
{ value: "order", text: "Work Order" },
|
|
||||||
{ value: "inspection", text: "Inspection" },
|
|
||||||
{ value: "complaint", text: "Complaint" }
|
|
||||||
]
|
|
||||||
|
|
||||||
const latLngToString = (lat, lng) =>
|
|
||||||
`${Math.abs(lng).toFixed(4)}°${lng > 0 ? "S" : "N"}, ${Math.abs(lat).toFixed(
|
|
||||||
4
|
|
||||||
)}°${lat > 0 ? "W" : "E"}`
|
|
||||||
const latLngStringToPoint = str => {
|
|
||||||
const parts = str.split(", ")
|
|
||||||
return {
|
|
||||||
type: "Point",
|
|
||||||
coordinates: [
|
|
||||||
new Number(parts[0].substring(0, parts[0].length - 2)),
|
|
||||||
new Number(parts[1].substring(0, parts[1].length - 2))
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export class WorkItem extends React.Component {
|
export class WorkItem extends React.Component {
|
||||||
static bindings = {
|
static bindings = {
|
||||||
header: {
|
header: {
|
||||||
noValue: true,
|
noValue: true,
|
||||||
isDisabled: r => !(r.anyModified && r.allValid)
|
isDisabled: (r) => !(r.anyModified && r.allValid),
|
||||||
},
|
},
|
||||||
location: {
|
location: {
|
||||||
isValid: true,
|
isValid: true,
|
||||||
isDisabled: true
|
isDisabled: true,
|
||||||
},
|
},
|
||||||
details: {
|
details: {
|
||||||
isValid: (r, v) => v !== ""
|
isValid: (r, v) => v !== "",
|
||||||
},
|
},
|
||||||
workItemType: {
|
workItemType: {
|
||||||
isValid: true,
|
isValid: true,
|
||||||
initValue: "order",
|
alwaysGet: true,
|
||||||
alwaysGet: true
|
},
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props)
|
super(props)
|
||||||
this.state = {
|
this.state = {
|
||||||
binder: new FormBinder({}, WorkItem.bindings),
|
binder: new FormBinder({}, WorkItem.bindings),
|
||||||
messageModal: null
|
messageModal: null,
|
||||||
|
}
|
||||||
|
|
||||||
|
const { search } = this.props.location
|
||||||
|
|
||||||
|
if (search) {
|
||||||
|
const id = new URLSearchParams(search).get("id")
|
||||||
|
|
||||||
|
if (id) {
|
||||||
|
api
|
||||||
|
.getWorkItem(id)
|
||||||
|
.then((workItem) => {
|
||||||
|
if (workItem) {
|
||||||
|
const [lng, lat] = workItem.location.coordinates
|
||||||
|
workItem.location = formatLatLng(lat, lng)
|
||||||
|
this.setState({
|
||||||
|
binder: new FormBinder(workItem, WorkItem.bindings),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
this.setState({
|
||||||
|
messageModal: {
|
||||||
|
icon: "hand",
|
||||||
|
message: "Unable to get work item details",
|
||||||
|
detail: err.message,
|
||||||
|
back: true,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -115,36 +125,36 @@ export class WorkItem extends React.Component {
|
|||||||
const { binder } = this.state
|
const { binder } = this.state
|
||||||
let obj = binder.getModifiedFieldValues()
|
let obj = binder.getModifiedFieldValues()
|
||||||
|
|
||||||
obj.location = latLngStringToPoint(obj.location)
|
obj.location = parseLatLng(obj.location)
|
||||||
|
|
||||||
if (!obj._id) {
|
if (!obj._id) {
|
||||||
api
|
api
|
||||||
.createWorkItem(obj)
|
.createWorkItem(obj)
|
||||||
.then(workItem => {
|
.then((workItem) => {
|
||||||
this.handleBackPress()
|
this.handleBackPress()
|
||||||
})
|
})
|
||||||
.catch(error => {
|
.catch((error) => {
|
||||||
this.setState({
|
this.setState({
|
||||||
messageModal: {
|
messageModal: {
|
||||||
icon: "hand",
|
icon: "hand",
|
||||||
message: "Unable to create work item",
|
message: "Unable to create work item",
|
||||||
detail: error.message
|
detail: error.message,
|
||||||
}
|
},
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
api
|
api
|
||||||
.updateWorkItem(obj)
|
.updateWorkItem(obj)
|
||||||
.then(workItem => {
|
.then((workItem) => {
|
||||||
this.handleBackPress()
|
this.handleBackPress()
|
||||||
})
|
})
|
||||||
.catch(error => {
|
.catch((error) => {
|
||||||
this.setState({
|
this.setState({
|
||||||
messageModal: {
|
messageModal: {
|
||||||
icon: "hand",
|
icon: "hand",
|
||||||
message: "Unable to update work item",
|
message: "Unable to update work item",
|
||||||
detail: error.message
|
detail: error.message,
|
||||||
}
|
},
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -152,7 +162,11 @@ export class WorkItem extends React.Component {
|
|||||||
|
|
||||||
@autobind
|
@autobind
|
||||||
handleMessageDismiss() {
|
handleMessageDismiss() {
|
||||||
|
const back = this.state.messageModal.back
|
||||||
this.setState({ messageModal: null })
|
this.setState({ messageModal: null })
|
||||||
|
if (back) {
|
||||||
|
this.handleBackPress()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@autobind
|
@autobind
|
||||||
@@ -162,7 +176,7 @@ export class WorkItem extends React.Component {
|
|||||||
this.setState(
|
this.setState(
|
||||||
binder.updateFieldValue(
|
binder.updateFieldValue(
|
||||||
"location",
|
"location",
|
||||||
latLngToString(region.latitude, region.longitude)
|
formatLatLng(region.latitude, region.longitude)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -185,7 +199,7 @@ export class WorkItem extends React.Component {
|
|||||||
binder={binder}
|
binder={binder}
|
||||||
name="workItemType"
|
name="workItemType"
|
||||||
label="Work Item Type:"
|
label="Work Item Type:"
|
||||||
options={workItemOptions}
|
options={workItemTypeEnum}
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
<View style={styles.panel}>
|
<View style={styles.panel}>
|
||||||
@@ -204,17 +218,16 @@ export class WorkItem extends React.Component {
|
|||||||
justifyContent: "center",
|
justifyContent: "center",
|
||||||
width: "100%",
|
width: "100%",
|
||||||
height: 400,
|
height: 400,
|
||||||
marginBottom: 10
|
marginBottom: 10,
|
||||||
}}
|
}}
|
||||||
zoomControlEnabled
|
zoomControlEnabled
|
||||||
initialRegion={{
|
initialRegion={{
|
||||||
latitude: 43.653908,
|
latitude: 43.653908,
|
||||||
longitude: -79.384293,
|
longitude: -79.384293,
|
||||||
latitudeDelta: 0.0922,
|
latitudeDelta: 0.0922,
|
||||||
longitudeDelta: 0.0421
|
longitudeDelta: 0.0421,
|
||||||
}}
|
}}
|
||||||
onRegionChange={this.handleRegionChange}
|
onRegionChange={this.handleRegionChange}>
|
||||||
>
|
|
||||||
<Icon
|
<Icon
|
||||||
name="target"
|
name="target"
|
||||||
size={24}
|
size={24}
|
||||||
@@ -227,8 +240,7 @@ export class WorkItem extends React.Component {
|
|||||||
<View style={styles.panel}>
|
<View style={styles.panel}>
|
||||||
<Text style={styles.label}>Pictures:</Text>
|
<Text style={styles.label}>Pictures:</Text>
|
||||||
<View
|
<View
|
||||||
style={{ flexDirection: "row", justifyContent: "space-between" }}
|
style={{ flexDirection: "row", justifyContent: "space-between" }}>
|
||||||
>
|
|
||||||
<PhotoButton />
|
<PhotoButton />
|
||||||
<PhotoButton />
|
<PhotoButton />
|
||||||
<PhotoButton />
|
<PhotoButton />
|
||||||
|
|||||||
@@ -6,122 +6,58 @@ import {
|
|||||||
TouchableOpacity,
|
TouchableOpacity,
|
||||||
Image,
|
Image,
|
||||||
FlatList,
|
FlatList,
|
||||||
Text
|
Text,
|
||||||
} from "react-native"
|
} from "react-native"
|
||||||
import { Icon, Header } from "../ui"
|
import { Icon, Header } from "../ui"
|
||||||
import { MessageModal } from "../Modal"
|
import { MessageModal } from "../Modal"
|
||||||
import autobind from "autobind-decorator"
|
import autobind from "autobind-decorator"
|
||||||
import { SwipeListView } from "react-native-swipe-list-view"
|
import { SwipeListView } from "react-native-swipe-list-view"
|
||||||
import { api } from "../API"
|
import { api } from "../API"
|
||||||
|
import { workItemTypeEnum, formatLatLng, parseLatLng } from "../util"
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
const styles = StyleSheet.create({
|
||||||
container: {
|
container: {
|
||||||
height: "100%",
|
height: "100%",
|
||||||
width: "100%",
|
width: "100%",
|
||||||
justifyContent: "flex-start",
|
justifyContent: "flex-start",
|
||||||
backgroundColor: "#FFFFFF"
|
backgroundColor: "#FFFFFF",
|
||||||
}
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
const data = [
|
const workItemTypeText = workItemTypeEnum.reduce((result, item) => {
|
||||||
{
|
result[item.value] = item.text
|
||||||
key: "1",
|
return result
|
||||||
type: "work",
|
}, {})
|
||||||
location: "Ossington Ave. | 0.2 mi.",
|
|
||||||
state: "open",
|
|
||||||
latlng: { latitude: 43.653226, longitude: -79.383184 }
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: "2",
|
|
||||||
type: "inspection",
|
|
||||||
location: "Alexandre St. | 0.7 mi.",
|
|
||||||
state: "open",
|
|
||||||
latlng: { latitude: 43.648118, longitude: 79.392636 }
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: "3",
|
|
||||||
type: "complaint",
|
|
||||||
location: "Bay St. | 0.8 mi.",
|
|
||||||
state: "open",
|
|
||||||
latlng: { latitude: 43.640168, longitude: -79.409373 }
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: "4",
|
|
||||||
type: "work",
|
|
||||||
location: "Bloor St. | 1.2 mi.",
|
|
||||||
state: "open",
|
|
||||||
latlng: { latitude: 43.63311, longitude: -79.41588 }
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: "5",
|
|
||||||
type: "inspection",
|
|
||||||
location: "Blue Jays Way | 2.2 mi.",
|
|
||||||
state: "open",
|
|
||||||
latlng: { latitude: 43.653526, longitude: -79.361385 }
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: "6",
|
|
||||||
type: "complaint",
|
|
||||||
location: "Christie St. | 3.0 mi.",
|
|
||||||
state: "open",
|
|
||||||
latlng: { latitude: 43.66387, longitude: -79.383705 }
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: "7",
|
|
||||||
type: "work",
|
|
||||||
location: "Cummer Ave. | 4.2 mi.",
|
|
||||||
state: "open",
|
|
||||||
latlng: { latitude: 43.659166, longitude: -79.39135 }
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: "8",
|
|
||||||
type: "complaint",
|
|
||||||
location: "Danforth Ave. | 4.7 mi.",
|
|
||||||
state: "open",
|
|
||||||
latlng: { latitude: 43.663538, longitude: -79.423212 }
|
|
||||||
}
|
|
||||||
]
|
|
||||||
|
|
||||||
const inspectionTypes = {
|
|
||||||
work: {
|
|
||||||
title: "Work Order"
|
|
||||||
},
|
|
||||||
inspection: {
|
|
||||||
title: "Inspection"
|
|
||||||
},
|
|
||||||
complaint: {
|
|
||||||
title: "Complaint"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export class WorkItemList extends React.Component {
|
export class WorkItemList extends React.Component {
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props)
|
super(props)
|
||||||
this.state = {
|
this.state = {
|
||||||
messageModal: null
|
messageModal: null,
|
||||||
}
|
}
|
||||||
api
|
api
|
||||||
.listWorkItems()
|
.listWorkItems()
|
||||||
.then(list => {})
|
.then((list) => {
|
||||||
|
this.setState({ listItems: list.items })
|
||||||
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
this.setState({
|
this.setState({
|
||||||
messageModal: {
|
messageModal: {
|
||||||
icon: "hand",
|
icon: "hand",
|
||||||
message: "Unable to get list of work items",
|
message: "Unable to get list of work items",
|
||||||
detail: error.message
|
detail: error.message,
|
||||||
}
|
},
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@autobind
|
@autobind
|
||||||
handleItemSelect(item, index) {
|
handleItemSelect(item, index) {
|
||||||
this.props.history.push("/workitem")
|
this.props.history.push(`/workitem?id=${item._id}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
@autobind
|
@autobind
|
||||||
handleItemDelete(item, index) {
|
handleItemDelete(item, index) {}
|
||||||
}
|
|
||||||
|
|
||||||
@autobind
|
@autobind
|
||||||
handleMessageDismiss() {
|
handleMessageDismiss() {
|
||||||
@@ -145,7 +81,7 @@ export class WorkItemList extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { messageModal } = this.state
|
const { listItems, messageModal } = this.state
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View style={styles.container}>
|
<View style={styles.container}>
|
||||||
@@ -160,9 +96,10 @@ export class WorkItemList extends React.Component {
|
|||||||
width: "100%",
|
width: "100%",
|
||||||
flexGrow: 1,
|
flexGrow: 1,
|
||||||
paddingTop: 20,
|
paddingTop: 20,
|
||||||
paddingBottom: 20
|
paddingBottom: 20,
|
||||||
}}
|
}}
|
||||||
data={data}
|
data={listItems}
|
||||||
|
keyExtractor={(item) => (item._id)}
|
||||||
renderItem={({ item, index }) => (
|
renderItem={({ item, index }) => (
|
||||||
<TouchableHighlight
|
<TouchableHighlight
|
||||||
style={{
|
style={{
|
||||||
@@ -171,16 +108,21 @@ export class WorkItemList extends React.Component {
|
|||||||
paddingRight: 20,
|
paddingRight: 20,
|
||||||
backgroundColor: "white",
|
backgroundColor: "white",
|
||||||
}}
|
}}
|
||||||
underlayColor='#EEEEEE'
|
underlayColor="#EEEEEE"
|
||||||
onPress={() => this.handleItemSelect(item, index)}
|
onPress={() => this.handleItemSelect(item, index)}>
|
||||||
>
|
<View
|
||||||
<View style={{ height: '100%', width: '100%', flexDirection: 'row' }}>
|
style={{ height: "100%", width: "100%", flexDirection: "row" }}>
|
||||||
<View style={{ flexGrow: 1, flexDirection: "column", justifyContent: 'center' }}>
|
<View
|
||||||
|
style={{
|
||||||
|
flexGrow: 1,
|
||||||
|
flexDirection: "column",
|
||||||
|
justifyContent: "center",
|
||||||
|
}}>
|
||||||
<Text style={{ fontSize: 20 }}>
|
<Text style={{ fontSize: 20 }}>
|
||||||
{inspectionTypes[item.type].title}
|
{workItemTypeText[item.workItemType]}
|
||||||
</Text>
|
</Text>
|
||||||
<Text style={{ fontSize: 14, color: "gray" }}>
|
<Text style={{ fontSize: 14, color: "gray" }}>
|
||||||
{item.location}
|
{`${item.address || "..."} | ??? mi`}
|
||||||
</Text>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
<Icon
|
<Icon
|
||||||
@@ -199,9 +141,14 @@ export class WorkItemList extends React.Component {
|
|||||||
height: 50,
|
height: 50,
|
||||||
backgroundColor: "red",
|
backgroundColor: "red",
|
||||||
}}
|
}}
|
||||||
onPress={() => this.handleItemDelete(item, index)}
|
onPress={() => this.handleItemDelete(item, index)}>
|
||||||
>
|
<View
|
||||||
<View style={{ flexDirection: 'column', justifyContent: 'center', backgroundColor: "red", width: 75 }}>
|
style={{
|
||||||
|
flexDirection: "column",
|
||||||
|
justifyContent: "center",
|
||||||
|
backgroundColor: "red",
|
||||||
|
width: 75,
|
||||||
|
}}>
|
||||||
<Text style={{ fontSize: 20, alignSelf: "center" }}>
|
<Text style={{ fontSize: 20, alignSelf: "center" }}>
|
||||||
Delete
|
Delete
|
||||||
</Text>
|
</Text>
|
||||||
@@ -209,7 +156,7 @@ export class WorkItemList extends React.Component {
|
|||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
)}
|
)}
|
||||||
rightOpenValue={-80}
|
rightOpenValue={-80}
|
||||||
stopLeftSwipe={100}
|
stopRightSwipe={-120}
|
||||||
disableRightSwipe
|
disableRightSwipe
|
||||||
/>
|
/>
|
||||||
<MessageModal
|
<MessageModal
|
||||||
|
|||||||
@@ -20,7 +20,16 @@ export class BoundInput extends React.Component {
|
|||||||
|
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props)
|
super(props)
|
||||||
this.state = props.binder.getFieldState(props.name)
|
|
||||||
|
const { name, binder } = this.props
|
||||||
|
|
||||||
|
this.state = binder.getFieldState(name)
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillReceiveProps(nextProps) {
|
||||||
|
if (nextProps.binder !== this.props.binder) {
|
||||||
|
this.setState(nextProps.binder.getFieldState(nextProps.name))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@autobind
|
@autobind
|
||||||
|
|||||||
@@ -1,15 +1,17 @@
|
|||||||
import React from 'react'
|
import React from "react"
|
||||||
import PropTypes from 'prop-types'
|
import PropTypes from "prop-types"
|
||||||
import { View, Text } from 'react-native'
|
import { View, Text } from "react-native"
|
||||||
import { OptionStrip } from '.'
|
import { OptionStrip } from "."
|
||||||
import autobind from 'autobind-decorator'
|
import autobind from "autobind-decorator"
|
||||||
|
|
||||||
export class BoundOptionStrip extends React.Component {
|
export class BoundOptionStrip extends React.Component {
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
name: PropTypes.string.isRequired,
|
name: PropTypes.string.isRequired,
|
||||||
label: PropTypes.string,
|
label: PropTypes.string,
|
||||||
binder: PropTypes.object.isRequired,
|
binder: PropTypes.object.isRequired,
|
||||||
options: PropTypes.arrayOf(PropTypes.shape({ value: PropTypes.string, text: PropTypes.string })).isRequired,
|
options: PropTypes.arrayOf(
|
||||||
|
PropTypes.shape({ value: PropTypes.string, text: PropTypes.string })
|
||||||
|
).isRequired,
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
@@ -18,13 +20,8 @@ export class BoundOptionStrip extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@autobind
|
@autobind
|
||||||
handleValueChange() {
|
handleValueChanged(newValue) {
|
||||||
const { binder, name } = this.props
|
this.setState(this.props.binder.updateFieldValue(this.props.name, newValue))
|
||||||
const state = binder.getFieldState(name)
|
|
||||||
|
|
||||||
if (!state.readOnly && !state.disabled) {
|
|
||||||
this.setState(binder.updateFieldValue(name, !state.value))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillReceiveProps(nextProps) {
|
componentWillReceiveProps(nextProps) {
|
||||||
@@ -38,14 +35,19 @@ export class BoundOptionStrip extends React.Component {
|
|||||||
const { visible, disabled, value } = this.state
|
const { visible, disabled, value } = this.state
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View style={{
|
<View
|
||||||
display: visible ? 'flex' : 'none',
|
style={{
|
||||||
flexDirection: 'column',
|
display: visible ? "flex" : "none",
|
||||||
|
flexDirection: "column",
|
||||||
}}>
|
}}>
|
||||||
<Text style={{ color: 'black', fontSize: 14, marginBottom: 5 }}>{label}</Text>
|
<Text style={{ color: "black", fontSize: 14, marginBottom: 5 }}>
|
||||||
|
{label}
|
||||||
|
</Text>
|
||||||
|
{/* TODO: Handle visible, disabled & read-only */}
|
||||||
<OptionStrip
|
<OptionStrip
|
||||||
value={value}
|
value={value}
|
||||||
options={options}
|
options={options}
|
||||||
|
onValueChanged={this.handleValueChanged}
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
)
|
)
|
||||||
|
|||||||
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 React, { Component } from "react"
|
||||||
import { View, Text, TouchableHighlight } from 'react-native'
|
import { View, Text, TouchableHighlight } from "react-native"
|
||||||
import PropTypes from 'prop-types'
|
import PropTypes from "prop-types"
|
||||||
import autobind from 'autobind-decorator';
|
import autobind from "autobind-decorator"
|
||||||
|
|
||||||
export class OptionStrip extends Component {
|
export class OptionStrip extends Component {
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
options: PropTypes.arrayOf(PropTypes.shape({ value: PropTypes.string, text: PropTypes.string })).isRequired,
|
options: PropTypes.arrayOf(
|
||||||
value: PropTypes.string.isRequired,
|
PropTypes.shape({ value: PropTypes.string, text: PropTypes.string })
|
||||||
|
).isRequired,
|
||||||
|
value: PropTypes.string,
|
||||||
onValueChanged: PropTypes.func,
|
onValueChanged: PropTypes.func,
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props)
|
super(props)
|
||||||
this.state = {
|
this.state = {
|
||||||
selectedOption: this.getSelectedOption(props.options, props.value)
|
selectedOption: this.getSelectedOption(props.options, props.value),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@autobind
|
@autobind
|
||||||
getSelectedOption(options, value) {
|
getSelectedOption(options, value) {
|
||||||
return options.find((option) => (value === option.value)) || options[0]
|
return options.find((option) => value === option.value) || null
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillReceiveProps(newProps) {
|
componentWillReceiveProps(newProps) {
|
||||||
if (newProps.options !== this.props.options || newProps.value !== this.props.value) {
|
if (
|
||||||
this.setState({ selectedIndex: this.getSelectedIndex(newProps.options, newProps.value)})
|
newProps.options !== this.props.options ||
|
||||||
|
newProps.value !== this.props.value
|
||||||
|
) {
|
||||||
|
this.setState({
|
||||||
|
selectedOption: this.getSelectedOption(
|
||||||
|
newProps.options,
|
||||||
|
newProps.value
|
||||||
|
),
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -42,25 +52,50 @@ export class OptionStrip extends Component {
|
|||||||
const { style, options, value } = this.props
|
const { style, options, value } = this.props
|
||||||
const { selectedOption } = this.state
|
const { selectedOption } = this.state
|
||||||
|
|
||||||
|
// TODO: Handle visible, disabled & read-only
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View style={{ flexDirection: 'row' }}>
|
<View style={{ flexDirection: "row" }}>
|
||||||
{options.map((option, index) => (
|
{options.map((option, index) => (
|
||||||
<TouchableHighlight
|
<TouchableHighlight
|
||||||
key={index}
|
key={index}
|
||||||
underlayColor='#3BB0FD'
|
underlayColor="#3BB0FD"
|
||||||
style={[
|
style={[
|
||||||
{ flexGrow: 1, flexBasis: 0, height: 40 },
|
{ flexGrow: 1, flexBasis: 0, height: 40 },
|
||||||
option === selectedOption && { backgroundColor: '#3BB0FD' },
|
option === selectedOption && { backgroundColor: "#3BB0FD" },
|
||||||
index === 0 && { borderTopLeftRadius: 6, borderBottomLeftRadius: 6 },
|
index === 0 && {
|
||||||
index === options.length - 1 && { borderTopRightRadius: 6, borderBottomRightRadius: 6 }
|
borderTopLeftRadius: 6,
|
||||||
|
borderBottomLeftRadius: 6,
|
||||||
|
},
|
||||||
|
index === options.length - 1 && {
|
||||||
|
borderTopRightRadius: 6,
|
||||||
|
borderBottomRightRadius: 6,
|
||||||
|
},
|
||||||
]}
|
]}
|
||||||
onPress={() => this.handlePress(option)}>
|
onPress={() => this.handlePress(option)}>
|
||||||
<View style={[
|
<View
|
||||||
{ flex: 1, justifyContent: 'center', borderTopWidth: 1, borderBottomWidth: 1, borderLeftWidth: 1, borderColor: 'black' },
|
style={[
|
||||||
index === 0 && { borderTopLeftRadius: 6, borderBottomLeftRadius: 6 },
|
{
|
||||||
index === options.length - 1 && { borderRightWidth: 1, borderTopRightRadius: 6, borderBottomRightRadius: 6 }
|
flex: 1,
|
||||||
]}>
|
justifyContent: "center",
|
||||||
<Text style={{ alignSelf: 'center', color: 'black' }}>{option.text}</Text>
|
borderTopWidth: 1,
|
||||||
|
borderBottomWidth: 1,
|
||||||
|
borderLeftWidth: 1,
|
||||||
|
borderColor: "black",
|
||||||
|
},
|
||||||
|
index === 0 && {
|
||||||
|
borderTopLeftRadius: 6,
|
||||||
|
borderBottomLeftRadius: 6,
|
||||||
|
},
|
||||||
|
index === options.length - 1 && {
|
||||||
|
borderRightWidth: 1,
|
||||||
|
borderTopRightRadius: 6,
|
||||||
|
borderBottomRightRadius: 6,
|
||||||
|
},
|
||||||
|
]}>
|
||||||
|
<Text style={{ alignSelf: "center", color: "black" }}>
|
||||||
|
{option.text}
|
||||||
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
</TouchableHighlight>
|
</TouchableHighlight>
|
||||||
))}
|
))}
|
||||||
|
|||||||
46
mobile/src/util.js
Normal file
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 passport from "passport"
|
||||||
import createError from 'http-errors'
|
import createError from "http-errors"
|
||||||
import autobind from 'autobind-decorator'
|
import autobind from "autobind-decorator"
|
||||||
|
|
||||||
@autobind
|
@autobind
|
||||||
export class WorkItemRoutes {
|
export class WorkItemRoutes {
|
||||||
@@ -12,14 +12,31 @@ export class WorkItemRoutes {
|
|||||||
this.mq = container.mq
|
this.mq = container.mq
|
||||||
this.ws = container.ws
|
this.ws = container.ws
|
||||||
|
|
||||||
app.route('/workitems')
|
app
|
||||||
.get(passport.authenticate('bearer', { session: false }), this.listWorkItems)
|
.route("/workitems")
|
||||||
.post(passport.authenticate('bearer', { session: false }), this.createWorkItem)
|
.get(
|
||||||
.put(passport.authenticate('bearer', { session: false }), this.updateWorkItem)
|
passport.authenticate("bearer", { session: false }),
|
||||||
|
this.listWorkItems
|
||||||
|
)
|
||||||
|
.post(
|
||||||
|
passport.authenticate("bearer", { session: false }),
|
||||||
|
this.createWorkItem
|
||||||
|
)
|
||||||
|
.put(
|
||||||
|
passport.authenticate("bearer", { session: false }),
|
||||||
|
this.updateWorkItem
|
||||||
|
)
|
||||||
|
|
||||||
app.route('/workitems/:_id([a-f0-9]{24})')
|
app
|
||||||
.get(passport.authenticate('bearer', { session: false }), this.getWorkItem)
|
.route("/workitems/:_id([a-f0-9]{24})")
|
||||||
.delete(passport.authenticate('bearer', { session: false }), this.deleteWorkItem)
|
.get(
|
||||||
|
passport.authenticate("bearer", { session: false }),
|
||||||
|
this.getWorkItem
|
||||||
|
)
|
||||||
|
.delete(
|
||||||
|
passport.authenticate("bearer", { session: false }),
|
||||||
|
this.deleteWorkItem
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
async listWorkItems(req, res, next) {
|
async listWorkItems(req, res, next) {
|
||||||
@@ -33,25 +50,29 @@ export class WorkItemRoutes {
|
|||||||
const total = await WorkItem.count({})
|
const total = await WorkItem.count({})
|
||||||
|
|
||||||
let workItems = []
|
let workItems = []
|
||||||
let cursor = WorkItem.find(query).limit(limit).skip(skip).cursor().map((doc) => {
|
let cursor = WorkItem.find(query)
|
||||||
return doc.toClient(partial)
|
.limit(limit)
|
||||||
})
|
.skip(skip)
|
||||||
|
.cursor()
|
||||||
|
.map((doc) => {
|
||||||
|
return doc.toClient(partial)
|
||||||
|
})
|
||||||
|
|
||||||
cursor.on('data', (doc) => {
|
cursor.on("data", (doc) => {
|
||||||
workItems.push(doc)
|
workItems.push(doc)
|
||||||
})
|
})
|
||||||
cursor.on('end', () => {
|
cursor.on("end", () => {
|
||||||
res.json({
|
res.json({
|
||||||
total: total,
|
total: total,
|
||||||
offset: skip,
|
offset: skip,
|
||||||
count: workItems.length,
|
count: workItems.length,
|
||||||
items: workItems
|
items: workItems,
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
cursor.on('error', (err) => {
|
cursor.on("error", (err) => {
|
||||||
throw createError.InternalServerError(err.message)
|
throw createError.InternalServerError(err.message)
|
||||||
})
|
})
|
||||||
} catch(err) {
|
} catch (err) {
|
||||||
if (err instanceof createError.HttpError) {
|
if (err instanceof createError.HttpError) {
|
||||||
next(err)
|
next(err)
|
||||||
} else {
|
} else {
|
||||||
@@ -61,7 +82,6 @@ export class WorkItemRoutes {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async createWorkItem(req, res, next) {
|
async createWorkItem(req, res, next) {
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const isAdmin = req.user.administrator
|
const isAdmin = req.user.administrator
|
||||||
|
|
||||||
@@ -77,7 +97,7 @@ export class WorkItemRoutes {
|
|||||||
const newWorkItem = await workItem.save()
|
const newWorkItem = await workItem.save()
|
||||||
|
|
||||||
res.json(newWorkItem.toClient())
|
res.json(newWorkItem.toClient())
|
||||||
} catch(err) {
|
} catch (err) {
|
||||||
if (err instanceof createError.HttpError) {
|
if (err instanceof createError.HttpError) {
|
||||||
next(err)
|
next(err)
|
||||||
} else {
|
} else {
|
||||||
@@ -96,7 +116,7 @@ export class WorkItemRoutes {
|
|||||||
|
|
||||||
// Do this here because Mongoose will add it automatically otherwise
|
// Do this here because Mongoose will add it automatically otherwise
|
||||||
if (!req.body._id) {
|
if (!req.body._id) {
|
||||||
throw createError.BadRequest('No _id given in body')
|
throw createError.BadRequest("No _id given in body")
|
||||||
}
|
}
|
||||||
|
|
||||||
let WorkItem = this.db.WorkItem
|
let WorkItem = this.db.WorkItem
|
||||||
@@ -105,13 +125,15 @@ export class WorkItemRoutes {
|
|||||||
try {
|
try {
|
||||||
workItemUpdates = new WorkItem(req.body)
|
workItemUpdates = new WorkItem(req.body)
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
throw createError.BadRequest('Invalid data')
|
throw createError.BadRequest("Invalid data")
|
||||||
}
|
}
|
||||||
|
|
||||||
const foundWorkItem = await WorkItem.findById(workItemUpdates._id)
|
const foundWorkItem = await WorkItem.findById(workItemUpdates._id)
|
||||||
|
|
||||||
if (!foundWorkItem) {
|
if (!foundWorkItem) {
|
||||||
return next(createError.NotFound(`WorkItem with _id ${_id} was not found`))
|
return next(
|
||||||
|
createError.NotFound(`WorkItem with _id ${_id} was not found`)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
foundWorkItem.merge(workItemUpdates)
|
foundWorkItem.merge(workItemUpdates)
|
||||||
@@ -119,7 +141,7 @@ export class WorkItemRoutes {
|
|||||||
const savedWorkItem = await foundWorkItem.save()
|
const savedWorkItem = await foundWorkItem.save()
|
||||||
|
|
||||||
res.json(savedWorkItem.toClient())
|
res.json(savedWorkItem.toClient())
|
||||||
} catch(err) {
|
} catch (err) {
|
||||||
if (err instanceof createError.HttpError) {
|
if (err instanceof createError.HttpError) {
|
||||||
next(err)
|
next(err)
|
||||||
} else {
|
} else {
|
||||||
@@ -139,7 +161,7 @@ export class WorkItemRoutes {
|
|||||||
}
|
}
|
||||||
|
|
||||||
res.json(workItem.toClient())
|
res.json(workItem.toClient())
|
||||||
} catch(err) {
|
} catch (err) {
|
||||||
if (err instanceof createError.HttpError) {
|
if (err instanceof createError.HttpError) {
|
||||||
next(err)
|
next(err)
|
||||||
} else {
|
} else {
|
||||||
@@ -165,7 +187,7 @@ export class WorkItemRoutes {
|
|||||||
}
|
}
|
||||||
|
|
||||||
res.json({})
|
res.json({})
|
||||||
} catch(err) {
|
} catch (err) {
|
||||||
if (err instanceof createError.HttpError) {
|
if (err instanceof createError.HttpError) {
|
||||||
next(err)
|
next(err)
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
Reference in New Issue
Block a user