diff --git a/.eslintignore b/.eslintignore new file mode 100644 index 00000000..416c47b1 --- /dev/null +++ b/.eslintignore @@ -0,0 +1,4 @@ +ereuse_devicehub/static/vendor +ereuse_devicehub/static/js/print.pdf.js +ereuse_devicehub/static/js/qrcode.js +*.min.js \ No newline at end of file diff --git a/.eslintrc.json b/.eslintrc.json new file mode 100644 index 00000000..0d9213bb --- /dev/null +++ b/.eslintrc.json @@ -0,0 +1,36 @@ +{ + "env": { + "browser": true, + "es2021": true, + "jquery": true + }, + "extends": [ + "airbnb", + "prettier" + ], + "plugins": [ + "prettier" + ], + "parserOptions": { + "ecmaVersion": "latest" + }, + "rules": { + "quotes": ["error","double"], + "no-use-before-define": "off", + "no-unused-vars": "warn", + "no-undef": "warn", + "camelcase": "off", + "no-console": "off", + "no-plusplus": "off", + "no-param-reassign": "off", + "no-new": "warn", + "strict": "off", + "class-methods-use-this": "off", + "eqeqeq": "warn", + "radix": "warn" + }, + "globals": { + "API_URLS": true, + "Api": true + } +} \ No newline at end of file diff --git a/.github/workflows/eslint.yml b/.github/workflows/eslint.yml new file mode 100644 index 00000000..7e42feaf --- /dev/null +++ b/.github/workflows/eslint.yml @@ -0,0 +1,55 @@ +# This workflow uses actions that are not certified by GitHub. +# They are provided by a third-party and are governed by +# separate terms of service, privacy policy, and support +# documentation. +# ESLint is a tool for identifying and reporting on patterns +# found in ECMAScript/JavaScript code. +# More details at https://github.com/eslint/eslint +# and https://eslint.org + +name: ESLint + +on: + push: + branches: [master, testing] + pull_request_target: + branches: [master, testing] + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - uses: actions/setup-node@v1 + with: + node-version: '16' + - name: Install dependencies + run: npm install + - name: Run linters + uses: wearerequired/lint-action@v1 + with: + eslint: true + prettier: false + commit_message: "Fix code style issues with ${linter}" + auto_fix: true + commit: true + github_token: "${{ secrets.GITHUB_TOKEN }}" + git_name: "Lint Action" + - name: Save Code Linting Report JSON + # npm script for ESLint + # eslint --output-file eslint_report.json --format json src + # See https://eslint.org/docs/user-guide/command-line-interface#options + run: npm run lint:report + # Continue to the next step even if this fails + continue-on-error: true + - name: Annotate Code Linting Results + uses: ataylorme/eslint-annotate-action@1.2.0 + with: + repo-token: ${{ secrets.GITHUB_TOKEN }} + report-json: "eslint_report.json" + only-pr-files: true + - name: Upload ESLint report + uses: actions/upload-artifact@v2 + with: + name: eslint_report.json + path: eslint_report.json diff --git a/.gitignore b/.gitignore index 6a130a6e..0d6f75c3 100644 --- a/.gitignore +++ b/.gitignore @@ -119,3 +119,11 @@ ENV/ # Temporal dir tmp/ + +# NPM modules +node_modules/ +yarn.lock + +# ESLint Report +eslint_report.json + diff --git a/.prettierrc.json b/.prettierrc.json new file mode 100644 index 00000000..f92e93ef --- /dev/null +++ b/.prettierrc.json @@ -0,0 +1,3 @@ +{ + "printWidth": 250 +} \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 542b5491..bf4a03f1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,7 +8,9 @@ ml). ## master ## testing +- [added] #219 Add functionality to searchbar (Lots and devices). - [changed] #211 Print DHID-QR label for selected devices. +- [changed] #218 Add reactivity to device lots. - [fixed] #214 Login workflow ## [2.0.0] - 2022-03-15 diff --git a/development-setup.md b/development-setup.md index c900574a..eb0fd221 100755 --- a/development-setup.md +++ b/development-setup.md @@ -30,11 +30,6 @@ Create a demo table export dhi=dbtest; dh dummy ``` -copy `examples/app.py` to project directory: -```bash -copy examples/app.py . -``` - ## Run project Run the app diff --git a/ereuse_devicehub/inventory/forms.py b/ereuse_devicehub/inventory/forms.py index 48484a1e..72f033f8 100644 --- a/ereuse_devicehub/inventory/forms.py +++ b/ereuse_devicehub/inventory/forms.py @@ -97,62 +97,6 @@ class FilterForm(FlaskForm): return ['Desktop', 'Laptop', 'Server'] -class LotDeviceForm(FlaskForm): - lot = StringField('Lot', [validators.UUID()]) - devices = StringField('Devices', [validators.length(min=1)]) - - def validate(self, extra_validators=None): - is_valid = super().validate(extra_validators) - - if not is_valid: - return False - - self._lot = ( - Lot.query.outerjoin(Trade) - .filter(Lot.id == self.lot.data) - .filter( - or_( - Trade.user_from == g.user, - Trade.user_to == g.user, - Lot.owner_id == g.user.id, - ) - ) - .one() - ) - - devices = set(self.devices.data.split(",")) - self._devices = ( - Device.query.filter(Device.id.in_(devices)) - .filter(Device.owner_id == g.user.id) - .distinct() - .all() - ) - - return bool(self._devices) - - def save(self, commit=True): - trade = self._lot.trade - if trade: - for dev in self._devices: - if trade not in dev.actions: - trade.devices.add(dev) - - if self._devices: - self._lot.devices.update(self._devices) - db.session.add(self._lot) - - if commit: - db.session.commit() - - def remove(self, commit=True): - if self._devices: - self._lot.devices.difference_update(self._devices) - db.session.add(self._lot) - - if commit: - db.session.commit() - - class LotForm(FlaskForm): name = StringField('Name', [validators.length(min=1)]) diff --git a/ereuse_devicehub/inventory/views.py b/ereuse_devicehub/inventory/views.py index 66b28563..38ff4bc6 100644 --- a/ereuse_devicehub/inventory/views.py +++ b/ereuse_devicehub/inventory/views.py @@ -16,7 +16,6 @@ from ereuse_devicehub.inventory.forms import ( AllocateForm, DataWipeForm, FilterForm, - LotDeviceForm, LotForm, NewActionForm, NewDeviceForm, @@ -109,7 +108,6 @@ class DeviceListMix(GenericMixView): self.context = { 'devices': devices, 'lots': lots, - 'form_lot_device': LotDeviceForm(), 'form_tag_device': TagDeviceForm(), 'form_new_action': form_new_action, 'form_new_allocate': form_new_allocate, @@ -153,46 +151,6 @@ class DeviceDetailView(GenericMixView): return flask.render_template(self.template_name, **context) -class LotDeviceAddView(View): - methods = ['POST'] - decorators = [login_required] - template_name = 'inventory/device_list.html' - - def dispatch_request(self): - form = LotDeviceForm() - if form.validate_on_submit(): - form.save(commit=False) - messages.success( - 'Add devices to lot "{}" successfully!'.format(form._lot.name) - ) - db.session.commit() - else: - messages.error('Error adding devices to lot!') - - next_url = request.referrer or url_for('inventory.devicelist') - return flask.redirect(next_url) - - -class LotDeviceDeleteView(View): - methods = ['POST'] - decorators = [login_required] - template_name = 'inventory/device_list.html' - - def dispatch_request(self): - form = LotDeviceForm() - if form.validate_on_submit(): - form.remove(commit=False) - messages.success( - 'Remove devices from lot "{}" successfully!'.format(form._lot.name) - ) - db.session.commit() - else: - messages.error('Error removing devices from lot!') - - next_url = request.referrer or url_for('inventory.devicelist') - return flask.redirect(next_url) - - class LotCreateView(GenericMixView): methods = ['GET', 'POST'] decorators = [login_required] @@ -607,12 +565,6 @@ devices.add_url_rule( devices.add_url_rule( '/lot//device/', view_func=DeviceListView.as_view('lotdevicelist') ) -devices.add_url_rule( - '/lot/devices/add/', view_func=LotDeviceAddView.as_view('lot_devices_add') -) -devices.add_url_rule( - '/lot/devices/del/', view_func=LotDeviceDeleteView.as_view('lot_devices_del') -) devices.add_url_rule('/lot/add/', view_func=LotCreateView.as_view('lot_add')) devices.add_url_rule( '/lot//del/', view_func=LotDeleteView.as_view('lot_del') diff --git a/ereuse_devicehub/static/css/devicehub.css b/ereuse_devicehub/static/css/devicehub.css new file mode 100644 index 00000000..e6ae1893 --- /dev/null +++ b/ereuse_devicehub/static/css/devicehub.css @@ -0,0 +1,25 @@ +/** +* eReuse CSS +*/ + +/*-------------------------------------------------------------- +# LotsSelector +--------------------------------------------------------------*/ + +#dropDownLotsSelector { + max-height: 500px; +} + +#dropDownLotsSelector>ul#LotsSelector { + list-style-type: none; + margin: 0; + padding: 0; + min-width: max-content; + max-height: 380px; + overflow-y: auto; +} + +#dropDownLotsSelector #ApplyDeviceLots { + padding-top: 0px; + padding-bottom: 5px; +} diff --git a/ereuse_devicehub/static/css/style.css b/ereuse_devicehub/static/css/style.css index 8e263853..1fd44f19 100644 --- a/ereuse_devicehub/static/css/style.css +++ b/ereuse_devicehub/static/css/style.css @@ -1,10 +1,10 @@ -/** -* Template Name: NiceAdmin - v2.2.0 -* Template URL: https://bootstrapmade.com/nice-admin-bootstrap-admin-html-template/ -* Author: BootstrapMade.com -* License: https://bootstrapmade.com/license/ -*/ - +/** +* Template Name: NiceAdmin - v2.2.0 +* Template URL: https://bootstrapmade.com/nice-admin-bootstrap-admin-html-template/ +* Author: BootstrapMade.com +* License: https://bootstrapmade.com/license/ +*/ + /*-------------------------------------------------------------- # General --------------------------------------------------------------*/ @@ -1081,4 +1081,4 @@ h1, h2, h3, h4, h5, h6 { text-align: center; font-size: 13px; color: #012970; -} \ No newline at end of file +} diff --git a/ereuse_devicehub/static/js/api.js b/ereuse_devicehub/static/js/api.js new file mode 100644 index 00000000..190e5d42 --- /dev/null +++ b/ereuse_devicehub/static/js/api.js @@ -0,0 +1,76 @@ +const Api = { + /** + * get lots id + * @returns get lots + */ + async get_lots() { + const request = await this.doRequest(API_URLS.lots, "GET", null); + if (request != undefined) return request.items; + throw request; + }, + + /** + * Get filtered devices info + * @param {number[]} ids devices ids + * @returns full detailed device list + */ + async get_devices(ids) { + const request = await this.doRequest(`${API_URLS.devices }?filter={"id": [${ ids.toString() }]}`, "GET", null); + if (request != undefined) return request.items; + throw request; + }, + + /** + * Get filtered devices info + * @param {number[]} ids devices ids + * @returns full detailed device list + */ + async search_device(id) { + const request = await this.doRequest(`${API_URLS.devices }?filter={"devicehub_id": ["${ id }"]}`, "GET", null) + if (request != undefined) return request.items + throw request + }, + + /** + * Add devices to lot + * @param {number} lotID lot id + * @param {number[]} listDevices list devices id + */ + async devices_add(lotID, listDevices) { + const queryURL = `${API_URLS.devices_modify.replace("UUID", lotID) }?${ listDevices.map(deviceID => `id=${ deviceID}`).join("&")}`; + return Api.doRequest(queryURL, "POST", null); + }, + + /** + * Remove devices from a lot + * @param {number} lotID lot id + * @param {number[]} listDevices list devices id + */ + async devices_remove(lotID, listDevices) { + const queryURL = `${API_URLS.devices_modify.replace("UUID", lotID) }?${ listDevices.map(deviceID => `id=${ deviceID}`).join("&")}`; + return Api.doRequest(queryURL, "DELETE", null); + }, + + /** + * + * @param {string} url URL to be requested + * @param {String} type Action type + * @param {String | Object} body body content + * @returns + */ + async doRequest(url, type, body) { + let result; + try { + result = await $.ajax({ + url, + type, + headers: { "Authorization": API_URLS.Auth_Token }, + body + }); + return result; + } catch (error) { + console.error(error); + throw error; + } + } +} \ No newline at end of file diff --git a/ereuse_devicehub/static/js/create_device.js b/ereuse_devicehub/static/js/create_device.js index 1c9e0655..a1b609b0 100644 --- a/ereuse_devicehub/static/js/create_device.js +++ b/ereuse_devicehub/static/js/create_device.js @@ -1,15 +1,15 @@ -$(document).ready(function() { +$(document).ready(() => { $("#type").on("change", deviceInputs); deviceInputs(); }) function deviceInputs() { - if ($("#type").val() == 'Monitor') { + if ($("#type").val() == "Monitor") { $("#screen").show(); $("#resolution").show(); $("#imei").hide(); $("#meid").hide(); - } else if (['Smartphone', 'Cellphone', 'Tablet'].includes($("#type").val())) { + } else if (["Smartphone", "Cellphone", "Tablet"].includes($("#type").val())) { $("#screen").hide(); $("#resolution").hide(); $("#imei").show(); diff --git a/ereuse_devicehub/static/js/main.js b/ereuse_devicehub/static/js/main.js index 695378db..55a4951c 100644 --- a/ereuse_devicehub/static/js/main.js +++ b/ereuse_devicehub/static/js/main.js @@ -4,7 +4,7 @@ * Author: BootstrapMade.com * License: https://bootstrapmade.com/license/ */ -(function() { +(function () { "use strict"; /** @@ -14,9 +14,9 @@ el = el.trim() if (all) { return [...document.querySelectorAll(el)] - } else { + } return document.querySelector(el) - } + } /** @@ -34,103 +34,101 @@ * Easy on scroll event listener */ const onscroll = (el, listener) => { - el.addEventListener('scroll', listener) + el.addEventListener("scroll", listener) } /** * Sidebar toggle */ - if (select('.toggle-sidebar-btn')) { - on('click', '.toggle-sidebar-btn', function(e) { - select('body').classList.toggle('toggle-sidebar') + if (select(".toggle-sidebar-btn")) { + on("click", ".toggle-sidebar-btn", (e) => { + select("body").classList.toggle("toggle-sidebar") }) } /** * Search bar toggle */ - if (select('.search-bar-toggle')) { - on('click', '.search-bar-toggle', function(e) { - select('.search-bar').classList.toggle('search-bar-show') + if (select(".search-bar-toggle")) { + on("click", ".search-bar-toggle", (e) => { + select(".search-bar").classList.toggle("search-bar-show") }) } /** * Navbar links active state on scroll */ - let navbarlinks = select('#navbar .scrollto', true) + const navbarlinks = select("#navbar .scrollto", true) const navbarlinksActive = () => { - let position = window.scrollY + 200 + const position = window.scrollY + 200 navbarlinks.forEach(navbarlink => { if (!navbarlink.hash) return - let section = select(navbarlink.hash) + const section = select(navbarlink.hash) if (!section) return if (position >= section.offsetTop && position <= (section.offsetTop + section.offsetHeight)) { - navbarlink.classList.add('active') + navbarlink.classList.add("active") } else { - navbarlink.classList.remove('active') + navbarlink.classList.remove("active") } }) } - window.addEventListener('load', navbarlinksActive) + window.addEventListener("load", navbarlinksActive) onscroll(document, navbarlinksActive) /** * Toggle .header-scrolled class to #header when page is scrolled */ - let selectHeader = select('#header') + const selectHeader = select("#header") if (selectHeader) { const headerScrolled = () => { if (window.scrollY > 100) { - selectHeader.classList.add('header-scrolled') + selectHeader.classList.add("header-scrolled") } else { - selectHeader.classList.remove('header-scrolled') + selectHeader.classList.remove("header-scrolled") } } - window.addEventListener('load', headerScrolled) + window.addEventListener("load", headerScrolled) onscroll(document, headerScrolled) } /** * Back to top button */ - let backtotop = select('.back-to-top') + const backtotop = select(".back-to-top") if (backtotop) { const toggleBacktotop = () => { if (window.scrollY > 100) { - backtotop.classList.add('active') + backtotop.classList.add("active") } else { - backtotop.classList.remove('active') + backtotop.classList.remove("active") } } - window.addEventListener('load', toggleBacktotop) + window.addEventListener("load", toggleBacktotop) onscroll(document, toggleBacktotop) } /** * Initiate tooltips */ - var tooltipTriggerList = [].slice.call(document.querySelectorAll('[data-bs-toggle="tooltip"]')) - var tooltipList = tooltipTriggerList.map(function(tooltipTriggerEl) { - return new bootstrap.Tooltip(tooltipTriggerEl) - }) + const tooltipTriggerList = [].slice.call(document.querySelectorAll("[data-bs-toggle=\"tooltip\"]")) + const tooltipList = tooltipTriggerList.map((tooltipTriggerEl) => new bootstrap.Tooltip(tooltipTriggerEl)) /** * Initiate quill editors */ - if (select('.quill-editor-default')) { - new Quill('.quill-editor-default', { - theme: 'snow' + if (select(".quill-editor-default")) { + new Quill(".quill-editor-default", { + theme: "snow" }); } - if (select('.quill-editor-bubble')) { - new Quill('.quill-editor-bubble', { - theme: 'bubble' + if (select(".quill-editor-bubble")) { + new Quill(".quill-editor-bubble", { + theme: "bubble" }); } - if (select('.quill-editor-full')) { + if (select(".quill-editor-full")) { new Quill(".quill-editor-full", { modules: { toolbar: [ @@ -141,31 +139,31 @@ }], ["bold", "italic", "underline", "strike"], [{ - color: [] - }, - { - background: [] - } + color: [] + }, + { + background: [] + } ], [{ - script: "super" - }, - { - script: "sub" - } + script: "super" + }, + { + script: "sub" + } ], [{ - list: "ordered" - }, - { - list: "bullet" - }, - { - indent: "-1" - }, - { - indent: "+1" - } + list: "ordered" + }, + { + list: "bullet" + }, + { + indent: "-1" + }, + { + indent: "+1" + } ], ["direction", { align: [] @@ -181,24 +179,24 @@ /** * Initiate Bootstrap validation check */ - var needsValidation = document.querySelectorAll('.needs-validation') + const needsValidation = document.querySelectorAll(".needs-validation") Array.prototype.slice.call(needsValidation) - .forEach(function(form) { - form.addEventListener('submit', function(event) { + .forEach((form) => { + form.addEventListener("submit", (event) => { if (!form.checkValidity()) { event.preventDefault() event.stopPropagation() } - form.classList.add('was-validated') + form.classList.add("was-validated") }, false) }) /** * Initiate Datatables */ - const datatables = select('.datatable', true) + const datatables = select(".datatable", true) datatables.forEach(datatable => { new simpleDatatables.DataTable(datatable); }) @@ -206,15 +204,178 @@ /** * Autoresize echart charts */ - const mainContainer = select('#main'); + const mainContainer = select("#main"); if (mainContainer) { setTimeout(() => { - new ResizeObserver(function() { - select('.echart', true).forEach(getEchart => { + new ResizeObserver(() => { + select(".echart", true).forEach(getEchart => { echarts.getInstanceByDom(getEchart).resize(); }) }).observe(mainContainer); }, 200); } + /** + * Select all functionality + */ + const btnSelectAll = document.getElementById("SelectAllBTN"); + const tableListCheckboxes = document.querySelectorAll(".deviceSelect"); + + function itemListCheckChanged(event) { + const isAllChecked = Array.from(tableListCheckboxes).map(itm => itm.checked); + if (isAllChecked.every(bool => bool == true)) { + btnSelectAll.checked = true; + btnSelectAll.indeterminate = false; + } else if (isAllChecked.every(bool => bool == false)) { + btnSelectAll.checked = false; + btnSelectAll.indeterminate = false; + } else { + btnSelectAll.indeterminate = true; + } + } + + tableListCheckboxes.forEach(item => { + item.addEventListener("click", itemListCheckChanged); + }) + + btnSelectAll.addEventListener("click", event => { + const checkedState = event.target.checked; + tableListCheckboxes.forEach(ckeckbox => {ckeckbox.checked = checkedState}); + }) + + /** + * Avoid hide dropdown when user clicked inside + */ + document.getElementById("dropDownLotsSelector").addEventListener("click", event => { + event.stopPropagation(); + }) + + /** + * Search form functionality + */ + window.addEventListener("DOMContentLoaded", () => { + const searchForm = document.getElementById("SearchForm") + const inputSearch = document.querySelector("#SearchForm > input") + const doSearch = true + + searchForm.addEventListener("submit", (event) => { + event.preventDefault(); + }) + + let timeoutHandler = setTimeout(() => { }, 1) + const dropdownList = document.getElementById("dropdown-search-list") + const defaultEmptySearch = document.getElementById("dropdown-search-list").innerHTML + + + inputSearch.addEventListener("input", (e) => { + clearTimeout(timeoutHandler) + const searchText = e.target.value + if (searchText == "") { + document.getElementById("dropdown-search-list").innerHTML = defaultEmptySearch; + return + } + + let resultCount = 0; + function searchCompleted() { + resultCount++; + setTimeout(() => { + if (resultCount == 2 && document.getElementById("dropdown-search-list").children.length == 2) { + document.getElementById("dropdown-search-list").innerHTML = ` + ` + } + }, 100) + } + + timeoutHandler = setTimeout(async () => { + dropdownList.innerHTML = ` + + `; + + + try { + Api.search_device(searchText.toUpperCase()).then(devices => { + dropdownList.querySelector("#deviceSearchLoader").style = "display: none" + + for (let i = 0; i < devices.length; i++) { + const device = devices[i]; + + // See: ereuse_devicehub/resources/device/models.py + const verboseName = `${device.type} ${device.manufacturer} ${device.model}` + + const templateString = ` +
  • + + + ${verboseName} + ${device.devicehubID} + +
  • `; + dropdownList.innerHTML += templateString + if (i == 4) { // Limit to 4 resullts + break; + } + } + + searchCompleted(); + }) + } catch (error) { + dropdownList.innerHTML += ` + `; + console.log(error); + } + + try { + Api.get_lots().then(lots => { + dropdownList.querySelector("#lotSearchLoader").style = "display: none" + for (let i = 0; i < lots.length; i++) { + const lot = lots[i]; + if (lot.name.toUpperCase().includes(searchText.toUpperCase())) { + const templateString = ` +
  • + + + ${lot.name} + +
  • `; + dropdownList.innerHTML += templateString + if (i == 4) { // Limit to 4 resullts + break; + } + } + } + searchCompleted(); + }) + + } catch (error) { + dropdownList.innerHTML += ` + `; + console.log(error); + } + }, 1000) + }) + }) + })(); diff --git a/ereuse_devicehub/static/js/main_inventory.js b/ereuse_devicehub/static/js/main_inventory.js index 86c25a82..c230363d 100644 --- a/ereuse_devicehub/static/js/main_inventory.js +++ b/ereuse_devicehub/static/js/main_inventory.js @@ -1,7 +1,7 @@ -$(document).ready(function() { - var show_allocate_form = $("#allocateModal").data('show-action-form'); - var show_datawipe_form = $("#datawipeModal").data('show-action-form'); - var show_trade_form = $("#tradeLotModal").data('show-action-form'); +$(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); @@ -18,7 +18,7 @@ $(document).ready(function() { }) function deviceSelect() { - var devices_count = $(".deviceSelect").filter(':checked').length; + const devices_count = $(".deviceSelect").filter(":checked").length; get_device_list(); if (devices_count == 0) { $("#addingLotModal .pol").show(); @@ -60,7 +60,7 @@ function deviceSelect() { } function removeLot() { - var devices = $(".deviceSelect"); + const devices = $(".deviceSelect"); if (devices.length > 0) { $("#btnRemoveLots .text-danger").show(); } else { @@ -70,10 +70,10 @@ function removeLot() { } function removeTag() { - var devices = $(".deviceSelect").filter(':checked'); - var devices_id = $.map(devices, function(x) { return $(x).attr('data')}); + const devices = $(".deviceSelect").filter(":checked"); + const devices_id = $.map(devices, (x) => $(x).attr("data")); if (devices_id.length == 1) { - var url = "/inventory/tag/devices/"+devices_id[0]+"/del/"; + const url = `/inventory/tag/devices/${devices_id[0]}/del/`; window.location.href = url; } else { $("#unlinkTagAlertModal").click(); @@ -81,8 +81,8 @@ function removeTag() { } function addTag() { - var devices = $(".deviceSelect").filter(':checked'); - var devices_id = $.map(devices, function(x) { return $(x).attr('data')}); + const devices = $(".deviceSelect").filter(":checked"); + const devices_id = $.map(devices, (x) => $(x).attr("data")); if (devices_id.length == 1) { $("#addingTagModal .pol").hide(); $("#addingTagModal .btn-primary").show(); @@ -95,20 +95,20 @@ function addTag() { } function newTrade(action) { - var title = "Trade " - var user_to = $("#user_to").data("email"); - var 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(''); + 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(''); + } 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); @@ -137,46 +137,272 @@ function newDataWipe(action) { } function get_device_list() { - var devices = $(".deviceSelect").filter(':checked'); + const devices = $(".deviceSelect").filter(":checked"); /* Insert the correct count of devices in actions form */ - var devices_count = devices.length; + 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 */ - var devices_id = $.map(devices, function(x) { return $(x).attr('data')}).join(","); - $.map($(".devicesList"), function(x) { + 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 */ - var computer = { + const computer = { "Desktop": "", "Laptop": "", }; - list_devices = devices.map(function (x) { - var typ = $(devices[x]).data("device-type"); - var manuf = $(devices[x]).data("device-manufacturer"); - var dhid = $(devices[x]).data("device-dhid"); + list_devices = devices.map((x) => { + let typ = $(devices[x]).data("device-type"); + const manuf = $(devices[x]).data("device-manufacturer"); + const dhid = $(devices[x]).data("device-dhid"); if (computer[typ]) { typ = computer[typ]; }; - return typ + " " + manuf + " " + dhid; + return `${typ } ${ manuf } ${ dhid}`; }); - description = $.map(list_devices, function(x) { return x }).join(", "); + description = $.map(list_devices, (x) => x).join(", "); $(".enumeration-devices").html(description); } function export_file(type_file) { - var devices = $(".deviceSelect").filter(':checked'); - var devices_id = $.map(devices, function(x) { return $(x).attr('data-device-dhid')}).join(","); + const devices = $(".deviceSelect").filter(":checked"); + const devices_id = $.map(devices, (x) => $(x).attr("data-device-dhid")).join(","); if (devices_id){ - var url = "/inventory/export/"+type_file+"/?ids="+devices_id; + const url = `/inventory/export/${type_file}/?ids=${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 {*} ev event (Should be a checkbox type) + * @param {string} lotID lot id + * @param {number} deviceID device id + */ + manage(event, lotID, deviceListID) { + event.preventDefault(); + const srcElement = event.srcElement.parentElement.children[0] + const {indeterminate} = srcElement; + const checked = !srcElement.checked; + + const found = this.list.filter(list => list.lotID == lotID)[0]; + const foundIndex = found != undefined ? this.list.findLastIndex(x => x.lotID == found.lotID) : -1; + + if (checked) { + if (found != undefined && found.type == "Remove") { + if (found.isFromIndeterminate == true) { + found.type = "Add"; + this.list[foundIndex] = found; + } else { + this.list = this.list.filter(list => list.lotID != lotID); + } + } else { + this.list.push({ type: "Add", lotID, devices: deviceListID, isFromIndeterminate: indeterminate }); + } + } else if (found != undefined && found.type == "Add") { + if (found.isFromIndeterminate == true) { + found.type = "Remove"; + this.list[foundIndex] = found; + } else { + this.list = this.list.filter(list => list.lotID != lotID); + } + } else { + this.list.push({ type: "Remove", lotID, devices: deviceListID, isFromIndeterminate: indeterminate }); + } + + 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 ${ isError ? "alert-danger" : "alert-success"}`; + toast.attributes["data-autohide"] = !isError; + toast.attributes.role = "alert"; + toast.style = "margin-left: auto; width: fit-content;"; + toast.innerHTML = `${title}`; + if (toastText && toastText.length > 0) { + toast.innerHTML += `
    ${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 { + await Api.devices_add(action.lotID, action.devices); + this.notifyUser("Devices sucefully aded 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 { + await Api.devices_remove(action.lotID, action.devices); + this.notifyUser("Devices sucefully removed from selected lot/s", "", false); + } catch (error) { + this.notifyUser("Fail to remove devices from selected lot/s", error.responseJSON.message, true); + } + } + requestCount += 1 + if (requestCount == this.list.length) { + this.reRenderTable(); + this.list = []; + } + }) + 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 oldTable = Array.from(document.querySelectorAll("table.table > tbody > tr .deviceSelect")).map(x => x.attributes["data-device-dhid"].value) + const newTable = Array.from(tmpDiv.querySelectorAll("table.table > tbody > tr .deviceSelect")).map(x => x.attributes["data-device-dhid"].value) + + for (let i = 0; i < oldTable.length; i++) { + if (!newTable.includes(oldTable[i])) { + // variable from device_list.html --> See: ereuse_devicehub\templates\inventory\device_list.html (Ln: 411) + table.rows().remove(i) + } + } + } + } + + let eventClickActions; + + /** + * Generates a list item with a correspondient checkbox state + * @param {String} lotID + * @param {String} lotName + * @param {Array} selectedDevicesIDs + * @param {HTMLElement} target + */ + function templateLot(lot, elementTarget, actions) { + elementTarget.innerHTML = "" + const {id, name, state} = lot; + + const htmlTemplate = ` + `; + + 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, id, selectedDevicesIDs)); + doc.children[1].addEventListener("mouseup", (ev) => actions.manage(ev, id, selectedDevicesIDs)); + elementTarget.append(doc); + } + + const listHTML = $("#LotsSelector") + + // Get selected devices + const selectedDevicesIDs = $.map($(".deviceSelect").filter(":checked"), (x) => parseInt($(x).attr("data"))); + if (selectedDevicesIDs.length <= 0) { + listHTML.html("
  • No devices selected
  • "); + return; + } + + // Initialize Actions list, and set checkbox triggers + const actions = new Actions(); + if (eventClickActions) { + document.getElementById("ApplyDeviceLots").removeEventListener(eventClickActions); + } + eventClickActions = document.getElementById("ApplyDeviceLots").addEventListener("click", () => actions.doActions()); + document.getElementById("ApplyDeviceLots").classList.add("disabled"); + + try { + listHTML.html("
  • ") + const devices = await Api.get_devices(selectedDevicesIDs); + let lots = await Api.get_lots(); + + lots = lots.map(lot => { + lot.devices = devices + .filter(device => device.lots.filter(devicelot => devicelot.id == lot.id).length > 0) + .map(device => parseInt(device.id)); + + switch (lot.devices.length) { + case 0: + lot.state = "false"; + break; + case selectedDevicesIDs.length: + lot.state = "true"; + break; + default: + lot.state = "indetermined"; + break; + } + + return lot; + }) + + + let lotsList = []; + lotsList.push(lots.filter(lot => lot.state == "true").sort((a,b) => a.name.localeCompare(b.name))); + lotsList.push(lots.filter(lot => lot.state == "indetermined").sort((a,b) => a.name.localeCompare(b.name))); + lotsList.push(lots.filter(lot => lot.state == "false").sort((a,b) => a.name.localeCompare(b.name))); + lotsList = lotsList.flat(); // flat array + + listHTML.html(""); + lotsList.forEach(lot => templateLot(lot, listHTML, actions)); + } catch (error) { + console.log(error); + listHTML.html("
  • Error feching devices and lots
    (see console for more details)
  • "); + } +} diff --git a/ereuse_devicehub/templates/ereuse_devicehub/base.html b/ereuse_devicehub/templates/ereuse_devicehub/base.html index 7b56a1ec..3b7c94bd 100644 --- a/ereuse_devicehub/templates/ereuse_devicehub/base.html +++ b/ereuse_devicehub/templates/ereuse_devicehub/base.html @@ -29,6 +29,7 @@ + + + + + diff --git a/ereuse_devicehub/templates/ereuse_devicehub/base_site.html b/ereuse_devicehub/templates/ereuse_devicehub/base_site.html index 1ffddfa0..4e721730 100644 --- a/ereuse_devicehub/templates/ereuse_devicehub/base_site.html +++ b/ereuse_devicehub/templates/ereuse_devicehub/base_site.html @@ -12,9 +12,21 @@ @@ -101,81 +113,82 @@