diff --git a/.babelrc.json b/.babelrc.json
new file mode 100644
index 00000000..15e24a3a
--- /dev/null
+++ b/.babelrc.json
@@ -0,0 +1,15 @@
+{
+ "presets": [
+ [
+ "@babel/preset-env",
+ {
+ "targets": {
+ "edge": "17",
+ "firefox": "60",
+ "chrome": "67",
+ "safari": "11.1"
+ }
+ }
+ ]
+ ]
+}
\ No newline at end of file
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index c34d93c5..142fd954 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -2,6 +2,15 @@
## Writing code
+### Javascript and compatibility with "old" browsers
+**Warning:** This project is using babel compiler... You need run an additional build step to make build js file
+```bash
+npm install
+npm run babel
+```
+NOTE: If you prefer you can use yarn instead, it's compatible
+NOTE2: This only affect to file `ereuse_devicehub/static/js/main_inventory.js`.
+
### Coding style
#### Python style
diff --git a/ereuse_devicehub/static/js/main_inventory.build.js b/ereuse_devicehub/static/js/main_inventory.build.js
new file mode 100644
index 00000000..09006905
--- /dev/null
+++ b/ereuse_devicehub/static/js/main_inventory.build.js
@@ -0,0 +1,581 @@
+"use strict";
+
+function _classStaticPrivateFieldSpecGet(receiver, classConstructor, descriptor) { _classCheckPrivateStaticAccess(receiver, classConstructor); _classCheckPrivateStaticFieldDescriptor(descriptor, "get"); return _classApplyDescriptorGet(receiver, descriptor); }
+
+function _classCheckPrivateStaticFieldDescriptor(descriptor, action) { if (descriptor === undefined) { throw new TypeError(`attempted to ${ action } private static field before its declaration`); } }
+
+function _classCheckPrivateStaticAccess(receiver, classConstructor) { if (receiver !== classConstructor) { throw new TypeError("Private static access of wrong provenance"); } }
+
+function _classApplyDescriptorGet(receiver, descriptor) { if (descriptor.get) { return descriptor.get.call(receiver); } return descriptor.value; }
+
+$(document).ready(() => {
+ const show_allocate_form = $("#allocateModal").data("show-action-form");
+ const show_datawipe_form = $("#datawipeModal").data("show-action-form");
+ const show_trade_form = $("#tradeLotModal").data("show-action-form");
+
+ if (show_allocate_form != "None") {
+ $("#allocateModal .btn-primary").show();
+ newAllocate(show_allocate_form);
+ } else if (show_datawipe_form != "None") {
+ $("#datawipeModal .btn-primary").show();
+ newDataWipe(show_datawipe_form);
+ } else if (show_trade_form != "None") {
+ $("#tradeLotModal .btn-primary").show();
+ newTrade(show_trade_form);
+ } else {
+ $(".deviceSelect").on("change", deviceSelect);
+ } // $('#selectLot').selectpicker();
+
+});
+
+class TableController {
+ /**
+ * @returns Selected inputs from device list
+ */
+ static getSelectedDevices() {
+ if (_classStaticPrivateFieldSpecGet(this, TableController, _tableRows).call(this) == undefined) return [];
+ return _classStaticPrivateFieldSpecGet(this, TableController, _tableRows).call(this).filter(element => element.querySelector("input").checked).map(element => element.querySelector("input"));
+ }
+ /**
+ * @returns Selected inputs in current page from device list
+ */
+
+
+ static getAllSelectedDevicesInCurrentPage() {
+ if (_classStaticPrivateFieldSpecGet(this, TableController, _tableRowsPage).call(this) == undefined) return [];
+ return _classStaticPrivateFieldSpecGet(this, TableController, _tableRowsPage).call(this).filter(element => element.querySelector("input").checked).map(element => element.querySelector("input"));
+ }
+ /**
+ * @returns All inputs from device list
+ */
+
+
+ static getAllDevices() {
+ if (_classStaticPrivateFieldSpecGet(this, TableController, _tableRows).call(this) == undefined) return [];
+ return _classStaticPrivateFieldSpecGet(this, TableController, _tableRows).call(this).map(element => element.querySelector("input"));
+ }
+ /**
+ * @returns All inputs from current page in device list
+ */
+
+
+ static getAllDevicesInCurrentPage() {
+ if (_classStaticPrivateFieldSpecGet(this, TableController, _tableRowsPage).call(this) == undefined) return [];
+ return _classStaticPrivateFieldSpecGet(this, TableController, _tableRowsPage).call(this).map(element => element.querySelector("input"));
+ }
+ /**
+ *
+ * @param {HTMLElement} DOMElements
+ * @returns Procesed input atributes to an Object class
+ */
+
+
+ static ProcessTR(DOMElements) {
+ return DOMElements.map(element => {
+ const info = {};
+ info.checked = element.checked;
+ Object.values(element.attributes).forEach(attrib => {
+ info[attrib.nodeName.replace(/-/g, "_")] = attrib.nodeValue;
+ });
+ return info;
+ });
+ }
+
+}
+/**
+ * Select all functionality
+ */
+
+
+var _tableRows = {
+ writable: true,
+ value: () => table.activeRows.length > 0 ? table.activeRows : []
+};
+var _tableRowsPage = {
+ writable: true,
+ value: () => table.pages[table.rows().dt.currentPage - 1]
+};
+window.addEventListener("DOMContentLoaded", () => {
+ const btnSelectAll = document.getElementById("SelectAllBTN");
+ const alertInfoDevices = document.getElementById("select-devices-info");
+
+ function itemListCheckChanged() {
+ const listDevices = TableController.getAllDevicesInCurrentPage();
+ const isAllChecked = listDevices.map(itm => itm.checked);
+
+ if (isAllChecked.every(bool => bool == true)) {
+ btnSelectAll.checked = true;
+ btnSelectAll.indeterminate = false;
+ alertInfoDevices.innerHTML = "Selected devices: ".concat(TableController.getSelectedDevices().length, "\n ").concat(TableController.getAllDevices().length != TableController.getSelectedDevices().length ? "Select all devices (".concat(TableController.getAllDevices().length, ")") : "Cancel selection");
+ alertInfoDevices.classList.remove("d-none");
+ } else if (isAllChecked.every(bool => bool == false)) {
+ btnSelectAll.checked = false;
+ btnSelectAll.indeterminate = false;
+ alertInfoDevices.classList.add("d-none");
+ } else {
+ btnSelectAll.indeterminate = true;
+ alertInfoDevices.classList.add("d-none");
+ }
+ }
+
+ TableController.getAllDevices().forEach(item => {
+ item.addEventListener("click", itemListCheckChanged);
+ });
+ btnSelectAll.addEventListener("click", event => {
+ const checkedState = event.target.checked;
+ TableController.getAllDevicesInCurrentPage().forEach(ckeckbox => {
+ ckeckbox.checked = checkedState;
+ });
+ itemListCheckChanged();
+ });
+ alertInfoDevices.addEventListener("click", () => {
+ const checkState = TableController.getAllDevices().length == TableController.getSelectedDevices().length;
+ TableController.getAllDevices().forEach(ckeckbox => {
+ ckeckbox.checked = !checkState;
+ });
+ itemListCheckChanged();
+ }); // https://github.com/fiduswriter/Simple-DataTables/wiki/Events
+
+ table.on("datatable.page", () => itemListCheckChanged());
+ table.on("datatable.perpage", () => itemListCheckChanged());
+ table.on("datatable.update", () => itemListCheckChanged());
+});
+
+function deviceSelect() {
+ const devices_count = TableController.getSelectedDevices().length;
+ get_device_list();
+
+ if (devices_count == 0) {
+ $("#addingLotModal .pol").show();
+ $("#addingLotModal .btn-primary").hide();
+ $("#removeLotModal .pol").show();
+ $("#removeLotModal .btn-primary").hide();
+ $("#addingTagModal .pol").show();
+ $("#addingTagModal .btn-primary").hide();
+ $("#actionModal .pol").show();
+ $("#actionModal .btn-primary").hide();
+ $("#allocateModal .pol").show();
+ $("#allocateModal .btn-primary").hide();
+ $("#datawipeModal .pol").show();
+ $("#datawipeModal .btn-primary").hide();
+ } else {
+ $("#addingLotModal .pol").hide();
+ $("#addingLotModal .btn-primary").show();
+ $("#removeLotModal .pol").hide();
+ $("#removeLotModal .btn-primary").show();
+ $("#actionModal .pol").hide();
+ $("#actionModal .btn-primary").show();
+ $("#allocateModal .pol").hide();
+ $("#allocateModal .btn-primary").show();
+ $("#datawipeModal .pol").hide();
+ $("#datawipeModal .btn-primary").show();
+ $("#addingTagModal .pol").hide();
+ $("#addingTagModal .btn-primary").show();
+ }
+}
+
+function removeLot() {
+ const devices = TableController.getAllDevices();
+
+ if (devices.length > 0) {
+ $("#btnRemoveLots .text-danger").show();
+ } else {
+ $("#btnRemoveLots .text-danger").hide();
+ }
+
+ $("#activeRemoveLotModal").click();
+}
+
+function removeTag() {
+ const devices = TableController.getSelectedDevices();
+ const devices_id = devices.map(dev => dev.data);
+
+ if (devices_id.length == 1) {
+ const url = "/inventory/tag/devices/".concat(devices_id[0], "/del/");
+ window.location.href = url;
+ } else {
+ $("#unlinkTagAlertModal").click();
+ }
+}
+
+function addTag() {
+ const devices = TableController.getSelectedDevices();
+ const devices_id = devices.map(dev => dev.data);
+
+ if (devices_id.length == 1) {
+ $("#addingTagModal .pol").hide();
+ $("#addingTagModal .btn-primary").show();
+ } else {
+ $("#addingTagModal .pol").show();
+ $("#addingTagModal .btn-primary").hide();
+ }
+
+ $("#addTagAlertModal").click();
+}
+
+function newTrade(action) {
+ let title = "Trade ";
+ const user_to = $("#user_to").data("email");
+ const user_from = $("#user_from").data("email");
+
+ if (action == "user_from") {
+ title = "Trade Incoming";
+ $("#user_to").attr("readonly", "readonly");
+ $("#user_from").prop("readonly", false);
+ $("#user_from").val("");
+ $("#user_to").val(user_to);
+ } else if (action == "user_to") {
+ title = "Trade Outgoing";
+ $("#user_from").attr("readonly", "readonly");
+ $("#user_to").prop("readonly", false);
+ $("#user_to").val("");
+ $("#user_from").val(user_from);
+ }
+
+ $("#tradeLotModal #title-action").html(title);
+ $("#activeTradeModal").click();
+}
+
+function newAction(action) {
+ $("#actionModal #type").val(action);
+ $("#actionModal #title-action").html(action);
+ deviceSelect();
+ $("#activeActionModal").click();
+}
+
+function newAllocate(action) {
+ $("#allocateModal #type").val(action);
+ $("#allocateModal #title-action").html(action);
+ deviceSelect();
+ $("#activeAllocateModal").click();
+}
+
+function newDataWipe(action) {
+ $("#datawipeModal #type").val(action);
+ $("#datawipeModal #title-action").html(action);
+ deviceSelect();
+ $("#activeDatawipeModal").click();
+}
+
+function get_device_list() {
+ const devices = TableController.getSelectedDevices();
+ /* Insert the correct count of devices in actions form */
+
+ const devices_count = devices.length;
+ $("#datawipeModal .devices-count").html(devices_count);
+ $("#allocateModal .devices-count").html(devices_count);
+ $("#actionModal .devices-count").html(devices_count);
+ /* Insert the correct value in the input devicesList */
+
+ const devices_id = $.map(devices, x => $(x).attr("data")).join(",");
+ $.map($(".devicesList"), x => {
+ $(x).val(devices_id);
+ });
+ /* Create a list of devices for human representation */
+
+ const computer = {
+ "Desktop": "",
+ "Laptop": ""
+ };
+ const list_devices = devices.map(x => {
+ let typ = $(x).data("device-type");
+ const manuf = $(x).data("device-manufacturer");
+ const dhid = $(x).data("device-dhid");
+
+ if (computer[typ]) {
+ typ = computer[typ];
+ }
+
+ ;
+ return "".concat(typ, " ").concat(manuf, " ").concat(dhid);
+ });
+ const description = $.map(list_devices, x => x).join(", ");
+ $(".enumeration-devices").html(description);
+}
+
+function export_file(type_file) {
+ const devices = TableController.getSelectedDevices();
+ const devices_id = $.map(devices, x => $(x).attr("data-device-dhid")).join(",");
+
+ if (devices_id) {
+ const url = "/inventory/export/".concat(type_file, "/?ids=").concat(devices_id);
+ window.location.href = url;
+ } else {
+ $("#exportAlertModal").click();
+ }
+}
+/**
+ * Reactive lots button
+ */
+
+
+async function processSelectedDevices() {
+ class Actions {
+ constructor() {
+ this.list = []; // list of petitions of requests @item --> {type: ["Remove" | "Add"], "LotID": string, "devices": number[]}
+ }
+ /**
+ * Manage the actions that will be performed when applying the changes
+ * @param {EventSource} ev event (Should be a checkbox type)
+ * @param {Lot} lot lot id
+ * @param {Device[]} selectedDevices device id
+ */
+
+
+ manage(event, lot, selectedDevices) {
+ event.preventDefault();
+ const lotID = lot.id;
+ const srcElement = event.srcElement.parentElement.children[0];
+ const checked = !srcElement.checked;
+ const found = this.list.filter(list => list.lot.id == lotID)[0];
+
+ if (checked) {
+ if (found && found.type == "Remove") {
+ found.type = "Add";
+ } else {
+ this.list.push({
+ type: "Add",
+ lot,
+ devices: selectedDevices
+ });
+ }
+ } else if (found && found.type == "Add") {
+ found.type = "Remove";
+ } else {
+ this.list.push({
+ type: "Remove",
+ lot,
+ devices: selectedDevices
+ });
+ }
+
+ if (this.list.length > 0) {
+ document.getElementById("ApplyDeviceLots").classList.remove("disabled");
+ } else {
+ document.getElementById("ApplyDeviceLots").classList.add("disabled");
+ }
+ }
+ /**
+ * Creates notification to give feedback to user
+ * @param {string} title notification title
+ * @param {string | null} toastText notification text
+ * @param {boolean} isError defines if a toast is a error
+ */
+
+
+ notifyUser(title, toastText, isError) {
+ const toast = document.createElement("div");
+ toast.classList = "alert alert-dismissible fade show ".concat(isError ? "alert-danger" : "alert-success");
+ toast.attributes["data-autohide"] = !isError;
+ toast.attributes.role = "alert";
+ toast.style = "margin-left: auto; width: fit-content;";
+ toast.innerHTML = "".concat(title, "");
+
+ if (toastText && toastText.length > 0) {
+ toast.innerHTML += "
".concat(toastText);
+ }
+
+ document.getElementById("NotificationsContainer").appendChild(toast);
+
+ if (!isError) {
+ setTimeout(() => toast.classList.remove("show"), 3000);
+ }
+
+ setTimeout(() => document.getElementById("NotificationsContainer").innerHTML == "", 3500);
+ }
+ /**
+ * Get actions and execute call request to add or remove devices from lots
+ */
+
+
+ doActions() {
+ let requestCount = 0; // This is for count all requested api count, to perform reRender of table device list
+
+ this.list.forEach(async action => {
+ if (action.type == "Add") {
+ try {
+ const devicesIDs = action.devices.filter(dev => !action.lot.devices.includes(dev.id)).map(dev => dev.id);
+ await Api.devices_add(action.lot.id, devicesIDs);
+ this.notifyUser("Devices sucefully added to selected lot/s", "", false);
+ } catch (error) {
+ this.notifyUser("Failed to add devices to selected lot/s", error.responseJSON.message, true);
+ }
+ } else if (action.type == "Remove") {
+ try {
+ const devicesIDs = action.devices.filter(dev => action.lot.devices.includes(dev.id)).map(dev => dev.id);
+ await Api.devices_remove(action.lot.id, devicesIDs);
+ this.notifyUser("Devices sucefully removed from selected lot/s", "", false);
+ } catch (error) {
+ this.notifyUser("Failed to remove devices from selected lot/s", error.responseJSON.message, true);
+ }
+ }
+
+ requestCount += 1;
+
+ if (requestCount == this.list.length) {
+ this.reRenderTable();
+ this.list = [];
+ }
+ });
+ $("#confirmLotsModal").modal("hide"); // Hide dialog when click "Save changes"
+
+ document.getElementById("dropDownLotsSelector").classList.remove("show");
+ }
+ /**
+ * Re-render list in table
+ */
+
+
+ async reRenderTable() {
+ const newRequest = await Api.doRequest(window.location);
+ const tmpDiv = document.createElement("div");
+ tmpDiv.innerHTML = newRequest;
+ const newTable = Array.from(tmpDiv.querySelectorAll("table.table > tbody > tr .deviceSelect")).map(x => x.attributes["data-device-dhid"].value); // https://github.com/fiduswriter/Simple-DataTables/wiki/rows()#removeselect-arraynumber
+
+ const rowsToRemove = [];
+
+ for (let i = 0; i < table.activeRows.length; i++) {
+ const row = table.activeRows[i];
+
+ if (!newTable.includes(row.querySelector("input").attributes["data-device-dhid"].value)) {
+ rowsToRemove.push(i);
+ }
+ }
+
+ table.rows().remove(rowsToRemove); // Restore state of checkbox
+
+ const selectAllBTN = document.getElementById("SelectAllBTN");
+ selectAllBTN.checked = false;
+ selectAllBTN.indeterminate = false;
+ }
+
+ }
+
+ let eventClickActions;
+ /**
+ * Generates a list item with a correspondient checkbox state
+ * @param {Object} lot Lot model server
+ * @param {Device[]} selectedDevices list selected devices
+ * @param {HTMLElement} elementTarget
+ * @param {Action[]} actions
+ */
+
+ function templateLot(lot, selectedDevices, elementTarget, actions) {
+ elementTarget.innerHTML = "";
+ const {
+ id,
+ name,
+ state
+ } = lot;
+ const htmlTemplate = "\n ");
+ const doc = document.createElement("li");
+ doc.innerHTML = htmlTemplate;
+
+ switch (state) {
+ case "true":
+ doc.children[0].checked = true;
+ break;
+
+ case "false":
+ doc.children[0].checked = false;
+ break;
+
+ case "indetermined":
+ doc.children[0].indeterminate = true;
+ break;
+
+ default:
+ console.warn("This shouldn't be happend: Lot without state: ", lot);
+ break;
+ }
+
+ doc.children[0].addEventListener("mouseup", ev => actions.manage(ev, lot, selectedDevices));
+ doc.children[1].addEventListener("mouseup", ev => actions.manage(ev, lot, selectedDevices));
+ elementTarget.append(doc);
+ }
+
+ const listHTML = $("#LotsSelector"); // Get selected devices
+
+ const selectedDevicesID = TableController.ProcessTR(TableController.getSelectedDevices()).map(item => item.data);
+
+ if (selectedDevicesID.length <= 0) {
+ listHTML.html("
\n ").concat(devices.map(item => { + const name = "".concat(item.type, " ").concat(item.manufacturer, " ").concat(item.model); + return "").concat(item.devicehubID, ""); + }).join(" "), "\n
\n