it_manager+reports: иерархия договоров, биллинг и месячные отчёты
Иерархия рамочных/дочерних договоров и платежей, интернет-биллинг, месячные IT-отчёты (импорт, DOCX/PDF, шаблоны выводов). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -49,7 +49,69 @@ function itContractServiceToggle(serviceTypeCode) {
|
||||
}
|
||||
}
|
||||
|
||||
function itContractFrameworkToggle(isFramework) {
|
||||
var parentWrap = document.getElementById('it-contract-parent-wrap');
|
||||
var locationWrap = document.getElementById('it-contract-location-wrap');
|
||||
var locationSelect = document.getElementById('it-contract-location-select');
|
||||
var locationRequired = document.getElementById('it-contract-location-required');
|
||||
|
||||
if (parentWrap) {
|
||||
parentWrap.style.display = isFramework ? 'none' : '';
|
||||
}
|
||||
if (isFramework) {
|
||||
var sel = document.getElementById('it-contract-parent-select');
|
||||
if (sel) sel.value = '';
|
||||
}
|
||||
|
||||
if (locationWrap) {
|
||||
locationWrap.style.display = isFramework ? 'none' : '';
|
||||
}
|
||||
if (locationSelect) {
|
||||
locationSelect.required = !isFramework;
|
||||
if (isFramework) locationSelect.value = '';
|
||||
}
|
||||
if (locationRequired) {
|
||||
locationRequired.style.display = isFramework ? 'none' : '';
|
||||
}
|
||||
}
|
||||
|
||||
function itContractFilterParents() {
|
||||
var providerSelect = document.getElementById('it-contract-provider-select');
|
||||
var parentSelect = document.getElementById('it-contract-parent-select');
|
||||
if (!providerSelect || !parentSelect) return;
|
||||
var providerId = providerSelect.value;
|
||||
Array.prototype.forEach.call(parentSelect.options, function (opt) {
|
||||
if (!opt.value) {
|
||||
opt.hidden = false;
|
||||
return;
|
||||
}
|
||||
opt.hidden = providerId && opt.getAttribute('data-provider') !== providerId;
|
||||
});
|
||||
if (parentSelect.selectedOptions[0] && parentSelect.selectedOptions[0].hidden) {
|
||||
parentSelect.value = '';
|
||||
}
|
||||
}
|
||||
|
||||
// --- Масовий вибір екземплярів обладнання ---
|
||||
// --- Выдача оборудования: тип → список ---
|
||||
function itAssignTargetChanged(selectEl) {
|
||||
var root = selectEl.closest('[data-assign-widget]');
|
||||
if (!root) return;
|
||||
var target = selectEl.value || '';
|
||||
root.querySelectorAll('[data-assign-pick]').forEach(function (pick) {
|
||||
var match = pick.getAttribute('data-assign-pick') === target;
|
||||
pick.style.display = match ? '' : 'none';
|
||||
pick.querySelectorAll('select').forEach(function (inp) {
|
||||
inp.disabled = !match;
|
||||
if (!match) {
|
||||
inp.value = '';
|
||||
// searchable-select слушает change
|
||||
inp.dispatchEvent(new Event('change', { bubbles: true }));
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function itBulkToggleAll(headerCheckbox) {
|
||||
document.querySelectorAll('.it-equip-check').forEach(function (cb) {
|
||||
cb.checked = headerCheckbox.checked;
|
||||
@@ -88,3 +150,117 @@ function itBulkClearSelection() {
|
||||
if (headerAll) headerAll.checked = false;
|
||||
itBulkUpdate();
|
||||
}
|
||||
|
||||
function itSetSearchableSelectValue(selectId, value) {
|
||||
var select = document.getElementById(selectId);
|
||||
if (!select) return;
|
||||
select.value = value || '';
|
||||
select.dispatchEvent(new Event('change', { bubbles: true }));
|
||||
var widget = select.closest('.searchable-select');
|
||||
if (widget && widget._search) {
|
||||
var opt = select.options[select.selectedIndex];
|
||||
widget._search.value = (opt && opt.value) ? opt.textContent : '';
|
||||
if (typeof widget._updateClear === 'function') widget._updateClear();
|
||||
else if (widget._clear) widget._clear.style.display = select.value ? 'flex' : 'none';
|
||||
}
|
||||
}
|
||||
|
||||
function itWorklogFromTicket(ticket) {
|
||||
if (!ticket || !ticket.id) return;
|
||||
itTabSwitch('worklog');
|
||||
itPanelOpen('it-worklog-panel');
|
||||
|
||||
itSetSearchableSelectValue('it-worklog-ticket', String(ticket.id));
|
||||
if (ticket.locationId) {
|
||||
itSetSearchableSelectValue('it-worklog-location', String(ticket.locationId));
|
||||
}
|
||||
var equipment = document.getElementById('it-worklog-equipment');
|
||||
if (equipment && ticket.equipmentId) {
|
||||
equipment.value = String(ticket.equipmentId);
|
||||
}
|
||||
var description = document.getElementById('it-worklog-description');
|
||||
if (description && !description.value) {
|
||||
description.value = 'Работа по заявке ' + ticket.number + (ticket.title ? ': ' + ticket.title : '');
|
||||
}
|
||||
var hint = document.getElementById('it-worklog-ticket-hint');
|
||||
if (hint) {
|
||||
hint.style.display = '';
|
||||
hint.textContent = 'Заявка ' + ticket.number + (ticket.title ? ' — ' + ticket.title : '');
|
||||
}
|
||||
var typeSelect = document.getElementById('it-worklog-type');
|
||||
if (typeSelect) typeSelect.focus();
|
||||
}
|
||||
|
||||
function itWorklogTicketChanged() {
|
||||
var select = document.getElementById('it-worklog-ticket');
|
||||
if (!select) return;
|
||||
var opt = select.options[select.selectedIndex];
|
||||
var hint = document.getElementById('it-worklog-ticket-hint');
|
||||
if (!opt || !opt.value) {
|
||||
if (hint) {
|
||||
hint.style.display = 'none';
|
||||
hint.textContent = '';
|
||||
}
|
||||
return;
|
||||
}
|
||||
var locationId = opt.getAttribute('data-location') || '';
|
||||
var equipmentId = opt.getAttribute('data-equipment') || '';
|
||||
if (locationId) itSetSearchableSelectValue('it-worklog-location', locationId);
|
||||
var equipment = document.getElementById('it-worklog-equipment');
|
||||
if (equipment && equipmentId) equipment.value = equipmentId;
|
||||
if (hint) {
|
||||
hint.style.display = '';
|
||||
hint.textContent = opt.textContent.trim();
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener('change', function (ev) {
|
||||
if (ev.target && ev.target.id === 'it-worklog-ticket') {
|
||||
itWorklogTicketChanged();
|
||||
}
|
||||
});
|
||||
|
||||
function itReportPreviewOpen(url, title) {
|
||||
var overlay = document.getElementById('it-report-preview-overlay');
|
||||
var modal = document.getElementById('it-report-preview-modal');
|
||||
var frame = document.getElementById('it-report-preview-frame');
|
||||
var subtitle = document.getElementById('it-report-preview-subtitle');
|
||||
var openTab = document.getElementById('it-report-preview-open-tab');
|
||||
if (!overlay || !modal || !frame) return;
|
||||
|
||||
if (subtitle) subtitle.textContent = title || '';
|
||||
if (openTab) openTab.href = url;
|
||||
frame.src = url;
|
||||
overlay.style.display = 'block';
|
||||
modal.style.display = 'flex';
|
||||
document.body.style.overflow = 'hidden';
|
||||
}
|
||||
|
||||
function itReportPreviewClose() {
|
||||
var overlay = document.getElementById('it-report-preview-overlay');
|
||||
var modal = document.getElementById('it-report-preview-modal');
|
||||
var frame = document.getElementById('it-report-preview-frame');
|
||||
if (overlay) overlay.style.display = 'none';
|
||||
if (modal) modal.style.display = 'none';
|
||||
if (frame) frame.src = 'about:blank';
|
||||
document.body.style.overflow = '';
|
||||
}
|
||||
|
||||
document.addEventListener('keydown', function (ev) {
|
||||
if (ev.key !== 'Escape') return;
|
||||
var modal = document.getElementById('it-report-preview-modal');
|
||||
if (modal && modal.style.display === 'flex') itReportPreviewClose();
|
||||
});
|
||||
|
||||
document.addEventListener('htmx:afterSwap', function (ev) {
|
||||
var target = ev.detail && ev.detail.target;
|
||||
if (!target || target.id !== 'main-content') return;
|
||||
var root = target.querySelector('[data-it-active-tab]');
|
||||
if (root && typeof itTabSwitch === 'function') {
|
||||
itTabSwitch(root.getAttribute('data-it-active-tab') || 'equipment');
|
||||
}
|
||||
if (typeof initSearchableSelects === 'function') {
|
||||
initSearchableSelects(target);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -0,0 +1,329 @@
|
||||
/**
|
||||
* Конструктор шаблонов отчётов (секции + подписи) и Ace для advanced HTML.
|
||||
*/
|
||||
(function () {
|
||||
var ACE_CDN = 'https://cdnjs.cloudflare.com/ajax/libs/ace/1.36.2/ace.js';
|
||||
var editors = {};
|
||||
var previewTimer = null;
|
||||
var aceReady = false;
|
||||
|
||||
function csrfToken() {
|
||||
var el = document.querySelector('#report-tpl-form input[name=csrfmiddlewaretoken]');
|
||||
if (el && el.value) return el.value;
|
||||
var m = document.cookie.match(/csrftoken=([^;]+)/);
|
||||
return m ? decodeURIComponent(m[1]) : '';
|
||||
}
|
||||
|
||||
function currentMode() {
|
||||
var root = document.getElementById('report-tpl-editor-root');
|
||||
return (root && root.getAttribute('data-mode')) || 'constructor';
|
||||
}
|
||||
|
||||
function setMode(mode) {
|
||||
var root = document.getElementById('report-tpl-editor-root');
|
||||
if (!root) return;
|
||||
root.setAttribute('data-mode', mode);
|
||||
var engine = document.getElementById('tpl-engine');
|
||||
if (engine) engine.value = mode === 'advanced' ? 'django_html' : 'blocks_v1';
|
||||
|
||||
var c = document.getElementById('tpl-panel-constructor');
|
||||
var a = document.getElementById('tpl-panel-advanced');
|
||||
if (c) c.hidden = mode !== 'constructor';
|
||||
if (a) a.hidden = mode !== 'advanced';
|
||||
|
||||
document.querySelectorAll('.tpl-mode-btn').forEach(function (btn) {
|
||||
var on = btn.getAttribute('data-mode') === mode;
|
||||
btn.style.fontWeight = on ? '600' : '';
|
||||
btn.style.borderBottom = on ? '2px solid var(--color-primary)' : '';
|
||||
btn.style.borderRadius = on ? '0' : '';
|
||||
});
|
||||
|
||||
if (mode === 'advanced') {
|
||||
ensureAce(function () {
|
||||
mountEditor('tpl-body-html', 'ace/mode/html');
|
||||
mountEditor('tpl-body-md', 'ace/mode/markdown');
|
||||
// sync blocks JSON into a visible ace if host exists
|
||||
var blocksTa = document.getElementById('tpl-blocks');
|
||||
var adv = document.getElementById('tpl-blocks-adv');
|
||||
if (blocksTa && adv) adv.value = blocksTa.value;
|
||||
mountEditor('tpl-blocks-adv', 'ace/mode/json');
|
||||
// mirror: when blocks-adv changes, copy to tpl-blocks
|
||||
if (editors['tpl-blocks-adv']) {
|
||||
editors['tpl-blocks-adv'].session.off('change', editors['tpl-blocks-adv']._blocksMirror);
|
||||
editors['tpl-blocks-adv']._blocksMirror = function () {
|
||||
var ta = document.getElementById('tpl-blocks');
|
||||
if (ta) ta.value = editors['tpl-blocks-adv'].getValue();
|
||||
schedulePreview();
|
||||
};
|
||||
editors['tpl-blocks-adv'].session.on('change', editors['tpl-blocks-adv']._blocksMirror);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
syncConstructorToBlocks();
|
||||
}
|
||||
schedulePreview();
|
||||
}
|
||||
|
||||
function loadAce(cb) {
|
||||
if (window.ace) {
|
||||
cb();
|
||||
return;
|
||||
}
|
||||
if (window._aceLoading) {
|
||||
window._aceLoading.push(cb);
|
||||
return;
|
||||
}
|
||||
window._aceLoading = [cb];
|
||||
var s = document.createElement('script');
|
||||
s.src = ACE_CDN;
|
||||
s.onload = function () {
|
||||
var q = window._aceLoading || [];
|
||||
window._aceLoading = null;
|
||||
q.forEach(function (fn) { fn(); });
|
||||
};
|
||||
s.onerror = function () {
|
||||
window._aceLoading = null;
|
||||
console.error('Не удалось загрузить Ace editor');
|
||||
};
|
||||
document.head.appendChild(s);
|
||||
}
|
||||
|
||||
function ensureAce(cb) {
|
||||
loadAce(function () {
|
||||
aceReady = true;
|
||||
cb();
|
||||
});
|
||||
}
|
||||
|
||||
function syncAceToTextareas() {
|
||||
Object.keys(editors).forEach(function (id) {
|
||||
var ta = document.getElementById(id);
|
||||
if (ta && editors[id]) ta.value = editors[id].getValue();
|
||||
});
|
||||
var adv = document.getElementById('tpl-blocks-adv');
|
||||
var blocks = document.getElementById('tpl-blocks');
|
||||
if (adv && blocks && currentMode() === 'advanced') {
|
||||
blocks.value = adv.value;
|
||||
}
|
||||
}
|
||||
|
||||
function mountEditor(textareaId, mode) {
|
||||
var root = document.getElementById('report-tpl-editor-root');
|
||||
var readOnly = root && root.getAttribute('data-readonly') === '1';
|
||||
var ta = document.getElementById(textareaId);
|
||||
if (!ta || !window.ace) return;
|
||||
var hostId = textareaId === 'tpl-blocks-adv' ? 'tpl-blocks-ace' : (textareaId + '-ace');
|
||||
var host = document.getElementById(hostId);
|
||||
if (!host) return;
|
||||
if (editors[textareaId]) {
|
||||
try { editors[textareaId].destroy(); } catch (e) { /* ignore */ }
|
||||
delete editors[textareaId];
|
||||
}
|
||||
host.innerHTML = '';
|
||||
host.style.display = 'block';
|
||||
ta.style.position = 'absolute';
|
||||
ta.style.left = '-9999px';
|
||||
ta.style.height = '1px';
|
||||
ta.style.width = '1px';
|
||||
ta.setAttribute('aria-hidden', 'true');
|
||||
|
||||
var ed = ace.edit(host);
|
||||
ed.setTheme('ace/theme/textmate');
|
||||
ed.session.setMode(mode);
|
||||
ed.setValue(ta.value || '', -1);
|
||||
ed.setReadOnly(!!readOnly);
|
||||
ed.setOptions({
|
||||
fontSize: '13px',
|
||||
showPrintMargin: false,
|
||||
wrap: true,
|
||||
useSoftTabs: true,
|
||||
tabSize: 2,
|
||||
});
|
||||
ed.session.on('change', function () {
|
||||
ta.value = ed.getValue();
|
||||
if (textareaId === 'tpl-body-html' || textareaId === 'tpl-blocks-adv') {
|
||||
schedulePreview();
|
||||
}
|
||||
});
|
||||
editors[textareaId] = ed;
|
||||
}
|
||||
|
||||
function readBlocksJson() {
|
||||
var ta = document.getElementById('tpl-blocks');
|
||||
try {
|
||||
return JSON.parse((ta && ta.value) || '{}');
|
||||
} catch (e) {
|
||||
return { labels: {}, sections: [] };
|
||||
}
|
||||
}
|
||||
|
||||
function writeBlocksJson(obj) {
|
||||
var ta = document.getElementById('tpl-blocks');
|
||||
if (ta) ta.value = JSON.stringify(obj, null, 2);
|
||||
if (editors['tpl-blocks-adv']) {
|
||||
editors['tpl-blocks-adv'].setValue(ta.value, -1);
|
||||
}
|
||||
}
|
||||
|
||||
function syncConstructorToBlocks() {
|
||||
var data = readBlocksJson();
|
||||
if (!data.labels || typeof data.labels !== 'object') data.labels = {};
|
||||
document.querySelectorAll('.tpl-label-input').forEach(function (inp) {
|
||||
var key = inp.getAttribute('data-label-key');
|
||||
if (key) data.labels[key] = inp.value;
|
||||
});
|
||||
var sections = [];
|
||||
document.querySelectorAll('#tpl-sections-list .tpl-section-row').forEach(function (row) {
|
||||
var on = row.querySelector('.tpl-section-on');
|
||||
if (on && on.checked) sections.push(row.getAttribute('data-section-id'));
|
||||
});
|
||||
data.sections = sections;
|
||||
writeBlocksJson(data);
|
||||
return data;
|
||||
}
|
||||
|
||||
function moveSection(row, dir) {
|
||||
var list = document.getElementById('tpl-sections-list');
|
||||
if (!list || !row) return;
|
||||
if (dir < 0 && row.previousElementSibling) {
|
||||
list.insertBefore(row, row.previousElementSibling);
|
||||
} else if (dir > 0 && row.nextElementSibling) {
|
||||
list.insertBefore(row.nextElementSibling, row);
|
||||
}
|
||||
syncConstructorToBlocks();
|
||||
schedulePreview();
|
||||
}
|
||||
|
||||
function schedulePreview() {
|
||||
if (previewTimer) clearTimeout(previewTimer);
|
||||
previewTimer = setTimeout(runPreview, 500);
|
||||
}
|
||||
|
||||
function runPreview() {
|
||||
var root = document.getElementById('report-tpl-editor-root');
|
||||
var frame = document.getElementById('report-tpl-preview-frame');
|
||||
if (!root || !frame) return;
|
||||
var pk = root.getAttribute('data-tpl-id');
|
||||
if (!pk) return;
|
||||
|
||||
if (currentMode() === 'constructor') {
|
||||
syncConstructorToBlocks();
|
||||
} else {
|
||||
syncAceToTextareas();
|
||||
}
|
||||
|
||||
var htmlTa = document.getElementById('tpl-body-html');
|
||||
var blocksTa = document.getElementById('tpl-blocks');
|
||||
var engineEl = document.getElementById('tpl-engine');
|
||||
var monthSel = document.getElementById('tpl-preview-month');
|
||||
var status = document.getElementById('report-tpl-preview-status');
|
||||
|
||||
var fd = new FormData();
|
||||
fd.append('engine', engineEl ? engineEl.value : 'blocks_v1');
|
||||
fd.append('body_html', htmlTa ? htmlTa.value : '');
|
||||
fd.append('blocks', blocksTa ? blocksTa.value : '{}');
|
||||
if (monthSel) fd.append('month', monthSel.value);
|
||||
|
||||
if (status) status.textContent = 'Обновление…';
|
||||
fetch('/reports/templates/' + pk + '/preview-draft/', {
|
||||
method: 'POST',
|
||||
headers: { 'X-CSRFToken': csrfToken() },
|
||||
body: fd,
|
||||
credentials: 'same-origin',
|
||||
}).then(function (res) {
|
||||
return res.text().then(function (text) {
|
||||
return { ok: res.ok, text: text };
|
||||
});
|
||||
}).then(function (r) {
|
||||
frame.srcdoc = r.text;
|
||||
if (status) status.textContent = r.ok ? 'Предпросмотр' : 'Ошибка в шаблоне';
|
||||
}).catch(function () {
|
||||
if (status) status.textContent = 'Сеть / ошибка запроса';
|
||||
});
|
||||
}
|
||||
|
||||
function bindConstructor() {
|
||||
var list = document.getElementById('tpl-sections-list');
|
||||
if (list && !list._bound) {
|
||||
list._bound = true;
|
||||
list.addEventListener('change', function (e) {
|
||||
if (e.target && e.target.classList.contains('tpl-section-on')) {
|
||||
var row = e.target.closest('.tpl-section-row');
|
||||
if (row) row.style.opacity = e.target.checked ? '' : '0.55';
|
||||
syncConstructorToBlocks();
|
||||
schedulePreview();
|
||||
}
|
||||
});
|
||||
list.addEventListener('click', function (e) {
|
||||
var up = e.target.closest('.tpl-sec-up');
|
||||
var down = e.target.closest('.tpl-sec-down');
|
||||
if (up) {
|
||||
e.preventDefault();
|
||||
moveSection(up.closest('.tpl-section-row'), -1);
|
||||
} else if (down) {
|
||||
e.preventDefault();
|
||||
moveSection(down.closest('.tpl-section-row'), 1);
|
||||
}
|
||||
});
|
||||
}
|
||||
document.querySelectorAll('.tpl-label-input').forEach(function (inp) {
|
||||
if (inp._bound) return;
|
||||
inp._bound = true;
|
||||
inp.addEventListener('input', function () {
|
||||
syncConstructorToBlocks();
|
||||
schedulePreview();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function init() {
|
||||
var root = document.getElementById('report-tpl-editor-root');
|
||||
if (!root) return;
|
||||
editors = {};
|
||||
|
||||
document.querySelectorAll('.tpl-mode-btn').forEach(function (btn) {
|
||||
if (btn._bound) return;
|
||||
btn._bound = true;
|
||||
btn.addEventListener('click', function () {
|
||||
setMode(btn.getAttribute('data-mode') || 'constructor');
|
||||
});
|
||||
});
|
||||
|
||||
bindConstructor();
|
||||
|
||||
var form = document.getElementById('report-tpl-form');
|
||||
if (form && !form._tplBound) {
|
||||
form._tplBound = true;
|
||||
form.addEventListener('submit', function () {
|
||||
if (currentMode() === 'constructor') {
|
||||
syncConstructorToBlocks();
|
||||
} else {
|
||||
syncAceToTextareas();
|
||||
}
|
||||
});
|
||||
}
|
||||
var monthSel = document.getElementById('tpl-preview-month');
|
||||
if (monthSel && !monthSel._tplBound) {
|
||||
monthSel._tplBound = true;
|
||||
monthSel.addEventListener('change', schedulePreview);
|
||||
}
|
||||
var btn = document.getElementById('report-tpl-preview-btn');
|
||||
if (btn && !btn._tplBound) {
|
||||
btn._tplBound = true;
|
||||
btn.addEventListener('click', function (e) {
|
||||
e.preventDefault();
|
||||
runPreview();
|
||||
});
|
||||
}
|
||||
|
||||
setMode(currentMode());
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', init);
|
||||
document.addEventListener('htmx:afterSwap', function (ev) {
|
||||
var t = ev.detail && ev.detail.target;
|
||||
if (t && (t.id === 'main-content' || (t.querySelector && t.querySelector('#report-tpl-editor-root')))) {
|
||||
init();
|
||||
}
|
||||
});
|
||||
})();
|
||||
Reference in New Issue
Block a user