Closing many tickets

This commit is contained in:
John Lyon-Smith
2018-04-22 15:22:36 -07:00
parent 4bc0a6cd30
commit 5cb13f7498
31 changed files with 392 additions and 100 deletions

Binary file not shown.

View File

@@ -2,7 +2,7 @@ import EventEmitter from "eventemitter3"
import io from "socket.io-client"
import { AsyncStorage } from "react-native"
import autobind from "autobind-decorator"
import { localIPAddr } from "./config"
import { config } from "./config"
const authTokenKeyName = "AuthToken"
const backendKeyName = "Backend"
@@ -37,7 +37,7 @@ class API extends EventEmitter {
static urls = {
normal: "https://dar.kss.us.com/api",
test: "https://dar-test.kss.us.com/api",
local: `http://${localIPAddr || "localhost"}:3001`,
local: `http://${config.localIPAddr || "localhost"}:3001`,
}
constructor() {
@@ -384,6 +384,45 @@ class API extends EventEmitter {
return this.delete("/activities/" + _id)
}
getAddress(coord) {
var promise = new Promise((resolve, reject) => {
let fetchOptions = {
method: "GET",
mode: "no-cors",
}
fetch(
config.googleGeocodeURL +
`?latlng=${coord.latitude},${coord.longitude}&key=${
config.googleAPIKey
}`,
fetchOptions
)
.then((res) => {
return Promise.all([Promise.resolve(res), res.json()])
})
.then((arr) => {
let [res, responseBody] = arr
if (res.ok) {
let address = ""
if (responseBody.results && responseBody.result.length > 0) {
address = responseBody.results[0].formatted_address
}
resolve(address)
} else {
reject(new APIError(res.status, responseBody.message))
}
})
.catch((error) => {
reject(new NetworkError(error.message))
})
})
return promise
}
upload(file, progressCallback) {
return new Promise((resolve, reject) => {
const chunkSize = 32 * 1024

View File

@@ -14,26 +14,20 @@ import {
} from "react-viro"
import autobind from "autobind-decorator"
import backImage from "./images/back.png"
const styles = {
buttons: {
height: 80,
width: 80,
},
}
import { config } from "../config"
const shapes = {
hardhat: {
shape: require("./models/hardhat_obj.obj"),
materials: [require("./models/hardhat.mtl")],
order: {
source: require("./models/hardhat_obj.obj"),
resources: [require("./models/hardhat.mtl")],
},
question: {
shape: require("./models/question_obj.obj"),
materials: [require("./models/question.mtl")],
complaint: {
source: require("./models/question_obj.obj"),
resources: [require("./models/question.mtl")],
},
clipboard: {
shape: require("./models/clipboard_obj.obj"),
materials: [require("./models/clipboard.mtl")],
inspection: {
source: require("./models/clipboard_obj.obj"),
resources: [require("./models/clipboard.mtl")],
},
}
const distance = (vectorA, vectorB) => {
@@ -139,11 +133,16 @@ class WorkItemSceneAR extends React.Component {
@autobind
handleClick(position, source) {
// this.props.history.replace("/activity")
const { workItemId } = this.props
this.props.history.replace(
`/activity${workItemId ? "?workItemId=" + workItemId : ""}`
)
}
render() {
const { position, scale, rotation, shouldBillboard } = this.state
const shape = shapes[this.props.workItemType]
return (
<ViroARScene ref={(ref) => (this.arScene = ref)}>
@@ -165,14 +164,16 @@ class WorkItemSceneAR extends React.Component {
shadowFarZ={6}
shadowOpacity={0.9}
/>
<Viro3DObject
position={[0, 0, -1]}
source={shapes["hardhat"].shape}
resources={shapes["hardhat"].materials}
type="OBJ"
onLoadEnd={this.handleLoadEnd}
onClick={this.handleClick}
/>
{shape && (
<Viro3DObject
position={[0, 0, -1]}
source={shape.source}
resources={shape.resources}
type="OBJ"
onLoadEnd={this.handleLoadEnd}
onClick={this.handleClick}
/>
)}
<ViroSurface
rotation={[-90, 0, 0]}
position={[0, -0.001, 0]}
@@ -190,6 +191,15 @@ class WorkItemSceneAR extends React.Component {
export class ARViewer extends React.Component {
constructor(props) {
super(props)
const { search } = this.props.location
if (search) {
const params = new URLSearchParams(search)
this.workItemId = params.get("workItemId")
this.workItemType = params.get("workItemType")
}
}
@autobind
@@ -202,13 +212,23 @@ export class ARViewer extends React.Component {
<View style={{ width: "100%", height: "100%" }}>
<ViroARSceneNavigator
style={{ width: "100%", height: "100%" }}
apiKey="06F37B6A-74DA-4A83-965A-7DE2209A5C46"
initialScene={{ scene: WorkItemSceneAR }}
apiKey={config.viroAPIKey}
initialScene={{
scene: WorkItemSceneAR,
passProps: {
history: this.props.history,
workItemId: this.workItemId,
workItemType: this.workItemType,
},
}}
/>
<View style={{ position: "absolute", left: 30, right: 0, top: 50 }}>
<TouchableHighlight
style={styles.buttons}
style={{
height: 80,
width: 80,
}}
onPress={this.handleBackPress}
underlayColor={"#00000000"}>
<Image source={backImage} />

View File

@@ -15,11 +15,11 @@ import { FormBinder } from "react-form-binder"
import {
Icon,
Header,
PhotoButton,
BoundInput,
BoundButton,
BoundOptionStrip,
BoundHeader,
PhotoPanel,
} from "../ui"
import { MessageModal } from "../Modal"
import autobind from "autobind-decorator"
@@ -73,6 +73,10 @@ export class Activity extends React.Component {
isValid: (r, v) => v !== "",
alwaysGet: true,
},
location: {
isValid: (r, v) => v !== "",
isReadOnly: true,
},
}
constructor(props) {
@@ -218,6 +222,8 @@ export class Activity extends React.Component {
<BoundInput binder={binder} name="location" label="Location:" />
<MapView
style={{
flexDirection: "column",
justifyContent: "center",
width: "100%",
height: 400,
marginTop: 10,
@@ -238,31 +244,20 @@ export class Activity extends React.Component {
longitude: -79.384293,
latitudeDelta: 0.0922,
longitudeDelta: 0.0421,
}}
/>
}}>
<Icon
name="target"
size={24}
pointerEvents={false}
style={{
position: "absolute",
alignSelf: "center",
}}
/>
</MapView>
</View>
<View style={styles.panel}>
<Text style={styles.label}>Pictures:</Text>
<View
style={{
flexDirection: "row",
justifyContent: "space-between",
marginBottom: 5,
}}>
<PhotoButton />
<PhotoButton />
<PhotoButton />
</View>
<View
style={{
flexDirection: "row",
justifyContent: "space-between",
marginTop: 5,
}}>
<PhotoButton />
<PhotoButton />
<PhotoButton />
</View>
<PhotoPanel />
</View>
{isIphoneX ? <View style={{ height: 30, width: "100%" }} /> : null}
</ScrollView>

View File

@@ -71,11 +71,11 @@ export default class App extends React.Component {
<ProtectedRoute exact path="/home" component={Home} />
<ProtectedRoute exact path="/arviewer" component={ARViewer} />
<ProtectedRoute exact path="/activity" component={Activity} />
<ProtectedRoute exact admin path="/workitem" component={WorkItem} />
<ProtectedRoute exact admin path="/workItem" component={WorkItem} />
<ProtectedRoute
exact
admin
path="/workitemlist"
path="/workItemList"
component={WorkItemList}
/>
<DefaultRoute />

View File

@@ -3,5 +3,5 @@ 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={"/workitem"} />} />
return <Route render={() => <Redirect to={"/activity"} />} />
}

View File

@@ -10,7 +10,7 @@ import {
Button,
TouchableWithoutFeedback,
} from "react-native"
import { MessageModal, ApiModal } from "../Modal"
import { MessageModal, ApiModal, WaitModal } from "../Modal"
import logoImage from "./images/deighton.png"
import { FormBinder } from "react-form-binder"
import { api } from "../API"
@@ -19,7 +19,7 @@ import KeyboardSpacer from "react-native-keyboard-spacer"
import { versionInfo } from "../version"
import autobind from "autobind-decorator"
import { isIphoneX } from "react-native-iphone-x-helper"
import { defaultUser } from "../config"
import { config } from "../config"
export class Login extends React.Component {
static bindings = {
@@ -76,9 +76,10 @@ export class Login extends React.Component {
constructor(props) {
super(props)
this.state = {
binder: new FormBinder({ email: defaultUser }, Login.bindings),
binder: new FormBinder({ email: config.defaultUser }, Login.bindings),
messageModal: null,
apiModal: null,
waitModal: null,
}
}
@@ -88,6 +89,7 @@ export class Login extends React.Component {
let { history } = this.props
if (obj) {
this.setState({ waitModal: { message: "Loggin In..." } })
api
.login(obj.email.trim(), obj.password, obj.rememberMe)
.then((user) => {
@@ -95,6 +97,7 @@ export class Login extends React.Component {
})
.catch((error) => {
this.setState({
waitModal: null,
messageModal: {
icon: "hand",
message: "Unable to login",
@@ -178,6 +181,10 @@ export class Login extends React.Component {
{versionInfo.fullVersion}
</Text>
</View>
<WaitModal
open={!!waitModal}
message={waitModal ? waitModal.message : ""}
/>
<MessageModal
open={!!messageModal}
icon={messageModal ? messageModal.icon : ""}

View File

@@ -11,18 +11,25 @@ import {
PermissionsAndroid,
Platform,
} from "react-native"
import MapView, { Marker } from "react-native-maps"
import MapView, { Marker, Callout } from "react-native-maps"
import { Icon, Header } from "../ui"
import { MessageModal } from "../Modal"
import { api } from "../API"
import autobind from "autobind-decorator"
import { ifIphoneX } from "react-native-iphone-x-helper"
import { workItemTypeText, pad, regionContainingPoints } from "../util"
import {
geoDistance,
workItemTypeText,
pad,
regionContainingPoints,
} from "../util"
import { ensurePermissions } from "../App"
import { versionInfo } from "../version"
import { minGPSAccuracy } from "../config"
import { config } from "../config"
import KeyboardSpacer from "react-native-keyboard-spacer"
import pinImage from "./images/pin.png"
import hardhatPinImage from "./images/hardhat.png"
import clipboardPinImage from "./images/clipboard.png"
import questionPinImage from "./images/question.png"
const neverAskForLocationPermissionKeyName = "NeverAskForLocationPermission"
const neverAskForCameraKeyName = "NeverAskForCameraPermission"
@@ -40,7 +47,8 @@ export class Home extends React.Component {
longitudeDelta: 0.0922,
},
positionInfo: null,
disableARViewer: true,
haveCameraPermission: false,
workItemDistance: -1,
}
ensurePermissions(
@@ -71,7 +79,7 @@ export class Home extends React.Component {
results[PermissionsAndroid.PERMISSIONS.CAMERA] ===
PermissionsAndroid.RESULTS.GRANTED
) {
this.setState({ disableARViewer: false })
this.setState({ haveCameraPerm: true })
}
},
() => {
@@ -80,9 +88,9 @@ export class Home extends React.Component {
icon: "hand",
message:
"You have denied the app access to phone features it needs to function. " +
"Some parts of the app are disabled. To enable these features in future " +
"please go to Settings.",
detail: "",
"Some parts of the app are disabled.",
detail:
"To enable these features in future " + "please go to Settings.",
},
})
}
@@ -130,11 +138,37 @@ export class Home extends React.Component {
viewOffset: 45,
})
}
if (this.state.positionInfo) {
const coords = this.state.positionInfo.coords
const workItem = sections[sectionIndex]
const [lng, lat] = workItem.location.coordinates
this.setState({
workItemDistance: geoDistance(
coords.latitude,
coords.longitude,
lat,
lng,
"K"
).toFixed(2),
})
}
}
@autobind
handleWorkItemsListPress() {
this.props.history.push("/workitemlist")
const { positionInfo } = this.state
this.props.history.push(
`/workItemList${
positionInfo
? "?latLng=" +
coords.latitude.toString() +
"," +
coords.longitude.toString()
: ""
}`
)
}
@autobind
@@ -149,7 +183,30 @@ export class Home extends React.Component {
@autobind
handleGlassesPress() {
this.props.history.push("/arviewer")
const { lat: lat1, lng: lng1 } = this.state.positionInfo.coords
const closestWorkItem = null
const shortestDistance = config.minDistanceToItem
this.state.sections.forEach((workItem) => {
const [lng2, lat2] = workItem.location.coordinates
const distance = geoDistance(lat1, lng1, lat2, lng2, "K") * 1000
if (distance <= shortestDistance) {
closestWorkItem = workItem
shortestDistance = distance
}
})
this.props.history.push(
`/arviewer${
closestWorkItem
? "?workItemId=" +
closestWorkItem._id +
"&workItemType=" +
closestWorkItem.workItemType
: ""
}`
)
}
@autobind
@@ -176,7 +233,8 @@ export class Home extends React.Component {
region,
positionInfo,
messageModal,
disableARViewer,
haveCameraPermission,
workItemDistance,
} = this.state
return (
@@ -191,8 +249,10 @@ export class Home extends React.Component {
leftButton={{ icon: "logout", onPress: this.handleLogoutPress }}
rightButton={{ icon: "glasses", onPress: this.handleGlassesPress }}
disabled={
!(positionInfo && positionInfo.coords.accuracy <= minGPSAccuracy) ||
disableARViewer
!(
positionInfo &&
positionInfo.coords.accuracy <= config.minGPSAccuracy
) || !haveCameraPermission
}
/>
<MapView
@@ -216,16 +276,31 @@ export class Home extends React.Component {
<Marker
key={index}
coordinate={workItem.coordinate}
title={
pad(workItem.ticketNumber, 4) +
": " +
workItemTypeText[workItem.workItemType]
}
description={workItem.address}
anchor={{ x: 0.5, y: 1.0 }}
image={pinImage}
onPress={(e) => this.handleMarkerPress(e, index)}
/>
image={
workItem.workItemType === "inspection"
? clipboardPinImage
: workItem.workItemType === "complaint"
? questionPinImage
: hardhatPinImage
}
onPress={(e) => this.handleMarkerPress(e, index)}>
<Callout>
<View>
<Text>
{pad(workItem.ticketNumber, 4) +
": " +
workItemTypeText[workItem.workItemType]}
</Text>
<Text>
{workItem.address} ({this.workItemDistance > 0
? this.workItemDistance.toString()
: "?"}{" "}
km)
</Text>
</View>
</Callout>
</Marker>
))}
</MapView>
<View
@@ -329,7 +404,7 @@ export class Home extends React.Component {
{activity.resolution}
</Text>
<Text style={{ fontSize: 14, color: "gray" }}>
{activity.address || "..."}
{activity.when}
</Text>
</View>
<Icon

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

View File

@@ -0,0 +1,50 @@
import React, { Component } from "react"
import Modal from "react-native-modal"
import PropTypes from "prop-types"
import { View, Image, TouchableOpacity } from "react-native"
import { Icon } from "../ui"
import autobind from "autobind-decorator"
export class ImageViewerModal extends Component {
static propTypes = {
open: PropTypes.bool,
imageURL: PropTypes.string.isRequired,
onDismiss: PropTypes.func,
}
@autobind
handleButtonPress() {
const { onDismiss } = this.props
if (onDismiss) {
onDismiss()
}
}
render() {
const { open, icon, message, detail } = this.props
return (
<Modal isVisible={open}>
<View
style={{
width: "100%",
height: "100%",
}}>
<Image source={imageURL} />
<View style={{ position: "absolute", left: 30, right: 0, top: 50 }}>
<TouchableHighlight
style={{
height: 80,
width: 80,
}}
onPress={this.handleBackPress}
underlayColor={"#00000000"}>
<Image source={backImage} />
</TouchableHighlight>
</View>
</View>
</Modal>
)
}
}

View File

@@ -0,0 +1,29 @@
import React, { Component } from "react"
import Modal from "react-native-modal"
import PropTypes from "prop-types"
import { View, ActivityIndicator } from "react-native"
export class WaitModal extends Component {
static propTypes = {
open: PropTypes.bool,
message: PropTypes.string.isRequired,
}
render() {
const { open, icon, message, detail } = this.props
return (
<Modal isVisible={open}>
<View
style={{
flexDirection: "column",
justifyContent: "center",
backgroundColor: "#FFFFFF",
}}>
<ActivityIndicator size="large" color="#0000FF" />
<Text style={{ marginTop: 5, fontSize: 18 }}>{message}</Text>
</View>
</Modal>
)
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@@ -1,2 +1,4 @@
export { MessageModal } from "./MessageModal"
export { ApiModal } from "./ApiModal"
export { WaitModal } from "./WaitModal"
export { ImageViewerModal } from "./ImageViewerModal"

View File

@@ -59,6 +59,10 @@ export class WorkItem extends React.Component {
isValid: (r, v) => v !== "",
isReadOnly: true,
},
address: {
isValid: true,
isReadOnly: true,
},
details: {
isValid: (r, v) => v !== "",
},
@@ -118,6 +122,12 @@ export class WorkItem extends React.Component {
}
}
componentWillUnmount() {
if (this.geoCodeTimer) {
clearTimeout(this.geoCodeTimer)
}
}
@autobind
handleBackPress() {
const { history } = this.props
@@ -180,11 +190,32 @@ export class WorkItem extends React.Component {
@autobind
handleRegionChange(region) {
const { latitude, longitude } = region
if (this.latLngInput) {
this.latLngInput.handleChangeText(
formatLatLng(region.latitude, region.longitude)
)
this.latLngInput.handleChangeText(formatLatLng(latitude, longitude))
}
if (this.geoCodeTimer) {
clearTimeout(this.geoCodeTimer)
}
this.geoCodeTimer = setTimeout(
() => this.handleStartAddressLookup({ latitude, longitude }),
config.geocodeDelayMilliseconds
)
}
@autobind
handleStartAddressLookup(latLng) {
api
.addressLookup(latLng)
.then((address) => {
this.setState({ address })
})
.catch(() => {
this.setState({ address: "" })
})
}
render() {
@@ -252,6 +283,12 @@ export class WorkItem extends React.Component {
name="location"
label="Location:"
/>
<BoundInput
ref={(ref) => (this.addressInput = ref)}
binder={binder}
name="address"
label="Address:"
/>
</View>
<View style={styles.panel}>
<PhotoPanel />

View File

@@ -19,6 +19,7 @@ import {
formatLatLng,
parseLatLng,
pad,
geoDistance,
} from "../util"
const styles = StyleSheet.create({
@@ -36,6 +37,25 @@ export class WorkItemList extends React.Component {
this.state = {
messageModal: null,
}
const { search } = this.props.location
if (search) {
const params = new URLSearchParams(search)
const latLng = params.get("latLng")
if (latLng) {
const [lat, lng] = latLng.split(",")
if (lat && lng) {
this.position = {
latitude: parseFloat(lat),
longitude: parseFloat(lng),
}
}
}
}
api
.listWorkItems()
.then((list) => {
@@ -54,7 +74,7 @@ export class WorkItemList extends React.Component {
@autobind
handleItemSelect(item, ref) {
this.props.history.push(`/workitem?id=${item._id}`)
this.props.history.push(`/workItem?id=${item._id}`)
}
@autobind
@@ -95,7 +115,7 @@ export class WorkItemList extends React.Component {
@autobind
handleDonePress() {
this.props.history.push("/workitem")
this.props.history.push("/workItem")
}
@autobind
@@ -159,7 +179,17 @@ export class WorkItemList extends React.Component {
{workItemTypeText[item.workItemType]}
</Text>
<Text style={{ fontSize: 14, color: "gray" }}>
{`${item.address || "..."} | ??? mi`}
{`${item.address || "..."} | ${
this.position
? geoDistance(
this.position.latitude,
this.position.longitude,
item.location.coordinates[1],
item.location.coordinates[0],
"K"
).toFixed(2)
: "?"
} km`}
</Text>
</View>
<Icon

View File

@@ -1,5 +1,18 @@
export const localIPAddr = "192.168.1.175"
export const defaultUser = "john@lyon-smith.org"
// export const defaultUser = ""
// export const minGPSAccuracy = 20
export const minGPSAccuracy = 100
import React from "react"
import { Platform } from "react-native"
export const config = {
localIPAddr: "192.168.1.175",
viroAPIKey: "06F37B6A-74DA-4A83-965A-7DE2209A5C46",
googleAPIKey:
Platform.os === "ios"
? "AIzaSyDN4E_vzO4cKjKHkMg_49hX1GBnU34kx4U"
: "AIzaSyAC7r1GjMFL1atZdbEcFSdCaXDrPnISqTc",
googleGeocodeURL: "https://maps.googleapis.com/maps/api/geocode/json",
defaultUser: "john@lyon-smith.org",
//defaultUser: "",
//minGPSAccuracy: 20,
minGPSAccuracy: 100,
minDistanceToItem: 10,
geocodeDelayMilliseconds: 500,
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 937 B

After

Width:  |  Height:  |  Size: 654 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 778 B

View File

@@ -16,11 +16,6 @@ export let activitySchema = new Schema(
},
notes: { type: String, required: true },
when: { type: Date, required: true },
location: {
type: { type: String },
coordinates: [Number],
},
address: String,
fromStreetNumber: Number,
toStreetNumber: Number,
photos: [Schema.Types.ObjectId],