Taskonator - TOT Batch

Biblioteka - Modul batch TOT z presetami dla Taskonatora

Ten skrypt nie powinien być instalowany bezpośrednio. Jest to biblioteka dla innych skyptów do włączenia dyrektywą meta // @require https://update.greasyfork.org/scripts/575279/1807562/Taskonator%20-%20TOT%20Batch.js

Aby zainstalować ten skrypt, wymagana jest instalacje jednego z następujących rozszerzeń: Tampermonkey, Greasemonkey lub Violentmonkey.

You will need to install an extension such as Tampermonkey to install this script.

Aby zainstalować ten skrypt, wymagana jest instalacje jednego z następujących rozszerzeń: Tampermonkey, Violentmonkey.

Aby zainstalować ten skrypt, wymagana będzie instalacja rozszerzenia Tampermonkey lub Userscripts.

You will need to install an extension such as Tampermonkey to install this script.

Aby zainstalować ten skrypt, musisz zainstalować rozszerzenie menedżera skryptów użytkownika.

(Mam już menedżera skryptów użytkownika, pozwól mi to zainstalować!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

Będziesz musiał zainstalować rozszerzenie menedżera stylów użytkownika, aby zainstalować ten styl.

Będziesz musiał zainstalować rozszerzenie menedżera stylów użytkownika, aby zainstalować ten styl.

Musisz zainstalować rozszerzenie menedżera stylów użytkownika, aby zainstalować ten styl.

(Mam już menedżera stylów użytkownika, pozwól mi to zainstalować!)

// ==UserScript==
// @name         Taskonator - TOT Batch
// @name:pl      Taskonator - TOT Batch
// @namespace    http://tampermonkey.net/
// @version      4.0.0
// @description  Library - TOT Batch module for Taskonator with presets and Vue.js
// @description:pl  Biblioteka - Modul batch TOT z presetami dla Taskonatora
// @author       @sulkpiot
// @license      MIT
// ==/UserScript==
/* ================================================================
   TASKONATOR MODULE: TOT Batch v4.0
   Tabela batch TOT na stronach /employee/*
   Wymaga Vue.js 2.x (ładowanego przez Loader @require)
   ================================================================ */

(function () {
    'use strict';

    function waitForCore(cb) {
        if (window.Taskonator) return cb(window.Taskonator);
        let tries = 0;
        const iv = setInterval(() => {
            tries++;
            if (window.Taskonator) { clearInterval(iv); cb(window.Taskonator); }
            else if (tries > 60) { clearInterval(iv); console.error('[TOTBatch] Core not found'); }
        }, 250);
    }

    waitForCore(function (Core) {
        Core.ModuleRegistry.register('TOTBatch', initTOTBatch);
    });

    function initTOTBatch(Core) {

        const DEFAULT_PRESETS = [
            { name: "V-Returns Ship", processLabel: "V-Returns Support", functionName: "V-Returns Ship", color: "#3b82f6" },
            { name: "Lead/PA", processLabel: "Transfer Out Lead/PA", functionName: "Transfer Out Lead/PA", color: "#8b5cf6" },
            { name: "Janitorial", processLabel: "Facility", functionName: "Facility Janitorial", color: "#10b981" },
            { name: "UIS Waterspider", processLabel: "Container Move", functionName: "UIS Flat Waterspider", color: "#f59e0b" },
            { name: "Trans Out Overflow", processLabel: "Transfer Out", functionName: "Trans Out Overflow", color: "#ef4444" },
            { name: "Dock Crew", processLabel: "Transfer Out Dock", functionName: "TransferOut DockCrew", color: "#ec4899" }
        ];

        // ─── Inject CSS ──────────────────────────────────────────

        const stEl = document.createElement('style');
        stEl.textContent = `[v-cloak]{display:none}.tot-message{color:#2f855a;margin:5px 0;font-weight:bold;padding:5px 10px;border-radius:3px;background-color:#f0fff4;border:1px solid #c6f6d5}.tot-message2{color:#2a4365;margin:5px 0;font-weight:bold;font-size:14px;padding:5px 10px;background-color:#ebf8ff;border-radius:3px;border:1px solid #bee3f8}.preset-container{margin:8px 0;display:flex;flex-wrap:wrap;gap:5px;align-items:center;justify-content:center}.preset-btn{padding:6px 14px;border:1px solid #cbd5e0;border-radius:5px;background-color:#ebf8ff;color:#2a4365;font-weight:bold;font-size:13px;cursor:pointer;transition:all .15s ease}.preset-btn:hover{background-color:#bee3f8;border-color:#90cdf4;transform:translateY(-1px)}.preset-btn.active{color:white;box-shadow:0 2px 4px rgba(0,0,0,0.2)}.preset-label{font-size:12px;color:#718096;font-weight:bold;margin-right:5px}.save-preset-btn,.settings-btn{padding:6px 14px;border:2px dashed #a0aec0;border-radius:5px;background-color:#f7fafc;color:#718096;font-size:13px;cursor:pointer;transition:all .15s ease}.save-preset-btn:hover{border-color:#68d391;background-color:#f0fff4;color:#2f855a}.settings-btn{border-style:solid;border-color:#a0aec0;font-size:15px;padding:4px 10px}.settings-btn:hover{background-color:#edf2f7}.settings-overlay{position:fixed;top:0;left:0;right:0;bottom:0;background:rgba(0,0,0,0.5);z-index:99998;display:flex;align-items:center;justify-content:center}.settings-panel{background:white;border-radius:12px;box-shadow:0 20px 60px rgba(0,0,0,0.3);width:680px;max-height:85vh;overflow-y:auto;z-index:99999}.settings-header{background:#2d3748;color:white;padding:16px 24px;border-radius:12px 12px 0 0;display:flex;justify-content:space-between;align-items:center;position:sticky;top:0;z-index:1}.settings-header h3{margin:0;font-size:16px}.settings-close{background:none;border:none;color:white;font-size:22px;cursor:pointer;opacity:.7}.settings-close:hover{opacity:1}.settings-body{padding:20px 24px}.settings-section{margin-bottom:20px}.settings-section h4{margin:0 0 10px;font-size:13px;text-transform:uppercase;color:#718096;letter-spacing:.5px}.preset-row{display:flex;align-items:center;gap:8px;padding:8px 10px;border:1px solid #e2e8f0;border-radius:6px;margin-bottom:6px;background:#fafbfc}.preset-row input[type="text"]{padding:5px 8px;border:1px solid #cbd5e0;border-radius:4px;font-size:12px}.preset-row input[type="color"]{width:32px;height:28px;border:1px solid #cbd5e0;border-radius:4px;cursor:pointer}.preset-row .field-name{width:110px;font-weight:bold}.preset-row .field-proc{width:140px}.preset-row .field-func{width:150px}.preset-row-label{font-size:10px;color:#a0aec0;display:block}.move-btn,.del-btn{border:none;border-radius:4px;cursor:pointer;font-size:14px;width:28px;height:28px;display:flex;align-items:center;justify-content:center}.move-btn{background:#edf2f7;color:#4a5568}.move-btn:hover{background:#e2e8f0}.move-btn:disabled{opacity:.3}.del-btn{background:#fff5f5;color:#e53e3e}.del-btn:hover{background:#fed7d7}.settings-actions{display:flex;flex-wrap:wrap;gap:8px;padding:16px 24px;border-top:1px solid #e2e8f0;background:#f7fafc;border-radius:0 0 12px 12px}.action-btn{padding:8px 16px;border:1px solid #cbd5e0;border-radius:6px;font-size:12px;font-weight:bold;cursor:pointer;background:white;color:#4a5568}.action-btn:hover{background:#edf2f7}.action-btn.primary{background:#3b82f6;color:white;border-color:#2563eb}.action-btn.primary:hover{background:#2563eb}.action-btn.danger{color:#e53e3e;border-color:#feb2b2}.action-btn.danger:hover{background:#fff5f5}.action-spacer{flex-grow:1}.import-area{width:100%;height:80px;font-family:monospace;font-size:11px;padding:8px;border:1px solid #cbd5e0;border-radius:6px;margin-top:8px;resize:vertical;box-sizing:border-box}.import-area:focus{outline:none;border-color:#3b82f6}.preset-count{font-size:12px;color:#a0aec0;margin-left:8px}`;
        document.head.appendChild(stEl);

        // ─── Parse editable segments ─────────────────────────────

        var editables = document.querySelectorAll('.time-segment.editable');
        var ea = [];
        for (var i = 0; i < editables.length; i++) {
            var parent = editables[i].parentNode;
            if (parent && parent.hasAttribute("onclick") && parent.attributes.onclick.nodeValue.includes("firePopup")) {
                ea.push(editables[i]);
            }
        }

        var parseFire = function (args) {
            return eval(args.replace('firePopup(', '[').replace(');', ']').replace(/\t/g, '').replace(/\s/g, ''));
        };

        var totParams = ea.map(ed => parseFire(ed.parentNode.attributes.onclick.nodeValue));
        window.globalThat = { totParams };

        function sublist(code, cb) {
            jediClient.getAllLaborFunctionsForLaborProcessId({
                ServiceName: 'FCLMJobEntryDomainInformationService',
                data: { laborProcessId: code },
                Method: 'GetAllLaborFunctionsForLaborProcessId',
                success: cb
            });
        }
        window.globalThat.sublist = sublist;

        // ─── Mount Vue Instance ──────────────────────────────────

        var contentPanel = document.getElementById('content-panel') || document.body;
        var root = document.createElement('div');
        root.id = 'tot-batch-root';
        contentPanel.appendChild(root);

        window.vueInstance = new Vue({
            el: '#tot-batch-root',
            created() { this.updateTotalDuration(); this.loadPresets(); },
            data: {
                totParams: totParams.map(p => { let np = [...p]; np[6] = false; return np; }),
                processOptions: [], selectedLaborProcess: -1,
                functionOptions: [{ laborFunctionId: -1, laborFunctionName: 'Choose Function' }],
                selectedLaborFunction: -1, message: '', message2: '', submittedlist: [],
                now: Date.now(), lastCodedProcess: '', lastCodedFunction: '',
                loadLastCoded: false, presets: [], activePreset: null,
                showSettings: false, showImport: false, importText: '', editPresets: []
            },
            mounted() { this.$nextTick(() => { this.updateTotalDuration(); }); },
            watch: {
                selectedLaborProcess: { handler() { this.newSubList(); }, immediate: true },
                selectedLaborFunction: { handler() { const m2 = this.message2; this.$nextTick(() => { this.message2 = m2; }); } },
                totParams: { handler() { this.$nextTick(() => { this.updateTotalDuration(); }); }, deep: true, immediate: true }
            },
            computed: {
                displayMessage() { return this.message || ''; },
                displayMessage2() { return this.message2 || ''; },
                presetCount() { return this.presets.length; }
            },
            methods: {
                formatTaskName(n) { return n || ''; },
                safeSet(o, i, v) { if (o && typeof i !== 'undefined') this.$set(o, i, v); },
                formatDateTime(s) { if (!s || s.length === 0) return "(current)"; return s.substring(0, 10) + ' ' + s.substring(10); },
                handleCheckboxChange(r, c) { this.safeSet(this.totParams[r], 6, c); this.$nextTick(() => { this.updateTotalDuration(); }); },
                newSubList() {
                    if (window.location.pathname.includes("ppaTimeDetails")) {
                        if (typeof processes !== 'undefined') {
                            var sl = this.processOptions.filter(x => x.value == this.selectedLaborProcess)[0];
                            if (sl && processes[sl.label]) {
                                var fl = processes[sl.label].attributes.job_role.sort();
                                this.functionOptions = [this.functionOptions[0]];
                                fl.forEach(x => this.functionOptions.push({ laborFunctionId: x, laborFunctionName: x }));
                                if (this.loadLastCoded) { var ff = this.functionOptions.find(o => o.laborFunctionName === this.lastCodedFunction); if (ff) this.selectedLaborFunction = ff.laborFunctionId.toString(); this.loadLastCoded = false; }
                            }
                        }
                    } else {
                        this.seekFunctions(false);
                        return sublist(this.selectedLaborProcess, (result) => {
                            if (result && result.laborFunctions) { this.functionOptions = result.laborFunctions.sort((a, b) => (a.laborFunctionName > b.laborFunctionName) ? 1 : -1); this.seekFunctions(true); }
                        });
                    }
                },
                updateTotalDuration() { var total = 0; this.totParams.forEach(bar => { if (bar[6] === true || bar[6] === 'true' || bar[6] === 1) total += this.getDuration(bar[1], bar[3]); }); this.$set(this, 'message2', 'Total selected: ' + (Math.round(total * 10) / 10) + ' minutes'); },
                fireTots() { window.localStorage.setItem("totProcess", this.selectedLaborProcess); window.localStorage.setItem("totFunction", this.selectedLaborFunction); var ts = this.totParams.map((t, i) => ({ ...t, index: i })).filter(t => t[6] === true || t[6] === 'true'); if (this.selectedLaborProcess > 0 && (this.selectedLaborFunction.length == 0 || this.selectedLaborFunction > 0 || (window.location.pathname.includes("ppa") && this.selectedLaborFunction.length > 0))) { this.submitTot(ts); this.message = "Submitting batch..."; } else { this.message = "Check process and function options before submitting!"; } },
                submitTot(tots) {
                    const cp = this.selectedLaborProcess, cf = this.selectedLaborFunction;
                    const gv = (id, d) => { const el = document.getElementById(id); return el ? el.value : d; };
                    const fd = { empId: gv("employeeId", ""), whId: gv("warehouseId", ""), startDate: gv("startDate", ""), startHour: gv("startHour", ""), startMinute: gv("startMinute", ""), endDate: gv("endDate", ""), endHour: gv("endHour", ""), endMinute: gv("endMinute", "") };
                    tots.forEach(tot => {
                        var enc = encodeURIComponent; const pp = tot[4] || '', pr = tot[5] || '';
                        var line = "startDate=" + enc(fd.startDate) + "&startHour=" + enc(fd.startHour) + "&startMinute=" + enc(fd.startMinute) + "&endDate=" + enc(fd.endDate) + "&endHour=" + enc(fd.endHour) + "&endMinute=" + enc(fd.endMinute) + "&employeeId=" + enc(fd.empId) + "&warehouseId=" + enc(fd.whId) + "&newLaborProcessId=" + enc(cp) + "&newLaborFunctionId=" + enc(cf) + "&laborFuncStartTime=" + enc(tot[1]) + "&laborFuncEndTime=" + enc(tot[3]) + "&previousLaborProcess=" + enc(pp) + "&previousJobRole=" + enc(pr.replace(/ /g, '+'));
                        if (window.location.pathname.includes("ppa")) { line = line.replace("warehouseId", "oldWarehouseId"); var loc = line.search("&newLaborProcessId"); line = line.slice(0, loc) + "&warehouseId=" + enc(fd.whId) + line.slice(loc); loc = line.search("&newLaborFunctionId"); line = line.slice(0, loc) + "&newJobRole=" + cf.replace(/ /g, '+'); }
                        var form = document.querySelector('form'); var actionUrl = form ? form.action : window.location.href;
                        $.ajax({ url: actionUrl, type: 'POST', data: line, success: (response) => { const index = tot.index !== undefined ? tot.index : this.totParams.indexOf(tot); this.processResponse(response, index, cp, cf); }, error: (x, s, error) => { this.message = "Error: " + error; } });
                    });
                },
                loadLastCodedBar() {
                    try {
                        var es = Array.from(document.querySelectorAll('.time-segment.editable'));
                        if (es.length > 0) {
                            var segs = es.slice(0, -1); var edited = segs.filter(s => s.closest('.function-seg.edited') !== null);
                            if (edited.length > 0) {
                                var ts = edited[edited.length - 1];
                                if (ts && ts.parentNode) {
                                    const oa = ts.parentNode.getAttribute('onclick');
                                    if (oa) {
                                        const params = this.parseFire(oa);
                                        if (params && params.length >= 6) {
                                            const nlcp = params[4].replace(/\s+/g, ''), nlcf = params[5].replace(/\s+/g, '');
                                            const po = this.processOptions.find(p => p.label.replace(/\s+/g, '') === nlcp);
                                            if (po) {
                                                this.selectedLaborProcess = po.value;
                                                setTimeout(() => { const fo = this.functionOptions.find(f => f.laborFunctionName.replace(/\s+/g, '') === nlcf); if (fo) { this.selectedLaborFunction = fo.laborFunctionId; this.message = 'Loaded: ' + po.label + ' - ' + fo.laborFunctionName; } else { this.message = "Found process but couldn't match function"; } }, 500);
                                            } else { this.message = "Couldn't match process name"; }
                                        }
                                    }
                                }
                            } else { this.message = "No previously edited tasks found"; }
                        }
                    } catch (e) { this.message = "Error loading previous task"; }
                },
                processResponse(response, totIndex, procId, funcId) {
                    try {
                        if (totIndex === undefined || !this.totParams[totIndex]) totIndex = this.totParams.findIndex(t => t[6] === true || t[6] === 'true');
                        if (totIndex === -1 || totIndex === undefined) { this.message = "Task updated successfully"; return; }
                        var po = this.processOptions.find(x => x.value == procId), fo = this.functionOptions.find(x => x.laborFunctionId == Number(funcId));
                        if (po && fo) {
                            this.$set(this.totParams[totIndex], 4, po.label); this.$set(this.totParams[totIndex], 5, fo.laborFunctionName);
                            if (!this.submittedlist.includes(totIndex)) this.submittedlist.push(totIndex);
                            const m2 = this.message2; this.$nextTick(() => { this.message2 = m2; });
                            setTimeout(() => { const cb = document.querySelector('.ui-dialog-titlebar-close'); if (cb) cb.click(); }, 1000);
                            this.message = "Task updated successfully"; setTimeout(() => { window.location.reload(); }, 2000);
                        } else { this.message = "Task updated successfully"; }
                    } catch (e) { this.message = "Task updated successfully"; setTimeout(() => { window.location.reload(); }, 2000); }
                },
                seekFunctions(isDone) { if (!isDone) { this.functionOptions = [{ laborFunctionId: -1, laborFunctionName: '-= Getting New Functions =-' }]; this.message = "Getting Functions..."; } else { this.message = ""; var sf = null; if (!this.loadLastCoded) { if (this.selectedLaborProcess == window.localStorage.getItem("totProcess")) sf = window.localStorage.getItem("totFunction"); } else { var fo = this.functionOptions.find(o => o.laborFunctionName === this.lastCodedFunction); if (fo) sf = fo.laborFunctionId.toString(); } if (sf != null) this.selectedLaborFunction = sf; this.loadLastCoded = false; } },
                getDuration(d1, d2) { d1 = new Date(d1); d2 = d2.length > 0 ? new Date(d2) : new Date(this.now); return Math.abs((d2 - d1) / 60000); },
                allTotDuration() { var total = 0; this.totParams.forEach(bar => (total += this.getDuration(bar[1], bar[3]))); return total > 0 ? " " + (Math.round(total * 10) / 10) + "m in " + this.totParams.length + " bars:" : " No editable time."; },
                selectAllBars() { this.totParams.forEach((t, i) => { this.$set(this.totParams[i], 6, true); }); this.$nextTick(() => { this.updateTotalDuration(); }); },
                parseFire(args) { return eval(args.replace('firePopup(', '[').replace(');', ']').replace(/\t/g, '').replace(/\s/g, '')); },
                loadPresets() { var saved = JSON.parse(window.localStorage.getItem("totPresets") || "null"); this.presets = saved || JSON.parse(JSON.stringify(DEFAULT_PRESETS)); },
                savePresets() { window.localStorage.setItem("totPresets", JSON.stringify(this.presets)); },
                applyPreset(preset) {
                    this.activePreset = preset.name; this.message = "\u23f3 Loading: " + preset.name + "...";
                    var np = preset.processLabel.replace(/\s+/g, '').toLowerCase();
                    var po = this.processOptions.find(p => p.label.replace(/\s+/g, '').toLowerCase() === np);
                    if (!po) { this.message = "\u274c Process not found: " + preset.processLabel; this.activePreset = null; return; }
                    this.selectedLaborProcess = po.value;
                    var wf = (retries) => { retries = retries || 0; setTimeout(() => { var nf = preset.functionName.replace(/\s+/g, '').toLowerCase(); var fo = this.functionOptions.find(f => f.laborFunctionName.replace(/\s+/g, '').toLowerCase() === nf); if (fo) { this.selectedLaborFunction = fo.laborFunctionId; this.message = "\u2705 " + preset.name + " ready!"; } else if (retries < 15) { wf(retries + 1); } else { this.message = "\u26a0\ufe0f Function '" + preset.functionName + "' not found."; } }, 300); }; wf(0);
                },
                saveCurrentAsPreset() { if (this.selectedLaborProcess == -1 || this.selectedLaborFunction == -1) { this.message = "Select a process and function first!"; return; } var po = this.processOptions.find(x => x.value == this.selectedLaborProcess); var fo = this.functionOptions.find(x => x.laborFunctionId == this.selectedLaborFunction); if (!po || !fo) { this.message = "Could not identify current selection."; return; } var exists = this.presets.find(p => p.processLabel.replace(/\s+/g, '').toLowerCase() === po.label.replace(/\s+/g, '').toLowerCase() && p.functionName.replace(/\s+/g, '').toLowerCase() === fo.laborFunctionName.replace(/\s+/g, '').toLowerCase()); if (exists) { this.message = "Preset '" + exists.name + "' already exists!"; return; } var name = prompt("Enter button name:", fo.laborFunctionName); if (!name) return; var colors = ["#3b82f6", "#10b981", "#f59e0b", "#ef4444", "#8b5cf6", "#ec4899", "#06b6d4", "#84cc16"]; this.presets.push({ name, processLabel: po.label, functionName: fo.laborFunctionName, color: colors[this.presets.length % colors.length] }); this.savePresets(); this.message = "\u2705 Preset '" + name + "' saved!"; },
                openSettings() { this.editPresets = JSON.parse(JSON.stringify(this.presets)); this.showImport = false; this.importText = ""; this.showSettings = true; },
                closeSettings() { this.showSettings = false; },
                saveSettings() { this.presets = JSON.parse(JSON.stringify(this.editPresets)); this.savePresets(); this.showSettings = false; this.message = "\u2705 Settings saved!"; },
                addEmptyPreset() { var colors = ["#3b82f6", "#10b981", "#f59e0b", "#ef4444", "#8b5cf6", "#ec4899", "#06b6d4", "#84cc16"]; this.editPresets.push({ name: "New Preset", processLabel: "", functionName: "", color: colors[this.editPresets.length % colors.length] }); },
                removeEditPreset(index) { this.editPresets.splice(index, 1); },
                movePreset(index, direction) { var ni = index + direction; if (ni < 0 || ni >= this.editPresets.length) return; var temp = this.editPresets.splice(index, 1)[0]; this.editPresets.splice(ni, 0, temp); },
                resetToDefaults() { if (!confirm("Reset all presets to defaults?")) return; this.editPresets = JSON.parse(JSON.stringify(DEFAULT_PRESETS)); },
                exportPresets() { this.importText = JSON.stringify(this.editPresets, null, 2); this.showImport = true; this.$nextTick(() => { var ta = document.querySelector('.import-area'); if (ta) { ta.select(); document.execCommand('copy'); } }); this.message = "\ud83d\udccb Presets copied!"; },
                toggleImport() { this.showImport = !this.showImport; if (this.showImport) this.importText = ""; },
                doImport() { try { var parsed = JSON.parse(this.importText); if (!Array.isArray(parsed)) throw new Error("Not an array"); parsed.forEach(p => { if (!p.name || !p.processLabel || !p.functionName) throw new Error("Missing fields"); if (!p.color) p.color = "#3b82f6"; }); this.editPresets = parsed; this.showImport = false; this.message = "\u2705 Imported " + parsed.length + " presets."; } catch (e) { alert("Invalid JSON: " + e.message); } }
            },

            template: `<div v-cloak><div><h3>Hi!{{allTotDuration()}}</h3><table align="center"><tbody><tr v-for="(tot,totRow) in totParams" :key="totRow"><td v-for="(attr,index) of tot" :key="index"><template v-if="index==6"><input type="checkbox" :checked="Boolean(tot[index])" @change="handleCheckboxChange(totRow,$event.target.checked)"></template><template v-else-if="index>6"></template><template v-else-if="index==0||index==2">{{formatDateTime(tot[index])}}</template><template v-else-if="index==1"></template><template v-else-if="index==3">{{Math.round(getDuration(tot[1],tot[3])).toString()+"m"}}</template><template v-else-if="index==4||index==5">{{formatTaskName(attr)}}</template><template v-else>{{attr}}</template></td></tr></tbody></table><div class="preset-container"><span class="preset-label">\u26a1 Quick Tasks:</span><button v-for="(preset,pIdx) in presets" :key="'p-'+pIdx" class="preset-btn" :class="{active:activePreset===preset.name}" :style="activePreset===preset.name?'background-color:'+preset.color+';border-color:'+preset.color+';color:white;':'border-left:3px solid '+preset.color" :title="preset.processLabel+' \\u2192 '+preset.functionName" @click="applyPreset(preset)">{{preset.name}}</button><button class="save-preset-btn" @click="saveCurrentAsPreset()" title="Save current selection as preset">+ Save Current</button><button class="settings-btn" @click="openSettings()" title="Customize presets">\u2699\ufe0f</button></div><div><select v-model="selectedLaborProcess"><option v-for="process in processOptions" :value="process.value" :key="process.value">{{process.label}}</option></select><select v-model="selectedLaborFunction"><option v-for="func in functionOptions" :value="func.laborFunctionId" :key="func.laborFunctionId">{{func.laborFunctionName}}</option></select></div><button @click="selectAllBars()">Select All</button><button @click="loadLastCodedBar()">Prev. Task</button><button @click="fireTots()">Submit</button><div class="tot-summary-section"><div class="tot-message" v-if="displayMessage">{{displayMessage}}</div><div class="tot-message2" v-if="displayMessage2">{{displayMessage2}}</div></div><div class="settings-overlay" v-if="showSettings" @click.self="closeSettings()"><div class="settings-panel"><div class="settings-header"><h3>\u2699\ufe0f Preset Settings <span class="preset-count">({{editPresets.length}} presets)</span></h3><button class="settings-close" @click="closeSettings()">\u2715</button></div><div class="settings-body"><div class="settings-section"><h4>Your Presets</h4><div v-if="editPresets.length===0" style="color:#a0aec0;text-align:center;padding:20px;">No presets yet.</div><div v-for="(ep,epIdx) in editPresets" :key="'ep-'+epIdx" class="preset-row"><input type="color" v-model="ep.color" title="Button color"><div><span class="preset-row-label">Name</span><input type="text" v-model="ep.name" class="field-name" placeholder="Button name"></div><div><span class="preset-row-label">Process</span><input type="text" v-model="ep.processLabel" class="field-proc" placeholder="Process name"></div><div><span class="preset-row-label">Function</span><input type="text" v-model="ep.functionName" class="field-func" placeholder="Function name"></div><button class="move-btn" :disabled="epIdx===0" @click="movePreset(epIdx,-1)" title="Move up">\u25b2</button><button class="move-btn" :disabled="epIdx===editPresets.length-1" @click="movePreset(epIdx,1)" title="Move down">\u25bc</button><button class="del-btn" @click="removeEditPreset(epIdx)" title="Delete">\ud83d\uddd1</button></div></div><div class="settings-section" v-if="showImport"><h4>\ud83d\udccb Import / Export JSON</h4><textarea class="import-area" v-model="importText" placeholder="Paste JSON here..."></textarea><div style="margin-top:8px;"><button class="action-btn primary" @click="doImport()">\ud83d\udce5 Import</button></div></div></div><div class="settings-actions"><button class="action-btn success" @click="addEmptyPreset()">+ Add Preset</button><button class="action-btn" @click="exportPresets()">\ud83d\udce4 Export</button><button class="action-btn" @click="toggleImport()">\ud83d\udce5 Import</button><button class="action-btn danger" @click="resetToDefaults()">\ud83d\udd04 Reset</button><span class="action-spacer"></span><button class="action-btn" @click="closeSettings()">Cancel</button><button class="action-btn primary" @click="saveSettings()">\ud83d\udcbe Save</button></div></div></div></div></div>`
        }).$mount(root);

        // ─── Populate process options from existing select ───────

        var processSelect = document.getElementById('newLaborProcessId') || document.querySelector('select[name="newLaborProcessId"]') || document.querySelector('select');
        if (processSelect && processSelect.options) {
            window.vueInstance.processOptions = Array.from(processSelect.options).map(o => ({ value: o.value, label: o.text }));
        } else {
            window.vueInstance.processOptions = [{ value: -1, label: 'Choose Process' }];
        }

        var sp = window.localStorage.getItem("totProcess");
        if (sp != null) window.vueInstance.selectedLaborProcess = sp;

        // ─── Update title with total duration ────────────────────

        var total = 0;
        var titleEls = document.getElementsByClassName("title");
        var title = titleEls.length > 0 ? titleEls[0] : document.querySelector('h1,h2,h3') || document.querySelector('.ganttChart');
        if (title) {
            window.vueInstance.totParams.forEach(bar => (total += window.vueInstance.getDuration(bar[1], bar[3])));
            if (title.innerText) title.innerText = title.innerText + "(" + Math.round(total) + "m)";
        }

        console.log('\u2705 TOT Batch module initialized');

    } // end initTOTBatch

})();