intra_monitor/public/index.html
dv e1693ae83b
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
фикс статистики 8
2026-04-15 11:50:26 +03:00

452 lines
26 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!DOCTYPE html>
<html lang="ru" id="html-root">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width,initial-scale=1.0">
<title>Заявки — CleverParking</title>
<link rel="stylesheet" href="/css/app.css">
<link rel="stylesheet" href="/css/keyboard-additions.css">
<script>if(localStorage.getItem('theme')==='dark')document.documentElement.classList.add('dark');</script>
</head>
<body>
<div class="toast-wrap" id="toast-wrap"></div>
<!-- LOGIN -->
<div class="login-overlay" id="login-overlay">
<div class="login-box">
<div class="login-logo"><div class="login-logo-icon">//</div><div class="login-logo-text">Заявки</div></div>
<div class="login-sub">Сервисная поддержка · CleverParking</div>
<div class="login-field"><label>Логин</label><input type="text" id="login-username" autocomplete="username" placeholder="username"></div>
<div class="login-field"><label>Пароль</label><input type="password" id="login-password" autocomplete="current-password" placeholder="••••••"></div>
<button class="login-btn" onclick="doLogin()">Войти</button>
<div class="login-error" id="login-error"></div>
</div>
</div>
<!-- SETTINGS MODAL -->
<div class="settings-overlay" id="settings-modal" onclick="if(event.target===this)closeSettingsModal()">
<div class="settings-box">
<div class="settings-header">
<div class="settings-title">⚙ Настройки</div>
<button class="modal-close-btn" onclick="closeSettingsModal()">×</button>
</div>
<div class="settings-body">
<div class="settings-tabs">
<button class="settings-tab-btn active" data-tab="api" onclick="openSettingsTab('api')">API / Refresh</button>
<button class="settings-tab-btn" data-tab="automation" onclick="openSettingsTab('automation')">Automation</button>
<button class="settings-tab-btn" data-tab="statuses" onclick="openSettingsTab('statuses')">Internal Statuses</button>
<button class="settings-tab-btn" data-tab="users" onclick="openSettingsTab('users')">Users</button>
<button class="settings-tab-btn" data-tab="intra-clients" onclick="openSettingsTab('intra-clients')">Клиенты IntraDesk</button>
</div>
<div class="settings-tab-pane" data-tab="api">
<div class="settings-section-title">IntraDesk API</div>
<div class="form-row">
<div class="form-field" style="flex:2;min-width:240px;">
<label>API Key <span id="apikey-status" style="color:var(--green);font-weight:500;text-transform:none;letter-spacing:0;"></span></label>
<input type="password" id="api-key" placeholder="Enter new key to update">
</div>
<div class="form-field"><label>Status IDs (CSV)</label><input type="text" id="status-ids" placeholder="68051,68046,..."></div>
</div>
<div class="form-row">
<div class="form-field"><label>Event Types Filter (empty = all)</label><input type="text" id="event-types" placeholder="50,55,10,30"></div>
<div class="form-field"><label>Auto Refresh (sec, 0=off)</label><input type="number" id="server-refresh-interval" placeholder="300" min="0"></div>
</div>
<button class="btn btn-primary" onclick="saveSettings()">Save</button>
<button class="btn btn-default" onclick="loadAllTasks()" id="refresh-all-btn" style="margin-left:8px;">Refresh All Tasks</button>
<span id="refresh-all-status" style="font-size:12px;color:var(--text3);margin-left:10px;"></span>
</div>
<div class="settings-tab-pane" data-tab="automation" style="display:none;">
<div class="settings-section-title">Status Automation</div>
<div class="form-row">
<label style="display:flex;align-items:center;gap:8px;font-size:13px;color:var(--text2);">
<input type="checkbox" id="setting-auto-reset-on-update" style="accent-color:var(--accent);">
Reset internal status on update (if no other rule matched)
</label>
</div>
<div class="form-row">
<label style="display:flex;align-items:center;gap:8px;font-size:13px;color:var(--text2);">
<input type="checkbox" id="setting-sync-reply-wait" style="accent-color:var(--accent);">
Sync reply/wait pair
</label>
</div>
<div class="form-row">
<label style="display:flex;align-items:center;gap:8px;font-size:13px;color:var(--text2);">
<input type="checkbox" id="setting-sync-done-intra" style="accent-color:var(--accent);">
Sync Done with Intra by status_id
</label>
</div>
<div class="form-row">
<label style="display:flex;align-items:center;gap:8px;font-size:13px;color:var(--text2);">
<input type="checkbox" id="setting-sync-addfield-status" style="accent-color:var(--accent);">
Синхронизировать «Служебный статус» с доп. полем IntraDesk (двусторонняя)
</label>
</div>
<div class="form-row">
<div class="form-field" style="flex:1;">
<label>Done Intra IDs (CSV)</label>
<input type="text" id="setting-intra-done-status-ids" placeholder="68045,68099" oninput="syncDoneSelectFromInput()">
</div>
<div class="form-field" style="flex:1;">
<label>Intra Status Dictionary (multi-select)</label>
<select id="setting-intra-done-select" multiple style="min-height:130px;" onchange="syncDoneIdsFromSelect()"></select>
</div>
</div>
<button class="btn btn-primary" onclick="saveSettings()">Save</button>
</div>
<div class="settings-tab-pane" data-tab="statuses" style="display:none;">
<div class="settings-section-title">Add Status</div>
<div class="form-row">
<div class="form-field"><label>Label</label><input type="text" id="new-status-label" placeholder="New status"></div>
<div class="form-field"><label>Key (optional)</label><input type="text" id="new-status-key" placeholder="new_status"></div>
<div class="form-field"><label>Dot</label><input type="color" id="new-status-dot" value="#1677ff"></div>
<div class="form-field"><label>Bg</label><input type="color" id="new-status-bg" value="#e6f4ff"></div>
<div class="form-field"><label>Order</label><input type="number" id="new-status-order" placeholder="1000"></div>
<div style="display:flex;align-items:flex-end;"><button class="btn btn-default" onclick="createInternalStatus()">+ Add</button></div>
</div>
<div class="settings-section-title" style="margin-top:14px;">Status Catalog</div>
<table class="users-table">
<thead>
<tr>
<th>Key</th>
<th>Label</th>
<th>Dot</th>
<th>Bg</th>
<th>Order</th>
<th>Active</th>
<th></th>
<th></th>
</tr>
</thead>
<tbody id="internal-statuses-tbody"></tbody>
</table>
</div>
<div class="settings-tab-pane" data-tab="users" style="display:none;">
<div class="settings-section-title">Users</div>
<div class="form-row">
<div class="form-field"><label>Username</label><input type="text" id="new-user-login" placeholder="username"></div>
<div class="form-field"><label>Password (min. 6 chars)</label><input type="password" id="new-user-pass" placeholder="******"></div>
<div style="display:flex;align-items:flex-end;padding-bottom:1px;"><button class="btn btn-default" onclick="createUser()">+ Create</button></div>
</div>
<div id="user-msg" style="font-size:12px;min-height:16px;margin-bottom:8px;"></div>
<table class="users-table">
<thead><tr><th>Username</th><th>Created</th><th>New Password</th><th></th></tr></thead>
<tbody id="users-tbody"></tbody>
</table>
</div>
<div class="settings-tab-pane" data-tab="intra-clients" style="display:none;">
<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:12px;">
<div class="settings-section-title" style="margin-bottom:0;">Пользователи клиентов IntraDesk</div>
<button class="btn btn-default" id="sync-clients-btn" onclick="syncIntraClients()">↻ Синхронизировать</button>
</div>
<div id="sync-clients-status" style="font-size:12px;color:var(--text3);margin-bottom:10px;min-height:16px;"></div>
<div id="intra-clients-list" style="max-height:420px;overflow-y:auto;display:flex;flex-direction:column;gap:6px;"></div>
</div>
</div>
</div>
</div>
<!-- CONFIRM MODAL -->
<div class="confirm-overlay" id="confirm-modal" onclick="if(event.target===this)this.classList.remove('open')">
<div class="confirm-box">
<div class="confirm-title">Подтверждение</div>
<div class="confirm-msg">Отметить <span id="confirm-count">0</span> заявок как прочитанных?</div>
<div class="confirm-actions">
<button class="btn btn-primary" onclick="markAllRead();document.getElementById('confirm-modal').classList.remove('open')">Да</button>
<button class="btn btn-default" onclick="document.getElementById('confirm-modal').classList.remove('open')">Отмена</button>
</div>
</div>
</div>
<!-- APP -->
<div class="app-layout" id="app-shell" style="display:none;">
<!-- скрытые элементы для обратной совместимости -->
<span id="sb-email" style="display:none;"></span>
<span id="nav-badge-tasks" style="display:none;"></span>
<div class="main-area">
<div class="topbar">
<!-- Логотип -->
<div class="topbar-logo">
<div class="topbar-logo-icon">//</div>
<span class="topbar-logo-text">Заявки</span>
</div>
<div class="topbar-sep"></div>
<!-- Навигационные табы (рядом с логотипом) -->
<div class="topbar-nav-tabs">
<button class="nav-tab nav-tab-active" id="nav-btn-tasks" onclick="closeStatsPage()">
<svg width="13" height="13" fill="none" viewBox="0 0 16 16"><rect x="1" y="2" width="14" height="2.5" rx="1" fill="currentColor"/><rect x="1" y="6.75" width="14" height="2.5" rx="1" fill="currentColor"/><rect x="1" y="11.5" width="9" height="2.5" rx="1" fill="currentColor"/></svg>
Заявки
</button>
<button class="nav-tab" id="nav-btn-stats" onclick="openStatsPage()">
<svg width="13" height="13" fill="none" viewBox="0 0 16 16"><rect x="1" y="9" width="3" height="6" rx="1" fill="currentColor"/><rect x="6" y="5" width="3" height="10" rx="1" fill="currentColor"/><rect x="11" y="1" width="3" height="14" rx="1" fill="currentColor"/></svg>
Статистика
</button>
</div>
<div style="flex:1;"></div>
<!-- Мета -->
<div class="topbar-meta">
<span>Обновлено: <span id="last-update"></span></span>
<span>Загружено: <span id="total-loaded"></span></span>
</div>
<!-- Обновить -->
<button class="btn btn-primary" id="load-btn" onclick="loadTasks()" style="font-size:12px;padding:6px 14px;">↻ Обновить</button>
<!-- Уведомления -->
<!--
<div class="topbar-notif-wrap">
<button class="topbar-icon-btn" id="notif-btn" onclick="toggleMentionsDropdown()" title="Упоминания">
<svg width="14" height="14" fill="none" viewBox="0 0 16 16"><path d="M8 2a5 5 0 00-5 5v2l-1 2h12l-1-2V7a5 5 0 00-5-5z" stroke="currentColor" stroke-width="1.3"/><path d="M6.5 13a1.5 1.5 0 003 0" stroke="currentColor" stroke-width="1.3"/></svg>
<span class="topbar-notif-badge" id="notif-badge" style="display:none;">0</span>
</button>
<div class="mentions-dropdown" id="mentions-dropdown">
<div class="mentions-dd-header">🔔 Упоминания</div>
<div id="mentions-list"></div>
</div>
</div>
-->
<!-- Пользователь -->
<div class="topbar-user">
<div class="user-avatar" id="sb-avatar" style="width:28px;height:28px;font-size:11px;flex-shrink:0;">?</div>
<span class="topbar-username" id="sb-username"></span>
</div>
<!-- Настройки (только admin) -->
<button class="topbar-icon-btn" id="nav-settings" onclick="openSettingsModal()" style="display:none;" title="Настройки">
<svg width="15" height="15" viewBox="0 0 24 24" fill="none">
<path
d="M12 8.5a3.5 3.5 0 1 0 0 7 3.5 3.5 0 0 0 0-7ZM19.4 13a7.9 7.9 0 0 0 .1-2l2-1.6-2-3.4-2.4 1a7.8 7.8 0 0 0-1.7-1L15 2h-6l-.4 3a7.8 7.8 0 0 0-1.7 1l-2.4-1-2 3.4 2 1.6a7.9 7.9 0 0 0 0 2l-2 1.6 2 3.4 2.4-1a7.8 7.8 0 0 0 1.7 1l.4 3h6l.4-3a7.8 7.8 0 0 0 1.7-1l2.4 1 2-3.4-2-1.6Z"
stroke="currentColor"
stroke-width="1.3"
stroke-linejoin="round"
/>
</svg>
</button>
<!-- Тёмная тема -->
<button class="topbar-icon-btn" id="theme-toggle-btn" onclick="toggleTheme()" title="Переключить тему" style="display:none;">
<svg id="theme-icon-light" width="15" height="15" fill="none" viewBox="0 0 16 16" style="display:none;"><circle cx="8" cy="8" r="3" stroke="currentColor" stroke-width="1.4"/><path d="M8 1v1.5M8 13.5V15M1 8h1.5M13.5 8H15M3.05 3.05l1.06 1.06M11.89 11.89l1.06 1.06M3.05 12.95l1.06-1.06M11.89 4.11l1.06-1.06" stroke="currentColor" stroke-width="1.4" stroke-linecap="round"/></svg>
<svg id="theme-icon-dark" width="15" height="15" fill="none" viewBox="0 0 16 16"><path d="M13.5 10A6 6 0 016 2.5a6 6 0 100 11 6 6 0 007.5-3.5z" stroke="currentColor" stroke-width="1.4" stroke-linejoin="round"/></svg>
</button>
<!-- Выйти -->
<button class="topbar-logout-btn" onclick="doLogout()">Выйти</button>
</div>
<div class="content-area" style="position:relative;">
<div class="update-banner" id="update-banner">
🔄 Появились новые данные на сервере
<button class="update-banner-btn" onclick="reloadData()">Обновить</button>
</div>
<!-- STATISTICS PAGE -->
<div id="stats-page" style="display:none;flex-direction:column;position:absolute;inset:0;overflow-y:auto;background:var(--bg);z-index:10;">
<div id="stats-content"></div>
</div>
<!-- MAIN TASKS AREA -->
<div id="tasks-main-area" style="display:contents;">
<!-- TASKS SPLIT (list + side panel) -->
<div class="tasks-split">
<!-- TASKS PANEL -->
<div class="tasks-panel">
<!-- Filters -->
<div class="filter-area">
<!-- Tab bar: Все / Открытые / Выполненные / Пустые -->
<div class="tab-bar" id="main-tab-bar">
<button class="tab-btn tab-btn-active" id="tabbtn-all" onclick="setMainTab('all',this)">Все заявки <span class="tab-cnt" id="tcnt-all">0</span></button>
<button class="tab-btn" id="tabbtn-open" onclick="setMainTab('open',this)">Открытые <span class="tab-cnt" id="tcnt-open">0</span></button>
<button class="tab-btn" id="tabbtn-done" onclick="setMainTab('done',this)">Выполненные <span class="tab-cnt" id="tcnt-done">0</span></button>
<button class="tab-btn" id="tabbtn-empty" onclick="setMainTab('empty',this)">Пустые <span class="tab-cnt" id="tcnt-empty">0</span></button>
</div>
<div class="filter-row">
<select class="filter-select" id="date-filter" onchange="renderTable()">
<option value="0">Все даты</option>
<option value="today">Сегодня</option>
<option value="yesterday">Вчера</option>
<option value="3">3 дня</option>
<option value="7">Неделя</option>
<option value="14">2 недели</option>
<option value="30">Месяц</option>
</select>
<select class="filter-select" id="opt-page-size" onchange="onPageSizeChange()">
<option value="25">25 на стр.</option>
<option value="50" selected>50 на стр.</option>
<option value="100">100 на стр.</option>
<option value="250">250 на стр.</option>
<option value="500">500 на стр.</option>
<option value="0">Все</option>
</select>
<label style="display:flex;align-items:center;gap:6px;font-size:12px;color:var(--text3);cursor:pointer;white-space:nowrap;">
<input type="checkbox" id="opt-remote-only" onchange="renderTable()" style="accent-color:var(--accent);"> Удалённая техподдержка
</label>
<!-- Status filter dropdown -->
<div class="chip-dropdown-wrap" id="chip-dropdown-wrap">
<button class="chip-dropdown-btn" id="chip-dropdown-btn" onclick="toggleChipDropdown()">
<span id="chip-dropdown-label">Все статусы</span>
<svg width="12" height="12" viewBox="0 0 12 12" fill="none"><path d="M2 4l4 4 4-4" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"/></svg>
</button>
<div class="chip-dropdown-panel" id="chip-dropdown-panel">
<div class="chip-dd-item chip-dd-active" id="chipdd-all" onclick="toggleAllChip()">
<span class="chip-dd-dot" style="background:var(--accent)"></span>
<span class="chip-dd-lbl">Все</span>
<span class="chip-dd-cnt" id="chip-all-cnt">0</span>
</div>
<div id="mark-chips-container"></div>
<div class="chip-dd-item" id="chipdd-unmarked" onclick="chipClickLabel('unmarked')">
<span class="chip-dd-dot" style="background:var(--text2)"></span>
<span class="chip-dd-lbl">Без статуса</span>
<span class="chip-dd-cnt" id="chip-unmarked-cnt">0</span>
</div>
</div>
</div>
<input class="filter-search" type="text" id="search-input" placeholder="Search..." oninput="renderTable()">
</div>
<div class="filter-row filter-row-advanced">
<div class="adv-filter-wrap" id="filter-who-wrap">
<button class="adv-filter-btn" type="button" onclick="toggleAdvancedFilterDropdown(event,'filter-who-select')">
<span id="filter-who-label">Кто ответил: все</span>
<svg width="12" height="12" viewBox="0 0 12 12" fill="none"><path d="M2 4l4 4 4-4" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"/></svg>
</button>
<div class="adv-filter-panel" id="filter-who-panel"></div>
<select class="filter-select filter-native-hidden" id="filter-who-select" multiple></select>
</div>
<div class="adv-filter-wrap" id="filter-executor-wrap">
<button class="adv-filter-btn" type="button" onclick="toggleAdvancedFilterDropdown(event,'filter-executor-select')">
<span id="filter-executor-label">Исполнители: все</span>
<svg width="12" height="12" viewBox="0 0 12 12" fill="none"><path d="M2 4l4 4 4-4" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"/></svg>
</button>
<div class="adv-filter-panel" id="filter-executor-panel"></div>
<select class="filter-select filter-native-hidden" id="filter-executor-select" multiple></select>
</div>
<div class="adv-filter-wrap" id="filter-object-wrap">
<button class="adv-filter-btn" type="button" onclick="toggleAdvancedFilterDropdown(event,'filter-object-select')">
<span id="filter-object-label">Объекты: все</span>
<svg width="12" height="12" viewBox="0 0 12 12" fill="none"><path d="M2 4l4 4 4-4" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"/></svg>
</button>
<div class="adv-filter-panel" id="filter-object-panel"></div>
<select class="filter-select filter-native-hidden" id="filter-object-select" multiple></select>
</div>
<div class="adv-filter-wrap" id="filter-intra-wrap">
<button class="adv-filter-btn" type="button" onclick="toggleAdvancedFilterDropdown(event,'filter-intra-select')">
<span id="filter-intra-label">Статус Intra: все</span>
<svg width="12" height="12" viewBox="0 0 12 12" fill="none"><path d="M2 4l4 4 4-4" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"/></svg>
</button>
<div class="adv-filter-panel" id="filter-intra-panel"></div>
<select class="filter-select filter-native-hidden" id="filter-intra-select" multiple></select>
</div>
<div class="adv-filter-wrap" id="filter-internal-wrap">
<button class="adv-filter-btn" type="button" onclick="toggleAdvancedFilterDropdown(event,'filter-internal-select')">
<span id="filter-internal-label">Внутр. статусы: все</span>
<svg width="12" height="12" viewBox="0 0 12 12" fill="none"><path d="M2 4l4 4 4-4" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"/></svg>
</button>
<div class="adv-filter-panel" id="filter-internal-panel"></div>
<select class="filter-select filter-native-hidden" id="filter-internal-select" multiple></select>
</div>
</div>
</div>
<!-- Bulk bar -->
<div class="bulk-bar">
<label class="bulk-check-wrap"><input type="checkbox" id="check-all" onchange="toggleSelectAll(this)"> Выбрать все</label>
<button class="bulk-action-btn" onclick="promptMarkAllRead()">✓ Прочитано</button>
<button class="bulk-action-btn" onclick="exportCSV()">↓ CSV</button>
<div class="bulk-sel-info" id="bulk-sel-info" style="display:none;">
<span class="bulk-sel-count" id="bulk-sel-count">0 выбрано</span>
<span class="bulk-sep">·</span>
<span class="bulk-status-label">Статус:</span>
<!-- options generated from MARKS_CONFIG by initMarkChips() -->
<select class="filter-select bulk-status-sel" id="bulk-status-sel" onchange="bulkApplyStatusFromSelect(this)">
<option value="" disabled selected>— установить —</option>
<option value="clear">✕ Снять статус</option>
</select>
</div>
<div class="tf-info" id="tf-info"></div>
<!-- Column chooser -->
<!-- <div class="col-chooser-wrap">
<button class="col-chooser-btn" onclick="toggleColMenu(event)">
<svg width="12" height="12" fill="none" viewBox="0 0 12 12"><rect x="1" y="2" width="10" height="1.5" rx=".75" fill="currentColor"/><rect x="1" y="5.25" width="7" height="1.5" rx=".75" fill="currentColor"/><rect x="1" y="8.5" width="5" height="1.5" rx=".75" fill="currentColor"/></svg>
Колонки
</button>
<div class="col-menu" id="col-menu"></div>
</div> -->
</div>
<!-- Table -->
<div class="table-wrap" id="table-wrap">
<div class="state-box"><div class="spinner"></div><div class="msg">Загружаю заявки...</div></div>
</div>
<!-- Pagination -->
<div class="pagination-bar" id="pagination-bar" style="display:none;"></div>
</div><!-- /tasks-panel -->
<!-- DETAIL SIDE PANEL -->
<div class="detail-col hidden" id="detail-col">
<div id="detail-content" style="display:flex;flex-direction:column;height:100%;overflow:hidden;"></div>
</div>
</div><!-- /tasks-split -->
</div><!-- /tasks-main-area -->
</div><!-- /content-area -->
</div><!-- /main-area -->
</div><!-- /app-layout -->
<script src="/js/statuses.js"></script>
<script src="/js/state.js"></script>
<script src="/js/api.js"></script>
<script src="/js/sse.js"></script>
<script src="/js/mentions.js"></script>
<script src="/js/notes.js"></script>
<script src="/js/comments.js"></script>
<script src="/js/ui.js"></script>
<script src="/js/export.js"></script>
<script src="/js/auth.js"></script>
<script src="/js/diff.js"></script>
<script src="/js/keyboard.js"></script>
<script src="/js/statistics.js"></script>
<script>
// ── Theme ──────────────────────────────────────────────────────────
(function() {
if (localStorage.getItem('theme') === 'dark') {
document.documentElement.classList.add('dark');
}
})();
function updateThemeIcon() {
const dark = document.documentElement.classList.contains('dark');
const li = document.getElementById('theme-icon-light');
const di = document.getElementById('theme-icon-dark');
if (li) li.style.display = dark ? 'block' : 'none';
if (di) di.style.display = dark ? 'none' : 'block';
}
function toggleTheme() {
const dark = document.documentElement.classList.toggle('dark');
localStorage.setItem('theme', dark ? 'dark' : 'light');
updateThemeIcon();
}
window.addEventListener('DOMContentLoaded', updateThemeIcon);
// ── Stats page toggle ──────────────────────────────────────────────
function toggleStatsPage() {
if (statsPageOpen) closeStatsPage();
else openStatsPage();
const btn = document.getElementById('stats-nav-btn');
if (btn) btn.classList.toggle('active', statsPageOpen);
}
</script>
</body>
</html>