Closing many tickets
@@ -2,7 +2,7 @@ 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"
|
||||||
import autobind from "autobind-decorator"
|
import autobind from "autobind-decorator"
|
||||||
import { localIPAddr } from "./config"
|
import { config } from "./config"
|
||||||
|
|
||||||
const authTokenKeyName = "AuthToken"
|
const authTokenKeyName = "AuthToken"
|
||||||
const backendKeyName = "Backend"
|
const backendKeyName = "Backend"
|
||||||
@@ -37,7 +37,7 @@ class API extends EventEmitter {
|
|||||||
static urls = {
|
static urls = {
|
||||||
normal: "https://dar.kss.us.com/api",
|
normal: "https://dar.kss.us.com/api",
|
||||||
test: "https://dar-test.kss.us.com/api",
|
test: "https://dar-test.kss.us.com/api",
|
||||||
local: `http://${localIPAddr || "localhost"}:3001`,
|
local: `http://${config.localIPAddr || "localhost"}:3001`,
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
@@ -384,6 +384,45 @@ class API extends EventEmitter {
|
|||||||
return this.delete("/activities/" + _id)
|
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) {
|
upload(file, progressCallback) {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const chunkSize = 32 * 1024
|
const chunkSize = 32 * 1024
|
||||||
|
|||||||
@@ -14,26 +14,20 @@ import {
|
|||||||
} from "react-viro"
|
} from "react-viro"
|
||||||
import autobind from "autobind-decorator"
|
import autobind from "autobind-decorator"
|
||||||
import backImage from "./images/back.png"
|
import backImage from "./images/back.png"
|
||||||
|
import { config } from "../config"
|
||||||
const styles = {
|
|
||||||
buttons: {
|
|
||||||
height: 80,
|
|
||||||
width: 80,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
const shapes = {
|
const shapes = {
|
||||||
hardhat: {
|
order: {
|
||||||
shape: require("./models/hardhat_obj.obj"),
|
source: require("./models/hardhat_obj.obj"),
|
||||||
materials: [require("./models/hardhat.mtl")],
|
resources: [require("./models/hardhat.mtl")],
|
||||||
},
|
},
|
||||||
question: {
|
complaint: {
|
||||||
shape: require("./models/question_obj.obj"),
|
source: require("./models/question_obj.obj"),
|
||||||
materials: [require("./models/question.mtl")],
|
resources: [require("./models/question.mtl")],
|
||||||
},
|
},
|
||||||
clipboard: {
|
inspection: {
|
||||||
shape: require("./models/clipboard_obj.obj"),
|
source: require("./models/clipboard_obj.obj"),
|
||||||
materials: [require("./models/clipboard.mtl")],
|
resources: [require("./models/clipboard.mtl")],
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
const distance = (vectorA, vectorB) => {
|
const distance = (vectorA, vectorB) => {
|
||||||
@@ -139,11 +133,16 @@ class WorkItemSceneAR extends React.Component {
|
|||||||
|
|
||||||
@autobind
|
@autobind
|
||||||
handleClick(position, source) {
|
handleClick(position, source) {
|
||||||
// this.props.history.replace("/activity")
|
const { workItemId } = this.props
|
||||||
|
|
||||||
|
this.props.history.replace(
|
||||||
|
`/activity${workItemId ? "?workItemId=" + workItemId : ""}`
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { position, scale, rotation, shouldBillboard } = this.state
|
const { position, scale, rotation, shouldBillboard } = this.state
|
||||||
|
const shape = shapes[this.props.workItemType]
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ViroARScene ref={(ref) => (this.arScene = ref)}>
|
<ViroARScene ref={(ref) => (this.arScene = ref)}>
|
||||||
@@ -165,14 +164,16 @@ class WorkItemSceneAR extends React.Component {
|
|||||||
shadowFarZ={6}
|
shadowFarZ={6}
|
||||||
shadowOpacity={0.9}
|
shadowOpacity={0.9}
|
||||||
/>
|
/>
|
||||||
<Viro3DObject
|
{shape && (
|
||||||
position={[0, 0, -1]}
|
<Viro3DObject
|
||||||
source={shapes["hardhat"].shape}
|
position={[0, 0, -1]}
|
||||||
resources={shapes["hardhat"].materials}
|
source={shape.source}
|
||||||
type="OBJ"
|
resources={shape.resources}
|
||||||
onLoadEnd={this.handleLoadEnd}
|
type="OBJ"
|
||||||
onClick={this.handleClick}
|
onLoadEnd={this.handleLoadEnd}
|
||||||
/>
|
onClick={this.handleClick}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
<ViroSurface
|
<ViroSurface
|
||||||
rotation={[-90, 0, 0]}
|
rotation={[-90, 0, 0]}
|
||||||
position={[0, -0.001, 0]}
|
position={[0, -0.001, 0]}
|
||||||
@@ -190,6 +191,15 @@ class WorkItemSceneAR extends React.Component {
|
|||||||
export class ARViewer extends React.Component {
|
export class ARViewer extends React.Component {
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(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
|
@autobind
|
||||||
@@ -202,13 +212,23 @@ export class ARViewer extends React.Component {
|
|||||||
<View style={{ width: "100%", height: "100%" }}>
|
<View style={{ width: "100%", height: "100%" }}>
|
||||||
<ViroARSceneNavigator
|
<ViroARSceneNavigator
|
||||||
style={{ width: "100%", height: "100%" }}
|
style={{ width: "100%", height: "100%" }}
|
||||||
apiKey="06F37B6A-74DA-4A83-965A-7DE2209A5C46"
|
apiKey={config.viroAPIKey}
|
||||||
initialScene={{ scene: WorkItemSceneAR }}
|
initialScene={{
|
||||||
|
scene: WorkItemSceneAR,
|
||||||
|
passProps: {
|
||||||
|
history: this.props.history,
|
||||||
|
workItemId: this.workItemId,
|
||||||
|
workItemType: this.workItemType,
|
||||||
|
},
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<View style={{ position: "absolute", left: 30, right: 0, top: 50 }}>
|
<View style={{ position: "absolute", left: 30, right: 0, top: 50 }}>
|
||||||
<TouchableHighlight
|
<TouchableHighlight
|
||||||
style={styles.buttons}
|
style={{
|
||||||
|
height: 80,
|
||||||
|
width: 80,
|
||||||
|
}}
|
||||||
onPress={this.handleBackPress}
|
onPress={this.handleBackPress}
|
||||||
underlayColor={"#00000000"}>
|
underlayColor={"#00000000"}>
|
||||||
<Image source={backImage} />
|
<Image source={backImage} />
|
||||||
|
|||||||
@@ -15,11 +15,11 @@ import { FormBinder } from "react-form-binder"
|
|||||||
import {
|
import {
|
||||||
Icon,
|
Icon,
|
||||||
Header,
|
Header,
|
||||||
PhotoButton,
|
|
||||||
BoundInput,
|
BoundInput,
|
||||||
BoundButton,
|
BoundButton,
|
||||||
BoundOptionStrip,
|
BoundOptionStrip,
|
||||||
BoundHeader,
|
BoundHeader,
|
||||||
|
PhotoPanel,
|
||||||
} from "../ui"
|
} from "../ui"
|
||||||
import { MessageModal } from "../Modal"
|
import { MessageModal } from "../Modal"
|
||||||
import autobind from "autobind-decorator"
|
import autobind from "autobind-decorator"
|
||||||
@@ -73,6 +73,10 @@ export class Activity extends React.Component {
|
|||||||
isValid: (r, v) => v !== "",
|
isValid: (r, v) => v !== "",
|
||||||
alwaysGet: true,
|
alwaysGet: true,
|
||||||
},
|
},
|
||||||
|
location: {
|
||||||
|
isValid: (r, v) => v !== "",
|
||||||
|
isReadOnly: true,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
@@ -218,6 +222,8 @@ export class Activity extends React.Component {
|
|||||||
<BoundInput binder={binder} name="location" label="Location:" />
|
<BoundInput binder={binder} name="location" label="Location:" />
|
||||||
<MapView
|
<MapView
|
||||||
style={{
|
style={{
|
||||||
|
flexDirection: "column",
|
||||||
|
justifyContent: "center",
|
||||||
width: "100%",
|
width: "100%",
|
||||||
height: 400,
|
height: 400,
|
||||||
marginTop: 10,
|
marginTop: 10,
|
||||||
@@ -238,31 +244,20 @@ export class Activity extends React.Component {
|
|||||||
longitude: -79.384293,
|
longitude: -79.384293,
|
||||||
latitudeDelta: 0.0922,
|
latitudeDelta: 0.0922,
|
||||||
longitudeDelta: 0.0421,
|
longitudeDelta: 0.0421,
|
||||||
}}
|
}}>
|
||||||
/>
|
<Icon
|
||||||
|
name="target"
|
||||||
|
size={24}
|
||||||
|
pointerEvents={false}
|
||||||
|
style={{
|
||||||
|
position: "absolute",
|
||||||
|
alignSelf: "center",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</MapView>
|
||||||
</View>
|
</View>
|
||||||
<View style={styles.panel}>
|
<View style={styles.panel}>
|
||||||
<Text style={styles.label}>Pictures:</Text>
|
<PhotoPanel />
|
||||||
<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>
|
|
||||||
</View>
|
</View>
|
||||||
{isIphoneX ? <View style={{ height: 30, width: "100%" }} /> : null}
|
{isIphoneX ? <View style={{ height: 30, width: "100%" }} /> : null}
|
||||||
</ScrollView>
|
</ScrollView>
|
||||||
|
|||||||
@@ -71,11 +71,11 @@ export default class App extends React.Component {
|
|||||||
<ProtectedRoute exact path="/home" component={Home} />
|
<ProtectedRoute exact path="/home" component={Home} />
|
||||||
<ProtectedRoute exact path="/arviewer" component={ARViewer} />
|
<ProtectedRoute exact path="/arviewer" component={ARViewer} />
|
||||||
<ProtectedRoute exact path="/activity" component={Activity} />
|
<ProtectedRoute exact path="/activity" component={Activity} />
|
||||||
<ProtectedRoute exact admin path="/workitem" component={WorkItem} />
|
<ProtectedRoute exact admin path="/workItem" component={WorkItem} />
|
||||||
<ProtectedRoute
|
<ProtectedRoute
|
||||||
exact
|
exact
|
||||||
admin
|
admin
|
||||||
path="/workitemlist"
|
path="/workItemList"
|
||||||
component={WorkItemList}
|
component={WorkItemList}
|
||||||
/>
|
/>
|
||||||
<DefaultRoute />
|
<DefaultRoute />
|
||||||
|
|||||||
@@ -3,5 +3,5 @@ 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={"/workitem"} />} />
|
return <Route render={() => <Redirect to={"/activity"} />} />
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import {
|
|||||||
Button,
|
Button,
|
||||||
TouchableWithoutFeedback,
|
TouchableWithoutFeedback,
|
||||||
} from "react-native"
|
} from "react-native"
|
||||||
import { MessageModal, ApiModal } from "../Modal"
|
import { MessageModal, ApiModal, WaitModal } from "../Modal"
|
||||||
import logoImage from "./images/deighton.png"
|
import logoImage from "./images/deighton.png"
|
||||||
import { FormBinder } from "react-form-binder"
|
import { FormBinder } from "react-form-binder"
|
||||||
import { api } from "../API"
|
import { api } from "../API"
|
||||||
@@ -19,7 +19,7 @@ import KeyboardSpacer from "react-native-keyboard-spacer"
|
|||||||
import { versionInfo } from "../version"
|
import { versionInfo } from "../version"
|
||||||
import autobind from "autobind-decorator"
|
import autobind from "autobind-decorator"
|
||||||
import { isIphoneX } from "react-native-iphone-x-helper"
|
import { isIphoneX } from "react-native-iphone-x-helper"
|
||||||
import { defaultUser } from "../config"
|
import { config } from "../config"
|
||||||
|
|
||||||
export class Login extends React.Component {
|
export class Login extends React.Component {
|
||||||
static bindings = {
|
static bindings = {
|
||||||
@@ -76,9 +76,10 @@ export class Login extends React.Component {
|
|||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props)
|
super(props)
|
||||||
this.state = {
|
this.state = {
|
||||||
binder: new FormBinder({ email: defaultUser }, Login.bindings),
|
binder: new FormBinder({ email: config.defaultUser }, Login.bindings),
|
||||||
messageModal: null,
|
messageModal: null,
|
||||||
apiModal: null,
|
apiModal: null,
|
||||||
|
waitModal: null,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -88,6 +89,7 @@ export class Login extends React.Component {
|
|||||||
let { history } = this.props
|
let { history } = this.props
|
||||||
|
|
||||||
if (obj) {
|
if (obj) {
|
||||||
|
this.setState({ waitModal: { message: "Loggin In..." } })
|
||||||
api
|
api
|
||||||
.login(obj.email.trim(), obj.password, obj.rememberMe)
|
.login(obj.email.trim(), obj.password, obj.rememberMe)
|
||||||
.then((user) => {
|
.then((user) => {
|
||||||
@@ -95,6 +97,7 @@ export class Login extends React.Component {
|
|||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
this.setState({
|
this.setState({
|
||||||
|
waitModal: null,
|
||||||
messageModal: {
|
messageModal: {
|
||||||
icon: "hand",
|
icon: "hand",
|
||||||
message: "Unable to login",
|
message: "Unable to login",
|
||||||
@@ -178,6 +181,10 @@ export class Login extends React.Component {
|
|||||||
{versionInfo.fullVersion}
|
{versionInfo.fullVersion}
|
||||||
</Text>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
|
<WaitModal
|
||||||
|
open={!!waitModal}
|
||||||
|
message={waitModal ? waitModal.message : ""}
|
||||||
|
/>
|
||||||
<MessageModal
|
<MessageModal
|
||||||
open={!!messageModal}
|
open={!!messageModal}
|
||||||
icon={messageModal ? messageModal.icon : ""}
|
icon={messageModal ? messageModal.icon : ""}
|
||||||
|
|||||||
@@ -11,18 +11,25 @@ import {
|
|||||||
PermissionsAndroid,
|
PermissionsAndroid,
|
||||||
Platform,
|
Platform,
|
||||||
} from "react-native"
|
} from "react-native"
|
||||||
import MapView, { Marker } from "react-native-maps"
|
import MapView, { Marker, Callout } from "react-native-maps"
|
||||||
import { Icon, Header } from "../ui"
|
import { Icon, Header } from "../ui"
|
||||||
import { MessageModal } from "../Modal"
|
import { MessageModal } from "../Modal"
|
||||||
import { api } from "../API"
|
import { api } from "../API"
|
||||||
import autobind from "autobind-decorator"
|
import autobind from "autobind-decorator"
|
||||||
import { ifIphoneX } from "react-native-iphone-x-helper"
|
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 { ensurePermissions } from "../App"
|
||||||
import { versionInfo } from "../version"
|
import { versionInfo } from "../version"
|
||||||
import { minGPSAccuracy } from "../config"
|
import { config } from "../config"
|
||||||
import KeyboardSpacer from "react-native-keyboard-spacer"
|
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 neverAskForLocationPermissionKeyName = "NeverAskForLocationPermission"
|
||||||
const neverAskForCameraKeyName = "NeverAskForCameraPermission"
|
const neverAskForCameraKeyName = "NeverAskForCameraPermission"
|
||||||
@@ -40,7 +47,8 @@ export class Home extends React.Component {
|
|||||||
longitudeDelta: 0.0922,
|
longitudeDelta: 0.0922,
|
||||||
},
|
},
|
||||||
positionInfo: null,
|
positionInfo: null,
|
||||||
disableARViewer: true,
|
haveCameraPermission: false,
|
||||||
|
workItemDistance: -1,
|
||||||
}
|
}
|
||||||
|
|
||||||
ensurePermissions(
|
ensurePermissions(
|
||||||
@@ -71,7 +79,7 @@ export class Home extends React.Component {
|
|||||||
results[PermissionsAndroid.PERMISSIONS.CAMERA] ===
|
results[PermissionsAndroid.PERMISSIONS.CAMERA] ===
|
||||||
PermissionsAndroid.RESULTS.GRANTED
|
PermissionsAndroid.RESULTS.GRANTED
|
||||||
) {
|
) {
|
||||||
this.setState({ disableARViewer: false })
|
this.setState({ haveCameraPerm: true })
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
() => {
|
() => {
|
||||||
@@ -80,9 +88,9 @@ export class Home extends React.Component {
|
|||||||
icon: "hand",
|
icon: "hand",
|
||||||
message:
|
message:
|
||||||
"You have denied the app access to phone features it needs to function. " +
|
"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 " +
|
"Some parts of the app are disabled.",
|
||||||
"please go to Settings.",
|
detail:
|
||||||
detail: "",
|
"To enable these features in future " + "please go to Settings.",
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -130,11 +138,37 @@ export class Home extends React.Component {
|
|||||||
viewOffset: 45,
|
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
|
@autobind
|
||||||
handleWorkItemsListPress() {
|
handleWorkItemsListPress() {
|
||||||
this.props.history.push("/workitemlist")
|
const { positionInfo } = this.state
|
||||||
|
this.props.history.push(
|
||||||
|
`/workItemList${
|
||||||
|
positionInfo
|
||||||
|
? "?latLng=" +
|
||||||
|
coords.latitude.toString() +
|
||||||
|
"," +
|
||||||
|
coords.longitude.toString()
|
||||||
|
: ""
|
||||||
|
}`
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@autobind
|
@autobind
|
||||||
@@ -149,7 +183,30 @@ export class Home extends React.Component {
|
|||||||
|
|
||||||
@autobind
|
@autobind
|
||||||
handleGlassesPress() {
|
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
|
@autobind
|
||||||
@@ -176,7 +233,8 @@ export class Home extends React.Component {
|
|||||||
region,
|
region,
|
||||||
positionInfo,
|
positionInfo,
|
||||||
messageModal,
|
messageModal,
|
||||||
disableARViewer,
|
haveCameraPermission,
|
||||||
|
workItemDistance,
|
||||||
} = this.state
|
} = this.state
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -191,8 +249,10 @@ export class Home extends React.Component {
|
|||||||
leftButton={{ icon: "logout", onPress: this.handleLogoutPress }}
|
leftButton={{ icon: "logout", onPress: this.handleLogoutPress }}
|
||||||
rightButton={{ icon: "glasses", onPress: this.handleGlassesPress }}
|
rightButton={{ icon: "glasses", onPress: this.handleGlassesPress }}
|
||||||
disabled={
|
disabled={
|
||||||
!(positionInfo && positionInfo.coords.accuracy <= minGPSAccuracy) ||
|
!(
|
||||||
disableARViewer
|
positionInfo &&
|
||||||
|
positionInfo.coords.accuracy <= config.minGPSAccuracy
|
||||||
|
) || !haveCameraPermission
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
<MapView
|
<MapView
|
||||||
@@ -216,16 +276,31 @@ export class Home extends React.Component {
|
|||||||
<Marker
|
<Marker
|
||||||
key={index}
|
key={index}
|
||||||
coordinate={workItem.coordinate}
|
coordinate={workItem.coordinate}
|
||||||
title={
|
|
||||||
pad(workItem.ticketNumber, 4) +
|
|
||||||
": " +
|
|
||||||
workItemTypeText[workItem.workItemType]
|
|
||||||
}
|
|
||||||
description={workItem.address}
|
|
||||||
anchor={{ x: 0.5, y: 1.0 }}
|
anchor={{ x: 0.5, y: 1.0 }}
|
||||||
image={pinImage}
|
image={
|
||||||
onPress={(e) => this.handleMarkerPress(e, index)}
|
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>
|
</MapView>
|
||||||
<View
|
<View
|
||||||
@@ -329,7 +404,7 @@ export class Home extends React.Component {
|
|||||||
{activity.resolution}
|
{activity.resolution}
|
||||||
</Text>
|
</Text>
|
||||||
<Text style={{ fontSize: 14, color: "gray" }}>
|
<Text style={{ fontSize: 14, color: "gray" }}>
|
||||||
{activity.address || "..."}
|
{activity.when}
|
||||||
</Text>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
<Icon
|
<Icon
|
||||||
|
|||||||
BIN
mobile/src/Home/images/clipboard-pin.png
Normal file
|
After Width: | Height: | Size: 2.6 KiB |
BIN
mobile/src/Home/images/clipboard-pin@2x.png
Normal file
|
After Width: | Height: | Size: 5.6 KiB |
BIN
mobile/src/Home/images/clipboard-pin@3x.png
Normal file
|
After Width: | Height: | Size: 8.8 KiB |
BIN
mobile/src/Home/images/hardhat-pin.png
Normal file
|
After Width: | Height: | Size: 3.0 KiB |
BIN
mobile/src/Home/images/hardhat-pin@2x.png
Normal file
|
After Width: | Height: | Size: 6.5 KiB |
BIN
mobile/src/Home/images/hardhat-pin@3x.png
Normal file
|
After Width: | Height: | Size: 10 KiB |
|
Before Width: | Height: | Size: 1.4 KiB |
|
Before Width: | Height: | Size: 2.8 KiB |
|
Before Width: | Height: | Size: 4.6 KiB |
BIN
mobile/src/Home/images/question-pin.png
Normal file
|
After Width: | Height: | Size: 3.1 KiB |
BIN
mobile/src/Home/images/question-pin@2x.png
Normal file
|
After Width: | Height: | Size: 6.5 KiB |
BIN
mobile/src/Home/images/question-pin@3x.png
Normal file
|
After Width: | Height: | Size: 10 KiB |
50
mobile/src/Modal/ImageViewerModal.js
Normal 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>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
29
mobile/src/Modal/WaitModal.js
Normal 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>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
BIN
mobile/src/Modal/images/back.png
Normal file
|
After Width: | Height: | Size: 1.5 KiB |
@@ -1,2 +1,4 @@
|
|||||||
export { MessageModal } from "./MessageModal"
|
export { MessageModal } from "./MessageModal"
|
||||||
export { ApiModal } from "./ApiModal"
|
export { ApiModal } from "./ApiModal"
|
||||||
|
export { WaitModal } from "./WaitModal"
|
||||||
|
export { ImageViewerModal } from "./ImageViewerModal"
|
||||||
|
|||||||
@@ -59,6 +59,10 @@ export class WorkItem extends React.Component {
|
|||||||
isValid: (r, v) => v !== "",
|
isValid: (r, v) => v !== "",
|
||||||
isReadOnly: true,
|
isReadOnly: true,
|
||||||
},
|
},
|
||||||
|
address: {
|
||||||
|
isValid: true,
|
||||||
|
isReadOnly: true,
|
||||||
|
},
|
||||||
details: {
|
details: {
|
||||||
isValid: (r, v) => v !== "",
|
isValid: (r, v) => v !== "",
|
||||||
},
|
},
|
||||||
@@ -118,6 +122,12 @@ export class WorkItem extends React.Component {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
componentWillUnmount() {
|
||||||
|
if (this.geoCodeTimer) {
|
||||||
|
clearTimeout(this.geoCodeTimer)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@autobind
|
@autobind
|
||||||
handleBackPress() {
|
handleBackPress() {
|
||||||
const { history } = this.props
|
const { history } = this.props
|
||||||
@@ -180,11 +190,32 @@ export class WorkItem extends React.Component {
|
|||||||
|
|
||||||
@autobind
|
@autobind
|
||||||
handleRegionChange(region) {
|
handleRegionChange(region) {
|
||||||
|
const { latitude, longitude } = region
|
||||||
|
|
||||||
if (this.latLngInput) {
|
if (this.latLngInput) {
|
||||||
this.latLngInput.handleChangeText(
|
this.latLngInput.handleChangeText(formatLatLng(latitude, longitude))
|
||||||
formatLatLng(region.latitude, region.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() {
|
render() {
|
||||||
@@ -252,6 +283,12 @@ export class WorkItem extends React.Component {
|
|||||||
name="location"
|
name="location"
|
||||||
label="Location:"
|
label="Location:"
|
||||||
/>
|
/>
|
||||||
|
<BoundInput
|
||||||
|
ref={(ref) => (this.addressInput = ref)}
|
||||||
|
binder={binder}
|
||||||
|
name="address"
|
||||||
|
label="Address:"
|
||||||
|
/>
|
||||||
</View>
|
</View>
|
||||||
<View style={styles.panel}>
|
<View style={styles.panel}>
|
||||||
<PhotoPanel />
|
<PhotoPanel />
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ import {
|
|||||||
formatLatLng,
|
formatLatLng,
|
||||||
parseLatLng,
|
parseLatLng,
|
||||||
pad,
|
pad,
|
||||||
|
geoDistance,
|
||||||
} from "../util"
|
} from "../util"
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
const styles = StyleSheet.create({
|
||||||
@@ -36,6 +37,25 @@ export class WorkItemList extends React.Component {
|
|||||||
this.state = {
|
this.state = {
|
||||||
messageModal: null,
|
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
|
api
|
||||||
.listWorkItems()
|
.listWorkItems()
|
||||||
.then((list) => {
|
.then((list) => {
|
||||||
@@ -54,7 +74,7 @@ export class WorkItemList extends React.Component {
|
|||||||
|
|
||||||
@autobind
|
@autobind
|
||||||
handleItemSelect(item, ref) {
|
handleItemSelect(item, ref) {
|
||||||
this.props.history.push(`/workitem?id=${item._id}`)
|
this.props.history.push(`/workItem?id=${item._id}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
@autobind
|
@autobind
|
||||||
@@ -95,7 +115,7 @@ export class WorkItemList extends React.Component {
|
|||||||
|
|
||||||
@autobind
|
@autobind
|
||||||
handleDonePress() {
|
handleDonePress() {
|
||||||
this.props.history.push("/workitem")
|
this.props.history.push("/workItem")
|
||||||
}
|
}
|
||||||
|
|
||||||
@autobind
|
@autobind
|
||||||
@@ -159,7 +179,17 @@ export class WorkItemList extends React.Component {
|
|||||||
{workItemTypeText[item.workItemType]}
|
{workItemTypeText[item.workItemType]}
|
||||||
</Text>
|
</Text>
|
||||||
<Text style={{ fontSize: 14, color: "gray" }}>
|
<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>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
<Icon
|
<Icon
|
||||||
|
|||||||
@@ -1,5 +1,18 @@
|
|||||||
export const localIPAddr = "192.168.1.175"
|
import React from "react"
|
||||||
export const defaultUser = "john@lyon-smith.org"
|
import { Platform } from "react-native"
|
||||||
// export const defaultUser = ""
|
|
||||||
// export const minGPSAccuracy = 20
|
export const config = {
|
||||||
export const minGPSAccuracy = 100
|
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,
|
||||||
|
}
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 937 B After Width: | Height: | Size: 654 B |
|
Before Width: | Height: | Size: 1.6 KiB After Width: | Height: | Size: 1.1 KiB |
|
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 778 B |
@@ -16,11 +16,6 @@ export let activitySchema = new Schema(
|
|||||||
},
|
},
|
||||||
notes: { type: String, required: true },
|
notes: { type: String, required: true },
|
||||||
when: { type: Date, required: true },
|
when: { type: Date, required: true },
|
||||||
location: {
|
|
||||||
type: { type: String },
|
|
||||||
coordinates: [Number],
|
|
||||||
},
|
|
||||||
address: String,
|
|
||||||
fromStreetNumber: Number,
|
fromStreetNumber: Number,
|
||||||
toStreetNumber: Number,
|
toStreetNumber: Number,
|
||||||
photos: [Schema.Types.ObjectId],
|
photos: [Schema.Types.ObjectId],
|
||||||
|
|||||||