diff --git a/mobile/src/WorkItem/WorkItem.js b/mobile/src/WorkItem/WorkItem.js index 20942ad..ed6ed5f 100644 --- a/mobile/src/WorkItem/WorkItem.js +++ b/mobile/src/WorkItem/WorkItem.js @@ -66,6 +66,7 @@ export class WorkItem extends React.Component { }, photos: { isValid: (r, v) => v && v.length > 0, + initValue: [], }, details: { isValid: (r, v) => v !== "", @@ -278,6 +279,7 @@ export class WorkItem extends React.Component { height: 400, marginBottom: 10, }} + showsUserLocation showsBuildings={false} showsTraffic={false} showsIndoors={false} diff --git a/mobile/src/util.js b/mobile/src/util.js index fe1f1d5..ed73071 100644 --- a/mobile/src/util.js +++ b/mobile/src/util.js @@ -69,7 +69,7 @@ export const dotify = (s) => { export const regionContainingPoints = (points, inset) => { let minX, maxX, minY, maxY - if (!points) { + if (!points || points.length === 0) { return null } diff --git a/server/src/api/routes/ActivityRoutes.js b/server/src/api/routes/ActivityRoutes.js index a71184f..db4ea94 100644 --- a/server/src/api/routes/ActivityRoutes.js +++ b/server/src/api/routes/ActivityRoutes.js @@ -38,6 +38,13 @@ export class ActivityRoutes { passport.authenticate("bearer", { session: false }), catchAll(this.deleteActivity) ) + + app + .route("/activities/all") + .delete( + passport.authenticate("bearer", { session: false }), + catchAll(this.deleteAllActivities) + ) } async listActivities(req, res, next) { @@ -156,4 +163,12 @@ export class ActivityRoutes { res.json({}) } + + async deleteAllActivities(req, res, next) { + const Activity = this.db.Activity + + await Activity.remove({}) + + res.json({}) + } } diff --git a/server/src/api/routes/TeamRoutes.js b/server/src/api/routes/TeamRoutes.js index cb651c0..7bde9ad 100644 --- a/server/src/api/routes/TeamRoutes.js +++ b/server/src/api/routes/TeamRoutes.js @@ -1,6 +1,8 @@ import passport from "passport" import createError from "http-errors" import autobind from "autobind-decorator" +import zlib from "zlib" +import { Readable } from "stream" import { catchAll } from "." @autobind @@ -38,6 +40,13 @@ export class TeamRoutes { passport.authenticate("bearer", { session: false }), catchAll(this.deleteTeam) ) + + app + .route("/teams/status") + .get( + passport.authenticate("bearer", { session: false }), + catchAll(this.getTeamStatus) + ) } async listTeams(req, res, next) { @@ -146,4 +155,32 @@ export class TeamRoutes { res.json({}) } + + async getTeamStatus(req, res, next) { + const Team = this.db.Team + const Activity = this.db.Activity + let teams = await Team.find({}).exec() + + teams = teams.map((doc) => doc.toObject({ versionKey: false })) + + for (let team of teams) { + let activities = await Activity.find({ team: team._id }).exec() + + team.activities = activities.map((doc) => + doc.toObject({ versionKey: false }) + ) + } + + const gzip = zlib.createGzip() + let readable = new Readable() + + readable.push(JSON.stringify(teams, null, " ")) + readable.push(null) + + res.writeHead(200, { + "Content-Type": "text/html", + "Content-Encoding": "gzip", + }) + readable.pipe(gzip).pipe(res) + } } diff --git a/server/src/api/routes/WorkItemRoutes.js b/server/src/api/routes/WorkItemRoutes.js index 5e1e493..a520b3f 100644 --- a/server/src/api/routes/WorkItemRoutes.js +++ b/server/src/api/routes/WorkItemRoutes.js @@ -45,6 +45,13 @@ export class WorkItemRoutes { passport.authenticate("bearer", { session: false }), catchAll(this.deleteWorkItem) ) + + app + .route("/workitems/all") + .delete( + passport.authenticate("bearer", { session: false }), + catchAll(this.deleteAllWorkItems) + ) } async listWorkItems(req, res, next) { @@ -189,4 +196,14 @@ export class WorkItemRoutes { res.json({}) } + + async deleteAllWorkItems(req, res, next) { + const Activity = this.db.Activity + const WorkItem = this.db.WorkItem + + await Activity.remove({}) + await WorkItem.remove({}) + + res.json({}) + } } diff --git a/server/src/database/schemas/activity.js b/server/src/database/schemas/activity.js index 15ce09a..d318253 100644 --- a/server/src/database/schemas/activity.js +++ b/server/src/database/schemas/activity.js @@ -16,7 +16,6 @@ export let activitySchema = new Schema( required: true, }, notes: { type: String, required: true }, - when: { type: Date, required: true }, fromStreetNumber: Number, toStreetNumber: Number, photos: [Schema.Types.ObjectId], @@ -25,5 +24,5 @@ export let activitySchema = new Schema( ) activitySchema.methods.toClient = function() { - return this.toObject() + return this.toObject({ versionKey: false }) } diff --git a/server/src/database/schemas/team.js b/server/src/database/schemas/team.js index 55e391e..3a061bc 100644 --- a/server/src/database/schemas/team.js +++ b/server/src/database/schemas/team.js @@ -1,9 +1,14 @@ -import { Schema } from 'mongoose' +import { Schema } from "mongoose" -export let teamSchema = new Schema({ - name: { type: String }, -}, { timestamps: true, id: false }) +export let teamSchema = new Schema( + { + name: { type: String }, + start: { type: Date }, + stop: { type: Date }, + }, + { timestamps: true, id: false } +) teamSchema.methods.toClient = function() { - return this.toObject() + return this.toObject({ versionKey: false }) } diff --git a/website/src/API.js b/website/src/API.js index d78993a..87bdef7 100644 --- a/website/src/API.js +++ b/website/src/API.js @@ -1,30 +1,30 @@ -import EventEmitter from 'eventemitter3' -import io from 'socket.io-client' -import autobind from 'autobind-decorator' +import EventEmitter from "eventemitter3" +import io from "socket.io-client" +import autobind from "autobind-decorator" -const authTokenName = 'AuthToken' +const authTokenName = "AuthToken" class NetworkError extends Error { constructor(message) { super(message) this.name = this.constructor.name - if (typeof Error.captureStackTrace === 'function') { + if (typeof Error.captureStackTrace === "function") { Error.captureStackTrace(this, this.constructor) } else { - this.stack = (new Error(message)).stack + this.stack = new Error(message).stack } } } class APIError extends Error { constructor(status, message) { - super(message || '') + super(message || "") this.status = status || 500 this.name = this.constructor.name - if (typeof Error.captureStackTrace === 'function') { + if (typeof Error.captureStackTrace === "function") { Error.captureStackTrace(this, this.constructor) } else { - this.stack = (new Error(message)).stack + this.stack = new Error(message).stack } } } @@ -35,49 +35,51 @@ class API extends EventEmitter { super() this.user = null - let token = localStorage.getItem(authTokenName) || sessionStorage.getItem(authTokenName) + let token = + localStorage.getItem(authTokenName) || + sessionStorage.getItem(authTokenName) if (token) { this.token = token this.user = { pending: true } this.who() - .then((user) => { - this.user = user - this.connectSocket() - this.emit('login') - }) - .catch(() => { - localStorage.removeItem(authTokenName) - sessionStorage.removeItem(authTokenName) - this.token = null - this.user = null - this.socket = null - this.emit('logout') - }) + .then((user) => { + this.user = user + this.connectSocket() + this.emit("login") + }) + .catch(() => { + localStorage.removeItem(authTokenName) + sessionStorage.removeItem(authTokenName) + this.token = null + this.user = null + this.socket = null + this.emit("logout") + }) } } connectSocket() { this.socket = io(window.location.origin, { - path: '/api/socketio', + path: "/api/socketio", query: { - auth_token: this.token - } + auth_token: this.token, + }, }) - this.socket.on('disconnect', (reason) => { + this.socket.on("disconnect", (reason) => { // Could happen if the auth_token is bad this.socket = null }) - this.socket.on('notify', (message) => { + this.socket.on("notify", (message) => { const { eventName, eventData } = message // Filter the few massages that affect our cached user data to avoid a server round trip switch (eventName) { - case 'newThumbnailImage': + case "newThumbnailImage": this.user.thumbnailImageId = eventData.imageId break - case 'newProfileImage': + case "newProfileImage": this.user.imageId = eventData.imageId break default: @@ -102,22 +104,27 @@ class API extends EventEmitter { makeImageUrl(id, size) { if (id) { - return '/api/assets/' + id + '?access_token=' + this.token + return "/api/assets/" + id + "?access_token=" + this.token } else if (size && size.width && size.height) { - return `/api/placeholders/${size.width}x${size.height}?access_token=${this.token}` + return `/api/placeholders/${size.width}x${size.height}?access_token=${ + this.token + }` } else { return null } } makeAssetUrl(id) { - return id ? '/api/assets/' + id + '?access_token=' + this.token : null + return id ? "/api/assets/" + id + "?access_token=" + this.token : null } static makeParams(params) { - return params ? '?' + Object.keys(params).map((key) => ( - [key, params[key]].map(encodeURIComponent).join('=') - )).join('&') : '' + return params + ? "?" + + Object.keys(params) + .map((key) => [key, params[key]].map(encodeURIComponent).join("=")) + .join("&") + : "" } request(method, path, requestBody, requestOptions) { @@ -125,82 +132,90 @@ class API extends EventEmitter { var promise = new Promise((resolve, reject) => { let fetchOptions = { method: method, - mode: 'cors', - cache: 'no-store' + mode: "cors", + cache: "no-store", } let headers = new Headers() if (this.token) { - headers.set('Authorization', 'Bearer ' + this.token) + headers.set("Authorization", "Bearer " + this.token) } - if (method === 'POST' || method === 'PUT') { + if (method === "POST" || method === "PUT") { if (requestOptions.binary) { - headers.set('Content-Type', 'application/octet-stream') - headers.set('Content-Length', requestOptions.binary.length) - headers.set('Range', 'byte ' + requestOptions.binary.offset) + headers.set("Content-Type", "application/octet-stream") + headers.set("Content-Length", requestOptions.binary.length) + headers.set("Range", "byte " + requestOptions.binary.offset) fetchOptions.body = requestBody } else { - headers.set('Content-Type', 'application/json') + headers.set("Content-Type", "application/json") fetchOptions.body = JSON.stringify(requestBody) } } fetchOptions.headers = headers - fetch('/api' + path, fetchOptions).then((res) => { - return Promise.all([ Promise.resolve(res), (requestOptions.binary && method === 'GET') ? res.blob() : res.json() ]) - }).then((arr) => { - let [ res, responseBody ] = arr - if (res.ok) { - if (requestOptions.wantHeaders) { - resolve({ body: responseBody, headers: res.headers }) + fetch("/api" + path, fetchOptions) + .then((res) => { + return Promise.all([ + Promise.resolve(res), + requestOptions.binary && method === "GET" ? res.blob() : res.json(), + ]) + }) + .then((arr) => { + let [res, responseBody] = arr + if (res.ok) { + if (requestOptions.wantHeaders) { + resolve({ body: responseBody, headers: res.headers }) + } else { + resolve(responseBody) + } } 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 } post(path, requestBody, options) { - return this.request('POST', path, requestBody, options) + return this.request("POST", path, requestBody, options) } put(path, requestBody, options) { - return this.request('PUT', path, requestBody, options) + return this.request("PUT", path, requestBody, options) } get(path, options) { - return this.request('GET', path, options) + return this.request("GET", path, options) } delete(path, options) { - return this.request('DELETE', path, options) + return this.request("DELETE", path, options) } login(email, password, remember) { return new Promise((resolve, reject) => { - this.post('/auth/login', { email, password }, { wantHeaders: true }).then((response) => { - // Save bearer token for later use - const authValue = response.headers.get('Authorization') - const [ scheme, token ] = authValue.split(' ') + this.post("/auth/login", { email, password }, { wantHeaders: true }) + .then((response) => { + // Save bearer token for later use + const authValue = response.headers.get("Authorization") + const [scheme, token] = authValue.split(" ") - if (scheme !== 'Bearer' || !token) { - reject(new APIError('Unexpected Authorization scheme or token')) - } + if (scheme !== "Bearer" || !token) { + reject(new APIError("Unexpected Authorization scheme or token")) + } - if (remember) { - localStorage.setItem(authTokenName, token) - } else { - sessionStorage.setItem(authTokenName, token) - } - this.token = token - this.user = response.body - this.connectSocket() - this.emit('login') - resolve(response.body) - }).catch((err) => { - reject(err) - }) + if (remember) { + localStorage.setItem(authTokenName, token) + } else { + sessionStorage.setItem(authTokenName, token) + } + this.token = token + this.user = response.body + this.connectSocket() + this.emit("login") + resolve(response.body) + }) + .catch((err) => { + reject(err) + }) }) } logout() { @@ -211,82 +226,91 @@ class API extends EventEmitter { this.token = null this.user = null this.disconnectSocket() - this.emit('logout') + this.emit("logout") } - return this.delete('/auth/login').then(cb, cb) + return this.delete("/auth/login").then(cb, cb) } who() { - return this.get('/auth/who') + return this.get("/auth/who") } confirmEmail(emailToken) { - return this.post('/auth/email/confirm/', { emailToken }) + return this.post("/auth/email/confirm/", { emailToken }) } sendConfirmEmail(emails) { - return this.post('/auth/email/send', emails) + return this.post("/auth/email/send", emails) } changePassword(passwords) { - return this.post('/auth/password/change', passwords) + return this.post("/auth/password/change", passwords) } sendResetPassword(email) { - return this.post('/auth/password/send', { email }) + return this.post("/auth/password/send", { email }) } confirmResetPassword(passwordToken) { - return this.post('/auth/password/confirm', { passwordToken }) + return this.post("/auth/password/confirm", { passwordToken }) } resetPassword(passwords) { - return this.post('/auth/password/reset', passwords) + return this.post("/auth/password/reset", passwords) } getUser(id) { - return this.get('/users/' + id) + return this.get("/users/" + id) } listUsers() { - return this.get('/users') + return this.get("/users") } listUsersForTeam(teamId) { return this.get(`/users?team=${teamId}`) } createUser(user) { - return this.post('/users', user) + return this.post("/users", user) } updateUser(user) { return new Promise((resolve, reject) => { - this.put('/users', user).then((user) => { - // If we just updated ourselves, update the internal cached copy - if (user._id === this.user._id) { - this.user = user - this.emit('login') - } - resolve(user) - }).catch((reason) => { - reject(reason) - }) + this.put("/users", user) + .then((user) => { + // If we just updated ourselves, update the internal cached copy + if (user._id === this.user._id) { + this.user = user + this.emit("login") + } + resolve(user) + }) + .catch((reason) => { + reject(reason) + }) }) } deleteUser(id) { - return this.delete('/users/' + id) + return this.delete("/users/" + id) } enterRoom(roomName) { - return this.put('/users/enter-room/' + (roomName || '')) + return this.put("/users/enter-room/" + (roomName || "")) } leaveRoom() { - return this.put('/users/leave-room') + return this.put("/users/leave-room") } getTeam(id) { - return this.get('/teams/' + id) + return this.get("/teams/" + id) } listTeams() { - return this.get('/teams') + return this.get("/teams") } createTeam(team) { - return this.post('/teams', team) + return this.post("/teams", team) } updateTeam(team) { - return this.put('/teams', team) + return this.put("/teams", team) } deleteTeam(id) { - return this.delete('/teams/' + id) + return this.delete("/teams/" + id) + } + + deleteAllActivities() { + return this.delete("/activities/all") + } + deleteAllWorkItems() { + return this.delete("/workitems/all") } upload(file, progressCallback) { @@ -302,36 +326,40 @@ class API extends EventEmitter { const buffer = e.target.result const bytesRead = buffer.byteLength - this.post('/assets/upload/' + uploadId, buffer, { - binary: { offset: chunk * chunkSize, length: bytesRead } - }).then((uploadData) => { - 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/" + uploadId, buffer, { + 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) + }) } - this.post('/assets/upload', { + this.post("/assets/upload", { fileName: file.name, fileSize, contentType: file.type, - numberOfChunks - }).then((uploadData) => { - uploadId = uploadData.uploadId - reader.readAsArrayBuffer(file.slice(0, chunkSize)) - }).catch((err) => { - reject(err) + numberOfChunks, }) + .then((uploadData) => { + uploadId = uploadData.uploadId + reader.readAsArrayBuffer(file.slice(0, chunkSize)) + }) + .catch((err) => { + reject(err) + }) }) } } diff --git a/website/src/System/System.js b/website/src/System/System.js index e8e4e5e..c8d7c45 100644 --- a/website/src/System/System.js +++ b/website/src/System/System.js @@ -1,10 +1,11 @@ -import React, { Component, Fragment } from 'react' -import PropTypes from 'prop-types' -import { Box, Image, Column, Row, Button } from 'ui' -import { MessageModal, WaitModal } from '../Modal' -// import { api } from 'src/API' -import { sizeInfo, colorInfo } from 'ui/style' -import headerLogo from 'images/deighton.png' +import React, { Component, Fragment } from "react" +import PropTypes from "prop-types" +import { Box, Image, Column, Row, Button } from "ui" +import { MessageModal, WaitModal, YesNoMessageModal } from "../Modal" +import { sizeInfo, colorInfo } from "ui/style" +import headerLogo from "images/deighton.png" +import autobind from "autobind-decorator" +import { api } from "../API" export class System extends Component { static propTypes = { @@ -16,19 +17,113 @@ export class System extends Component { this.state = { messageModal: null, waitModal: null, + yesNoModal: null, } } componentDidMount(props) { - this.props.changeTitle('System') + this.props.changeTitle("System") } componentWillUnmount() { - this.props.changeTitle('') + this.props.changeTitle("") + } + + @autobind + handleDeleteActivities() { + this.setState({ + yesNoModal: { + question: + "Are you sure you want to delete all activities in the system?", + onDismiss: this.handleDeleteActivitiesDismiss, + }, + }) + } + + @autobind + handleDeleteActivitiesDismiss(yes) { + if (yes) { + this.setState({ waitModal: { message: "Deleting All Activities..." } }) + api + .deleteAllActivities() + .then(() => { + this.setState({ + waitModal: null, + messageModal: { + icon: "thumb", + message: "All logged activities have been deleted", + }, + }) + }) + .catch((error) => { + this.setState({ + waitModal: null, + messageModal: { + icon: "hand", + message: "Unable to request delete activities.", + detail: error.message, + }, + }) + }) + } + + this.setState({ + yesNoModal: null, + }) + } + + @autobind + handleDeleteWorkItems() { + this.setState({ + yesNoModal: { + question: + "Are you sure you want to delete all work items & activities in the system?", + onDismiss: this.handleDeleteWorkItemsDismiss, + }, + }) + } + + @autobind + handleDeleteWorkItemsDismiss(yes) { + if (yes) { + this.setState({ + waitModal: { message: "Deleting All Work Items & Activities..." }, + }) + api + .deleteAllWorkItems() + .then(() => { + this.setState({ + waitModal: null, + messageModal: { + icon: "thumb", + message: "All work items and logged activities have been deleted", + }, + }) + }) + .catch((error) => { + this.setState({ + waitModal: null, + messageModal: { + icon: "hand", + message: "Unable to delete work items and activities.", + detail: error.message, + }, + }) + }) + } + + this.setState({ + yesNoModal: null, + }) + } + + @autobind + handleMessageModalDismiss() { + this.setState({ messageModal: null }) } render() { - const { messageModal, waitModal } = this.state + const { messageModal, yesNoModal, waitModal } = this.state return ( @@ -38,14 +133,22 @@ export class System extends Component { - + - + @@ -55,7 +158,11 @@ export class System extends Component { -