forked from Netsyms/PackageHelper
You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
638 lines
23 KiB
JavaScript
638 lines
23 KiB
JavaScript
/*
|
|
* 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/.
|
|
*/
|
|
|
|
var packages = [];
|
|
|
|
if (getStorage("packages") != null) {
|
|
packages = JSON.parse(getStorage("packages"));
|
|
}
|
|
|
|
/**
|
|
* Count how many items are still undelivered for a location.
|
|
* @param {type} location An item in the packages array.
|
|
* @returns {Number}
|
|
*/
|
|
function getUndeliveredCount(location) {
|
|
var undelivered = 0;
|
|
for (var i = 0; i < location.items.length; i++) {
|
|
if (!location.items[i].delivered) {
|
|
undelivered++;
|
|
}
|
|
}
|
|
return undelivered;
|
|
}
|
|
|
|
function getPackage(packageid) {
|
|
for (var i = 0; i < packages.length; i++) {
|
|
for (var j = 0; j < packages[i].items.length; j++) {
|
|
if (packages[i].items[j].id == packageid) {
|
|
return packages[i].items[j];
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
function getIconForType(type) {
|
|
return SETTINGS.itemtypes[type].icon;
|
|
}
|
|
|
|
function getMapIconForItems(items) {
|
|
var types = {};
|
|
var deliveredcount = 0;
|
|
var type = "package";
|
|
for (var i = 0; i < items.length; i++) {
|
|
// Don't consider delivered packages when determining icon,
|
|
// only count them to check if everything is delivered
|
|
if (items[i].delivered) {
|
|
deliveredcount++;
|
|
continue;
|
|
}
|
|
if (isNaN(types[items[i].type])) {
|
|
types[items[i].type] = 0;
|
|
}
|
|
types[items[i].type]++;
|
|
}
|
|
|
|
if (deliveredcount == items.length) {
|
|
return "check";
|
|
}
|
|
|
|
item_types = 0;
|
|
icon = "multiple-items";
|
|
// Count how many types we have, and set/overwrite the icon assuming we
|
|
// only have that type. If we end up with multiple types, we return that
|
|
// icon instead of a specific one.
|
|
//console.log(types);
|
|
for (var type in types) {
|
|
//console.log(type);
|
|
item_types++;
|
|
if (types[type] == 1) {
|
|
icon = SETTINGS.itemtypes[type].mapicon;
|
|
} else {
|
|
icon = SETTINGS.itemtypes[type].pluralmapicon;
|
|
}
|
|
//console.log(icon);
|
|
}
|
|
if (item_types > 1) {
|
|
return "multiple-items";
|
|
}
|
|
return icon;
|
|
}
|
|
|
|
function addPackage(address, latitude, longitude, type, callback, deadline) {
|
|
var added = false;
|
|
if (typeof type == 'undefined') {
|
|
type = SETTINGS.itemtypes[0].id;
|
|
}
|
|
if (typeof deadline == 'undefined') {
|
|
deadline = false;
|
|
}
|
|
|
|
if (typeof address == "object") {
|
|
var extendedaddress = address;
|
|
address = extendedaddress.address;
|
|
} else {
|
|
var extendedaddress = false;
|
|
}
|
|
|
|
// Extra precision makes the map stupider,
|
|
// and doesn't really increase accuracy since four decimal places ~= 11m
|
|
latitude = +(parseFloat("" + latitude).toFixed(4));
|
|
longitude = +(parseFloat("" + longitude).toFixed(4));
|
|
|
|
var packageID = uuidv4();
|
|
var coordsID = "";
|
|
|
|
for (var i = 0; i < packages.length; i++) {
|
|
if (packages[i].coords[0] == latitude && packages[i].coords[1] == longitude) {
|
|
coordsID = packages[i].id;
|
|
packages[i].items.push({
|
|
extended: extendedaddress,
|
|
address: address,
|
|
delivered: false,
|
|
type: type,
|
|
deadline: deadline,
|
|
id: packageID
|
|
});
|
|
added = true;
|
|
break;
|
|
}
|
|
}
|
|
if (!added) {
|
|
coordsID = uuidv4();
|
|
packages.push({
|
|
coords: [
|
|
latitude,
|
|
longitude
|
|
],
|
|
id: coordsID,
|
|
address: address,
|
|
items: [
|
|
{
|
|
extended: extendedaddress,
|
|
address: address,
|
|
delivered: false,
|
|
type: type,
|
|
deadline: deadline,
|
|
id: packageID
|
|
}
|
|
]
|
|
});
|
|
}
|
|
setStorage("packages", JSON.stringify(packages));
|
|
|
|
appendActivityLog("Added", SETTINGS.itemtypes[type].name, address, "fas fa-truck-loading");
|
|
|
|
playSound("ok");
|
|
|
|
app.toast.show({
|
|
text: SETTINGS.itemtypes[type].name + ' Added!<br><span style="font-size: 80%;">' + address + "</span>",
|
|
position: "bottom",
|
|
destroyOnClose: true,
|
|
closeTimeout: 1000 * 3
|
|
});
|
|
|
|
if (map != null) {
|
|
reloadMap();
|
|
}
|
|
|
|
if (typeof callback == 'function') {
|
|
callback({
|
|
coordsID: coordsID,
|
|
packageID: packageID
|
|
});
|
|
}
|
|
|
|
addAutofillEntry(address);
|
|
}
|
|
|
|
/**
|
|
* Import a second package list and merge it with the existing one.
|
|
* @param {type} newlist
|
|
* @return {number} The number of packages that were skipped because they already exist locally.
|
|
*/
|
|
function importPackageList(newlist) {
|
|
skipped = 0;
|
|
let count = 0;
|
|
for (latlng in newlist) {
|
|
var latitude = newlist[latlng].coords[0];
|
|
var longitude = newlist[latlng].coords[1];
|
|
|
|
latitude = +(parseFloat("" + latitude).toFixed(4));
|
|
longitude = +(parseFloat("" + longitude).toFixed(4));
|
|
|
|
for (pkg in newlist[latlng].items) {
|
|
var added = false;
|
|
for (var i = 0; i < packages.length; i++) {
|
|
if (+(parseFloat("" + packages[i].coords[0]).toFixed(4)) == latitude && +(parseFloat("" + packages[i].coords[1]).toFixed(4)) == longitude) {
|
|
var newpackage = newlist[latlng].items[pkg];
|
|
for (var j in packages[i].items) {
|
|
if (packages[i].items[j].id == newpackage.id) {
|
|
// This package already exists in the local database.
|
|
added = true;
|
|
skipped++;
|
|
}
|
|
}
|
|
if (!added) {
|
|
packages[i].items.push(package);
|
|
count++;
|
|
added = true;
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
if (!added) {
|
|
packages.push(newlist[latlng]);
|
|
}
|
|
}
|
|
}
|
|
setStorage("packages", JSON.stringify(packages));
|
|
|
|
appendActivityLog("Imported List", count + " items added", "", "fas fa-file-download");
|
|
if (map != null) {
|
|
reloadMap();
|
|
}
|
|
return skipped;
|
|
}
|
|
|
|
function mapCalibrate(item, packagesentry) {
|
|
// Determine if the delivery location isn't near the map pin
|
|
if (userPosition.coords.accuracy < 20 && timeDiff(userPosition.updated) < 10) {
|
|
// User location is accurate, check distance
|
|
var distance = getDistance(packagesentry.coords[0], packagesentry.coords[1], userPosition.coords.latitude, userPosition.coords.longitude);
|
|
var lat = userPosition.coords.latitude;
|
|
var lon = userPosition.coords.longitude;
|
|
if (distance > 100) { // Over 100 meters distance
|
|
if (typeof item.extended == "object") {
|
|
// we have all the info we need
|
|
var fixmap = function (item, latitude, longitude, locationtype, pkgsentry) {
|
|
$.ajax({
|
|
type: "POST",
|
|
url: SETTINGS.mapfixapi,
|
|
data: {
|
|
number: item.extended.number,
|
|
unit: item.extended.unit,
|
|
street: item.extended.street,
|
|
citystate: item.extended.citystate,
|
|
zip: item.extended.zip,
|
|
latitude: latitude,
|
|
longitude: longitude,
|
|
locationtype: locationtype
|
|
},
|
|
success: function () {
|
|
appendActivityLog("Map Calibrated",
|
|
item.address,
|
|
"Thanks for improving the map accuracy!<br>"
|
|
+ "Old: <a class='geolink' href='geo:" + pkgsentry.coords[0] + "," + pkgsentry.coords[1] + "'>" + pkgsentry.coords[0] + ", " + pkgsentry.coords[1] + "</a><br>"
|
|
+ "New: <a class='geolink' href='geo:" + latitude + "," + longitude + "'>" + latitude + ", " + longitude + "</a>",
|
|
"fas fa-map-marked-alt"
|
|
);
|
|
},
|
|
error: function () {
|
|
// try again in five minutes
|
|
setTimeout(function () {
|
|
fixmap(item, latitude, longitude, locationtype, pkgsentry);
|
|
}, 1000 * 60 * 5);
|
|
},
|
|
dataType: "json"
|
|
});
|
|
};
|
|
|
|
app.dialog.create({
|
|
title: 'Map Calibration',
|
|
text: "Your actual location doesn't match the map location for the " + SETTINGS.itemtypes[item.type].name + " at " + item.address + ". Where are you?",
|
|
buttons: [
|
|
{
|
|
text: 'Address',
|
|
close: true
|
|
},
|
|
{
|
|
text: 'Mailbox/CBU',
|
|
close: true
|
|
},
|
|
{
|
|
text: 'Parcel Locker',
|
|
close: true
|
|
},
|
|
{
|
|
text: "Other/Cancel",
|
|
close: true
|
|
}
|
|
],
|
|
verticalButtons: true,
|
|
onClick: function (dialog, index) {
|
|
switch (index) {
|
|
case 0:
|
|
fixmap(item, lat, lon, "address", packagesentry);
|
|
break;
|
|
case 1:
|
|
fixmap(item, lat, lon, "mailbox", packagesentry);
|
|
break;
|
|
case 2:
|
|
fixmap(item, lat, lon, "locker", packagesentry);
|
|
break;
|
|
default:
|
|
return;
|
|
}
|
|
}
|
|
}).open();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
function markDelivered(id, delivered) {
|
|
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) {
|
|
|
|
if (typeof delivered == 'undefined') {
|
|
if (packages[i].items[j].delivered == false) {
|
|
delivered = true;
|
|
} else {
|
|
delivered = false;
|
|
}
|
|
}
|
|
|
|
packages[i].items[j].delivered = delivered;
|
|
let gpslink = "";
|
|
if (userPosition.coords.accuracy < 40 && timeDiff(userPosition.updated) < 15) {
|
|
var lat = parseFloat(userPosition.coords.latitude).toFixed(5);
|
|
var lon = parseFloat(userPosition.coords.longitude).toFixed(5);
|
|
gpslink = "<br /><a class='geolink' href='geo:" + lat + "," + lon + "'>" + lat + ", " + lon + "</a> (±" + getDisplayDistance(userPosition.coords.accuracy, false) + ")";
|
|
}
|
|
if (delivered) {
|
|
packages[i].items[j].deliverytimestamp = time();
|
|
appendActivityLog("Delivered", SETTINGS.itemtypes[packages[i].items[j].type].name, packages[i].items[j].address + gpslink, "far fa-check-circle");
|
|
mapCalibrate(packages[i].items[j], packages[i]);
|
|
} 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
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
function confirmDeletePackage(package, callback) {
|
|
app.dialog.confirm(
|
|
"Delete " + SETTINGS.itemtypes[package.type].name.toLowerCase() + " at " + package.address + "?",
|
|
"Confirm",
|
|
function () {
|
|
// delete
|
|
deletePackage(package.id, callback);
|
|
},
|
|
function () {
|
|
// cancel
|
|
}
|
|
);
|
|
}
|
|
|
|
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) {
|
|
packages.splice(i, 1);
|
|
}
|
|
|
|
setStorage("packages", JSON.stringify(packages));
|
|
loadPackageList();
|
|
|
|
if (typeof callback == 'function') {
|
|
callback(id);
|
|
}
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
function countRemainingPackages() {
|
|
var undelivered = 0;
|
|
for (var i = 0; i < packages.length; i++) {
|
|
for (var j = 0; j < packages[i].items.length; j++) {
|
|
if (packages[i].items[j].delivered != true) {
|
|
undelivered++;
|
|
}
|
|
}
|
|
}
|
|
return undelivered;
|
|
}
|
|
|
|
function countPackages() {
|
|
var count = 0;
|
|
for (var i = 0; i < packages.length; i++) {
|
|
for (var j = 0; j < packages[i].items.length; j++) {
|
|
count++;
|
|
}
|
|
}
|
|
return count;
|
|
}
|
|
|
|
function addPackageByAddress(number, unit, street, citystate, zip, type, callback) {
|
|
var requestfinished = false;
|
|
var searchingdialogopen = false;
|
|
var deadline = false;
|
|
|
|
var ajaxlookup = function () {
|
|
var geocodecache = getStorage("geocode_cache");
|
|
if (geocodecache == null) {
|
|
geocodecache = "{}";
|
|
setStorage("geocode_cache", "{}");
|
|
}
|
|
|
|
geocodecache = JSON.parse(geocodecache);
|
|
var cachekey = number + " || " + street + " || " + citystate + " || " + zip + " || " + SETTINGS.itemtypes[type].allowedlocationtypes;
|
|
if (unit != '') {
|
|
cachekey = number + " || " + unit + " || " + street + " || " + citystate + " || " + zip + " || " + SETTINGS.itemtypes[type].allowedlocationtypes;
|
|
}
|
|
var cacheitem = geocodecache[cachekey];
|
|
var timestamp = Math.floor(Date.now() / 1000);
|
|
if (typeof cacheitem != 'undefined') {
|
|
if (cacheitem.added + SETTINGS.geocodecacheexpiry < timestamp) {
|
|
console.log("Info", "Removing expired geocode cache item " + cachekey);
|
|
delete geocodecache[cachekey];
|
|
setStorage("geocode_cache", JSON.stringify(geocodecache));
|
|
} else {
|
|
console.log("Info", "Using cached geocode result", cacheitem);
|
|
addPackage({
|
|
address: cacheitem.address,
|
|
number: number,
|
|
unit: unit,
|
|
street: street,
|
|
citystate: citystate,
|
|
zip: zip
|
|
}, cacheitem.latitude, cacheitem.longitude, type, callback, deadline);
|
|
return;
|
|
}
|
|
}
|
|
|
|
$.ajax({
|
|
url: SETTINGS.geocodeapi,
|
|
dataType: 'json',
|
|
data: {
|
|
number: number,
|
|
unit: unit,
|
|
street: street,
|
|
citystate: citystate,
|
|
zip: zip,
|
|
type: SETTINGS.itemtypes[type].allowedlocationtypes
|
|
},
|
|
timeout: 15 * 1000,
|
|
success: function (resp) {
|
|
if (searchingdialogopen) {
|
|
app.dialog.close();
|
|
searchingdialogopen = false;
|
|
}
|
|
requestfinished = true;
|
|
if (resp.status == "OK") {
|
|
if (resp.accuracy.ok) {
|
|
addPackage({
|
|
address: resp.address.street,
|
|
number: number,
|
|
unit: unit,
|
|
street: street,
|
|
citystate: citystate,
|
|
zip: zip
|
|
}, resp.coords[0], resp.coords[1], type, callback, deadline);
|
|
geocodecache[cachekey] = {
|
|
number: resp.address.number,
|
|
unit: unit,
|
|
address: resp.address.street,
|
|
latitude: resp.coords[0],
|
|
longitude: resp.coords[1],
|
|
added: Math.floor(Date.now() / 1000)
|
|
};
|
|
setStorage("geocode_cache", JSON.stringify(geocodecache));
|
|
} else {
|
|
playSound("error");
|
|
app.dialog.confirm(
|
|
"The address \"" + address + "\" couldn't be reliably located. Add it anyways?",
|
|
"Accuracy Warning",
|
|
function (ok) {
|
|
if (resp.address.street == "") {
|
|
addPackage({
|
|
address: address,
|
|
number: number,
|
|
unit: unit,
|
|
street: street,
|
|
citystate: citystate,
|
|
zip: zip
|
|
}, resp.coords[0], resp.coords[1], type, callback, deadline);
|
|
} else {
|
|
addPackage({
|
|
address: resp.address.street,
|
|
number: number,
|
|
unit: unit,
|
|
street: street,
|
|
citystate: citystate,
|
|
zip: zip
|
|
}, resp.coords[0], resp.coords[1], type, callback, deadline);
|
|
}
|
|
}
|
|
);
|
|
}
|
|
} else {
|
|
playSound("error");
|
|
app.dialog.alert(resp.message, "Error");
|
|
}
|
|
},
|
|
error: function (jqXHR, status, errorThrown) {
|
|
if (searchingdialogopen) {
|
|
app.dialog.close();
|
|
searchingdialogopen = false;
|
|
}
|
|
requestfinished = true;
|
|
playSound("error");
|
|
app.dialog.alert("There was a network issue while finding the address. Please try adding the item again.", "Error");
|
|
}
|
|
});
|
|
|
|
// Open a loading message if there's a delay finding the address
|
|
setTimeout(function () {
|
|
if (!requestfinished) {
|
|
app.dialog.preloader("Searching for address...");
|
|
searchingdialogopen = true;
|
|
}
|
|
}, 750);
|
|
}
|
|
|
|
var prelookup = function () {
|
|
if (type == "express") {
|
|
if (getStorage("deadlinealarm_minutes") == null) {
|
|
setStorage("deadlinealarm_minutes", 20);
|
|
}
|
|
var minutes = getStorage("deadlinealarm_minutes");
|
|
app.dialog.create({
|
|
title: 'Express Item',
|
|
text: 'Set a reminder for ' + minutes + ' minutes before:',
|
|
buttons: [
|
|
{
|
|
text: '10:30 AM',
|
|
close: true
|
|
},
|
|
{
|
|
text: '12:00 PM',
|
|
close: true
|
|
},
|
|
{
|
|
text: '3:00 PM',
|
|
close: true
|
|
},
|
|
{
|
|
text: "No reminder",
|
|
color: "red",
|
|
close: true
|
|
}
|
|
],
|
|
verticalButtons: true,
|
|
onClick: function (dialog, index) {
|
|
deadline = new Date();
|
|
switch (index) {
|
|
case 0:
|
|
deadline.setMinutes(30);
|
|
deadline.setHours(10);
|
|
break;
|
|
case 1:
|
|
deadline.setMinutes(00);
|
|
deadline.setHours(12);
|
|
break;
|
|
case 2:
|
|
deadline.setMinutes(00);
|
|
deadline.setHours(12 + 3);
|
|
break;
|
|
case 3:
|
|
default:
|
|
deadline = false;
|
|
break;
|
|
}
|
|
|
|
if (deadline != false) {
|
|
deadline = deadline.getTime() / 1000;
|
|
}
|
|
ajaxlookup();
|
|
}
|
|
}).open();
|
|
} else {
|
|
ajaxlookup();
|
|
}
|
|
}
|
|
|
|
var deliverable = isDeliverable(number, street);
|
|
|
|
if (deliverable.ok) {
|
|
prelookup();
|
|
} else {
|
|
app.dialog.confirm(
|
|
"A route note says this address " + deliverable.reason + ". Add item anyways?",
|
|
"Confirm",
|
|
function () {
|
|
prelookup();
|
|
},
|
|
function () {
|
|
// cancel
|
|
}
|
|
);
|
|
}
|
|
}
|
|
|
|
function checkDeadlines() {
|
|
if (getStorage("deadlinealarm_minutes") == null) {
|
|
setStorage("deadlinealarm_minutes", 20);
|
|
}
|
|
var minutes = getStorage("deadlinealarm_minutes");
|
|
var currentTime = new Date().getTime() / 1000;
|
|
var deadlineTime = currentTime + (minutes * 60);
|
|
for (i in packages) {
|
|
for (j in packages[i].items) {
|
|
var item = packages[i].items[j];
|
|
if (typeof item.deadline != 'undefined' && item.deadline != false && item.delivered != true) {
|
|
if ((typeof item.deadlinealarmed == 'undefined' || item.deadlinealarmed != true) && item.deadline <= deadlineTime) {
|
|
playSound("alert");
|
|
app.dialog.alert(
|
|
"Item at " + item.address + " needs to be delivered by " + timestampToTimeString(item.deadline) + " (" + Math.floor((item.deadline - currentTime) / 60) + " minutes from now).",
|
|
"Delivery Alarm",
|
|
function () {
|
|
|
|
}
|
|
);
|
|
packages[i].items[j].deadlinealarmed = true;
|
|
setStorage("packages", JSON.stringify(packages));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
setInterval(checkDeadlines, 15 * 1000); |