Taskonator - Panel

Library - Side panel module for Taskonator with task management

Ce script ne devrait pas être installé directement. C'est une librairie créée pour d'autres scripts. Elle doit être inclus avec la commande // @require https://update.greasyfork.org/scripts/575278/1807561/Taskonator%20-%20Panel.js

Vous devrez installer une extension telle que Tampermonkey, Greasemonkey ou Violentmonkey pour installer ce script.

Vous devrez installer une extension telle que Tampermonkey pour installer ce script.

Vous devrez installer une extension telle que Tampermonkey ou Violentmonkey pour installer ce script.

Vous devrez installer une extension telle que Tampermonkey ou Userscripts pour installer ce script.

Vous devrez installer une extension telle que Tampermonkey pour installer ce script.

Vous devrez installer une extension de gestionnaire de script utilisateur pour installer ce script.

(J'ai déjà un gestionnaire de scripts utilisateur, laissez-moi l'installer !)

Vous devrez installer une extension telle que Stylus pour installer ce style.

Vous devrez installer une extension telle que Stylus pour installer ce style.

Vous devrez installer une extension telle que Stylus pour installer ce style.

Vous devrez installer une extension du gestionnaire de style pour utilisateur pour installer ce style.

Vous devrez installer une extension du gestionnaire de style pour utilisateur pour installer ce style.

Vous devrez installer une extension du gestionnaire de style pour utilisateur pour installer ce style.

(J'ai déjà un gestionnaire de style utilisateur, laissez-moi l'installer!)

// ==UserScript==
// @name         Taskonator - Panel
// @name:pl      Taskonator - Panel
// @namespace    http://tampermonkey.net/
// @version      4.0.0
// @description  Library - Side panel module for Taskonator with task management
// @description:pl  Biblioteka - Modul panelu bocznego z taskami dla Taskonatora
// @author       @sulkpiot
// @license      MIT
// ==/UserScript==

(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('[TaskonatorPanel] Core not found'); }
        }, 250);
    }

    waitForCore(function (Core) {
        Core.ModuleRegistry.register('TaskonatorPanel', initTaskonatorPanel);
    });

    function initTaskonatorPanel(Core) {
        const { Toast, Theme, THEMES, GM_xmlhttpRequest, getWarehouseIdFromURL } = Core;

        const CONFIG = {
            WAREHOUSE_ID: getWarehouseIdFromURL(),
            API_URL: 'https://fcmenu-dub-regionalized.corp.amazon.com/do/laborTrackingKiosk',
            REQUEST_TIMEOUT: 10000, MAX_HISTORY: 10, TOAST_DURATION: 3000,
            NOTES_SAVE_DELAY: 500, PANEL_WIDTH: 290, RELOAD_DELAY: 3
        };

        const SK = {
            TASKS: 'taskonator_tasks_v4', THEME: 'taskonator_theme',
            LAST_TASK: 'taskonator_lastTask', LAST_TIME: 'taskonator_lastTime',
            HISTORY: 'taskonator_history', COLLAPSED: 'taskonator_collapsed',
            CONFIRM: 'taskonator_confirm', NOTES: 'taskonator_notes'
        };

        const S = {
            get(k, fb = null) { try { const d = localStorage.getItem(k); return d ? JSON.parse(d) : fb; } catch { return fb; } },
            set(k, v) { try { localStorage.setItem(k, JSON.stringify(v)); } catch (e) { console.warn('[Taskonator] localStorage full', e); } }
        };

        function empId() {
            const el = document.getElementById('employeeId');
            if (el?.value) return el.value.trim();
            return new URLSearchParams(window.location.search).get('employeeId') || null;
        }
        function whId() { return getWarehouseIdFromURL(); }

        // ─── Task Manager ────────────────────────────────────────

        const TM = (() => {
            let sec = {};
            const DEF = {
                'Lead': [{ n: 'LSHIP', c: 'LSHIP' }, { n: 'SHIPCL', c: 'SHIPCL' }, { n: 'TSO Clerk', c: 'SHPCL' }, { n: 'FACJAN', c: 'FACJAN' }],
                'PPJ': [{ n: 'OVRFLW', c: 'OVRFLW' }, { n: 'DOCKPL', c: 'DOCKPL' }],
                'Ship': [{ n: 'UISFLTWR', c: 'UISFLTWR' }, { n: 'PRGSHIP', c: 'PRGSHIP' }, { n: 'Offline Mods', c: 'OFFMODXO' }],
                'TSO': [{ n: 'TRFOCR', c: 'TRFOCR' }, { n: 'TSO Clerk', c: 'SHPCL' }],
                'Crits': [{ n: 'TOTOL', c: 'TOTOL' }, { n: 'ROPER', c: 'ROPER' }, { n: 'ROBWS', c: 'ROBWS' }, { n: 'TOWTSP', c: 'TOWTSP' }],
                'STOPY': [{ n: 'MSTOP', c: 'MSTOP' }, { n: 'ISTOP', c: 'ISTOP' }],
                'Spotkania': [{ n: 'EMP', c: 'OPSEMPENG' }, { n: 'BHP', c: 'SFTDRLL' }],
                'X-TRAIN': [{ n: 'Instruktor', c: 'SAMB' }, { n: 'Trening', c: 'SHPTR' }]
            };
            function init() { sec = S.get(SK.TASKS, DEF); }
            function all() { return sec; }
            function save() { S.set(SK.TASKS, sec); }
            function flat() { return Object.entries(sec).flatMap(([s, ts]) => ts.map(t => ({ ...t, section: s }))); }
            function addSec(name) { const nm = name.trim().replace(/[<>]/g, ''); if (nm.length < 2 || nm.length > 30) throw new Error('2-30'); if (sec[nm]) throw new Error('Istnieje'); sec[nm] = []; save(); }
            function rmSec(name) { if (!sec[name]) throw new Error('Brak'); delete sec[name]; save(); }
            function renameSec(o, n) { const nm = n.trim().replace(/[<>]/g, ''); if (nm.length < 2 || nm.length > 30) throw new Error('2-30'); if (!sec[o]) throw new Error('Brak'); if (o !== nm && sec[nm]) throw new Error('Duplikat'); if (o === nm) return; const e = Object.entries(sec); const ns = {}; e.forEach(([k, v]) => { ns[k === o ? nm : k] = v; }); sec = ns; save(); }
            function addTask(s, name, code) { if (!sec[s]) throw new Error('Brak'); const nm = name.trim().replace(/[<>]/g, ''), cd = code.trim().toUpperCase(); if (nm.length < 2 || nm.length > 50) throw new Error('2-50'); if (!/^[A-Z0-9]{2,20}$/.test(cd)) throw new Error('Kod'); if (sec[s].some(t => t.c === cd || t.n === nm)) throw new Error('Duplikat'); sec[s].push({ n: nm, c: cd }); save(); }
            function rmTask(s, i) { if (!sec[s]?.[i]) throw new Error('Brak'); sec[s].splice(i, 1); save(); }
            function updTask(s, i, name, code) { if (!sec[s]?.[i]) throw new Error('Brak'); sec[s][i] = { n: name.trim().replace(/[<>]/g, ''), c: code.trim().toUpperCase() }; save(); }
            function reorder(s, from, to) { if (!sec[s]) return; const t = sec[s].splice(from, 1)[0]; sec[s].splice(to, 0, t); save(); }
            function exprt() { return JSON.stringify(sec, null, 2); }
            function imprt(str) { try { sec = JSON.parse(str); save(); return true; } catch { return false; } }
            function hist(code) { const h = S.get(SK.HISTORY, []); h.unshift({ c: code, t: Date.now() }); S.set(SK.HISTORY, h.slice(0, CONFIG.MAX_HISTORY)); }
            return { init, all, save, flat, addSec, rmSec, renameSec, addTask, rmTask, updTask, reorder, exprt, imprt, hist };
        })();

        const Col = { is(n) { return (S.get(SK.COLLAPSED, {}))[n] === true; }, tog(n) { const s = S.get(SK.COLLAPSED, {}); s[n] = !s[n]; S.set(SK.COLLAPSED, s); } };
        const Confirm = { on() { return S.get(SK.CONFIRM, false); }, tog() { const v = !this.on(); S.set(SK.CONFIRM, v); return v; } };
        const Notes = {
            get(id) { return (S.get(SK.NOTES, {}))[id] || { text: '', at: null }; },
            save(id, txt) { const a = S.get(SK.NOTES, {}); a[id] = { text: txt, at: Date.now() }; S.set(SK.NOTES, a); },
            del(id) { const a = S.get(SK.NOTES, {}); delete a[id]; S.set(SK.NOTES, a); }
        };

        // ─── Confirm dialog ──────────────────────────────────────

        function ask(msg, onYes) {
            const t = Theme.t();
            const ov = document.createElement('div');
            Object.assign(ov.style, { position: 'fixed', inset: '0', background: 'rgba(0,0,0,0.35)', zIndex: '99998', display: 'flex', alignItems: 'center', justifyContent: 'center' });
            ov.innerHTML = `<div style="background:${t.bg};color:${t.text};padding:28px 34px;border-radius:14px;box-shadow:${t.dropShadow};text-align:center;min-width:260px;border:1px solid ${t.border};font-family:system-ui,sans-serif"><p style="margin:0 0 22px;font-size:14px;font-weight:600;line-height:1.4">${msg}</p><div style="display:flex;gap:10px;justify-content:center"><button id="_dy" style="padding:9px 28px;border:none;border-radius:8px;background:${t.orange};color:#fff;font-weight:700;cursor:pointer;font-size:13px">Tak</button><button id="_dn" style="padding:9px 28px;border:none;border-radius:8px;background:${t.bgCard};color:${t.text};font-weight:600;cursor:pointer;font-size:13px;border:1px solid ${t.border}">Nie</button></div></div>`;
            document.body.appendChild(ov);
            ov.querySelector('#_dy').onclick = () => { ov.remove(); onYes?.(); };
            ov.querySelector('#_dn').onclick = () => ov.remove();
            ov.onclick = e => { if (e.target === ov) ov.remove(); };
        }

        // ─── API (task submission) ───────────────────────────────

        const API = {
            _reloadTimer: null,
            _getBadgeId() {
                try { const el = document.getElementsByClassName("list-side-by-side")[0]?.children[5]; if (el && el.innerText && el.innerText.trim()) return el.innerText.trim(); } catch { }
                try { const d = document.querySelector('.debug p'); if (d && d.textContent.trim()) return d.textContent.trim(); } catch { }
                const inp = document.getElementById('employeeId');
                if (inp && inp.value) return inp.value.trim();
                return new URLSearchParams(window.location.search).get('employeeId') || null;
            },
            send(code, btnEl) {
                const badgeId = this._getBadgeId();
                if (!badgeId) { Toast.show('Brak Badge ID!', 'error'); return; }
                if (btnEl) {
                    btnEl.dataset.origText = btnEl.textContent; btnEl.textContent = '\u23f3 ...';
                    const th = Theme.t();
                    btnEl.style.background = th.sendingBg; btnEl.style.borderColor = th.sendingBorder;
                    btnEl.style.color = th.sendingText; btnEl.style.pointerEvents = 'none';
                    btnEl.style.animation = 'tsk-pulse 1s ease infinite';
                }
                const self = this;
                GM_xmlhttpRequest({
                    method: "POST", url: CONFIG.API_URL,
                    headers: { "Content-Type": "application/x-www-form-urlencoded" },
                    data: `warehouseId=${whId()}&calmCode=${code}&trackingBadgeId=${badgeId}`,
                    timeout: CONFIG.REQUEST_TIMEOUT,
                    onload(r) {
                        if (r.status >= 200 && r.status < 300) {
                            TM.hist(code); S.set(SK.LAST_TASK, code); S.set(SK.LAST_TIME, Date.now());
                            if (btnEl) { btnEl.textContent = '\u2705 OK'; const t = Theme.t(); btnEl.style.background = t.successBg; btnEl.style.borderColor = t.successBorder; btnEl.style.color = t.successText; btnEl.style.animation = 'none'; }
                            self._showConfirmBar(code);
                        } else { self._resetBtn(btnEl); Toast.show('HTTP ' + r.status, 'error'); }
                    },
                    onerror() { self._resetBtn(btnEl); Toast.show('B\u0142\u0105d', 'error'); },
                    ontimeout() { self._resetBtn(btnEl); Toast.show('Timeout', 'warning'); }
                });
            },
            _resetBtn(b) {
                if (!b) return; const t = Theme.t();
                b.textContent = b.dataset.origText || '?';
                b.style.background = t.taskBg; b.style.borderColor = t.taskBorder;
                b.style.color = t.textSec; b.style.pointerEvents = ''; b.style.animation = 'none';
            },
            _showConfirmBar(code) {
                const old = document.getElementById('tsk-confirm-bar'); if (old) old.remove();
                if (this._reloadTimer) clearInterval(this._reloadTimer);
                const t = Theme.t(); let cd = CONFIG.RELOAD_DELAY;
                const bar = document.createElement('div'); bar.id = 'tsk-confirm-bar';
                Object.assign(bar.style, {
                    position: 'absolute', bottom: '0', left: '0', right: '0', zIndex: '1000',
                    padding: '12px 16px', background: t.confirmBarBg, borderTop: '1px solid ' + t.confirmBarBorder,
                    display: 'flex', justifyContent: 'space-between', alignItems: 'center',
                    fontFamily: 'system-ui,sans-serif', fontSize: '12px', color: t.confirmBarText
                });
                bar.innerHTML = `<div><div style="font-weight:700">\u2705 Zataskowany: ${code}</div><div id="tsk-countdown" style="font-size:11px;opacity:.8;margin-top:2px">Od\u015bwie\u017cenie za ${cd}s...</div></div><button id="tsk-cancel-reload" style="background:none;border:1px solid ${t.confirmBarBorder};border-radius:6px;padding:4px 12px;cursor:pointer;font-size:11px;color:${t.confirmBarText};font-family:inherit;font-weight:600">Cofnij?</button>`;
                const panel = document.getElementById('tsk-panel'); if (panel) panel.appendChild(bar);
                const cdEl = bar.querySelector('#tsk-countdown'); const self = this;
                this._reloadTimer = setInterval(() => {
                    cd--;
                    if (cd <= 0) { clearInterval(self._reloadTimer); location.reload(); }
                    else { cdEl.textContent = `Od\u015bwie\u017cenie za ${cd}s...`; }
                }, 1000);
                bar.querySelector('#tsk-cancel-reload').onclick = () => {
                    clearInterval(self._reloadTimer); bar.remove();
                    Toast.show('Anulowano', 'info');
                    document.querySelectorAll('.tsk-btn').forEach(b => { API._resetBtn(b); });
                };
            },
            go(code, btnEl) {
                if (Confirm.on()) { ask(`Wys\u0142a\u0107 task ${code}?`, () => { this.send(code, btnEl); }); }
                else { this.send(code, btnEl); }
            }
        };

        // ─── CSS ─────────────────────────────────────────────────

        function injectCSS() {
            if (document.getElementById('tsk-css')) return;
            const s = document.createElement('style'); s.id = 'tsk-css';
            s.textContent = `#main-panel{position:relative!important;overflow:visible!important}#tsk-tab{position:absolute;top:50%;left:0;z-index:998;border:none;border-radius:0 10px 10px 0;padding:16px 9px;cursor:pointer;font-size:12px;writing-mode:vertical-rl;text-orientation:mixed;letter-spacing:2.5px;font-weight:800;font-family:system-ui,-apple-system,sans-serif;box-shadow:2px 0 12px rgba(0,0,0,.12);transition:all .3s cubic-bezier(.4,0,.2,1);transform:translateY(-50%);text-transform:uppercase}#tsk-tab:hover{padding-left:14px}#tsk-tab.open{left:${CONFIG.PANEL_WIDTH}px}#tsk-panel{position:absolute;top:0;left:0;width:${CONFIG.PANEL_WIDTH}px;height:100%;z-index:999;display:flex;flex-direction:column;transform:translateX(-100%);transition:transform .3s cubic-bezier(.4,0,.2,1);font-family:system-ui,-apple-system,'Segoe UI',sans-serif;font-size:13px;overflow:hidden}#tsk-panel.open{transform:translateX(0)}#tsk-panel *{box-sizing:border-box}#tsk-body{flex:1;overflow-y:auto;scrollbar-width:thin}#tsk-body::-webkit-scrollbar{width:4px}#tsk-body::-webkit-scrollbar-thumb{background:#aaa;border-radius:2px}.tsk-btn{border:none;border-radius:20px;padding:6px 14px;cursor:pointer;font-size:11.5px;font-weight:500;font-family:inherit;transition:all .2s ease;white-space:nowrap}.tsk-btn:hover{transform:translateY(-1px);color:#fff!important}.tsk-btn:active{transform:translateY(0)}.tsk-sec{display:flex;align-items:center;gap:5px;padding:6px 2px;cursor:pointer;user-select:none;transition:opacity .15s}.tsk-sec:hover{opacity:.7}#tsk-search{border:none;border-radius:8px;padding:8px 12px 8px 32px;font-size:12px;font-family:inherit;width:100%;outline:none;transition:box-shadow .2s ease}#tsk-search:focus{box-shadow:0 0 0 2px rgba(255,153,0,.4)}#tsk-search::placeholder{font-style:italic}.tsk-note{border-radius:8px;resize:vertical;font-family:inherit;font-size:12px;padding:8px 10px;width:100%;min-height:50px;outline:none;transition:border-color .2s}.tsk-note:focus{border-color:#ff9900!important}#tsk-drop{position:absolute;top:46px;left:12px;border-radius:10px;padding:4px 0;min-width:180px;z-index:10;opacity:0;transform:translateY(-8px) scale(.96);pointer-events:none;transition:all .2s cubic-bezier(.4,0,.2,1)}#tsk-drop.show{opacity:1;transform:translateY(0) scale(1);pointer-events:auto}.tsk-mi{display:flex;align-items:center;gap:10px;padding:9px 16px;cursor:pointer;border:none;background:none;width:100%;text-align:left;font-size:12px;font-family:inherit;border-radius:0;transition:background .12s}.tsk-mtab{padding:6px 14px;border-radius:8px;border:none;cursor:pointer;font-size:11px;font-weight:600;font-family:inherit;transition:all .15s;white-space:nowrap}.tsk-mtab:hover{opacity:.8}.tsk-mtab-add{padding:6px 10px;border-radius:8px;cursor:pointer;font-size:13px;font-family:inherit;transition:all .15s}.tsk-mtab-add:hover{opacity:.7}@keyframes tsk-pulse{0%,100%{opacity:1}50%{opacity:.6}}.tsk-drag-row{transition:transform .15s ease,opacity .15s ease}.tsk-drag-row.dragging{opacity:.5;background:transparent!important}.tsk-drag-handle{cursor:grab;color:#d1d5db;font-size:14px;padding:4px;user-select:none;touch-action:none}.tsk-drag-handle:active{cursor:grabbing}`;
            document.head.appendChild(s);
        }

        // ─── UI Builder ──────────────────────────────────────────

        const UI = (() => {
            let isOpen = false, notesOn = false, menuOn = false, searchQ = '';

            function toggle() {
                isOpen = !isOpen;
                document.getElementById('tsk-panel')?.classList.toggle('open', isOpen);
                document.getElementById('tsk-tab')?.classList.toggle('open', isOpen);
            }

            function build() {
                document.getElementById('tsk-tab')?.remove();
                document.getElementById('tsk-panel')?.remove();
                const mp = document.getElementById('main-panel'); if (!mp) return;
                const t = Theme.t(), eid = empId(), confOn = Confirm.on();

                const tab = document.createElement('button'); tab.id = 'tsk-tab';
                tab.textContent = '\u26a1 TASKONATOR';
                tab.style.background = t.tabBg; tab.style.color = t.tabText;
                if (isOpen) tab.classList.add('open'); mp.appendChild(tab);

                const pan = document.createElement('div'); pan.id = 'tsk-panel';
                pan.style.background = t.bg; pan.style.borderRight = '1px solid ' + t.border;
                pan.style.boxShadow = '4px 0 20px rgba(0,0,0,0.08)'; pan.style.color = t.text;
                if (isOpen) pan.classList.add('open');

                let html = `<div style="position:relative;padding:13px 16px;border-bottom:1px solid ${t.border};display:flex;justify-content:space-between;align-items:center;flex-shrink:0;background:${t.bg}"><div style="display:flex;align-items:baseline;gap:6px"><span style="font-size:14px;font-weight:800;color:${t.accent};letter-spacing:.3px">Taskonator</span></div><button id="tsk-ham" style="background:none;border:1px solid ${t.border};border-radius:6px;padding:4px 9px;cursor:pointer;font-size:14px;color:${t.textSec};line-height:1" title="Menu">\u2630</button><div id="tsk-drop" style="background:${t.dropBg};border:1px solid ${t.border};box-shadow:${t.dropShadow}"><button class="tsk-mi" id="_m_theme" style="color:${t.text}"><span style="width:18px;text-align:center">${Theme._c === 'light' ? '\ud83c\udf19' : '\u2600\ufe0f'}</span>Zmie\u0144 motyw</button><button class="tsk-mi" id="_m_lock" style="color:${confOn ? t.red : t.text}"><span style="width:18px;text-align:center">${confOn ? '\ud83d\udd12' : '\ud83d\udd13'}</span>${confOn ? 'Potw. W\u0141' : 'Potw. WY\u0141'}</button><button class="tsk-mi" id="_m_notes" style="color:${t.text}"><span style="width:18px;text-align:center">\ud83d\udcdd</span>Notatki</button><div style="height:1px;background:${t.border};margin:4px 12px"></div><button class="tsk-mi" id="_m_edit" style="color:${t.text}"><span style="width:18px;text-align:center">\u270f\ufe0f</span>Edytuj taski</button><button class="tsk-mi" id="_m_exp" style="color:${t.text}"><span style="width:18px;text-align:center">\ud83d\udcbe</span>Eksportuj</button><button class="tsk-mi" id="_m_imp" style="color:${t.text}"><span style="width:18px;text-align:center">\ud83d\udcc2</span>Importuj</button></div></div>`;
                html += '<div id="tsk-body">';

                // Notes section
                if (notesOn && eid) {
                    const note = Notes.get(eid);
                    html += `<div style="margin:12px;padding:12px;background:${t.noteBg};border:1px solid ${t.noteBorder};border-radius:10px"><div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:8px"><span style="font-size:10px;font-weight:700;color:${t.noteText};text-transform:uppercase;letter-spacing:.5px">\ud83d\udcdd Notatki (${eid})</span><button id="_ndel" style="background:none;border:none;cursor:pointer;font-size:12px;padding:2px 4px;opacity:.6">\ud83d\uddd1\ufe0f</button></div><textarea id="_ntxt" class="tsk-note" placeholder="Wpisz notatk\u0119..." rows="3" style="border:1px solid ${t.noteBorder};background:${t.inputBg};color:${t.text}">${note.text}</textarea>${note.at ? `<div style="font-size:9px;color:${t.textMut};margin-top:4px">Zapisano: ${new Date(note.at).toLocaleString('pl-PL')}</div>` : ''}</div>`;
                }

                // Search
                html += `<div style="margin:12px 12px 8px;position:relative"><span style="position:absolute;left:10px;top:50%;transform:translateY(-50%);font-size:13px;color:${t.textMut};pointer-events:none">\ud83d\udd0d</span><input id="tsk-search" placeholder="Szukaj taska..." value="${searchQ}" style="background:${t.searchBg};color:${t.text};border:1px solid ${t.border}"></div>`;

                const q = searchQ.toLowerCase();
                if (q.length >= 2) {
                    const matches = TM.flat().filter(tk => tk.n.toLowerCase().includes(q) || tk.c.toLowerCase().includes(q));
                    html += '<div style="margin:0 12px 12px">';
                    if (!matches.length) html += `<div style="text-align:center;padding:16px 0;color:${t.textMut};font-size:12px">Brak wynik\u00f3w</div>`;
                    else {
                        html += `<div style="font-size:10px;color:${t.textMut};margin-bottom:6px">${matches.length} wynik${matches.length === 1 ? '' : matches.length < 5 ? 'i' : '\u00f3w'}</div><div style="display:flex;flex-wrap:wrap;gap:5px">`;
                        matches.forEach(tk => { html += `<button class="tsk-btn" data-code="${tk.c}" style="background:${t.taskBg};color:${t.textSec};box-shadow:${t.shadow};border:1px solid ${t.taskBorder}">${tk.n} <span style="font-size:9px;opacity:.5">${tk.section}</span></button>`; });
                        html += '</div>';
                    }
                    html += '</div>';
                } else {
                    html += '<div style="margin:0 12px 12px">';
                    Object.entries(TM.all()).forEach(([name, tasks]) => {
                        const col = Col.is(name);
                        html += `<div style="margin-bottom:6px"><div class="tsk-sec" data-sec="${name}"><span style="font-size:9px;color:${t.textMut};display:inline-block;transition:transform .2s;${col ? 'transform:rotate(-90deg)' : ''}">&#9662;</span><span style="font-size:10px;font-weight:700;color:${t.textSec};text-transform:uppercase;letter-spacing:1px">${name}</span><span style="font-size:9px;color:${t.textMut}">${tasks.length}</span></div>`;
                        if (!col) { html += '<div style="display:flex;flex-wrap:wrap;gap:5px;margin-top:5px;padding-left:2px">'; tasks.forEach(tk => { html += `<button class="tsk-btn" data-code="${tk.c}" style="background:${t.taskBg};color:${t.textSec};box-shadow:${t.shadow};border:1px solid ${t.taskBorder}">${tk.n}</button>`; }); html += '</div>'; }
                        html += '</div>';
                    });
                    html += '</div>';
                }
                html += '</div>';
                html += `<div style="padding:8px 12px;border-top:1px solid ${t.border};background:${t.bgCard};font-size:9px;color:${t.textMut};text-align:center;flex-shrink:0;letter-spacing:.2px">Taskonator by <strong>@sulkpiot</strong></div>`;
                pan.innerHTML = html; mp.appendChild(pan); wire(tab, pan);
            }

            function wire(tab, pan) {
                const t = Theme.t(); tab.onclick = toggle;
                const ham = pan.querySelector('#tsk-ham'), drop = pan.querySelector('#tsk-drop');
                ham.onclick = e => { e.stopPropagation(); menuOn = !menuOn; drop.classList.toggle('show', menuOn); };
                pan.addEventListener('click', e => { if (menuOn && !drop.contains(e.target) && e.target !== ham) { menuOn = false; drop.classList.remove('show'); } });
                pan.querySelectorAll('.tsk-mi').forEach(mi => { mi.onmouseover = () => { mi.style.background = t.menuHover; }; mi.onmouseout = () => { mi.style.background = 'none'; }; });
                pan.querySelector('#_m_theme').onclick = () => { Theme.toggle(); menuOn = false; build(); };
                pan.querySelector('#_m_lock').onclick = () => { Confirm.tog(); Toast.show(Confirm.on() ? 'Potw. W\u0141 \ud83d\udd12' : 'Potw. WY\u0141 \ud83d\udd13'); menuOn = false; build(); };
                pan.querySelector('#_m_notes').onclick = () => { notesOn = !notesOn; menuOn = false; build(); };
                pan.querySelector('#_m_edit').onclick = () => { menuOn = false; drop.classList.remove('show'); openMgmt(); };
                pan.querySelector('#_m_exp').onclick = () => { const b = new Blob([TM.exprt()], { type: 'application/json' }); const u = URL.createObjectURL(b); Object.assign(document.createElement('a'), { href: u, download: 'taskonator-' + Date.now() + '.json' }).click(); URL.revokeObjectURL(u); Toast.show('Wyeksportowano', 'success'); menuOn = false; };
                pan.querySelector('#_m_imp').onclick = () => { const inp = Object.assign(document.createElement('input'), { type: 'file', accept: '.json' }); inp.onchange = e => { const f = e.target.files[0]; if (!f) return; const r = new FileReader(); r.onload = ev => { TM.imprt(ev.target.result) ? (Toast.show('Zaimportowano', 'success'), build()) : Toast.show('B\u0142\u0105d', 'error'); }; r.readAsText(f); }; inp.click(); menuOn = false; };
                const ntxt = pan.querySelector('#_ntxt');
                if (ntxt) { let tm; ntxt.oninput = () => { clearTimeout(tm); tm = setTimeout(() => { const id = empId(); if (id) { Notes.save(id, ntxt.value); Toast.show('Zapisano', 'success', 1500); } }, CONFIG.NOTES_SAVE_DELAY); }; }
                pan.querySelector('#_ndel')?.addEventListener('click', () => { const id = empId(); if (id) { Notes.del(id); Toast.show('Usuni\u0119to', 'success'); build(); } });
                const search = pan.querySelector('#tsk-search');
                if (search) { search.oninput = () => { searchQ = search.value; build(); }; requestAnimationFrame(() => { const s = document.getElementById('tsk-search'); if (s && searchQ) { s.focus(); s.setSelectionRange(s.value.length, s.value.length); } }); }
                pan.querySelectorAll('.tsk-sec').forEach(h => { h.onclick = () => { Col.tog(h.dataset.sec); build(); }; });
                pan.querySelectorAll('.tsk-btn').forEach(btn => {
                    btn.onclick = () => API.go(btn.dataset.code, btn);
                    btn.onmouseover = () => { if (btn.style.animation !== 'tsk-pulse 1s ease infinite') { btn.style.background = t.orange; btn.style.borderColor = t.orange; } };
                    btn.onmouseout = () => { if (btn.style.animation !== 'tsk-pulse 1s ease infinite') { btn.style.background = t.taskBg; btn.style.borderColor = t.taskBorder; } };
                });
            }

            function openMgmt() {
                if (document.getElementById('tsk-ov')) return;
                const t = Theme.t(), sNames = Object.keys(TM.all()); let activeSec = sNames[0] || '';
                const ov = document.createElement('div'); ov.id = 'tsk-ov';
                Object.assign(ov.style, { position: 'fixed', inset: '0', background: 'rgba(0,0,0,0.35)', zIndex: '99998', display: 'flex', alignItems: 'center', justifyContent: 'center' });
                const p = document.createElement('div');
                Object.assign(p.style, { background: t.bg, color: t.text, borderRadius: '16px', padding: '0', maxWidth: '560px', width: '94%', maxHeight: '85vh', overflow: 'hidden', boxShadow: t.dropShadow, border: '1px solid ' + t.border, fontFamily: 'system-ui,-apple-system,sans-serif', display: 'flex', flexDirection: 'column' });
                ov.appendChild(p); document.body.appendChild(ov);
                ov.onclick = e => { if (e.target === ov) ov.remove(); };
                let dragFrom = null;

                function renderMgmt() {
                    const secNames = Object.keys(TM.all()); if (!secNames.includes(activeSec)) activeSec = secNames[0] || '';
                    const tasks = TM.all()[activeSec] || [];
                    let h = `<div style="padding:16px 20px;border-bottom:1px solid ${t.border};display:flex;justify-content:space-between;align-items:center;flex-shrink:0"><span style="font-size:16px;font-weight:700">\u2699\ufe0f Edytuj taski</span><button id="_mc" style="background:none;border:1px solid ${t.border};border-radius:8px;width:34px;height:34px;cursor:pointer;font-size:16px;color:${t.textSec};display:flex;align-items:center;justify-content:center">\u2715</button></div>`;
                    h += `<div style="padding:12px 16px;border-bottom:1px solid ${t.border};overflow-x:auto;flex-shrink:0"><div style="display:flex;gap:4px;align-items:center;flex-wrap:wrap">`;
                    secNames.forEach(name => { const isA = name === activeSec; h += `<button class="tsk-mtab" data-tab="${name}" style="background:${isA ? t.activeTabBg : 'transparent'};color:${isA ? t.activeTabText : t.inactiveTabText}">${name}</button>`; });
                    h += `<button class="tsk-mtab-add" id="_add_sec" style="background:none;border:2px dashed ${t.border};color:${t.textMut}">+</button></div></div>`;

                    if (activeSec) {
                        h += '<div style="flex:1;overflow-y:auto;padding:16px 20px">';
                        h += `<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:12px"><div><span style="font-size:13px;font-weight:700;color:${t.text}">${activeSec}</span><span style="font-size:11px;color:${t.textMut};margin-left:6px">${tasks.length} task${tasks.length !== 1 ? '\u00f3w' : ''}</span></div><button id="_rm_sec" style="background:none;border:none;cursor:pointer;font-size:11px;color:${t.red};font-weight:500;padding:4px 8px;border-radius:6px">\ud83d\uddd1\ufe0f Usu\u0144 sekcj\u0119</button></div>`;
                        if (tasks.length > 0) h += `<div style="display:grid;grid-template-columns:24px 1fr 1fr 70px;gap:8px;padding:6px 4px;margin-bottom:4px"><span></span><span style="font-size:10px;font-weight:600;color:${t.textMut};text-transform:uppercase;letter-spacing:.5px">Nazwa</span><span style="font-size:10px;font-weight:600;color:${t.textMut};text-transform:uppercase;letter-spacing:.5px">Kod</span><span style="font-size:10px;font-weight:600;color:${t.textMut};text-transform:uppercase;letter-spacing:.5px;text-align:center">Akcje</span></div>`;
                        h += '<div class="tsk-drop-zone" data-pos="0" style="height:2px;margin:0 4px"></div>';
                        tasks.forEach((tk, i) => {
                            h += `<div class="tsk-drag-row" draggable="true" data-idx="${i}" style="display:grid;grid-template-columns:24px 1fr 1fr 70px;gap:8px;padding:6px 0;border-top:1px solid ${t.borderSoft};align-items:center"><span class="tsk-drag-handle">\u22ee\u22ee</span><input data-i="${i}" data-f="n" value="${tk.n}" style="padding:7px 10px;border-radius:6px;border:1px solid ${t.inputBorder};background:${t.bgCard};color:${t.text};font-size:12px;font-family:inherit;outline:none"><input data-i="${i}" data-f="c" value="${tk.c}" style="padding:7px 10px;border-radius:6px;border:1px solid ${t.inputBorder};background:${t.bgCard};color:${t.text};font-size:12px;font-family:monospace;outline:none;text-transform:uppercase"><div style="display:flex;gap:2px;justify-content:center"><button class="_sv" data-i="${i}" style="cursor:pointer;border:none;background:none;font-size:15px;padding:4px;border-radius:4px">\ud83d\udcbe</button><button class="_rm" data-i="${i}" style="cursor:pointer;border:none;background:none;font-size:15px;padding:4px;border-radius:4px">\ud83d\uddd1\ufe0f</button></div></div><div class="tsk-drop-zone" data-pos="${i + 1}" style="height:2px;margin:0 4px"></div>`;
                        });
                        h += `<div style="display:grid;grid-template-columns:24px 1fr 1fr 70px;gap:8px;padding:10px 0;margin-top:6px;border-top:2px dashed ${t.border};align-items:center"><span></span><input id="_atn" placeholder="Nazwa..." style="padding:7px 10px;border-radius:6px;border:1px solid ${t.inputBorder};background:${t.inputBg};color:${t.text};font-size:12px;font-family:inherit;outline:none"><input id="_atc" placeholder="KOD..." style="padding:7px 10px;border-radius:6px;border:1px solid ${t.inputBorder};background:${t.inputBg};color:${t.text};font-size:12px;font-family:monospace;text-transform:uppercase;outline:none"><button id="_at" style="padding:7px 4px;border:none;border-radius:8px;background:${t.orange};color:#fff;font-weight:700;cursor:pointer;font-size:11px">+ Dodaj</button></div>`;
                        h += `<div style="margin-top:16px;padding-top:14px;border-top:1px solid ${t.border}"><span style="font-size:10px;font-weight:600;color:${t.textMut};text-transform:uppercase;letter-spacing:.5px;display:block;margin-bottom:6px">Zmie\u0144 nazw\u0119 sekcji</span><div style="display:flex;gap:8px"><input id="_rn" value="${activeSec}" style="flex:1;padding:8px 12px;border-radius:8px;border:1px solid ${t.inputBorder};background:${t.bgCard};color:${t.text};font-size:13px;font-family:inherit;outline:none"><button id="_rnb" style="padding:8px 16px;border:none;border-radius:8px;background:${t.accent};color:${t.activeTabText};font-weight:700;cursor:pointer;font-size:12px;white-space:nowrap">Zmie\u0144</button></div></div></div>`;
                    } else { h += `<div style="flex:1;display:flex;align-items:center;justify-content:center;padding:40px;color:${t.textMut};font-size:13px">Kliknij + \u017ceby doda\u0107 sekcj\u0119</div>`; }
                    h += `<div style="padding:12px 20px;border-top:1px solid ${t.border};display:flex;gap:8px;flex-shrink:0;background:${t.bgCard}"><button id="_fexp" style="flex:1;padding:8px;border:1px solid ${t.border};border-radius:8px;background:none;color:${t.textSec};cursor:pointer;font-size:12px;font-weight:500;font-family:inherit">\ud83d\udcbe Eksportuj</button><button id="_fimp" style="flex:1;padding:8px;border:1px solid ${t.border};border-radius:8px;background:none;color:${t.textSec};cursor:pointer;font-size:12px;font-weight:500;font-family:inherit">\ud83d\udcc2 Importuj</button></div>`;
                    p.innerHTML = h;

                    // Wire events
                    p.querySelector('#_mc').onclick = () => ov.remove();
                    p.querySelectorAll('.tsk-mtab').forEach(btn => { btn.onclick = () => { activeSec = btn.dataset.tab; renderMgmt(); }; });
                    p.querySelector('#_add_sec').onclick = () => { const name = prompt('Nazwa nowej sekcji:'); if (!name) return; try { TM.addSec(name); activeSec = name.trim().replace(/[<>]/g, ''); Toast.show('Dodano', 'success'); renderMgmt(); build(); } catch (e) { Toast.show(e.message, 'error'); } };
                    p.querySelector('#_rm_sec')?.addEventListener('click', () => { ask(`Usun\u0105\u0107 sekcj\u0119 \u201e${activeSec}\u201d?`, () => { try { TM.rmSec(activeSec); activeSec = ''; Toast.show('Usuni\u0119to', 'success'); renderMgmt(); build(); } catch (e) { Toast.show(e.message, 'error'); } }); });
                    p.querySelector('#_at')?.addEventListener('click', () => { try { TM.addTask(activeSec, p.querySelector('#_atn')?.value, p.querySelector('#_atc')?.value); Toast.show('Dodano', 'success'); renderMgmt(); build(); } catch (e) { Toast.show(e.message, 'error'); } });
                    ['#_atn', '#_atc'].forEach(sel => { p.querySelector(sel)?.addEventListener('keydown', e => { if (e.key === 'Enter') p.querySelector('#_at')?.click(); }); });
                    p.querySelectorAll('._sv').forEach(b => { b.onclick = () => { const i = +b.dataset.i; try { TM.updTask(activeSec, i, p.querySelector(`[data-i="${i}"][data-f="n"]`)?.value, p.querySelector(`[data-i="${i}"][data-f="c"]`)?.value); Toast.show('Zapisano', 'success'); renderMgmt(); build(); } catch (e) { Toast.show(e.message, 'error'); } }; });
                    p.querySelectorAll('._rm').forEach(b => { b.onclick = () => { const i = +b.dataset.i; const tasks = TM.all()[activeSec] || []; ask(`Usun\u0105\u0107 \u201e${tasks[i]?.n}\u201d?`, () => { try { TM.rmTask(activeSec, i); Toast.show('Usuni\u0119to', 'success'); renderMgmt(); build(); } catch (e) { Toast.show(e.message, 'error'); } }); }; });
                    p.querySelector('#_rnb')?.addEventListener('click', () => { try { TM.renameSec(activeSec, p.querySelector('#_rn')?.value); activeSec = p.querySelector('#_rn')?.value.trim().replace(/[<>]/g, ''); Toast.show('Zmieniono', 'success'); renderMgmt(); build(); } catch (e) { Toast.show(e.message, 'error'); } });
                    p.querySelector('#_rn')?.addEventListener('keydown', e => { if (e.key === 'Enter') p.querySelector('#_rnb')?.click(); });
                    p.querySelector('#_fexp').onclick = () => { const b = new Blob([TM.exprt()], { type: 'application/json' }); const u = URL.createObjectURL(b); Object.assign(document.createElement('a'), { href: u, download: 'taskonator-' + Date.now() + '.json' }).click(); URL.revokeObjectURL(u); Toast.show('Wyeksportowano', 'success'); };
                    p.querySelector('#_fimp').onclick = () => { const inp = Object.assign(document.createElement('input'), { type: 'file', accept: '.json' }); inp.onchange = e => { const f = e.target.files[0]; if (!f) return; const r = new FileReader(); r.onload = ev => { TM.imprt(ev.target.result) ? (Toast.show('Zaimportowano', 'success'), renderMgmt(), build()) : Toast.show('B\u0142\u0105d', 'error'); }; r.readAsText(f); }; inp.click(); };

                    // Drag & drop reordering
                    const rows = p.querySelectorAll('.tsk-drag-row'), zones = p.querySelectorAll('.tsk-drop-zone');
                    rows.forEach(row => {
                        row.addEventListener('dragstart', e => { dragFrom = +row.dataset.idx; row.classList.add('dragging'); e.dataTransfer.effectAllowed = 'move'; e.dataTransfer.setData('text/plain', String(dragFrom)); });
                        row.addEventListener('dragend', () => { dragFrom = null; row.classList.remove('dragging'); zones.forEach(z => { z.style.background = 'transparent'; z.style.opacity = '0'; }); });
                    });
                    zones.forEach(zone => {
                        zone.addEventListener('dragover', e => { e.preventDefault(); e.dataTransfer.dropEffect = 'move'; zone.style.background = t.dragLine; zone.style.opacity = '1'; zone.style.height = '3px'; });
                        zone.addEventListener('dragleave', () => { zone.style.background = 'transparent'; zone.style.opacity = '0'; zone.style.height = '2px'; });
                        zone.addEventListener('drop', e => {
                            e.preventDefault(); const from = dragFrom; let to = +zone.dataset.pos;
                            if (from === null || from === undefined) return;
                            if (to > from) to--;
                            if (from !== to) { TM.reorder(activeSec, from, to); Toast.show('Zmieniono kolejno\u015b\u0107', 'success', 1500); renderMgmt(); build(); }
                            zone.style.background = 'transparent'; zone.style.opacity = '0';
                        });
                    });
                }
                renderMgmt();
            }

            return { build };
        })();

        // ─── INIT ────────────────────────────────────────────────

        injectCSS();
        TM.init();
        UI.build();
        console.log('\u2705 Taskonator Panel module initialized');

    } // end initTaskonatorPanel

})();