394 lines
22 KiB
HTML
394 lines
22 KiB
HTML
<!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>
|
||
</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">
|
||
<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>
|
||
</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" fill="none" viewBox="0 0 16 16"><circle cx="8" cy="8" r="2" stroke="currentColor" stroke-width="1.3"/><path d="M8 2v1M8 13v1M2 8h1M13 8h1M3.5 3.5l.7.7M11.8 11.8l.7.7M3.5 12.5l.7-.7M11.8 4.2l.7-.7" stroke="currentColor" stroke-width="1.3" stroke-linecap="round"/></svg>
|
||
</button>
|
||
|
||
<!-- Тёмная тема -->
|
||
<button class="topbar-icon-btn" id="theme-toggle-btn" onclick="toggleTheme()" title="Переключить тему">
|
||
<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">
|
||
<select class="filter-select" id="filter-who-select" onchange="renderTable()">
|
||
<option value="all">Who answered: all</option>
|
||
<option value="internal">Internal</option>
|
||
<option value="external">External</option>
|
||
<option value="nocomment">No comment</option>
|
||
</select>
|
||
<select class="filter-select filter-multi" id="filter-executor-select" multiple onchange="renderTable()"></select>
|
||
<select class="filter-select filter-multi" id="filter-object-select" multiple onchange="renderTable()"></select>
|
||
<select class="filter-select filter-multi" id="filter-intra-select" multiple onchange="renderTable()"></select>
|
||
<select class="filter-select filter-multi" id="filter-internal-select" multiple onchange="renderTable()"></select>
|
||
</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>
|