var jQuery = require('jquery');
window.jQuery = jQuery
window.$ = jQuery

// window._ = require('lodash'); //the only place I saw this used was vue.js, and we don't use that anymore

/****************************************
 Much of what you'll see below is just plain require()'ed, this is because
 it is mostly jQuery stuff, which attaches itself to the $() function/object
 So we don't have to assign it to anything, it will just automagically attach
 itself
 *****************************************/

require("jquery-ui/dist/jquery-ui")
jQuery.fn.uitooltip = jQuery.fn.tooltip;
require('bootstrap-less');
require('select2');
require('admin-lte');
require('tether');
require('jquery-slimscroll');
require('jquery.iframe-transport'); //probably not needed anymore, if I'm honest
require('blueimp-file-upload')
require('bootstrap-colorpicker')
// eonasdan-bootstrap-datetimepicker (BS3) needs moment on window before it loads
window.moment = require('moment')
require('eonasdan-bootstrap-datetimepicker')
require('ekko-lightbox') //TODO - this doesn't seem jquery-ish, we might need to do something weird here
                         // it *does* require Bootstrap, which requires jquery, so maybe that's OK
                         // it seems to work...
require('./extensions/pGenerator.jquery'); //WEIRD, but works
//require('chart.js') // Weirdly, this seems to "just work." Without this line, the dashboard blows up
// but it's *HUGE* - and we only use it one place. So we're taking it out of the bundle
window.SignaturePad = require('./signature_pad'); //ALSO WEIRD - but works
require('jquery-validation')
window.List = require('list.js')
window.ClipboardJS = require('clipboard')
// TODO - find everything using moment.js and kill it or upgrade it? It's huge
// - adminLTE (UGH)
// - bootstrap-daterangepicker
// - fullcalendar (what's that? it's used by AdminLTE)

/**
 * Module containing core application logic.
 * @param  {jQuery} $        Insulated jQuery object
 * @param  {JSON} settings Insulated `window.snipeit.settings` object.
 * @return {IIFE}          Immediately invoked. Returns self.
 */

lineOptions = {

        legend: {
            position: "bottom"
        },
        scales: {
            yAxes: [{
                ticks: {
                    fontColor: "rgba(0,0,0,0.5)",
                    fontStyle: "bold",
                    beginAtZero: true,
                    maxTicksLimit: 5,
                    padding: 20
                },
                gridLines: {
                    drawTicks: false,
                    display: false
                }
            }],
            xAxes: [{
                gridLines: {
                    zeroLineColor: "transparent"
                },
                ticks: {
                    padding: 20,
                    fontColor: "rgba(0,0,0,0.5)",
                    fontStyle: "bold"
                }
            }]
        }

};

pieOptions = {
    //Boolean - Whether we should show a stroke on each segment
    segmentShowStroke: true,
    //String - The colour of each segment stroke
    segmentStrokeColor: "#fff",
    //Number - The width of each segment stroke
    segmentStrokeWidth: 1,
    //Number - The percentage of the chart that we cut out of the middle
    percentageInnerCutout: 50, // This is 0 for Pie charts
    //Number - Amount of animation steps
    animationSteps: 100,
    //String - Animation easing effect
    animationEasing: "easeOutBounce",
    //Boolean - Whether we animate the rotation of the Doughnut
    animateRotate: true,
    //Boolean - Whether we animate scaling the Doughnut from the centre
    animateScale: false,
    //Boolean - whether to make the chart responsive to window resizing
    responsive: true,
    // Boolean - whether to maintain the starting aspect ratio or not when responsive, if set to false, will take up entire container
    maintainAspectRatio: false,

    //String - A legend template
    legendTemplate: "<ul class=\"<%=name.toLowerCase()%>-legend\"><% for (var i=0; i<segments.length; i++){%><li>" +
    "<i class='fas fa-circle-o' style='color: <%=segments[i].fillColor%>'></i>" +
    "<%if(segments[i].label){%><%=segments[i].label%><%}%> foo</li><%}%></ul>",
    //String - A tooltip template
    tooltipTemplate: "<%=value %> <%=label%> "
};

//-----------------
//- END PIE CHART -
//-----------------

var baseUrl = $('meta[name="baseUrl"]').attr('content');



$(function () {

    var $el = $('body');

    // confirm restore modal

    $el.on('click', '.restore-asset', function (evnt) {
        var $context = $(this);
        var $restoreConfirmModal = $('#restoreConfirmModal');
        var href = $context.attr('href');
        var message = $context.attr('data-content');
        var title = $context.attr('data-title');

        $('#confirmModalLabel').text(title);
        $restoreConfirmModal.find('.modal-body').text(message);
        $('#restoreForm').attr('action', href);
        $restoreConfirmModal.modal({
            show: true
        });
        return false;
    });

    // Mark-a-maintenance-complete modal (green checkmark button in the
    // maintenances table actions column). Sets the modal form's action to
    // the row's completion URL and opens it.
    $el.on('click', '.complete-maintenance', function () {
        var url = $(this).data('url');
        $('#completeMaintenanceForm').attr('action', url);
        $('#completionNote').val('');
        $('#completeMaintenanceModal').modal('show');
    });

    // Adjust-quantity modal (plus-minus button on accessory/consumable/
    // component list and view pages). Sets the modal form's action from
    // the trigger's data-adjust-url and populates the header + current
    // quantity display from the trigger's data attributes. Clears the
    // signed amount + note + order every open so nothing bleeds between
    // clicks.
    $el.on('click', '.adjust-quantity', function () {
        var $btn = $(this);
        var $modal = $('#adjustQuantityModal');
        var $amount = $modal.find('#adjustQuantityAmount');

        // data-available is the trigger's authoritative floor for a decrement
        // (available = qty - currentlyInUseCount). A delta smaller than
        // -available would decrement the on-hand qty below what's already
        // checked out, and AdjustsQuantity::adjustQuantity throws
        // DomainException. Mirror that server-side floor on the input's min
        // attribute so the browser stepper refuses to go below and the
        // constraint-validation message surfaces before submit.
        var available = parseInt($btn.data('available'), 10);

        $('#adjustQuantityForm').attr('action', $btn.data('adjust-url'));
        $modal.find('.adjust-quantity-item-name').text($btn.data('item-name') || '');
        $modal.find('.adjust-quantity-available').text(!isNaN(available) ? available : '');

        if (!isNaN(available)) {
            $amount.attr('min', -available);
        } else {
            $amount.removeAttr('min');
        }

        $amount.val('');
        $modal.find('#adjustQuantityOrder').val('');
        // Reset the acquisition-metadata fields between opens so an
        // order left half-filled by one operator doesn't bleed into the
        // next click. Supplier is a select2, so use .val('').trigger('change')
        // rather than setting the raw <select>; currency reverts to
        // whatever the modal was originally rendered with (its DOM value
        // attribute, i.e. the system default_currency).
        $modal.find('#adjustQuantitySupplier').val('').trigger('change');
        // Reset purchase_date to today on every open (server-rendered
        // default is today too). Prevents an operator's earlier
        // backdate from bleeding into the next event.
        var todayIso = new Date().toISOString().slice(0, 10);
        $modal.find('#adjustQuantityPurchaseDate').val(todayIso);

        // Pre-populate unit_cost + currency from the trigger's data-last-*
        // attrs (server-rendered from the item's most recent OrderItem).
        // When both are present the "pre-populated from last order" hint
        // shows underneath the row; the hint gets hidden as soon as the
        // operator edits either field so it disappears the moment they
        // override the pre-fill.
        var lastUnitCost = $btn.data('last-unit-cost');
        var lastCurrency = $btn.data('last-currency');
        var $unitCost = $modal.find('#adjustQuantityUnitCost');
        var $currency = $modal.find('#adjustQuantityCurrency');
        var $costHint = $modal.find('#adjustQuantityCostHint');

        $unitCost.val(lastUnitCost !== undefined && lastUnitCost !== '' ? lastUnitCost : '');
        $currency.val(lastCurrency !== undefined && lastCurrency !== '' ? lastCurrency : ($currency.prop('defaultValue') || ''));

        if ((lastUnitCost !== undefined && lastUnitCost !== '') || (lastCurrency !== undefined && lastCurrency !== '')) {
            $costHint.show();
        } else {
            $costHint.hide();
        }

        // Rebind on every open so multiple modal opens don't stack listeners.
        $unitCost.off('input.adjustCostHint').on('input.adjustCostHint', function () { $costHint.hide(); });
        $currency.off('input.adjustCostHint').on('input.adjustCostHint', function () { $costHint.hide(); });

        $modal.find('#adjustQuantityNote').val('');
        $modal.find('#adjustQuantityFile').val('');
        // js-uploadFile paints selected filenames into #{id}-info; clear it too
        // so stale filenames from a previous open don't linger in the new modal.
        $modal.find('#adjustQuantityFile-info').empty();

        // Acquisition-metadata fields (order number, supplier, unit cost,
        // currency) only make sense when the qty change is a positive
        // addition (a purchase). Zero or negative amounts represent
        // corrections / consumption / losses, not acquisitions, so hide
        // those fields — and blank their values so a submit from that
        // state doesn't ship stale purchase metadata alongside the log
        // entry. Show them again the moment the operator types a
        // positive number. The date label swaps to a generic "Date"
        // when the event isn't a purchase.
        //
        // On modal open the amount is empty ("we don't know yet"), so
        // stay in the default acquisition-visible state — prefilled
        // supplier / cost / currency from the last order are preserved
        // and the hint stays visible if it was shown. We only clear
        // when the operator actually commits to a 0/negative value.
        var $acquisitionFields = $modal.find('#adjustQuantityAcquisitionFields');
        var $costRow = $modal.find('#adjustQuantityCostRow');
        var $dateLabel = $modal.find('#adjustQuantityPurchaseDateLabel');
        var purchaseLabel = $dateLabel.data('label-purchase');
        var genericLabel = $dateLabel.data('label-generic');
        var syncAcquisitionFieldsVisibility = function () {
            var raw = $amount.val();
            // Treat "no value yet" as still-a-purchase for the visibility
            // toggle: prefilled acquisition metadata stays intact until
            // the operator explicitly types a non-positive number.
            if (raw === '' || raw === null || raw === undefined) {
                $acquisitionFields.show();
                $costRow.show();
                $dateLabel.text(purchaseLabel);
                return;
            }
            var num = parseFloat(raw);
            var isPurchase = !isNaN(num) && num > 0;
            $acquisitionFields.toggle(isPurchase);
            $costRow.toggle(isPurchase);
            $costHint.toggle(isPurchase && $costHint.data('has-prefill') === true);
            $dateLabel.text(isPurchase ? purchaseLabel : genericLabel);
            if (!isPurchase) {
                $modal.find('#adjustQuantityOrder').val('');
                $modal.find('#adjustQuantitySupplier').val('').trigger('change');
                $modal.find('#adjustQuantityUnitCost').val('');
                $modal.find('#adjustQuantityCurrency').val('');
            }
        };
        // Track the prefill state on the hint so the visibility toggle
        // can restore it correctly when qty flips positive again.
        $costHint.data('has-prefill', $costHint.is(':visible'));
        $amount.off('input.adjustAcquisition').on('input.adjustAcquisition', syncAcquisitionFieldsVisibility);
        // Initial call keeps everything in the default "purchase" state
        // (empty amount) so the prefill logic above stays authoritative.
        syncAcquisitionFieldsVisibility();

        $modal.modal('show');
    });

    // confirm delete modal
    $el.on('click', '.delete-asset', function (evnt) {
        var $context = $(this);
        var $dataConfirmModal = $('#dataConfirmModal');
        // Anchors keep the URL in href; buttons keep it in data-href
        // (buttons don't semantically support href per HTML5).
        var href = $context.attr('data-href') || $context.attr('href');
        var message = $context.attr('data-content');
        var headericon = $context.attr('data-icon');
        var title = $context.attr('data-title');

        // deleteForm is the ID of the modal form itself
        $('#deleteForm').attr('action', href);
        $dataConfirmModal.find('.modal-header-icon').addClass(headericon);
        $dataConfirmModal.find('.modal-title').text('').text(title).prepend('<i class="fa ' + headericon + '"></i> ');
        $dataConfirmModal.find('.modal-body').text('').text(message);
        $dataConfirmModal.attr('action', href);

        // Fire the modal
        $dataConfirmModal.modal({
            show: true
        });
        return false;
    });



     /*
     * Select2
     */

        $('select.select2:not(".select2-hidden-accessible")').each(function (i,obj) {
            {
                $(obj).select2();
            }
        });


    // $('.datepicker').datepicker();
    // var datepicker = $.fn.datepicker.noConflict(); // return $.fn.datepicker to previously assigned value
    // $.fn.bootstrapDP = datepicker;
    // $('.datepicker').datepicker();

    // Crazy select2 rich dropdowns with images!
    $('.js-data-ajax').each( function (i,item) {
        var link = $(item);
        var endpoint = link.data("endpoint");
        var select = link.data("select");

        link.select2({

            /**
             * Adds an empty placeholder, allowing every select2 instance to be cleared.
             * This placeholder can be overridden with the "data-placeholder" attribute.
             */
            placeholder: '',
            allowClear: true,
            language: $('meta[name="language"]').attr('content'),
            dir: $('meta[name="language-direction"]').attr('content'),
            
            ajax: {

                // the baseUrl includes a trailing slash
                url: baseUrl + 'api/v1/' + endpoint + '/selectlist',
                dataType: 'json',
                delay: 250,
                headers: {
                    "X-Requested-With": 'XMLHttpRequest',
                    "X-CSRF-TOKEN": $('meta[name="csrf-token"]').attr('content')
                },
                data: function (params) {
                    var data = {
                        search: params.term,
                        page: params.page || 1,
                        statusType: link.data("asset-status-type"),
                        companyId: link.data("company-ids") || link.data("company-id"),
                        excludeId: link.data("exclude-id"),
                        // When true, the companies selectlist marks child companies
                        // (those with a parent of their own) as disabled — used by
                        // the parent-company picker so users can't choose options
                        // that would fail the parent_must_be_top_level validator.
                        onlyTopLevel: link.data("only-top-level"),
                    };
                    return data;
                },
                /* processResults: function (data, params) {

                    params.page = params.page || 1;

                    var answer =  {
                        results: data.items,
                        pagination: {
                            more: data.pagination.more
                        }
                    };

                    return answer;
                }, */
                cache: true
            },
            //escapeMarkup: function (markup) { return markup; }, // let our custom formatter work
            templateResult: formatDatalistSafe,
            //templateSelection: formatDataSelection
        });

    });

	function getSelect2Value(element) {
		
		// if the passed object is not a jquery object, assuming 'element' is a selector
		if (!(element instanceof jQuery)) element = $(element);

		var select = element.data("select2");

		// There's two different locations where the select2-generated input element can be. 
		searchElement = select.dropdown.$search || select.$container.find(".select2-search__field");

		var value = searchElement.val();
		return value;
	}
	
	$(".select2-hidden-accessible").on('select2:selecting', function (e) {
		var data = e.params.args.data;
		var isMouseUp = false;
		var element = $(this);
		var value = getSelect2Value(element);
		
		if(e.params.args.originalEvent) isMouseUp = e.params.args.originalEvent.type == "mouseup";
		
		// if selected item does not match typed text, do not allow it to pass - force close for ajax.
		if(!isMouseUp) {
			if(value.toLowerCase() && data.text.toLowerCase().indexOf(value) < 0) {
				e.preventDefault();

				element.select2('close');
				
			// if it does match, we set a flag in the event (which gets passed to subsequent events), telling it not to worry about the ajax
			} else if(value.toLowerCase() && data.text.toLowerCase().indexOf(value) > -1) {
				e.params.args.noForceAjax = true;
			}
		}
	});
	
	$(".select2-hidden-accessible").on('select2:closing', function (e) {
		var element = $(this);
		var value = getSelect2Value(element);
		var noForceAjax = false;
		var isMouseUp = false;
		if(e.params.args.originalSelect2Event) noForceAjax = e.params.args.originalSelect2Event.noForceAjax;
		if(e.params.args.originalEvent) isMouseUp = e.params.args.originalEvent.type == "mouseup";
		
		if(value && !noForceAjax && !isMouseUp) {
			var endpoint = element.data("endpoint");
            var statusType = element.data("asset-status-type");
			$.ajax({
                url: baseUrl + 'api/v1/' + endpoint + '/selectlist?search=' + value + '&page=1' + (statusType ? '&statusType=' + statusType : ''),
				dataType: 'json',
				headers: {
					"X-Requested-With": 'XMLHttpRequest',
					"X-CSRF-TOKEN": $('meta[name="csrf-token"]').attr('content')
				},
			}).done(function(response) {
				var currentlySelected = element.select2('data').map(function (x){ 
                    return +x.id;
                }).filter(function (x) {
                    return x !== 0;
                });
				
				// makes sure we're not selecting the same thing twice for multiples
				var filteredResponse = response.results.filter(function(item) {
					return currentlySelected.indexOf(+item.id) < 0;
				});

				var first = (currentlySelected.length > 0) ? filteredResponse[0] : response.results[0];
				
				if(first && first.id) {
					first.selected = true;
					
					if($("option[value='" + first.id + "']", element).length < 1) {
						var option = new Option(first.text, first.id, true, true);
						element.append(option);
					} else {
						var isMultiple = element.attr("multiple") == "multiple";
						element.val(isMultiple? element.val().concat(first.id) : element.val(first.id));
					}
					element.trigger('change');

					element.trigger({
						type: 'select2:select',
						params: {
							data: first
						}
					});
				}
			});
		}
	});

    function formatDatalist (datalist) {
        var loading_markup = '<i class="fas fa-spinner fa-spin" aria-hidden="true"></i> Loading...';
        if (datalist.loading) {
            return loading_markup;
        }

        var markup = '<div class="clearfix">' ;
        markup += '<div class="pull-left" style="padding-right: 10px;">';
        if (datalist.image) {
            markup += "<div style='width: 30px;'><img src='" + datalist.image + "' style='max-height: 20px; max-width: 30px;' alt='" +  datalist.text + "'></div>";
        } else {
            markup += '<div style="height: 20px; width: 30px;"></div>';
        }

        markup += "</div><div>" + datalist.text + "</div>";
        markup += "</div>";
        return markup;
    }

    function formatDatalistSafe(datalist) {

        if (datalist.loading) {
            return $('<i class="fas fa-spinner fa-spin" aria-hidden="true"></i> Loading...');
        }

        var root_div = $("<div class='clearfix'>") ;
        var left_pull = $("<div class='pull-left' style='padding-right: 10px;'>");
        if (datalist.image) {
            var inner_div = $("<div style='width: 20px;'>");
            /******************************************************************
             *
             * We are specifically chosing empty alt-text below, because this
             * image conveys no additional information, relative to the text
             * that will *always* be there in any select2 list that is in use
             * in Snipe-IT. If that changes, we would probably want to change
             * some signatures of some functions, but right now, we don't want
             * screen readers to say "HP SuperJet 5000, .... picture of HP
             * SuperJet 5000..." and so on, for every single row in a list of
             * assets or models or whatever.
             *
             *******************************************************************/
            var img = $("<img src='' style='max-height: 20px; max-width: 20px;' alt=''>");
            img.attr("src", datalist.image);
            inner_div.append(img)
        } else if (datalist.tag_color) {
            var inner_div = $("<div style='width: 20px;'>");
            var icon = $('<i class="fa-solid fa-square" style="font-size: 20px;" aria-hidden="true"></i>');
            icon.css("color", datalist.tag_color );
            inner_div.append(icon)
        } else {
            var inner_div=$("<div style='height: 20px; width: 20px;'></div>");
        }
        left_pull.append(inner_div);
        root_div.append(left_pull);
        var name_div = $("<div>");
        name_div.text(datalist.text);
        root_div.append(name_div)
        var safe_html = root_div.get(0).outerHTML;
        var old_html = formatDatalist(datalist);
        if(safe_html != old_html) {
            //console.log("HTML MISMATCH: ");
            //console.log("FormatDatalistSafe: ");
            // console.dir(root_div.get(0));
            //console.log(safe_html);
            //console.log("FormatDataList: ");
            //console.log(old_html);
        }
        return root_div;

    }

    function formatDataSelection (datalist) {
        // This a heinous workaround for a known bug in Select2.
        // Without this, the rich selectlists are vulnerable to XSS.
        // Many thanks to @uberbrady for this fix. It ain't pretty,
        // but it resolves the issue until Select2 addresses it on their end.
        //
        // Bug was reported in 2016 :{
        // https://github.com/select2/select2/issues/4587

        return datalist.text.replace(/>/g, '&gt;')
            .replace(/</g, '&lt;')
            .replace(/"/g, '&quot;')
            .replace(/'/g, '&#039;');
    }

    // This handles the radio button selectors for the checkout-to-foo options
    // on asset checkout and also on asset edit
    $(function() {
        var checkoutToTypeInputs = $('input[name=checkout_to_type]');

        if (!checkoutToTypeInputs.length) {
            return;
        }

        function syncCheckoutToTypeUi(resetSelections) {
            var assignto_type = $('input[name=checkout_to_type]:checked').val();
            var userid = $('#assigned_user option:selected').val();

            if (assignto_type == 'asset') {
                $('#current_assets_box').fadeOut();
                $('#assigned_asset').show();
                $('#assigned_user').hide();
                $('#assigned_location').hide();
                $('.notification-callout').fadeOut();

                if (resetSelections) {
                    $('[name="assigned_location"]').val('').trigger('change.select2');
                    $('[name="assigned_user"]').val('').trigger('change.select2');
                }
            } else if (assignto_type == 'location') {
                $('#current_assets_box').fadeOut();
                $('#assigned_asset').hide();
                $('#assigned_user').hide();
                $('#assigned_location').show();
                $('.notification-callout').fadeOut();

                if (resetSelections) {
                    $('[name="assigned_asset"]').val('').trigger('change.select2');
                    $('[name="assigned_user"]').val('').trigger('change.select2');
                }
            } else {
                $('#assigned_asset').hide();
                $('#assigned_user').show();
                $('#assigned_location').hide();
                if (userid) {
                    $('#current_assets_box').fadeIn();
                }
                $('.notification-callout').fadeIn();

                if (resetSelections) {
                    $('[name="assigned_asset"]').val('').trigger('change.select2');
                    $('[name="assigned_location"]').val('').trigger('change.select2');
                }
            }
        }

        checkoutToTypeInputs.on('change', function () {
            syncCheckoutToTypeUi(true);
        });

        // Expose so pages that reveal #assignto_selector later (asset edit's
        // user_add() flow, etc.) can trigger the sync once the selector is
        // visible. Standalone checkout pages don't need to call this — the
        // initial-render block below handles them.
        window.snipeitSyncCheckoutToTypeUi = syncCheckoutToTypeUi;

        // Apply the current radio selection on initial render unless the page
        // has explicitly hidden the selector via an inline style="display:none"
        // (asset create/edit start that way and reveal it from user_add() after
        // a deployability AJAX call). Using getAttribute('style') instead of
        // jQuery's :visible avoids false negatives on pages like the standalone
        // /hardware/{id}/checkout, where the selector is visible from the start
        // but :visible can transiently return false during select2 boot — that
        // was what hid the acceptance-options callout until a radio was toggled.
        var selectorStyle = ($('#assignto_selector').attr('style') || '').toLowerCase();
        if (selectorStyle.indexOf('display:none') === -1 && selectorStyle.indexOf('display: none') === -1) {
            syncCheckoutToTypeUi(false);
        }
    });


    // ------------------------------------------------
    // Deep linking for Bootstrap tabs
    // ------------------------------------------------
    var taburl = document.location.toString();

    // Allow full page URL to activate a tab's ID
    // ------------------------------------------------
    // This allows linking to a tab on page load via the address bar.
    // So a URL such as, http://snipe-it.local/hardware/2/#my_tab will
    // cause the tab on that page with an ID of “my_tab” to be active.
    if (taburl.match('#') ) {
        $('.nav-tabs a[href="#'+taburl.split('#')[1]+'"]').tab('show');
    }

    // Allow internal page links to activate a tab's ID.
    // ------------------------------------------------
    // This allows you to link to a tab from anywhere on the page
    // including from within another tab. Also note that internal page
    // links either inside or out of the tabs need to include data-toggle="tab"
    // Ex: <a href="#my_tab" data-toggle="tab">Click me</a>
    $('a[data-toggle="tab"]').click(function (e) {
        var href = $(this).attr("href");
        history.pushState(null, null, href);
        e.preventDefault();
        $('a[href="' + $(this).attr('href') + '"]').tab('show');
    });

    // Bootstrap-table's fixed-columns extension computes the overlay widths
    // at init time. Tables inside a hidden tab pane initialize with a
    // zero-width container and the fixed left/right columns never recover
    // on their own once the pane becomes visible. Force a resetView on any
    // snipe-tables inside the newly-shown pane so fixed columns line up.
    $('body').on('shown.bs.tab', 'a[data-toggle="tab"]', function (e) {
        var pane = $(e.target).attr('href');
        if (!pane) return;
        $(pane).find('.snipe-table').each(function () {
            if ($(this).data('bootstrap.table')) {
                $(this).bootstrapTable('resetView');
            }
        });
    });

    // Same story for viewport resizes: the fixed-columns overlay caches
    // widths from the initial layout and doesn't recompute when the window
    // width changes. Debounce so a drag-resize doesn't fire resetView on
    // every intermediate pixel.
    var snipeTableResizeTimer;
    $(window).on('resize', function () {
        clearTimeout(snipeTableResizeTimer);
        snipeTableResizeTimer = setTimeout(function () {
            $('.snipe-table').each(function () {
                if ($(this).data('bootstrap.table')) {
                    $(this).bootstrapTable('resetView');
                }
            });
        }, 150);
    });

    // ------------------------------------------------
    // End Deep Linking for Bootstrap tabs
    // ------------------------------------------------



    // Image preview
    function readURL(input, $preview) {
        if (input.files && input.files[0]) {
            var reader = new FileReader();
            reader.onload = function(e) {
                $preview.attr('src', e.target.result);
            };
            reader.readAsDataURL(input.files[0]);
        }
    }

    function formatBytes(bytes) {
        if(bytes < 1024) return bytes + " Bytes";
        else if(bytes < 1048576) return(bytes / 1024).toFixed(2) + " KB";
        else if(bytes < 1073741824) return(bytes / 1048576).toFixed(2) + " MB";
        else return(bytes / 1073741824).toFixed(2) + " GB";
    }

     // File size validation
    $('.js-uploadFile').bind('change', function() {
        var $this = $(this);
        var id = '#' + $this.attr('id');
        var status = id + '-status';
        var $status = $(status);
        var delete_id = $(id + '-deleteCheckbox');
        var preview_container = $(id + '-previewContainer');



        $status.removeClass('text-success').removeClass('text-danger');
        $(status + ' .goodfile').remove();
        $(status + ' .badfile').remove();
        $(status + ' .previewSize').hide();
        preview_container.hide();
        $(id + '-info').html('');

        var max_size = $this.data('maxsize');
        var total_size = 0;

        for (var i = 0; i < this.files.length; i++) {
            total_size += this.files[i].size;
            $(id + '-info').append('<span class="label label-default">' + htmlEntities(this.files[i].name) + ' (' + formatBytes(this.files[i].size) + ')</span> ');
        }

        if (total_size > max_size) {
            $status.addClass('text-danger').removeClass('help-block').prepend('<i class="badfile fas fa-times"></i> ').append('<span class="previewSize"> Upload is ' + formatBytes(total_size) + '.</span>');
        } else {
            $status.addClass('text-success').removeClass('help-block').prepend('<i class="goodfile fas fa-check"></i> ');
            var $preview =  $(id + '-imagePreview');
            readURL(this, $preview);
            $preview.fadeIn();
            preview_container.fadeIn();
            delete_id.hide();
        }


    });

});

function htmlEntities(str) {
    return String(str).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
}



/**
 * Toggle disabled
 */
(function($){
		
    $.fn.toggleDisabled = function(callback){
        return this.each(function(){
            var disabled, $this = $(this);
            if($this.attr('disabled')){
                $this.removeAttr('disabled');
                disabled = false;
            } else {
                $this.attr('disabled', 'disabled');
                disabled = true;
            }

            if(callback && typeof callback === 'function'){
                callback(this, disabled);
            }
        });
    };
    
})(jQuery);

$(document).ready(function () {
    // Password-reveal eye. data-toggle is a jQuery selector — usually one
    // input id, but a multi-selector like "#password, #password_confirm"
    // lets a single click flip every matched input at once (the confirm
    // field on the user create/edit form uses this so revealing the
    // password reveals its confirmation too). Every .toggle-password
    // sharing the same data-toggle string flips its icon together so the
    // eye state doesn't visually drift between the two addons.
    $(document).on('click', '.toggle-password', function () {
        var toggleTarget = $(this).attr('data-toggle');
        var $inputs = $(toggleTarget);
        var reveal = $inputs.first().attr('type') === 'password';
        $inputs.attr('type', reveal ? 'text' : 'password');
        var $eyes = $('.toggle-password[data-toggle="' + toggleTarget + '"]');
        $eyes.toggleClass('fa-eye', ! reveal);
        $eyes.toggleClass('fa-eye-slash', reveal);
    });

    // Auto-init eonasdan datetimepickers. bootstrap-datepicker has a native
    // data-provide auto-init; eonasdan does not, so we do it ourselves.
    // Options are read from data-attributes on the wrapper so blade components
    // can tune format/side-by-side without touching this JS.
    //
    // Icon set is overridden to Font Awesome — the picker defaults to
    // Glyphicon classes, which we do not ship, so up/down arrows and clock
    // glyphs would otherwise render as empty boxes.
    // Exposed so callers who insert new [data-provide="datetimepicker"]
    // wrappers into the DOM post-load (e.g., AJAX-loaded custom fields on
    // asset create/edit when the model changes) can re-run the init on the
    // freshly-inserted elements. Pass a jQuery scope to narrow the search;
    // omit to init every uninitialised picker on the page.
    window.snipeitInitDatetimepickers = function (scope) {
        var $targets = scope ? $(scope).find('[data-provide="datetimepicker"]') : $('[data-provide="datetimepicker"]');
        $targets.each(initDatetimepicker);
    };

    function initDatetimepicker() {
        var $wrapper = $(this);
        // Skip if this wrapper already has an eonasdan instance attached
        // (data('DateTimePicker') is set by the library on init).
        if ($wrapper.data('DateTimePicker')) {
            return;
        }
        var $input = $wrapper.find('input');
        var existingValue = ($input.val() || '').trim();

        var options = {
            format: $wrapper.data('format') || 'YYYY-MM-DD HH:mm:ss',
            // Default to the compact (collapsed) view — calendar shows first
            // and a small clock icon toggles the time view. Callers that want
            // date + time visible side by side can set data-side-by-side="true".
            sideBySide: $wrapper.data('side-by-side') === true,
            showClear: true,
            showClose: true,
            showTodayButton: true,
            // In sideBySide mode the toolbar row (Today/Clear/Close) is only
            // rendered when placement is explicitly 'top' or 'bottom'; the
            // library drops it entirely on the default 'default' placement.
            toolbarPlacement: 'bottom',
            // Open the popup on any focus/click of the input (not just the
            // calendar addon icon), matching the behavior of the bootstrap
            // datepicker used elsewhere in the app.
            allowInputToggle: true,
            locale: $wrapper.data('locale') || 'en',
            icons: {
                time: 'fa-regular fa-clock',
                date: 'fa-regular fa-calendar',
                up: 'fa-solid fa-chevron-up',
                down: 'fa-solid fa-chevron-down',
                previous: 'fa-solid fa-chevron-left',
                next: 'fa-solid fa-chevron-right',
                today: 'fa-solid fa-calendar-day',
                clear: 'fa-solid fa-trash',
                close: 'fa-solid fa-xmark',
            },
        };

        // Pre-fill empty inputs with the user's current local datetime by
        // default. Callers that render a picker where "now" is NOT a safe
        // default (e.g., user-defined custom fields) can opt out by setting
        // data-default-now="false" on the wrapper.
        var wantsDefaultNow = $wrapper.data('default-now') !== false;
        if (existingValue === '' && wantsDefaultNow) {
            options.defaultDate = moment();
        }

        // data-max-date="today" caps the picker at today (replaces the
        // bootstrap-datepicker era's data-date-end-date="0d"); any other
        // value is parsed as a moment-compatible date string.
        var maxDate = $wrapper.data('max-date');
        if (maxDate) {
            options.maxDate = maxDate === 'today' ? moment().endOf('day') : moment(maxDate);
        }

        $wrapper.datetimepicker(options);
    }

    // Wires up the linked-pickers pattern for <x-input.date-range>. Each
    // .js-date-range wrapper holds a .js-date-range-start and .js-date-range-end
    // datetimepicker; changing one bounds the other so a user can't pick an
    // end date before the start (or vice versa). Runs after the plain
    // datetimepicker init above so both instances already exist.
    function initDateRangeLinking() {
        $('.js-date-range').each(function () {
            var $start = $(this).find('.js-date-range-start');
            var $end = $(this).find('.js-date-range-end');
            if (!$start.length || !$end.length) {
                return;
            }
            $start.off('dp.change.snipeitDateRange').on('dp.change.snipeitDateRange', function (e) {
                var picker = $end.data('DateTimePicker');
                if (picker) {
                    picker.minDate(e.date);
                }
            });
            $end.off('dp.change.snipeitDateRange').on('dp.change.snipeitDateRange', function (e) {
                var picker = $start.data('DateTimePicker');
                if (picker) {
                    picker.maxDate(e.date);
                }
            });
        });
    }

    // Push the app's "week starts on" setting into moment's active locale so
    // the eonasdan datetimepicker (which reads firstDayOfWeek from moment
    // locale data, not from its own options) opens with the calendar column
    // order the admin picked in Localization settings. Runs once before any
    // picker is initialized; downstream code that formats using moment's w/W
    // tokens will pick up the same value.
    if (window.snipeit && window.snipeit.settings && typeof window.snipeit.settings.first_day_of_week === 'number') {
        moment.updateLocale(moment.locale(), { week: { dow: window.snipeit.settings.first_day_of_week } });
    }

    window.snipeitInitDatetimepickers();
    initDateRangeLinking();
});



/**
 * Universal Livewire Select2 integration
 *
 * How to use:
 *
 * 1. Set the class of your select2 elements to 'livewire-select2').
 * 2. Name your element to match a property in your Livewire component
 * 3. Add an attribute called 'data-livewire-component' that points to $this->getId() (via `{{ }}` if you're in a blade,
 *    or just $this->getId() if not).
 */
document.addEventListener('livewire:init', () => {
    $('.livewire-select2').select2()

    $(document).on('select2:select', '.livewire-select2', function (event) {
        var target = $(event.target)
        if(!event.target.name || !target.data('livewire-component')) {
            console.error("You need to set both name (which should match a Livewire property) and data-livewire-component on your Livewire-ed select2 elements!")
            console.error("For data-livewire-component, you probably want to use $this->getId() or {{ $this->getId() }}, as appropriate")
            return false
        }
        // PHP property names cannot start with a digit — skip bare numeric names (e.g. "0") that would cause a 500
        if (/^\d+$/.test(event.target.name)) {
            console.error("Livewire select2: name attribute '" + event.target.name + "' is not a valid Livewire property name — skipping")
            return false
        }
        Livewire.find(target.data('livewire-component')).set(event.target.name, this.options[this.selectedIndex].value)
    });

  Livewire.interceptMessage(({ onFinish }) => {
    onFinish(() => {
      // Runs after DOM morph completes (or on error/cancel)
        queueMicrotask(() => {
          $(".livewire-select2").select2();
        });
      });
    }
  );
});




// Check/Uncheck all radio buttons in the permissions group
$('.header-row input:radio').change(function() {
    value = $(this).attr('value');
    area = $(this).data('checker-group');
    $('.radiochecker-'+area+'[value='+value+']').prop('checked', true);
});

// Generic toggleable callouts with remember state
$(".remember-toggle").on("click",function(){

    var toggleable_callout_id = $(this).attr('id');
    var toggle_content_class = 'toggle-content-'+$(this).attr('id');
    var toggle_arrow = '#toggle-arrow-' + toggleable_callout_id;
    var toggle_cookie_name='toggle_state_'+toggleable_callout_id;

    $('.'+toggle_content_class).fadeToggle(100);
    $(toggle_arrow).toggleClass('fa-caret-right fa-caret-down');
    var toggle_open = $(toggle_arrow).hasClass('fa-caret-down');
    document.cookie=toggle_cookie_name+"="+toggle_open+';path=/';
});

var all_cookies = document.cookie.split(';')
for (var i in all_cookies) {
    var trimmed_cookie = all_cookies[i].trim(' ')
    elems = trimmed_cookie.split('=', 2);

    // We have to do more here since we don't know the name of the selector
    if (trimmed_cookie.startsWith('toggle_state_')) {

        var toggle_selector_name = elems[0].replace('toggle_state_','');

        if (elems[1] != "true") {
            $('#'+toggle_selector_name+'.remember-toggle').trigger('click')
        }
    }

}


/**
 * This handles the show/hide of superuser and admin specific permissions
 * on the group edit and user edit pages
 */
if ($("#superuser_allow").is(':checked')) {

    // Hide here instead of fadeout on pageload to prevent what looks like Flash Of Unstyled Content (FOUC)
    $(".nonsuperuser").hide();
    $(".nonsuperuser").attr('display','none');
}


$(".superuser").change(function() {
    if ($(this).val() == '1') {
        $(".nonsuperuser").fadeOut();
        $(".nonsuperuser").attr('display','none');
        $(".nonadmin").fadeOut();
        $(".nonadmin").attr('display','none');
    } else if ($(this).val() != '1') {
        $(".nonsuperuser").fadeIn();
        $(".nonsuperuser").attr('display','block');

        // If the superuser button has been set to deny, we need to
        // check that the admin button isn't set to allow, before we show non-admin stuff
        if ($("#admin_allow").is(':checked')) {

            // Hide here instead of fadeout on pageload to prevent what looks like Flash Of Unstyled Content (FOUC)
            $(".nonadmin").hide();
            $(".nonadmin").attr('display','none');
        }

    }
});



if ($("#admin_allow").is(':checked')) {

    // Hide here instead of fadeout on pageload to prevent what looks like Flash Of Unstyled Content (FOUC)
    $(".nonadmin").hide();
    $(".nonadmin").attr('display','none');
}

$(".admin").change(function() {
    if ($(this).val() == '1') {
        $(".nonadmin").fadeOut();
        $(".nonadmin").attr('display','none');
    } else if ($(this).val() != '1') {
        $(".nonadmin").fadeIn();
        $(".nonadmin").attr('display','block');
    }
});

// Handle the select/deselect of the select boxes with the button from right to left

$(function () {

    function moveItems(origin, dest) {
        $(origin).find(':selected').appendTo(dest);
        $(dest).attr('selected', true);
        $(dest).sort_select_box();
    }

    function moveAllItems(origin, dest) {
        $(origin).children("option:visible").appendTo(dest);
        $(dest).attr('selected', true);
        $(dest).sort_select_box();
    }

    $('.left').on('click', function () {
        var container = $(this).closest('.addremove-multiselect');
        moveItems($(container).find('select.multiselect.selected'), $(container).find('select.multiselect.available'));
    });

    $('.right').on('click', function () {
        var container = $(this).closest('.addremove-multiselect');
        moveItems($(container).find('select.multiselect.available'), $(container).find('select.multiselect.selected'));

    });

    $('.leftall').on('click', function () {
        var container = $(this).closest('.addremove-multiselect');
        moveAllItems($(container).find('select.multiselect.selected'), $(container).find('select.multiselect.available'));
    });

    $('.rightall').on('click', function () {
        var container = $(this).closest('.addremove-multiselect');
        moveAllItems($(container).find('select.multiselect.available'), $(container).find('select.multiselect.selected'));
    });

    $('select.multiselect.selected').on('dblclick keyup',function(e){
        if(e.which == 13 || e.type == 'dblclick') {
            var container = $(this).closest('.addremove-multiselect');
            moveItems($(container).find('select.multiselect.selected'), $(container).find('select.multiselect.available'));
        }
    });

    $('select.multiselect.available').on('dblclick keyup',function(e){
        if(e.which == 13 || e.type == 'dblclick') {
            var container = $(this).closest('.addremove-multiselect');
            moveItems($(container).find('select.multiselect.available'), $(container).find('select.multiselect.selected'));
            $('#hidden_ids_box').val($('#selected-select').val());
        }
    });


});

$.fn.sort_select_box = function(){
    // Get options from select box
    var selected_options = $(this).children('option');
    // sort alphabetically
    selected_options.sort(function(a,b) {
        if (a.text > b.text) return 1;
        else if (a.text < b.text) return -1;
        else return 0
    })
    //replace with sorted my_options;
    $(this).empty().append(selected_options);

    var selected_in_box =  $('#selected-select option').toArray().map(item => item.value).join();

    $('#hidden_ids_box').empty().val(selected_in_box);

    $('#count_selected_box').html($('#selected-select option').length);
    $('#count_unselected_box').html($('#available-select option').length);

    // clearing any selections
    $("#"+this.attr('id')+" option").attr('selected', true);
}


/*
 * Data-attribute driven initializers. Blades attach behavior by adding
 * `data-toggle="..."` (plus supporting data-* attributes) to elements
 * instead of shipping an inline <script> block. Add new handlers here
 * as inline scripts get migrated out of blades.
 */
$(function () {

    // Sound preview on account/profile. Fires the URL in data-sound-url
    // when the user toggles the checkbox on.
    $(document).on('click', '[data-toggle="sound-test"]', function () {
        if (!$(this).is(':checked')) return;
        var url = $(this).data('sound-url');
        if (!url) return;
        new Audio(url).play();
    });

    // Confetti preview on account/profile. Same shape as sound-test.
    $(document).on('click', '[data-toggle="confetti-test"]', function () {
        if (!$(this).is(':checked')) return;

        var duration = 1500;
        var animationEnd = Date.now() + duration;
        var defaults = { startVelocity: 30, spread: 360, ticks: 60, zIndex: 0 };

        function randomInRange(min, max) {
            return Math.random() * (max - min) + min;
        }

        var interval = setInterval(function () {
            var timeLeft = animationEnd - Date.now();
            if (timeLeft <= 0) {
                return clearInterval(interval);
            }
            var particleCount = 50 * (timeLeft / duration);
            confetti({
                ...defaults,
                particleCount,
                origin: { x: randomInRange(0.1, 0.3), y: Math.random() - 0.2 },
            });
            confetti({
                ...defaults,
                particleCount,
                origin: { x: randomInRange(0.7, 0.9), y: Math.random() - 0.2 },
            });
        }, 250);
    });

    // Live color preview for the nav-link colorpicker on account/profile.
    // The colorpicker widget itself is initialized by $(".color").colorpicker()
    // in the default layout; this just wires the changeColor listener.
    if ($('#nav-link-color').length) {
        $('#nav-link-color').on('changeColor', function (e) {
            var color = e.color.toString('rgba');
            $('.navbar-nav > li > a:link').attr('style', 'color: ' + color + ' !important');
            $('.btn-theme').attr('style', 'color: ' + color + ' !important');
        });
    }

    // Branding settings page: live preview + reset for the four tenant
    // colorpickers. The pickers themselves are initialized by the global
    // $(".color").colorpicker() call in the default layout; this only wires
    // the changeColor listeners and the reset button. Guarded on the reset
    // button ID so it only runs on Settings > Branding, not on every page
    // that happens to have a #header-color or #nav-link-color widget.
    //
    // Only header + nav-link get live preview. Link light/dark previews are
    // deliberately skipped: they would recolor the buttons and other UI on
    // this form itself, which can make it unreadable if the operator picks
    // a low-contrast color. Those two settings just save and take effect on
    // reload.
    if (document.getElementById('branding-colors-reset')) {
        var BRANDING_DEFAULTS = {
            header_color: '#3c8dbc',
            nav_link_color: '#ffffff',
            link_light_color: '#296282',
            link_dark_color: '#5fa4cc',
        };

        // Live preview works by writing the tenant CSS variables inline on
        // <html>. Inline element style beats the :root and [data-theme]
        // declarations in overrides.less on specificity, so this works even
        // for the many rules those declarations lock in with !important.
        var applyBrandingHeader = function (color) {
            document.documentElement.style.setProperty('--main-theme-color', color);
        };

        var applyBrandingNavLink = function (color) {
            document.documentElement.style.setProperty('--btn-theme-text-color', color);
            document.documentElement.style.setProperty('--nav-hover-text-color', color);
            document.documentElement.style.setProperty('--nav-primary-text-color', color);
        };

        $('#header-color').on('changeColor', function (e) {
            applyBrandingHeader(e.color.toString('rgba'));
        });

        $('#nav-link-color').on('changeColor', function (e) {
            applyBrandingNavLink(e.color.toString('rgba'));
        });

        // Reset: restore each picker's swatch and input to the stock
        // defaults. Using setValue on the plugin (not just .val() on the
        // input) fires the plugin's internal changeColor event, which
        // re-runs applyBrandingHeader / applyBrandingNavLink automatically.
        $('#branding-colors-reset').on('click', function () {
            $('#header-color').colorpicker('setValue', BRANDING_DEFAULTS.header_color);
            $('#nav-link-color').colorpicker('setValue', BRANDING_DEFAULTS.nav_link_color);
            $('#link-light-color').colorpicker('setValue', BRANDING_DEFAULTS.link_light_color);
            $('#link-dark-color').colorpicker('setValue', BRANDING_DEFAULTS.link_dark_color);
        });
    }

    // Reset the localStorage theme override when the user clicks the
    // "system default" link (any element carrying data-theme-toggle-clear).
    document.querySelectorAll('[data-theme-toggle-clear]').forEach(function (el) {
        el.addEventListener('click', function () {
            localStorage.removeItem('theme');
        });
    });

    // Master checkbox → target field disabled state. Callers pair a
    // <input type="checkbox" data-toggle="disable-when-unchecked"
    // data-disable-target="#some-field"> with a target rendered
    // server-side with the matching @disabled state (avoids FOUC).
    // Handler keeps them in sync on change.
    $(document).on('change', '[data-toggle="disable-when-unchecked"]', function () {
        var target = $(this).data('disable-target');
        if (target) {
            $(target).prop('disabled', !$(this).is(':checked'));
        }
    });

    // Disable empty REQUIRED inputs on submit so browser HTML5 validation
    // doesn't block the request before Laravel's form-request validator
    // gets a chance to return a nicer error. Non-required empties (like a
    // "Do not change" select with an explicit value="" option) are left
    // enabled so they submit their intentional empty value. Opt in per
    // form with data-disable-empty-on-submit.
    $(document).on('submit', 'form[data-disable-empty-on-submit]', function () {
        $(this).find(':input[required]').filter(function () { return !this.value; }).attr('disabled', 'disabled');
    });

    // Master checkbox → toggle every non-disabled checkbox in the closest
    // form or table (or a caller-specified selector via data-check-scope).
    // Used by bulk-delete confirmation pages to select or deselect the
    // whole list of rows at once.
    $(document).on('change', '[data-toggle="check-all"]', function () {
        var $master = $(this);
        var scope = $master.data('check-scope');
        var $container = scope ? $(scope) : $master.closest('form, table');
        $container.find('input[type="checkbox"]').not($master).not(':disabled').prop('checked', $master.prop('checked'));
    });

    // When the "This user can login" (activated) checkbox is off, the
    // password + confirmation fields are functionally useless because
    // login is gated by the activated flag. Hide the whole form-group
    // (or dynamic-form-row in the modal) so the form doesn't show
    // fields the user can't meaningfully fill in, and also drop the
    // HTML `required` attribute so the browser doesn't block submission.
    // The server side already skips the password rule for this case
    // via SaveUserRequest::rules(), and the controller stores
    // User::noPassword() raw so no Hash::check can ever match.
    // Applies to both the main users/edit create form and the
    // users/modal form since they share the input names.
    //
    // Required-state preservation: the server renders password/password_
    // confirmation with `required` only on create (see users/edit.blade.php
    // and modals/user.blade.php). We cache that server-rendered state on
    // the first call so subsequent activated-toggles only ever re-apply
    // the ORIGINAL server intent — otherwise editing an existing
    // (activated) user would silently flip password to required on page
    // load and jQuery Validate would block Save with the password empty.
    var syncPasswordFields = function ($checkbox) {
        var $form = $checkbox.closest('form');
        var $passwords = $form.find(
            'input[name="password"], input[name="password_confirmation"]'
        );
        var activated = $checkbox.is(':checked');
        $passwords.each(function () {
            if (this.dataset.serverRequired === undefined) {
                this.dataset.serverRequired = this.required ? '1' : '0';
            }
            this.required = activated && this.dataset.serverRequired === '1';
            var $wrap = $(this).closest('.form-group, .dynamic-form-row');
            if (activated) {
                $wrap.show();
            } else {
                $wrap.hide();
            }
        });
    };

    // Sensitive fields (username, email, password) ship with a
    // `readonly` + onfocus-removes-readonly anti-autofill trick to
    // stop password managers from prefilling or overwriting the
    // operator's own login credentials on user-create forms. The
    // side-effect is that HTML5 `required` constraint validation is
    // SILENTLY skipped for readonly inputs, so hitting submit without
    // ever focusing a required field lets the empty form through the
    // browser check entirely.
    //
    // On submit-button click we strip `readonly` from any
    // required+readonly input inside the form. The browser then runs
    // its normal constraint check (all fields participating) and
    // shows the "please fill in this field" popup on empties. Autofill
    // was already prevented at page load, so removing readonly at
    // click time doesn't reopen that hole.
    $(document).on('click', 'button[type="submit"], input[type="submit"]', function () {
        var $form = $(this).closest('form');
        if (! $form.length) {
            return;
        }
        $form.find('input[required][readonly]').each(function () {
            this.removeAttribute('readonly');
        });
    });
    $('input[name="activated"][type="checkbox"]').each(function () {
        syncPasswordFields($(this));
    });
    $(document).on('change', 'input[name="activated"][type="checkbox"]', function () {
        syncPasswordFields($(this));
    });

    // Generic "typing into input A enables checkbox B" pattern. Server
    // marks the input with data-toggles-checkbox="{selector-of-target}".
    // Threshold is 6 chars, which matches the legacy user-create
    // behaviour of only enabling the send-welcome checkbox once the
    // email is plausibly valid. Server omits the data-attribute when
    // the enable-side should never fire (e.g. app.lock_passwords is on)
    // so the target stays permanently disabled.
    $(document).on('keyup', 'input[data-toggles-checkbox]', function () {
        var $target = $($(this).data('toggles-checkbox'));
        if (! $target.length) {
            return;
        }
        if (this.value.length > 5) {
            $target.prop('disabled', false);
            $target.closest('.form-control').removeClass('form-control--disabled');
        } else {
            $target.prop('disabled', true).prop('checked', false);
            $target.closest('.form-control').addClass('form-control--disabled');
        }
    });

    // Bootstrap tooltips on any element carrying .tooltip-base.
    // Attaching to body avoids clipping inside overflow-hidden panels.
    $('.tooltip-base').tooltip({ container: 'body' });

    // Password generator button. Server puts the desired length on the
    // button as data-password-length (typically pwd_secure_min + 9) so
    // this JS doesn't have to know about app settings. Falls back to 16
    // if the attribute is missing.
    $('a[id="genPassword"], button[id="genPassword"]').each(function () {
        var $btn = $(this);
        if (typeof $btn.pGenerator !== 'function' || ! $('#password').length) {
            return;
        }
        $btn.pGenerator({
            bind: 'click',
            passwordElement: '#password',
            passwordLength: parseInt($btn.data('password-length') || '16', 10),
            uppercase: true,
            lowercase: true,
            numbers: true,
            specialChars: true,
            onPasswordGenerated: function () {
                $('#password_confirm').val($('#password').val());
            },
        });
    });

    // A <select data-gates-submit> disables the submit button(s) in its
    // form until a value is chosen. Used by users/confirm-bulk-delete
    // where the operator must pick a status for the deleted users' assets
    // before the form can be submitted. Runs once on load to reflect
    // whatever value was pre-selected (old input after a validation
    // redirect) and re-syncs on change and on select2's own event.
    $('select[data-gates-submit]').each(function () {
        var $select = $(this);
        var $submits = $select.closest('form').find(':submit');
        var sync = function () {
            $submits.prop('disabled', ! $select.val());
        };
        sync();
        $select.on('change select2:select', sync);
    });

    // Auto-focus the first select2 search input on pages that ask for it.
    // Bulk-checkout uses this so the operator lands directly on the
    // assets-to-checkout picker and can start typing immediately. Results
    // are hidden until the first keystroke so the operator doesn't see a
    // full-list flash on open.
    if ($('[data-autofocus-select2-search]').length) {
        setTimeout(function () {
            var $searchField = $('.select2-search__field');
            var $results = $('.select2-results');
            $searchField.focus();
            $results.hide();
            $searchField.on('input', function () {
                $results.show();
            });
        }, 0);
    }

    // Hardware bulk edit: clear-radio buttons blank every input of a
    // named radio group so the caller can back out of a picked value.
    // The .clear-radio button carries a data-target-name matching the
    // radio group's name attribute.
    document.querySelectorAll('.clear-radio').forEach(function (button) {
        button.addEventListener('click', function () {
            var name = this.dataset.targetName;
            var radios = document.querySelectorAll('input[type="radio"][name="' + name + '"]');
            radios.forEach(function (radio) { radio.checked = false; });
        });
    });

    // Hardware bulk edit: live status-deployable check. When the user
    // picks a status, hit the deployable API and update the inline
    // status indicator so the operator knows whether that status will
    // pull an asset out of active service. Translated labels ride on
    // the element's data attributes so this handler doesn't need to be
    // a Blade-compiled inline script. Guarded on the data-deployable-
    // label attribute (not just the id) because #selected_status_status
    // also appears in partials/forms/edit/status.blade.php — used by
    // hardware/edit — which has its own inline user_add() handler and
    // doesn't render the labels, so we'd otherwise double-fire and
    // overwrite that handler's output with an icon-only string.
    var statusStatusEl = document.getElementById('selected_status_status');
    if (statusStatusEl && statusStatusEl.dataset.deployableLabel) {
        var deployableLabel = statusStatusEl.dataset.deployableLabel || '';
        var notDeployableLabel = statusStatusEl.dataset.notDeployableLabel || '';

        var runStatusDeployableCheck = function () {
            var statusId = $('select[name="status_id"]').val();
            if (statusId === '') {
                return;
            }
            $('.status_spinner').css('display', 'inline');
            $.ajax({
                url: '/api/v1/statuslabels/' + statusId + '/deployable',
                headers: {
                    'X-Requested-With': 'XMLHttpRequest',
                    'X-CSRF-TOKEN': $('meta[name="csrf-token"]').attr('content'),
                },
                success: function (data) {
                    $('.status_spinner').css('display', 'none');
                    $('#selected_status_status').fadeIn();
                    if (data == true) {
                        $('#selected_status_status')
                            .removeClass('text-danger')
                            .addClass('text-success')
                            .html('<i class="fa-solid fa-check" aria-hidden="true"></i> ' + deployableLabel);
                    } else {
                        $('#assignto_selector').hide();
                        $('#selected_status_status')
                            .removeClass('text-success')
                            .addClass('text-danger')
                            .html('<i class="fa-solid fa-triangle-exclamation" aria-hidden="true"></i> ' + notDeployableLabel);
                    }
                },
            });
        };

        $('select[name="status_id"]').on('change', runStatusDeployableCheck);
    }

    // Hardware checkin: requestable-toggle wrapper. Show or hide the
    // "make this asset requestable after checkin" checkbox depending on
    // whether the currently-selected status is deployable. Preserve the
    // checkbox state when hiding so a status bounce doesn't blow it
    // away — the server only applies the value when the status is
    // deployable anyway.
    var requestableWrapper = document.getElementById('requestable-wrapper');
    if (requestableWrapper) {
        var deployableStatusIds = [];
        try {
            deployableStatusIds = JSON.parse(requestableWrapper.dataset.deployableStatusIds || '[]');
        } catch (e) {
            // Malformed data — leave the wrapper in its server-rendered state.
        }

        var statusSelect = document.getElementById('modal-statuslabel_types')
            || document.querySelector('select[name="status_id"]');

        if (statusSelect) {
            var toggleRequestableWrapper = function () {
                var value = statusSelect.value;
                var statusId = Number.parseInt(value, 10);
                var isDeployable = value !== ''
                    && Number.isInteger(statusId)
                    && deployableStatusIds.indexOf(statusId) !== -1;
                requestableWrapper.style.display = isDeployable ? '' : 'none';
            };

            statusSelect.addEventListener('change', toggleRequestableWrapper);
            if (window.jQuery) {
                window.jQuery(statusSelect).on('select2:select select2:clear', toggleRequestableWrapper);
            }
            toggleRequestableWrapper();
        }
    }

    // Hardware checkin: per-user localStorage preference for the
    // requestable-checkbox default. Namespaced by user id so a shared
    // browser doesn't leak one user's habit to another. Bypassed when
    // the checkbox was repopulated from a validation-error redirect —
    // old() beats the stored preference. On submit, save whatever the
    // user actually chose so the preference tracks their real habit.
    var requestableCheckbox = document.getElementById('requestable');
    if (requestableCheckbox && requestableCheckbox.dataset.userPreferenceKey) {
        var storageKey = requestableCheckbox.dataset.userPreferenceKey;
        var hadOldInput = requestableCheckbox.dataset.hadOldInput === '1';
        var form = requestableCheckbox.closest('form');

        if (form) {
            if (!hadOldInput) {
                var stored = null;
                try {
                    stored = window.localStorage.getItem(storageKey);
                } catch (e) {
                    // localStorage may be unavailable (private mode, disabled).
                }
                if (stored === '1' || stored === '0') {
                    requestableCheckbox.checked = stored === '1';
                }
            }

            form.addEventListener('submit', function () {
                try {
                    window.localStorage.setItem(storageKey, requestableCheckbox.checked ? '1' : '0');
                } catch (e) {
                    // Non-fatal: preference just won't persist this time.
                }
            });
        }
    }
});
