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
// ==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)' : ''}">▾</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
})();