Project import generated by Copybara.

GitOrigin-RevId: 63746295f1a5ab5a619056791995793d65529e62
diff --git a/src/js/calendar.js b/src/js/calendar.js
new file mode 100644
index 0000000..920bf98
--- /dev/null
+++ b/src/js/calendar.js
@@ -0,0 +1,8 @@
+document.addEventListener("DOMContentLoaded", _ => {
+  document.querySelectorAll("select").forEach(el => {
+    el.addEventListener("change", _ => {
+      el.setAttribute("data-value", el.value);
+    });
+    el.setAttribute("data-value", el.value);
+  });
+});
diff --git a/src/js/categories.js b/src/js/categories.js
new file mode 100644
index 0000000..412f585
--- /dev/null
+++ b/src/js/categories.js
@@ -0,0 +1,6 @@
+window.addEventListener("load", function() {
+  document.querySelector(".addcategory").addEventListener("click", function() {
+    document.querySelector("#addcategory").showModal();
+    /* Or dialog.show(); to show the dialog without a backdrop. */
+  });
+});
diff --git a/src/js/common.js b/src/js/common.js
new file mode 100644
index 0000000..445016e
--- /dev/null
+++ b/src/js/common.js
@@ -0,0 +1,242 @@
+/**
+  * Functions hasClass(), addClass() and removeClass() developed by Jake Trent (http://jaketrent.com/post/addremove-classes-raw-javascript/)
+  */
+function hasClass(el, className) {
+  if (el.classList)
+    return el.classList.contains(className)
+  else
+    return !!el.className.match(new RegExp('(\\s|^)' + className + '(\\s|$)'))
+}
+
+function addClass(el, className) {
+  if (el.classList)
+    el.classList.add(className)
+  else if (!hasClass(el, className)) el.className += " " + className
+}
+
+function removeClass(el, className) {
+  if (el.classList)
+    el.classList.remove(className)
+  else if (hasClass(el, className)) {
+    var reg = new RegExp('(\\s|^)' + className + '(\\s|$)')
+    el.className=el.className.replace(reg, ' ')
+  }
+}
+
+// MultiSelect implementation:
+var MultiSelect = function MultiSelect(element) {
+  this.element_ = element;
+  this.selected_ = [];
+
+  var forElId = this.element_.getAttribute("for") || this.element_.getAttribute("data-for");
+  if (forElId) {
+    this.forEl_ = (forElId ? document.getElementById(this.element_.getAttribute("for")) : null);
+  }
+
+  this.init();
+};
+
+MultiSelect.prototype.renderSummary = function() {
+  if (this.forEl_) {
+    this.forEl_.innerText = this.selected_.join(", ") || "-";
+  }
+};
+
+MultiSelect.prototype.init = function() {
+  if (this.element_) {
+    this.element_.addEventListener("click", e => {
+      e.stopImmediatePropagation();
+    }, true);
+
+    this.element_.querySelectorAll(".mdl-custom-multiselect__item").forEach(item => {
+      var checkbox = item.querySelector("input[type=\"checkbox\"]");
+      var label = item.querySelector(".mdl-checkbox__label").innerText;
+      checkbox.addEventListener("change", e => {
+        if(checkbox.checked) {
+          this.selected_.push(label);
+        } else {
+          this.selected_ = this.selected_.filter(item => item !== label);
+        }
+        this.renderSummary();
+
+        var customEvent = new Event('custom-multiselect-change');
+        this.element_.dispatchEvent(customEvent);
+      });
+    });
+  }
+};
+
+var dynDialog = {
+  didItInit: false,
+  dialog: null,
+  url: null,
+  load: function(url, reload) {
+    if (this.didItInit === false) {
+      this.init();
+    }
+
+    if (this.url == url && reload !== true) {
+      this.show();
+      return;
+    }
+
+    this.url = url;
+
+    fetch(url).then(response => response.text()).then(response => {
+      if (this.dialog.open) {
+        this.close();
+      }
+
+      this.dialog.innerHTML = response;
+      componentHandler.upgradeElements(this.dialog);
+
+      this.dialog.querySelectorAll("[data-required]").forEach(input => {
+        input.setAttribute("required", "true");
+      });
+
+      var script = this.dialog.querySelectorAll("dynscript");
+      if (script.length > 0) {
+        for (var i = 0; i < script.length; i++) {
+          eval(script[i].innerText);
+        }
+      }
+      this.dialog.querySelectorAll("[data-dyndialog-close]").forEach(btn => {
+        btn.addEventListener("click", e => {
+          e.preventDefault();
+          this.close();
+        });
+      });
+
+      this.dialog.showModal();
+    });
+  },
+  reload: function() {
+    this.load(this.url, true);
+  },
+  show: function() {
+    this.dialog.showModal();
+  },
+  close: function() {
+    this.dialog.close();
+  },
+  init: function() {
+    this.dialog = document.createElement("dialog");
+    this.dialog.setAttribute("id", "dynDialog");
+    this.dialog.setAttribute("class", "mdl-dialog");
+    dialogPolyfill.registerDialog(this.dialog);
+    document.body.appendChild(this.dialog);
+
+    this.didItInit = true;
+  }
+};
+
+// From nodep-date-input-polyfill
+function isDateInputSupported() {
+  const input = document.createElement("input");
+  input.setAttribute("type", "date");
+
+  const notADateValue = "not-a-date";
+  input.setAttribute("value", notADateValue);
+
+  return (input.value !== notADateValue);
+}
+
+document.addEventListener("DOMContentLoaded", function() {
+  var dialogs = document.querySelectorAll("dialog");
+  for (var i = 0; i < dialogs.length; i++) {
+    dialogPolyfill.registerDialog(dialogs[i]);
+  }
+
+  document.querySelectorAll("[data-dyndialog-href]").forEach(link => {
+    link.addEventListener("click", e => {
+      e.preventDefault();
+      dynDialog.load(link.getAttribute("data-dyndialog-href"));
+    });
+  });
+
+  document.querySelectorAll(".mdl-custom-multiselect-js").forEach(menu => {
+    new MultiSelect(menu);
+  });
+});
+
+function importCSS(url) {
+  var link = document.createElement("link");
+  link.setAttribute("rel", "stylesheet");
+  link.setAttribute("href", url);
+  document.head.appendChild(link);
+}
+
+var loadScriptAsync = function(uri) {
+  return new Promise((resolve, reject) => {
+    var script = document.createElement('script');
+    script.src = uri;
+    script.async = true;
+    script.onload = () => {
+      resolve();
+    };
+    document.head.appendChild(script);
+  });
+}
+
+function hasParentDialog(el) {
+  while (el != document.documentElement) {
+    if (el.tagName == "DIALOG") {
+      return el;
+    }
+
+    el = el.parentNode;
+  }
+
+  return undefined;
+}
+
+function polyfillDateSupport(container) {
+  container.querySelectorAll("input[type=\"date\"]").forEach(el => {
+    el.setAttribute("type", "text");
+
+    dialogParent = hasParentDialog(el);
+
+    var options = {
+      format: "yyyy-mm-dd",
+      todayHighlight: true,
+      weekStart: 1,
+      zIndexOffset: 100,
+      container: dialogParent || container
+    };
+
+    if (el.hasAttribute("max")) {
+      options.endDate = el.getAttribute("max");
+    }
+
+    $(el).datepicker(options);
+  });
+
+  container.querySelectorAll("input[type=\"time\"]").forEach(el => {
+    el.setAttribute("placeholder", "hh:mm");
+  });
+}
+
+function initPolyfillDateSupport() {
+  console.info("Polyfilling date support");
+
+  importCSS("https://unpkg.com/bootstrap-datepicker@1.9.0/dist/css/bootstrap-datepicker3.standalone.min.css", "css");
+
+  loadScriptAsync("https://unpkg.com/jquery@3.4.1/dist/jquery.min.js", "js").then(_ => {
+    return loadScriptAsync("https://unpkg.com/bootstrap-datepicker@1.9.0/dist/js/bootstrap-datepicker.min.js");
+  }).then(_ => {
+    return loadScriptAsync("https://unpkg.com/bootstrap-datepicker@1.9.0/dist/locales/bootstrap-datepicker.es.min.js");
+  }).then(_ => {
+    console.log("[Date polyfill] Scripts loaded.");
+    polyfillDateSupport(document.documentElement);
+  });
+}
+
+window.addEventListener("load", function() {
+  document.querySelectorAll("[data-required]").forEach(input => {
+    input.setAttribute("required", "true");
+  });
+
+  if (!isDateInputSupported()) {
+    initPolyfillDateSupport();
+  }
+});
diff --git a/src/js/common_webauthn.js b/src/js/common_webauthn.js
new file mode 100644
index 0000000..fa95364
--- /dev/null
+++ b/src/js/common_webauthn.js
@@ -0,0 +1,44 @@
+/**
+ * convert RFC 1342-like base64 strings to array buffer
+ * @param {mixed} obj
+ * @returns {undefined}
+ */
+function recursiveBase64StrToArrayBuffer(obj) {
+    let prefix = '=?BINARY?B?';
+    let suffix = '?=';
+    if (typeof obj === 'object') {
+        for (let key in obj) {
+            if (typeof obj[key] === 'string') {
+                let str = obj[key];
+                if (str.substring(0, prefix.length) === prefix && str.substring(str.length - suffix.length) === suffix) {
+                    str = str.substring(prefix.length, str.length - suffix.length);
+
+                    let binary_string = window.atob(str);
+                    let len = binary_string.length;
+                    let bytes = new Uint8Array(len);
+                    for (var i = 0; i < len; i++)        {
+                        bytes[i] = binary_string.charCodeAt(i);
+                    }
+                    obj[key] = bytes.buffer;
+                }
+            } else {
+                recursiveBase64StrToArrayBuffer(obj[key]);
+            }
+        }
+    }
+}
+
+/**
+ * Convert a ArrayBuffer to Base64
+ * @param {ArrayBuffer} buffer
+ * @returns {String}
+ */
+function arrayBufferToBase64(buffer) {
+    var binary = '';
+    var bytes = new Uint8Array(buffer);
+    var len = bytes.byteLength;
+    for (var i = 0; i < len; i++) {
+        binary += String.fromCharCode( bytes[ i ] );
+    }
+    return window.btoa(binary);
+}
diff --git a/src/js/companies.js b/src/js/companies.js
new file mode 100644
index 0000000..aa8d9dd
--- /dev/null
+++ b/src/js/companies.js
@@ -0,0 +1,6 @@
+window.addEventListener("load", function() {
+  document.querySelector(".addcompany").addEventListener("click", function() {
+    document.querySelector("#addcompany").showModal();
+    /* Or dialog.show(); to show the dialog without a backdrop. */
+  });
+});
diff --git a/src/js/export.js b/src/js/export.js
new file mode 100644
index 0000000..9bdbbbe
--- /dev/null
+++ b/src/js/export.js
@@ -0,0 +1,71 @@
+function toggleTr(tr, show) {
+  var checkbox = tr.querySelector("label").MaterialCheckbox;
+  if (show) {
+    tr.style.display = "table-row";
+    checkbox.enable();
+  } else {
+    tr.style.display = "none";
+    checkbox.disable();
+  }
+}
+
+window.addEventListener("load", function() {
+  document.querySelectorAll("tr[data-worker-id]").forEach(tr => {
+    var checkbox = tr.querySelector("input[type=\"checkbox\"]");
+
+    checkbox.setAttribute("name", "workers[]");
+    checkbox.setAttribute("value", tr.getAttribute("data-worker-id"));
+
+    toggleTr(tr, false);
+  });
+
+  document.querySelectorAll(".select-all").forEach(el => {
+    el.addEventListener("click", e => {
+      var allchecked = true;
+      el.getAttribute("data-workers").split(",").forEach(workerid => {
+        var tr = document.querySelector("tr[data-worker-id=\""+workerid+"\"]");
+        var checkbox = tr.querySelector("label").MaterialCheckbox;
+        if (checkbox.inputElement_.disabled) return;
+        if (!checkbox.inputElement_.checked) allchecked = false;
+        tr.classList.add("is-selected");
+        checkbox.check();
+      });
+
+      if (allchecked) {
+        el.getAttribute("data-workers").split(",").forEach(workerid => {
+          var tr = document.querySelector("tr[data-worker-id=\""+workerid+"\"]");
+          var checkbox = tr.querySelector("label").MaterialCheckbox;
+          tr.classList.remove("is-selected");
+          checkbox.uncheck();
+        });
+      }
+    });
+  });
+
+  var multiselectEl = document.querySelector(".mdl-custom-multiselect");
+  if (multiselectEl !== null) {
+    multiselectEl.addEventListener("custom-multiselect-change", e => {
+      var companies = [];
+      document.querySelectorAll(".mdl-custom-multiselect .mdl-custom-multiselect__item input[type=\"checkbox\"]").forEach(checkbox => {
+        if (checkbox.checked) {
+          companies.push(checkbox.value);
+        }
+      });
+
+      document.querySelectorAll("tr[data-worker-id]").forEach(tr => {
+        toggleTr(tr, companies.includes(tr.getAttribute("data-company-id")));
+      });
+    });
+  }
+
+  document.querySelectorAll("input[name=\"companies\[\]\"]").forEach(input => {
+    input.checked = true;
+    var customevent = document.createEvent("HTMLEvents");
+    customevent.initEvent("change", false, true);
+    input.dispatchEvent(customevent);
+  });
+
+  document.getElementById("format").addEventListener("change", e => {
+    document.getElementById("pdf").style.display = (document.getElementById("format").value != "1" && document.getElementById("format").value != "2" ? "none" : "block");
+  });
+});
diff --git a/src/js/incidents.js b/src/js/incidents.js
new file mode 100644
index 0000000..0397cac
--- /dev/null
+++ b/src/js/incidents.js
@@ -0,0 +1,79 @@
+function getFormData() {
+  var incidents = [];
+
+  document.querySelectorAll("input[type=\"checkbox\"][data-incident]:checked").forEach(el => {
+    incidents.push(el.getAttribute("data-incident"));
+  });
+
+  return incidents;
+}
+
+function getParameters() {
+  var parameters = [];
+  var incidents = getFormData();
+  incidents.forEach(incident => {
+    parameters.push("incidents[]="+incident);
+  });
+
+  if (parameters.length == 0) return false;
+
+  return parameters.join("&");
+}
+
+window.addEventListener("load", function() {
+  document.querySelector(".addincident").addEventListener("click", function() {
+    document.querySelector("#addincident").showModal();
+    /* Or dialog.show(); to show the dialog without a backdrop. */
+  });
+
+  document.querySelector(".addrecurringincident").addEventListener("click", function() {
+    document.querySelector("#addrecurringincident").showModal();
+    /* Or dialog.show(); to show the dialog without a backdrop. */
+  });
+
+  document.querySelector(".filter").addEventListener("click", function() {
+    document.querySelector("#filter").showModal();
+    /* Or dialog.show(); to show the dialog without a backdrop. */
+  });
+
+  if (_showResultsPaginated) {
+    document.getElementById("limit-change").addEventListener("change", _ => {
+      var limit = parseInt(document.getElementById("limit-change").value);
+      var firstIncidentPos = _page*_limit;
+      var page = Math.floor(firstIncidentPos/limit) + 1;
+
+      var url = new URL(location.href);
+      url.searchParams.set("limit", limit);
+      url.searchParams.set("page", page);
+      location.href = url;
+    });
+  }
+
+  document.querySelectorAll(".mdl-checkbox[data-check-all=\"true\"] input[type=\"checkbox\"]").forEach(el => {
+    el.addEventListener("change", e => {
+      el.parentElement.parentElement.parentElement.parentElement.parentElement.querySelectorAll(".mdl-checkbox:not([data-check-all=\"true\"])").forEach(input => {
+        var checkbox = input.MaterialCheckbox;
+        if (checkbox.inputElement_.disabled) return;
+
+        if (el.checked) checkbox.check();
+        else checkbox.uncheck();
+      });
+    });
+  });
+
+  document.getElementById("deleteincidentsbulk").addEventListener("click", e => {
+    var parameters = getParameters();
+
+    if (parameters === false) {
+      document.querySelector(".mdl-js-snackbar").MaterialSnackbar.showSnackbar({
+        message: "Debes seleccionar al menos una incidencia para poder eliminar.",
+        timeout: 5000
+      });
+
+      return;
+    }
+
+    var url = "dynamic/deleteincidentsbulk.php?"+parameters;
+    dynDialog.load(url);
+  });
+});
diff --git a/src/js/incidentsgeneric.js b/src/js/incidentsgeneric.js
new file mode 100644
index 0000000..5d1323f
--- /dev/null
+++ b/src/js/incidentsgeneric.js
@@ -0,0 +1,13 @@
+window.addEventListener("load", function() {
+
+  document.querySelectorAll(".custom-actions-btn").forEach(el => {
+    el.addEventListener("click", e => {
+      var forId = el.getAttribute("id");
+      var menu = document.querySelector("[for=\""+forId+"\"]").parentElement;
+      var overflow = menu.parentElement;
+
+      menu.style.left = el.offsetLeft - menu.offsetWidth + el.offsetWidth - overflow.scrollLeft + 'px';
+      menu.style.top = el.offsetTop + el.offsetHeight - overflow.scrollTop  + 'px';
+    });
+  });
+});
diff --git a/src/js/incidenttypes.js b/src/js/incidenttypes.js
new file mode 100644
index 0000000..48e8ae4
--- /dev/null
+++ b/src/js/incidenttypes.js
@@ -0,0 +1,6 @@
+window.addEventListener("load", function() {
+  document.querySelector(".addincident").addEventListener("click", function() {
+    document.querySelector("#addincident").showModal();
+    /* Or dialog.show(); to show the dialog without a backdrop. */
+  });
+});
diff --git a/src/js/index.js b/src/js/index.js
new file mode 100644
index 0000000..8af86b3
--- /dev/null
+++ b/src/js/index.js
@@ -0,0 +1,8 @@
+window.addEventListener("load", function() {
+  if (document.querySelector("#recoverybtn")) {
+    document.querySelector("#recoverybtn").addEventListener("click", function(e) {
+      e.preventDefault();
+      document.querySelector("#recovery").showModal();
+    });
+  }
+});
diff --git a/src/js/invalidatebulkrecords.js b/src/js/invalidatebulkrecords.js
new file mode 100644
index 0000000..cfbe2d5
--- /dev/null
+++ b/src/js/invalidatebulkrecords.js
@@ -0,0 +1,8 @@
+window.addEventListener("load", function() {
+  document.querySelectorAll("tr[data-worker-id]").forEach(tr => {
+    var checkbox = tr.querySelector("input[type=\"checkbox\"]");
+
+    checkbox.setAttribute("name", "workers[]");
+    checkbox.setAttribute("value", tr.getAttribute("data-worker-id"));
+  });
+});
diff --git a/src/js/registry.js b/src/js/registry.js
new file mode 100644
index 0000000..807c4fd
--- /dev/null
+++ b/src/js/registry.js
@@ -0,0 +1,5 @@
+window.addEventListener("load", _ => {
+  document.getElementById("showinvalidated").addEventListener("change", e => {
+    document.getElementById("show-invalidated-form").submit();
+  });
+});
diff --git a/src/js/schedule.js b/src/js/schedule.js
new file mode 100644
index 0000000..d804872
--- /dev/null
+++ b/src/js/schedule.js
@@ -0,0 +1,6 @@
+window.addEventListener("load", function() {
+  document.querySelector(".addday").addEventListener("click", function() {
+    document.querySelector("#addday").showModal();
+    /* Or dialog.show(); to show the dialog without a backdrop. */
+  });
+});
diff --git a/src/js/scheduletemplates.js b/src/js/scheduletemplates.js
new file mode 100644
index 0000000..a236064
--- /dev/null
+++ b/src/js/scheduletemplates.js
@@ -0,0 +1,6 @@
+window.addEventListener("load", function() {
+  document.querySelector(".addtemplate").addEventListener("click", function() {
+    document.querySelector("#addtemplate").showModal();
+    /* Or dialog.show(); to show the dialog without a backdrop. */
+  });
+});
diff --git a/src/js/secondfactor.js b/src/js/secondfactor.js
new file mode 100644
index 0000000..f537146
--- /dev/null
+++ b/src/js/secondfactor.js
@@ -0,0 +1,107 @@
+function verify() {
+  if (!document.getElementById("code").checkValidity()) {
+    document.querySelector(".mdl-js-snackbar").MaterialSnackbar.showSnackbar({
+      message: "El código de verificación debe tener 6 cifras."
+    });
+
+    return;
+  }
+
+  var body = {
+    code: document.getElementById("code").value
+  };
+
+  var content = document.getElementById("content");
+  content.innerHTML = '<div class="mdl-spinner mdl-js-spinner is-active"></div>';
+  content.style.textAlign = "center";
+  componentHandler.upgradeElements(content);
+
+  fetch("ajax/verifysecuritycode.php", {
+    method: "POST",
+    headers: {
+      "Content-Type": "application/json"
+    },
+    body: JSON.stringify(body)
+  }).then(response => {
+    if (response.status !== 200) {
+      throw new Error("HTTP status is not 200.");
+    }
+
+    return response.json();
+  }).then(response => {
+    switch (response.status) {
+      case "ok":
+      document.location = "index.php";
+      break;
+
+      case "wrongCode":
+      document.location = "index.php?msg=secondfactorwrongcode";
+      break;
+
+      default:
+      console.error("An unknown status code was returned.");
+    }
+  }).catch(err => console.error("An unexpected error occurred.", err));
+}
+
+function verifyKeypress(e) {
+  if (event.keyCode == 13) {
+    verify();
+  }
+}
+
+function startWebauthn() {
+  fetch("ajax/startwebauthnauthentication.php", {
+    method: "POST"
+  }).then(response => {
+    if (response.status !== 200) {
+      response.text(); // @TODO: Remove this. It is only used so the response is available in Chrome Dev Tools
+      throw new Error("HTTP status is not 200.");
+    }
+
+    return response.json();
+  }).then(response => {
+    recursiveBase64StrToArrayBuffer(response);
+    return response;
+  }).then(getCredentialArgs => {
+    return navigator.credentials.get(getCredentialArgs);
+  }).then(cred => {
+    return {
+      id: cred.rawId ? arrayBufferToBase64(cred.rawId) : null,
+      clientDataJSON: cred.response.clientDataJSON  ? arrayBufferToBase64(cred.response.clientDataJSON) : null,
+      authenticatorData: cred.response.authenticatorData ? arrayBufferToBase64(cred.response.authenticatorData) : null,
+      signature : cred.response.signature ? arrayBufferToBase64(cred.response.signature) : null
+    };
+  }).then(JSON.stringify).then(AuthenticatorAttestationResponse => {
+    return window.fetch("ajax/completewebauthnauthentication.php", {
+      method: "POST",
+      body: AuthenticatorAttestationResponse,
+    });
+  }).then(response => {
+    if (response.status !== 200) {
+      response.text(); // @TODO: remove this. It is only used so the response is available in Chrome Dev Tools
+      throw new Error("HTTP status is not 200 (2).");
+    }
+
+    return response.json();
+  }).then(json => {
+    if (json.status == "ok") {
+      document.location = "index.php";
+    }
+  }).catch(err => console.error("An unexpected error occurred.", err));
+}
+
+window.addEventListener("load", function() {
+  if (document.getElementById("totp")) {
+    document.getElementById("verify").addEventListener("click", verify);
+    document.getElementById("code").addEventListener("keypress", verifyKeypress);
+    document.getElementById("code").focus();
+    document.querySelector("a[href=\"#totp\"]").addEventListener("click", _ => {
+      document.getElementById("code").focus();
+    });
+  }
+
+  if (document.getElementById("startwebauthn")) {
+    document.getElementById("startwebauthn").addEventListener("click", startWebauthn);
+  }
+});
diff --git a/src/js/securitykeys.js b/src/js/securitykeys.js
new file mode 100644
index 0000000..9f08e21
--- /dev/null
+++ b/src/js/securitykeys.js
@@ -0,0 +1,50 @@
+window.addEventListener("load", function() {
+  document.querySelector(".addsecuritykey").addEventListener("click", function() {
+    document.querySelector("#addsecuritykey").showModal();
+    /* Or dialog.show(); to show the dialog without a backdrop. */
+  });
+
+  document.getElementById("registersecuritykey").addEventListener("click", e => {
+    e.preventDefault();
+
+    if (document.getElementById("addsecuritykeyform").reportValidity()) {
+      fetch("ajax/addsecuritykey.php", {
+        method: "POST"
+      }).then(response => {
+        if (response.status !== 200) {
+          response.text(); // @TODO: Remove this. It is only used so the response is available in Chrome Dev Tools
+          throw new Error("HTTP status is not 200.");
+        }
+
+        return response.json();
+      }).then(response => {
+        recursiveBase64StrToArrayBuffer(response);
+        return response;
+      }).then(createCredentialArgs => {
+        return navigator.credentials.create(createCredentialArgs);
+      }).then(cred => {
+        return {
+            clientDataJSON: cred.response.clientDataJSON  ? arrayBufferToBase64(cred.response.clientDataJSON) : null,
+            attestationObject: cred.response.attestationObject ? arrayBufferToBase64(cred.response.attestationObject) : null,
+            name: document.getElementById("name").value
+        };
+      }).then(JSON.stringify).then(AuthenticatorAttestationResponse => {
+        return window.fetch("ajax/addsecuritykey2.php", {
+          method: "POST",
+          body: AuthenticatorAttestationResponse,
+        });
+      }).then(response => {
+        if (response.status !== 200) {
+          response.text(); // @TODO: remove this. It is only used so the response is available in Chrome Dev Tools
+          throw new Error("HTTP status is not 200 (2).");
+        }
+
+        return response.json();
+      }).then(json => {
+        if (json.status == "ok") {
+          document.location = "securitykeys.php?msg=securitykeyadded";
+        }
+      }).catch(err => console.error("An unexpected error occurred.", err));
+    }
+  });
+});
diff --git a/src/js/sendbulkpasswords.js b/src/js/sendbulkpasswords.js
new file mode 100644
index 0000000..bb6ac26
--- /dev/null
+++ b/src/js/sendbulkpasswords.js
@@ -0,0 +1,8 @@
+window.addEventListener("load", function() {
+  document.querySelectorAll("tr[data-person-id]").forEach(tr => {
+    var checkbox = tr.querySelector("input[type=\"checkbox\"]");
+
+    checkbox.setAttribute("name", "people[]");
+    checkbox.setAttribute("value", tr.getAttribute("data-person-id"));
+  });
+});
diff --git a/src/js/userincidents.js b/src/js/userincidents.js
new file mode 100644
index 0000000..48e8ae4
--- /dev/null
+++ b/src/js/userincidents.js
@@ -0,0 +1,6 @@
+window.addEventListener("load", function() {
+  document.querySelector(".addincident").addEventListener("click", function() {
+    document.querySelector("#addincident").showModal();
+    /* Or dialog.show(); to show the dialog without a backdrop. */
+  });
+});
diff --git a/src/js/users.js b/src/js/users.js
new file mode 100644
index 0000000..9eb383e
--- /dev/null
+++ b/src/js/users.js
@@ -0,0 +1,28 @@
+window.addEventListener("load", function() {
+  var datatable = $('.datatable').DataTable({
+    paging:   false,
+    ordering: false,
+    info:     false,
+    searching:true
+  });
+
+  document.querySelector("#usuario").addEventListener("input", function(evt) {
+    this.search(evt.target.value);
+    this.draw(true);
+  }.bind(datatable));
+
+  document.querySelector(".adduser").addEventListener("click", function() {
+    document.querySelector("#adduser").showModal();
+    /* Or dialog.show(); to show the dialog without a backdrop. */
+  });
+
+  document.querySelector(".importcsv").addEventListener("click", function() {
+    document.querySelector("#importcsv").showModal();
+    /* Or dialog.show(); to show the dialog without a backdrop. */
+  });
+
+  document.querySelector(".filter").addEventListener("click", function() {
+    document.querySelector("#filter").showModal();
+    /* Or dialog.show(); to show the dialog without a backdrop. */
+  });
+});
diff --git a/src/js/userschedule.js b/src/js/userschedule.js
new file mode 100644
index 0000000..0fa39cb
--- /dev/null
+++ b/src/js/userschedule.js
@@ -0,0 +1,6 @@
+window.addEventListener("load", function() {
+  document.querySelector(".addschedule").addEventListener("click", function() {
+    document.querySelector("#addschedule").showModal();
+    /* Or dialog.show(); to show the dialog without a backdrop. */
+  });
+});
diff --git a/src/js/validations.js b/src/js/validations.js
new file mode 100644
index 0000000..cecd3b7
--- /dev/null
+++ b/src/js/validations.js
@@ -0,0 +1,55 @@
+function getFormData() {
+  var data = {
+    "incidents": [],
+    "records": []
+  };
+
+  ["incident", "record"].forEach(key => {
+    document.querySelectorAll("input[type=\"checkbox\"][data-"+key+"]:checked").forEach(el => {
+      data[key+"s"].push(el.getAttribute("data-"+key));
+    });
+  });
+
+  return data;
+}
+
+window.addEventListener("load", function() {
+  document.querySelectorAll(".mdl-checkbox[data-check-all=\"true\"] input[type=\"checkbox\"]").forEach(el => {
+    el.addEventListener("change", e => {
+      el.parentElement.parentElement.parentElement.parentElement.parentElement.querySelectorAll(".mdl-checkbox:not([data-check-all=\"true\"])").forEach(input => {
+        var checkbox = input.MaterialCheckbox;
+
+        if (el.checked) checkbox.check();
+        else checkbox.uncheck();
+      });
+    });
+  });
+
+  document.querySelector("#submit").addEventListener("click", e => {
+    var data = getFormData();
+
+    if (data.incidents.length == 0 && data.records.length == 0) {
+      document.querySelector(".mdl-js-snackbar").MaterialSnackbar.showSnackbar({
+        message: "Debes seleccionar al menos una incidencia o registro para poder validar.",
+        timeout: 5000
+      });
+
+      return;
+    }
+
+    var form = document.createElement("form");
+    form.setAttribute("action", "interstitialvalidations.php");
+    form.setAttribute("method", "POST");
+    form.style.display = "none";
+
+    ["incidents", "records"].forEach(key => {
+      var input = document.createElement("input");
+      input.setAttribute("name", key);
+      input.setAttribute("value", data[key]);
+      form.appendChild(input);
+    });
+
+    document.body.appendChild(form);
+    form.submit();
+  });
+});
diff --git a/src/js/workers.js b/src/js/workers.js
new file mode 100644
index 0000000..88ac687
--- /dev/null
+++ b/src/js/workers.js
@@ -0,0 +1,71 @@
+function getRawWorkers() {
+  var parameters = [];
+  document.querySelectorAll("tr[data-worker-id]").forEach(tr => {
+    if (tr.querySelector("input[type=\"checkbox\"]").checked) {
+      parameters.push(tr.getAttribute("data-worker-id"));
+    }
+  });
+
+  return parameters;
+}
+
+function getParameters() {
+  var parameters = [];
+  var workers = getRawWorkers();
+  workers.forEach(worker => {
+    parameters.push("workers[]="+worker);
+  });
+
+  if (parameters.length == 0) return false;
+
+  return parameters.join("&");
+}
+
+window.addEventListener("load", function() {
+  var datatable = $('.datatable').DataTable({
+    paging:   false,
+    ordering: false,
+    info:     false,
+    searching:true
+  });
+
+  document.querySelector("#usuario").addEventListener("input", function(evt) {
+    this.search(evt.target.value);
+    this.draw(true);
+  }.bind(datatable));
+
+  document.querySelector(".filter").addEventListener("click", function() {
+    document.querySelector("#filter").showModal();
+    /* Or dialog.show(); to show the dialog without a backdrop. */
+  });
+
+  ["copytemplate", "addincidentbulk"].forEach(action => {
+    document.getElementById(action).addEventListener("click", function() {
+      var parameters = getParameters();
+      if (parameters === false) return;
+
+      var url = "dynamic/"+action+".php?"+parameters;
+      dynDialog.load(url);
+    });
+  });
+
+  document.getElementById("addrecurringincident").addEventListener("click", function () {
+    var workers = getRawWorkers();
+    if (workers.length > 1) {
+      if (document.querySelector(".mdl-js-snackbar") === null) {
+        document.body.insertAdjacentHTML('beforeend', '<div class="mdl-snackbar mdl-js-snackbar"><div class="mdl-snackbar__text"></div><button type="button" class="mdl-snackbar__action"></button></div>');
+        componentHandler.upgradeElement(document.querySelector(".mdl-js-snackbar"));
+      }
+
+      document.querySelector(".mdl-js-snackbar").MaterialSnackbar.showSnackbar(
+        {
+          message: "Solo se puede añadir una incidencia recurrente a un solo trabajador.",
+          timeout: 5000
+        }
+      );
+      // Display error message
+    } else if (workers.length == 1) {
+      window.location = "incidents.php?openRecurringFormWorker="+workers[0];
+    }
+  });
+});