it_manager+reports: иерархия договоров, биллинг и месячные отчёты

Иерархия рамочных/дочерних договоров и платежей, интернет-биллинг,
месячные IT-отчёты (импорт, DOCX/PDF, шаблоны выводов).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-09-09 18:03:44 +03:00
parent 897ffc99c9
commit 0da9c13192
59 changed files with 6861 additions and 267 deletions
+176
View File
@@ -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);
}
});
+329
View File
@@ -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();
}
});
})();