Taskonator - Panel

Library - Side panel module for Taskonator with task management

此脚本不应直接安装。它是供其他脚本使用的外部库,要使用该库请加入元指令 // @require https://update.greasyfork.org/scripts/575278/1807561/Taskonator%20-%20Panel.js

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==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

})();