diff --git a/www/assets/js/activitylog.js b/www/assets/js/activitylog.js new file mode 100644 index 0000000..c91a608 --- /dev/null +++ b/www/assets/js/activitylog.js @@ -0,0 +1,108 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + + +function appendActivityLog(title, subtitle, content, icon, timestamp) { + if (typeof timestamp == "undefined") { + timestamp = time(); + } + let entry = { + title: title, + subtitle: subtitle, + content: content, + icon: icon, + timestamp: timestamp + }; + + let log = getStorage("activitylog"); + if (log == null) { + log = []; + } else { + try { + log = JSON.parse(log); + } catch (ex) { + log = []; + } + } + + let pushed = false; + let datestr = formatTimestamp("Y-m-d", timestamp); + for (var i = 0; i < log.length; i++) { + if (formatTimestamp("Y-m-d", log[i].date) == datestr) { + log[i].entries.push(entry); + pushed = true; + break; + } + } + if (!pushed) { + log.push({ + date: timestamp, + entries: [ + entry + ] + }); + } + + console.log("Added activity log entry", entry); + console.log(log); + + setStorage("activitylog", JSON.stringify(log)); + + // Trim the log soon but don't block for it + setTimeout(trimActivityLog, 100); +} + +function clearActivityLog() { + setStorage("activitylog", "[]"); +} + +function trimActivityLog() { + let log = JSON.parse(getStorage("activitylog")); + + log.sort(function (x, y) { + if (x.date < y.date) { + return 1; + } else if (x.date > y.date) { + return -1; + } + return 0; + }); + + let entries = 0; + let allowed = SETTINGS.activitylog_maxlength; + + let newlog = []; + + for (var i = 0; i < log.length; i++) { + let logdate = { + date: log[i].date, + entries: [] + }; + for (var j = 0; j < log[i].entries.length; j++) { + if (entries < allowed) { + logdate.entries.push(log[i].entries[j]); + } + entries++; + } + if (logdate.entries.length > 0) { + newlog.push(logdate); + } + } + + if (entries - allowed > 0) { + newlog[newlog.length - 1].entries.push({ + title: "Log Trimmed", + subtitle: "", + content: (entries - allowed) + " older " + ((entries - allowed) == 1 ? "entry was" : "entries were") + " removed from the log.", + icon: "fas fa-cut", + timestamp: newlog[newlog.length - 1].date + }); + } + + console.log(log); + console.log(newlog); + setStorage("activitylog", JSON.stringify(newlog)); +} \ No newline at end of file diff --git a/www/assets/js/list.js b/www/assets/js/list.js index 311053f..2d0938d 100644 --- a/www/assets/js/list.js +++ b/www/assets/js/list.js @@ -183,8 +183,11 @@ function confirmDeleteAllPackages() { "Clear Packages", function () { // clear + let count = countPackages(); + let remaining = countRemainingPackages(); packages = []; setStorage("packages", JSON.stringify(packages)); + appendActivityLog("Cleared List", count + " " + (count != 1 ? "items" : "item") + " removed.", (remaining > 0 ? remaining + " " + (remaining != 1 ? "were" : "was") + " not delivered." : ""), "fas fa-trash"); loadPackageList(); if (map != null) { map.updatePackageLayer(packages); diff --git a/www/assets/js/notes.js b/www/assets/js/notes.js index 746f1f7..fccbcc7 100644 --- a/www/assets/js/notes.js +++ b/www/assets/js/notes.js @@ -1,4 +1,4 @@ -/* +/* * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. @@ -49,7 +49,7 @@ function isDeliverable(number, street) { }; } -function saveNote(id) { +function saveNote(id) { var exists = false; var index = -1; for (var i = 0; i < notes.length; i++) { @@ -94,7 +94,7 @@ function saveNote(id) { app.dialog.alert("Fill in a route (examples: C123, R001, H050).", "Error"); return false; } - + setStorage("lastrouteid", note.route); setStorage("zipcode", note.zipcode); @@ -111,6 +111,12 @@ function saveNote(id) { setStorage("notes", JSON.stringify(notes)); + if (exists) { + appendActivityLog("Edited note", note.number + " " + note.street, "", "fas fa-sticky-note"); + } else { + appendActivityLog("Added note", note.number + " " + note.street, "", "fas fa-sticky-note"); + } + app.toast.show({ text: " Note saved!", position: "bottom", @@ -120,12 +126,15 @@ function saveNote(id) { } function deleteNote(id) { + var note = {}; for (var i = 0; i < notes.length; i++) { if (notes[i].id == id) { + note = notes[i]; notes.splice(i, 1); } } setStorage("notes", JSON.stringify(notes)); + appendActivityLog("Deleted note", note.number + " " + note.street, "", "fas fa-sticky-note"); } $(".view-main").on("click", "#savenotebtn", function () { diff --git a/www/assets/js/packages.js b/www/assets/js/packages.js index dc2f012..5711312 100644 --- a/www/assets/js/packages.js +++ b/www/assets/js/packages.js @@ -144,6 +144,8 @@ function addPackage(address, latitude, longitude, type, callback, deadline) { } setStorage("packages", JSON.stringify(packages)); + appendActivityLog("Added", SETTINGS.itemtypes[type].name, address, "fas fa-truck-loading"); + playSound("ok"); app.toast.show({ @@ -174,6 +176,7 @@ function addPackage(address, latitude, longitude, type, callback, deadline) { */ function importPackageList(newlist) { skipped = 0; + let count = 0; for (latlng in newlist) { var latitude = newlist[latlng].coords[0]; var longitude = newlist[latlng].coords[1]; @@ -195,6 +198,7 @@ function importPackageList(newlist) { } if (!added) { packages[i].items.push(package); + count++; added = true; } break; @@ -206,6 +210,8 @@ function importPackageList(newlist) { } } setStorage("packages", JSON.stringify(packages)); + + appendActivityLog("Imported List", count + " items added", "", "fas fa-file-download"); if (map != null) { reloadMap(); } @@ -237,12 +243,9 @@ function mapCalibrate(item, packagesentry) { locationtype: locationtype }, success: function () { - app.toast.show({ - text: "Calibration recorded. Thank you for improving the map!", - position: "bottom", - destroyOnClose: true, - closeTimeout: 1000 * 3 - }); + appendActivityLog("Map Calibrated", item.extended.number + " " + item.extended.street, + "Thanks for improving the map accuracy!
Old: " + packagesentry.coords[0] + ", " + packagesentry.coords[1] + "
" + + "New: " + latitude + ", " + longitude + "", "fas fa-map-marked-alt"); }, error: function () { // try again in five minutes @@ -312,14 +315,15 @@ function markDelivered(id, delivered) { packages[i].items[j].delivered = delivered; if (delivered) { - packages[i].items[j].deliverytimestamp = Date.now(); - - setStorage("packages", JSON.stringify(packages)); - + packages[i].items[j].deliverytimestamp = time(); + appendActivityLog("Delivered", SETTINGS.itemtypes[packages[i].items[j].type].name, packages[i].items[j].address, "far fa-check-circle"); mapCalibrate(packages[i].items[j], packages[i]); - - return; // so we don't keep looping over the rest of the packages + } else { + packages[i].items[j].deliverytimestamp = null; + appendActivityLog("Undelivered", SETTINGS.itemtypes[packages[i].items[j].type].name, packages[i].items[j].address, "fas fa-undo"); } + setStorage("packages", JSON.stringify(packages)); + return; // so we don't keep looping over the rest of the packages } } } @@ -343,6 +347,9 @@ function deletePackage(id, callback) { for (var i = 0; i < packages.length; i++) { for (var j = 0; j < packages[i].items.length; j++) { if (packages[i].items[j].id == id) { + + appendActivityLog("Deleted", SETTINGS.itemtypes[packages[i].items[j].type].name, packages[i].items[j].address, "fas fa-trash"); + packages[i].items.splice(j, 1); if (packages[i].items.length == 0) { diff --git a/www/assets/js/toolbox_log.js b/www/assets/js/toolbox_log.js new file mode 100644 index 0000000..387be00 --- /dev/null +++ b/www/assets/js/toolbox_log.js @@ -0,0 +1,21 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + + +function confirmDeleteActivityLog() { + app.dialog.confirm( + "Really delete all log entries?", + "Clear Log", + function () { + // clear + clearActivityLog(); + router.refreshPage(); + }, + function () { + // cancel + } + ); +} \ No newline at end of file diff --git a/www/assets/js/toolbox_scanner.js b/www/assets/js/toolbox_scanner.js index 14d1a09..8d358a3 100644 --- a/www/assets/js/toolbox_scanner.js +++ b/www/assets/js/toolbox_scanner.js @@ -101,7 +101,7 @@ function addCodeToScannerList(code) { } else { app.dialog.confirm( "It looks like this item might require a signature or other special procedures. Add it anyways?", - "Signature Item", + "Special Item", function () { $("#codelist").append(codeEntryTemplate({ code: code @@ -195,6 +195,8 @@ function saveScanCode(code) { var events = JSON.parse(getStorage("scanevents")); events.push(code); setStorage("scanevents", JSON.stringify(events)); + + appendActivityLog("Scanned Item", code.event.join(' '), code.code + (code.form3849 == "" ? "" : "
Form 3849: " + code.form3849), "fas fa-barcode"); } $(".view-main").off("click", "#codelist li.codelist-entry"); diff --git a/www/assets/js/toolbox_sharelist.js b/www/assets/js/toolbox_sharelist.js index 7a84db1..93394b9 100644 --- a/www/assets/js/toolbox_sharelist.js +++ b/www/assets/js/toolbox_sharelist.js @@ -33,6 +33,7 @@ function uploadList() { height: 40 }); $("#listidbarcodeli").css("display", ""); + appendActivityLog("Shared List", countPackages() + " items sent", "", "fas fa-file-upload"); } else { app.dialog.alert(resp.message, "Error"); } diff --git a/www/assets/js/util.js b/www/assets/js/util.js index 2982c4c..e0819ee 100644 --- a/www/assets/js/util.js +++ b/www/assets/js/util.js @@ -1,4 +1,4 @@ -/* +/* * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. @@ -16,16 +16,233 @@ function uuidv4() { }); } -function timestampToDateTimeString(timestamp) { +/** + * Take a UNIX timestamp (seconds since Jan 1 1970) and format it. + * (Mostly) compatible with PHP's date() function. + * @param {String} date format string, see https://www.php.net/manual/en/function.date.php + * @param {Integer} timestamp UNIX timestamp + * @return {String} + */ +function formatTimestamp(format, timestamp) { + if (typeof timestamp == "undefined") { + timestamp = time(); + } var date = new Date(timestamp * 1000); - var pm = date.getHours() >= 12; - var hours = date.getHours() > 12 ? date.getHours() - 12 : date.getHours(); - hours = (hours == 0 ? 12 : hours); - var minutes = date.getMinutes(); - var time = hours + ":" + (minutes < 10 ? "0" + minutes : minutes) + " " + (pm ? "PM" : "AM"); + var out = ""; - return date.toLocaleDateString() + " " + time; + var months = ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"]; + var days = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"]; + for (var i = 0; i < format.length; i++) { + var c = format.charAt(i); + // Handle backslash-escaped characters + if (c == "\\" && i < format.length - 1) { + out += format.charAt(i + 1); + i++; + continue; + } + switch (c) { + case "d": + var d = date.getDate(); + if (d < 10) { + out += "0"; + } + out += d; + break; + case "D": + out += days[date.getDay()].substring(0, 3); + break; + case "j": + out += date.getDate(); + break; + case "l": + out += days[date.getDay()]; + break; + case "N": + // TODO + break; + case "S": + // TODO + break; + case "w": + out += date.getDay(); + break; + case "z": + // TODO + break; + case "W": + // TODO + break; + case "F": + out += months[date.getMonth()]; + break; + case "m": + var m = date.getMonth() + 1; + if (m < 10) { + out += "0"; + } + out += m; + break; + case "M": + out += months[date.getMonth()].substring(0, 3); + break; + case "n": + out += date.getMonth() + 1; + break; + case "t": + // TODO + break; + case "L": + // TODO + break; + case "o": + // TODO + break; + case "Y": + out += date.getFullYear(); + break; + case "y": + var y = (date.getFullYear() + ""); + out += y.substring(y.length - 2); + break; + case "a": + if (date.getHours() < 12) { + out += "am"; + } else { + out += "pm"; + } + break; + case "A": + if (date.getHours() < 12) { + out += "AM"; + } else { + out += "PM"; + } + break; + case "B": + // TODO + break; + case "g": + var h = date.getHours() % 12; + if (h == 0) { + h = 12; + } + out += h; + break; + case "G": + out += date.getHours(); + break; + case "h": + var h = date.getHours() % 12; + if (h == 0) { + h = 12; + } + if (h < 10) { + out += "0"; + } + out += h; + break; + case "H": + var h = date.getHours(); + if (h < 10) { + out += "0"; + } + out += h; + break; + case "i": + var ii = date.getMinutes(); + if (ii < 10) { + out += "0"; + } + out += ii; + break; + case "s": + var s = date.getSeconds(); + if (s < 10) { + out += "0"; + } + out += s; + break; + case "u": + out += date.getMilliseconds() * 1000; + break; + case "v": + out += date.getMilliseconds(); + break; + case "e": + // TODO + break; + case "I": + // TODO + break; + case "O": + var off = date.getTimezoneOffset(); + var m = off % 60; + var h = (off - m) / 60; + if (off >= 0) { + out += "+"; + } else { + out += "-"; + } + if (h < 10) { + out += "0"; + } + out += h; + if (m < 10) { + out += "0"; + } + out += m; + break; + case "P": + var off = date.getTimezoneOffset(); + var m = off % 60; + var h = (off - m) / 60; + if (off >= 0) { + out += "+"; + } else { + out += "-"; + } + if (h < 10) { + out += "0"; + } + out += h; + out += ":"; + if (m < 10) { + out += "0"; + } + out += m; + break; + case "T": + // TODO + break; + case "Z": + out += date.getTimezoneOffset() * 60; + break; + case "c": + out += formatTimestamp(timestamp, "Y-m-d\\TH:i:sP"); + break; + case "r": + out += formatTimestamp(timestamp, "D, j M Y G:i:s O"); + break; + case "U": + out += Math.round(timestamp); + break; + default: + out += c; + } + } + + return out; +} + +function timestampToDateTimeString(timestamp) { + return timestampToDateString(timestamp) + " " + timestampToTimeString(timestamp); +} + +function timestampToDateString(timestamp) { + var date = new Date(timestamp * 1000); + + return date.toLocaleDateString(); } function timestampToTimeString(timestamp) { diff --git a/www/index.html b/www/index.html index bacdf9e..7bb39f2 100644 --- a/www/index.html +++ b/www/index.html @@ -53,6 +53,7 @@ + @@ -61,7 +62,6 @@ - diff --git a/www/pages/toolbox.html b/www/pages/toolbox.html index 4549a25..47686d6 100644 --- a/www/pages/toolbox.html +++ b/www/pages/toolbox.html @@ -53,6 +53,14 @@ +
  • + +
    +
    +
    Activity Log
    +
    +
    +
  • diff --git a/www/pages/toolbox/log.html b/www/pages/toolbox/log.html new file mode 100644 index 0000000..9d97971 --- /dev/null +++ b/www/pages/toolbox/log.html @@ -0,0 +1,57 @@ + + +
    + + + +
    +
    +
    + {{#if events}} +
    + {{#each events}} +
    +
    {{date}} {{addldate}}
    +
    +
    + {{#each entries}} +
    +
    {{time}}
    +
    {{title}}
    +
    {{subtitle}}
    +
    {{content}}
    +
    + {{/each}} +
    +
    + {{/each}} +
    + {{else}} +
    + +
    Log empty! When you mark packages as delivered, add notes, and more, this log will automatically gain entries.
    +
    + {{/if}} +
    +
    +
    + + +
    diff --git a/www/routes.js b/www/routes.js index 9eece32..3229918 100644 --- a/www/routes.js +++ b/www/routes.js @@ -313,6 +313,77 @@ var routes = [ url: './pages/toolbox/addrlookup.html', name: 'addrlookup' }, + { + path: '/log', + name: 'log', + async: function (routeTo, routeFrom, resolve, reject) { + var activitylog = getStorage("activitylog"); + let events = []; + if (activitylog != null) { + try { + events = JSON.parse(activitylog); + events.sort(function (x, y) { + if (x.date < y.date) { + return 1; + } else if (x.date > y.date) { + return -1; + } + return 0; + }); + let year = formatTimestamp("Y"); + let yearmonth = formatTimestamp("Y-m"); + let day = formatTimestamp("j"); + for (var i = 0; i < events.length; i++) { + let evtdate = events[i].date; + if (formatTimestamp("Y-m", evtdate) == yearmonth) { + // same year and month + if (formatTimestamp("J", evtdate) - day < 7) { + // same week + events[i].date = formatTimestamp("l", evtdate); + events[i].addldate = formatTimestamp("M j", evtdate); + } else { + events[i].date = formatTimestamp("D", evtdate); + events[i].addldate = formatTimestamp("M j", evtdate); + } + } else if (formatTimestamp("Y", evtdate) == year) { + // same year + events[i].date = formatTimestamp("M j", evtdate); + events[i].addldate = year; + } else { + // different year + events[i].date = formatTimestamp("M j", evtdate); + events[i].addldate = formatTimestamp("Y", evtdate); + } + events[i].entries.sort(function (x, y) { + if (x.timestamp < y.timestamp) { + return 1; + } else if (x.timestamp > y.timestamp) { + return -1; + } + return 0; + }); + for (var j = 0; j < events[i].entries.length; j++) { + events[i].entries[j].time = formatTimestamp("g:i a", events[i].entries[j].timestamp); + } + } + console.log(events); + } catch (ex) { + events = []; + } + } + console.log(events); + if (events == null || (typeof events == "object" && events.length == 0)) { + events = false; + } + resolve({ + templateUrl: './pages/toolbox/log.html' + }, { + context: { + events: events + } + }); + } + }, { path: '/sharelist', url: './pages/toolbox/sharelist.html', diff --git a/www/settings.js b/www/settings.js index 52e1343..c194fc3 100644 --- a/www/settings.js +++ b/www/settings.js @@ -6,6 +6,7 @@ var SETTINGS = { cacheversion: "v1.5.0_0", + activitylog_maxlength: 500, maptileurls: { liberty: { url: "https://maps.netsyms.net/styles/osm-liberty/{z}/{x}/{y}.png",