v1.8.6: сортировка таблиц, спиннеры number input, единый стиль — fix broken HTML, modal-actions → form-actions, card-wrapper
Tests / test (push) Has been cancelled
Tests / test-max-bot (push) Has been cancelled

This commit is contained in:
2026-06-02 21:25:12 +03:00
parent b2a270cb2e
commit 1199407a72
27 changed files with 279 additions and 60 deletions
+14
View File
@@ -1,5 +1,19 @@
# Changelog — AegisOne Service Portal
## 1.8.6 (02.06.2026)
### Новые функции
- **sort-table.js:** универсальный модуль сортировки таблиц по столбцам (текст/число/дата), подключён глобально
- **Number input:** спиннеры скрыты через CSS (`appearance: textfield`) на всех страницах
### Исправления
- **customers.html:** восстановлена сломанная HTML-структура — `.card` > `.card-header`, поиск в `.filter-bar`
- **bot_dialogues.html:** обёрнут в `.card` > `.card-header`, фильтры в `.filter-bar`, удалены кастомные `.page-header`/`.header-controls`
- **checklist.html:** `<h3>` обёрнут в `.card-header`
- **18 страниц:** `modal-actions``form-actions` (единый класс для кнопок в модалках)
- **charts.html:** `btn-xs``btn-sm`
- **assignments.html:** добавлена модалка подтверждения отвязки сотрудника
- **4 AJAX-страницы:** `sortTables()` вызывается после загрузки данных (bot_kb, bot_unknown, bot_consent, bot_tickets)
## 1.8.5 (02.06.2026)
### Новые функции
- **AGENTS.md (раздел 12):** правила по Идеям — агент может видеть, добавлять, менять статус, предлагать к реализации, реализовывать
+2
View File
@@ -227,4 +227,6 @@ showNotification('Текст уведомления', 'info'); // сине
- [ ] Кнопки редактирования: `✎` (btn-secondary)
- [ ] Порядок кнопок в модалках: Действие → Отмена
- [ ] Уведомления через `showNotification()`
- [ ] Таблицы используют `sort-table.js` (автоматически через base.html)
- [ ] Number input без спиннеров (CSS в service.css)
- [ ] CSS-классы модалок: `.modal-overlay` / `.modal`
+11
View File
@@ -164,6 +164,17 @@ img { max-width:100%; height:auto; }
resize: vertical;
}
/* Скрытие стандартных спиннеров на number input */
input[type="number"]::-webkit-inner-spin-button,
input[type="number"]::-webkit-outer-spin-button {
-webkit-appearance: none;
margin: 0;
}
input[type="number"] {
-moz-appearance: textfield;
appearance: textfield;
}
.btn {
display: inline-flex;
align-items: center;
+164
View File
@@ -0,0 +1,164 @@
/**
* sort-table.js Универсальный модуль сортировки таблиц.
*
* Автоматически находит все <table> на странице и делает
* заголовки <th> кликабельными для сортировки по столбцам.
*
* Использование:
* 1. Подключить <script src="/service/static/js/sort-table.js"></script>
* 2. Таблицы автоматически получают сортировку при загрузке
* 3. Для AJAX-таблиц: вызвать sortTables() после заполнения <tbody>
*/
(function(){
'use strict';
var SORT_CSS = [
'.sort-th { cursor:pointer; user-select:none; position:relative; padding-right:18px !important; }',
'.sort-th:hover { color:var(--accent); }',
'.sort-th::after { content:""; position:absolute; right:6px; top:50%; transform:translateY(-50%); width:0; height:0; border-left:4px solid transparent; border-right:4px solid transparent; border-top:5px solid var(--text-muted); opacity:0.4; }',
'.sort-th.sort-asc::after { border-top:none; border-bottom:5px solid var(--accent); opacity:1; }',
'.sort-th.sort-desc::after { border-top:5px solid var(--accent); opacity:1; }'
].join('\n');
/* Встраиваем стили один раз */
function injectStyles(){
if(document.getElementById('sort-table-styles')) return;
var style = document.createElement('style');
style.id = 'sort-table-styles';
style.textContent = SORT_CSS;
document.head.appendChild(style);
}
/* Определение типа значения ячейки */
function getCellValue(tr, idx){
var td = tr.children[idx];
if(!td) return '';
var text = (td.textContent || '').trim();
return text;
}
/* Парсинг числа из текста */
function parseNumber(text){
if(!text || text === '—' || text === '-') return NaN;
/* Убираем пробелы,.currency-символы, % */
var cleaned = text.replace(/[^\d,\.\-]/g, '').replace(',', '.');
var num = parseFloat(cleaned);
return isNaN(num) ? NaN : num;
}
/* Парсинг даты из текста (DD.MM.YYYY или YYYY-MM-DD) */
function parseDate(text){
if(!text) return NaN;
/* DD.MM.YYYY HH:MM */
var m = text.match(/(\d{2})\.(\d{2})\.(\d{4})/);
if(m) return new Date(m[3], m[2]-1, m[1]).getTime();
/* YYYY-MM-DD */
m = text.match(/(\d{4})-(\d{2})-(\d{2})/);
if(m) return new Date(m[1], m[2]-1, m[3]).getTime();
return NaN;
}
/* Сравнение двух значений */
function compare(a, b, type){
if(type === 'number'){
var na = parseNumber(a), nb = parseNumber(b);
if(isNaN(na) && isNaN(nb)) return 0;
if(isNaN(na)) return 1;
if(isNaN(nb)) return -1;
return na - nb;
}
if(type === 'date'){
var da = parseDate(a), db = parseDate(b);
if(isNaN(da) && isNaN(db)) return 0;
if(isNaN(da)) return 1;
if(isNaN(db)) return -1;
return da - db;
}
/* Текст */
return a.localeCompare(b, 'ru');
}
/* Определение типа столбца по первым строкам */
function detectColumnType(tbody, idx){
var rows = tbody.children;
var samples = [];
var limit = Math.min(rows.length, 10);
for(var i = 0; i < limit; i++){
samples.push(getCellValue(rows[i], idx));
}
var numCount = 0, dateCount = 0;
for(var j = 0; j < samples.length; j++){
if(parseNumber(samples[j]) !== NaN && samples[j] !== '') numCount++;
if(parseDate(samples[j]) !== NaN && samples[j] !== '') dateCount++;
}
if(samples.length > 0 && numCount / samples.length > 0.6) return 'number';
if(samples.length > 0 && dateCount / samples.length > 0.6) return 'date';
return 'text';
}
/* Привязка сортировки к таблице */
function attachSort(table){
if(table.dataset.sortAttached) return;
table.dataset.sortAttached = '1';
var thead = table.querySelector('thead');
var tbody = table.querySelector('tbody');
if(!thead || !tbody) return;
var headers = thead.querySelectorAll('th');
headers.forEach(function(th, idx){
/* Пропускаем колонку действий */
if(th.classList.contains('actions')) return;
th.classList.add('sort-th');
th.dataset.sortIdx = idx;
th.dataset.sortDir = 'none';
th.addEventListener('click', function(){
var colIdx = parseInt(this.dataset.sortIdx);
var currentDir = this.dataset.sortDir;
var newDir = currentDir === 'asc' ? 'desc' : 'asc';
/* Сброс других столбцов */
headers.forEach(function(h){ h.classList.remove('sort-asc', 'sort-desc'); h.dataset.sortDir = 'none'; });
this.classList.remove('sort-asc', 'sort-desc');
this.classList.add('sort-' + newDir);
this.dataset.sortDir = newDir;
/* Определение типа данных */
var colType = detectColumnType(tbody, colIdx);
/* Сбор строк с значениями */
var rowsArray = Array.prototype.slice.call(tbody.querySelectorAll('tr'));
rowsArray.sort(function(rowA, rowB){
var valA = getCellValue(rowA, colIdx);
var valB = getCellValue(rowB, colIdx);
var result = compare(valA, valB, colType);
return newDir === 'asc' ? result : -result;
});
/* Перестановка строк */
rowsArray.forEach(function(row){ tbody.appendChild(row); });
});
});
}
/* Публичная функция: инициализация/переинициализация всех таблиц */
function sortTables(){
injectStyles();
var tables = document.querySelectorAll('table');
for(var i = 0; i < tables.length; i++){
attachSort(tables[i]);
}
}
/* Автозапуск после загрузки DOM */
if(document.readyState === 'loading'){
document.addEventListener('DOMContentLoaded', sortTables);
} else {
sortTables();
}
/* Экспорт в глобальную область */
window.sortTables = sortTables;
})();
+1
View File
@@ -55,6 +55,7 @@
<div class="toast-container" id="toast-container"></div>
<script src="/service/static/js/theme.js"></script>
<script src="/service/static/js/role-switcher.js"></script>
<script src="/service/static/js/sort-table.js"></script>
<script>
function showNotification(msg, type){
type = type || 'success';
@@ -10,10 +10,7 @@
<td><span class="badge badge-{{ a.role }}">{{ {'engineer':'Инженер','technician':'Техник'}[a.role] }}</span></td>
<td>{{ a.assigned_at.strftime('%d.%m.%Y %H:%M') if a.assigned_at else '' }}</td>
<td class="actions">
<form method="post" action="/service/assignments/unassign" style="display:inline">
<input type="hidden" name="id" value="{{ a.id }}">
<button class="btn btn-danger btn-sm">Отвязать</button>
</form>
<button class="btn btn-danger btn-sm" onclick="openConfirmUnassign({{ a.id }},'{{ (a.user.full_name if a.user else '')|e }}','{{ (a.object.name if a.object else '')|e }}')" title="Отвязать"></button>
</td>
</tr>{% endfor %}</tbody></table>
</div>
@@ -25,7 +22,38 @@
<div class="form-group"><label>Объект</label><select name="object_id" required>{% for o in objects %}<option value="{{ o.id }}">{{ o.name }}</option>{% endfor %}</select></div>
<div class="form-group"><label>Сотрудник</label><select name="user_id" required>{% for u in users %}<option value="{{ u.id }}">{{ u.full_name }} ({{ u.role }})</option>{% endfor %}</select></div>
<div class="form-actions"><button type="submit" class="btn btn-primary">Назначить</button>
<button type="button" class="btn btn-secondary" onclick="this.closest('.modal-overlay').classList.remove('open')">Отмена</button></div>
<button type="button" class="btn" onclick="this.closest('.modal-overlay').classList.remove('open')">Отмена</button></div>
</form>
</div></div>
<div class="modal-overlay" id="confirm-unassign">
<div class="modal" style="max-width:420px;">
<h2 style="font-size:16px;margin-bottom:8px;">Отвязать сотрудника?</h2>
<p style="color:var(--text-secondary);font-size:14px;margin-bottom:20px;">
Назначение <strong id="unassign-info"></strong> будет удалено. Это действие нельзя отменить.
</p>
<div class="form-actions" style="justify-content:flex-end;">
<button type="button" class="btn btn-danger" onclick="confirmUnassign()">Отвязать</button>
<button type="button" class="btn" onclick="document.getElementById('confirm-unassign').classList.remove('open')">Отмена</button>
</div>
</div>
</div>
<script>
var unassignId = null;
function openConfirmUnassign(id, userName, objectName){
unassignId = id;
document.getElementById('unassign-info').textContent = userName + ' → ' + objectName;
document.getElementById('confirm-unassign').classList.add('open');
}
function confirmUnassign(){
if(!unassignId) return;
var form = document.createElement('form');
form.method = 'POST';
form.action = '/service/assignments/unassign';
var input = document.createElement('input');
input.type = 'hidden'; input.name = 'id'; input.value = unassignId;
form.appendChild(input);
document.body.appendChild(form);
form.submit();
}
</script>
{% endblock %}
+1 -1
View File
@@ -39,7 +39,7 @@
<p style="color:var(--text-secondary);font-size:14px;margin-bottom:20px;">
<strong id="delete-blog-name"></strong> будет удалён навсегда. Это действие нельзя отменить.
</p>
<div class="modal-actions" style="justify-content:flex-end;">
<div class="form-actions" style="justify-content:flex-end;">
<button type="button" class="btn btn-danger" onclick="confirmDeleteBlog()">Удалить</button>
<button type="button" class="btn" onclick="closeConfirmDeleteBlog()">Отмена</button>
</div>
@@ -36,7 +36,7 @@
.modal { background:var(--bg-card); border:1px solid var(--border); border-radius:12px; padding:24px; width:420px; max-width:90vw; }
.modal h2 { color:var(--text-primary); }
.modal p { color:var(--text-secondary); }
.modal-actions { display:flex; gap:10px; }
.form-actions { display:flex; gap:10px; }
.btn-danger { background:var(--danger, #dc3545); color:#fff; border:none; padding:8px 16px; border-radius:6px; cursor:pointer; font-weight:600; }
.btn { background:var(--bg-input); color:var(--text-primary); border:1px solid var(--border); padding:8px 16px; border-radius:6px; cursor:pointer; }
</style>
@@ -109,7 +109,7 @@
Все его диалоги и сообщения будут удалены. Заявки останутся.
Это действие нельзя отменить.
</p>
<div class="modal-actions" style="justify-content:flex-end;">
<div class="form-actions" style="justify-content:flex-end;">
<button type="button" class="btn btn-danger" onclick="confirmDeleteUser()">Удалить</button>
<button type="button" class="btn" onclick="closeConfirmDeleteUser()">Отмена</button>
</div>
@@ -165,6 +165,7 @@ function renderConsents(list){
html += '</tr>';
});
tbody.innerHTML = html;
if(typeof sortTables === 'function') sortTables();
}
function filterConsents(){
@@ -1,27 +1,26 @@
{% extends "page.html" %}
{% block page_content %}
<div class="page-header">
<h1>Диалоги чат-бота</h1>
<div class="header-controls">
<input type="text" id="search-input" placeholder="Поиск по ID или имени..." oninput="filterConversations()" class="filter-input">
<button onclick="loadConversations()" class="btn btn-primary">Обновить</button>
<div class="card">
<div class="card-header"><h2>Диалоги чат-бота</h2>
<button onclick="loadConversations()" class="btn btn-primary btn-sm">Обновить</button>
</div>
<div class="filter-bar">
<input type="text" id="search-input" placeholder="Поиск по ID или имени..." oninput="filterConversations()">
<select id="status-filter" onchange="filterConversations()" class="filter-select">
<option value="">Все статусы</option>
<option value="greeting">Приветствие</option>
<option value="collecting_name">Сбор имени</option>
<option value="collecting_phone">Сбор телефона</option>
<option value="collecting_email">Сбор email</option>
<option value="ticket_creation">Создание заявки</option>
<option value="question">Вопрос</option>
<option value="done">Завершён</option>
</select>
</div>
<script>var USER_ROLE = '{{ user.role }}';</script>
<div id="conversations-container">
<div class="loading"><div class="spinner"></div></div>
</div>
</div>
<script>var USER_ROLE = '{{ user.role }}';</script>
<div style="margin-bottom:12px;display:flex;gap:8px;flex-wrap:wrap;">
<select id="status-filter" onchange="filterConversations()" class="filter-select">
<option value="">Все статусы</option>
<option value="greeting">Приветствие</option>
<option value="collecting_name">Сбор имени</option>
<option value="collecting_phone">Сбор телефона</option>
<option value="collecting_email">Сбор email</option>
<option value="ticket_creation">Создание заявки</option>
<option value="question">Вопрос</option>
<option value="done">Завершён</option>
</select>
</div>
<div id="conversations-container">
<div class="loading"><div class="spinner"></div></div>
</div>
<div class="modal-overlay" id="dialog-modal">
@@ -44,7 +43,7 @@
<p style="color:var(--text-secondary);font-size:14px;margin-bottom:20px;">
Диалог с <strong id="delete-conv-name"></strong> будет удалён навсегда. Это действие нельзя отменить.
</p>
<div class="modal-actions" style="justify-content:flex-end;">
<div class="form-actions" style="justify-content:flex-end;">
<button type="button" class="btn btn-danger" onclick="confirmDelete()">Удалить</button>
<button type="button" class="btn" onclick="closeDeleteModal()">Отмена</button>
</div>
@@ -76,13 +75,8 @@
.log-time { font-size:10px; color:var(--text-muted,#6B8299); }
.tab-btn { padding:6px 14px; border:1px solid var(--border,#253248); background:transparent; color:var(--text-primary,#E8EDF2); border-radius:4px; cursor:pointer; font-size:12px; }
.tab-btn.active { background:var(--accent,#00ADEF); border-color:var(--accent,#00ADEF); color:#fff; }
.page-header { display:flex; justify-content:space-between; align-items:center; margin-bottom:16px; flex-wrap:wrap; gap:8px; }
.header-controls { display:flex; gap:8px; align-items:center; }
.filter-input { padding:6px 10px; border:1px solid var(--border,#253248); border-radius:4px; background:var(--bg-body,#1B2838); color:var(--text-primary,#E8EDF2); font-size:12px; width:200px; }
.filter-select { padding:6px 10px; border:1px solid var(--border,#253248); border-radius:4px; background:var(--bg-body,#1B2838); color:var(--text-primary,#E8EDF2); font-size:12px; }
.conv-delete { background:none; border:none; cursor:pointer; font-size:14px; padding:2px 6px; border-radius:4px; opacity:0.5; transition:opacity 0.2s; margin-left:auto; }
.conv-delete:hover { opacity:1; background:rgba(232,107,107,0.15); }
.conv-header { display:flex; align-items:center; gap:8px; }
#delete-modal .modal { max-width:420px; }
#delete-modal h2 { font-size:16px; margin-bottom:8px; }
#delete-modal p { color:var(--text-secondary); font-size:14px; }
+3 -2
View File
@@ -73,7 +73,7 @@
<input type="url" name="source_url" id="kb-source-url" placeholder="https://aegisone.ru/...">
</div>
<div class="form-check"><input type="checkbox" name="is_active" id="kb-is-active" checked><label for="kb-is-active">Активная карточка</label></div>
<div class="modal-actions" style="justify-content:flex-end;">
<div class="form-actions" style="justify-content:flex-end;">
<button type="submit" class="btn btn-primary">Сохранить</button>
<button type="button" class="btn" onclick="closeModal()">Отмена</button>
</div>
@@ -87,7 +87,7 @@
<p style="color:var(--text-secondary);font-size:14px;margin-bottom:20px;">
Карточка <strong id="delete-kb-name"></strong> будет удалена навсегда. Это действие нельзя отменить.
</p>
<div class="modal-actions" style="justify-content:flex-end;">
<div class="form-actions" style="justify-content:flex-end;">
<button type="button" class="btn btn-danger" onclick="confirmDeleteKB()">Удалить</button>
<button type="button" class="btn" onclick="closeConfirmDeleteKB()">Отмена</button>
</div>
@@ -129,6 +129,7 @@ function renderKB(cards){
'</tr>';
});
tbody.innerHTML = html;
if(typeof sortTables === 'function') sortTables();
}
function filterKB(){
+2 -2
View File
@@ -36,7 +36,7 @@ body { font-family:'Inter','Segoe UI',sans-serif; background:var(--bg-body, #1B2
.modal { background:var(--bg-card, #0A1628); border:1px solid var(--border, #253248); border-radius:12px; padding:24px; width:420px; max-width:90vw; }
.modal h2 { color:var(--text-primary, #E8EDF2); }
.modal p { color:var(--text-muted, #6B8299); }
.modal-actions { display:flex; gap:10px; }
.form-actions { display:flex; gap:10px; }
.btn-danger { background:var(--danger, #dc3545); color:#fff; border:none; padding:8px 16px; border-radius:6px; cursor:pointer; font-weight:600; }
.btn { background:var(--bg-input, #253248); color:var(--text-primary, #E8EDF2); border:1px solid var(--border, #3a4a60); padding:8px 16px; border-radius:6px; cursor:pointer; }
</style>
@@ -72,7 +72,7 @@ body { font-family:'Inter','Segoe UI',sans-serif; background:var(--bg-body, #1B2
Удалить все тестовые диалоги, сообщения и логи обработки?
Это действие нельзя отменить.
</p>
<div class="modal-actions" style="justify-content:flex-end;">
<div class="form-actions" style="justify-content:flex-end;">
<button type="button" class="btn btn-danger" onclick="confirmClearTest()">Сбросить</button>
<button type="button" class="btn" onclick="closeConfirmClearTest()">Отмена</button>
</div>
@@ -121,6 +121,7 @@ function renderTickets(tickets){
'</tr>';
});
tbody.innerHTML = html;
if(typeof sortTables === 'function') sortTables();
}
function filterTickets(){
@@ -79,7 +79,7 @@
<option value="8">Отрасли и кейсы</option>
</select>
</div>
<div class="modal-actions" style="justify-content:flex-end;">
<div class="form-actions" style="justify-content:flex-end;">
<button type="submit" class="btn btn-primary">Опубликовать в базу знаний</button>
<button type="button" class="btn" onclick="closeModal()">Отмена</button>
</div>
@@ -93,7 +93,7 @@
<p style="color:var(--text-secondary);font-size:14px;margin-bottom:20px;">
Вопрос <strong id="ignore-uq-text"></strong> будет отмечен как игнорированный.
</p>
<div class="modal-actions" style="justify-content:flex-end;">
<div class="form-actions" style="justify-content:flex-end;">
<button type="button" class="btn btn-danger" onclick="confirmIgnoreUQ()">Игнорировать</button>
<button type="button" class="btn" onclick="closeConfirmIgnoreUQ()">Отмена</button>
</div>
@@ -142,6 +142,7 @@ function renderUQ(items){
html += '</td></tr>';
});
tbody.innerHTML = html;
if(typeof sortTables === 'function') sortTables();
}
function openReview(id){
+1 -1
View File
@@ -37,7 +37,7 @@
<p style="color:var(--text-secondary);font-size:14px;margin-bottom:20px;">
<strong id="delete-case-name"></strong> будет удалён навсегда. Это действие нельзя отменить.
</p>
<div class="modal-actions" style="justify-content:flex-end;">
<div class="form-actions" style="justify-content:flex-end;">
<button type="button" class="btn btn-danger" onclick="confirmDeleteCase()">Удалить</button>
<button type="button" class="btn" onclick="closeConfirmDeleteCase()">Отмена</button>
</div>
+5 -5
View File
@@ -6,7 +6,7 @@
.modal { background:var(--bg-card); border:1px solid var(--border); border-radius:12px; padding:24px; width:420px; max-width:90vw; }
.modal h2 { color:var(--text-primary); }
.modal p { color:var(--text-secondary); }
.modal-actions { display:flex; gap:10px; }
.form-actions { display:flex; gap:10px; }
.btn-danger { background:var(--danger, #dc3545); color:#fff; border:none; padding:8px 16px; border-radius:6px; cursor:pointer; font-weight:600; }
.btn-primary { background:var(--accent, #00ADEF); color:#fff; border:none; padding:8px 16px; border-radius:6px; cursor:pointer; font-weight:600; }
.btn { background:var(--bg-input); color:var(--text-primary); border:1px solid var(--border); padding:8px 16px; border-radius:6px; cursor:pointer; }
@@ -42,11 +42,11 @@
</div>
<div class="filter-bar" style="margin-top:8px;flex-wrap:wrap;gap:6px;">
<span style="font-size:12px;color:var(--text-muted);">Пользовательские графики:</span>
<button class="btn btn-secondary btn-xs" onclick="saveCustomChart()">Сохранить текущий</button>
<button class="btn btn-secondary btn-sm" onclick="saveCustomChart()">Сохранить текущий</button>
<select id="custom-chart-select" onchange="loadCustomChart(this.value)">
<option value="">— Загрузить —</option>
</select>
<button class="btn btn-danger btn-xs" onclick="deleteCustomChart()">Удалить</button>
<button class="btn btn-danger btn-sm" onclick="deleteCustomChart()">Удалить</button>
</div>
<div style="position:relative;height:400px;">
<canvas id="chart-canvas"></canvas>
@@ -59,7 +59,7 @@
<div class="form-group">
<input type="text" id="chart-name-input" placeholder="Введите название..." style="width:100%;padding:8px 12px;border:1px solid var(--border);border-radius:6px;font-size:14px;background:var(--bg-card);color:var(--text);box-sizing:border-box;">
</div>
<div class="modal-actions" style="justify-content:flex-end;margin-top:16px;">
<div class="form-actions" style="justify-content:flex-end;margin-top:16px;">
<button type="button" class="btn btn-primary" onclick="confirmSaveChart()">Сохранить</button>
<button type="button" class="btn" onclick="closeChartNameModal()">Отмена</button>
</div>
@@ -73,7 +73,7 @@
<strong id="delete-chart-name"></strong> будет удалён навсегда.
Это действие нельзя отменить.
</p>
<div class="modal-actions" style="justify-content:flex-end;">
<div class="form-actions" style="justify-content:flex-end;">
<button type="button" class="btn btn-danger" onclick="confirmDeleteChart()">Удалить</button>
<button type="button" class="btn" onclick="closeConfirmDeleteChart()">Отмена</button>
</div>
@@ -2,7 +2,7 @@
{% block page_content %}
{% for t in tasks %}
<div class="card">
<h3>{{ t.title }}</h3>
<div class="card-header"><h3>{{ t.title }}</h3></div>
<div style="font-size:12px;color:var(--text-muted);margin-bottom:12px;">
{{ t.object.name if t.object else '' }} — {{ t.object.address if t.object else '' }}
<span class="badge badge-{{ {'P1':'critical','P2':'high','P3':'medium','P4':'low'}[t.priority] }}" style="margin-left:8px;">{{ t.priority }}</span>
@@ -71,7 +71,7 @@
<p style="color:var(--text-secondary);font-size:14px;margin-bottom:20px;">
<strong id="delete-coeff-name"></strong> будет удалён навсегда. Это действие нельзя отменить.
</p>
<div class="modal-actions" style="justify-content:flex-end;">
<div class="form-actions" style="justify-content:flex-end;">
<button type="button" class="btn btn-danger" onclick="confirmDeleteCoeff()">Удалить</button>
<button type="button" class="btn" onclick="closeConfirmDeleteCoeff()">Отмена</button>
</div>
@@ -3,10 +3,9 @@
<div class="card">
<div class="card-header"><h2>Клиенты</h2>
{% if error %}<div class="alert alert-error" style="margin-bottom:12px;padding:8px 12px;background:rgba(239,68,68,0.1);border:1px solid rgba(239,68,68,0.3);border-radius:6px;color:var(--danger);font-size:13px;">{{ error }}</div>{% endif %}
</div>
<button class="btn btn-primary btn-sm" onclick="document.getElementById('modal-customer').classList.add('open')">+ Добавить</button>
</div>
<form method="get" class="search-box"><input type="text" name="search" placeholder="Поиск по названию или ИНН..." value="{{ search }}"><button class="btn btn-primary btn-sm">Найти</button></form>
<form method="get" class="filter-bar"><input type="text" name="search" placeholder="Поиск по названию или ИНН..." value="{{ search }}"><button class="btn btn-primary btn-sm">Найти</button></form>
<div class="table-wrap">
<table><thead><tr><th>Название</th><th>ИНН</th><th>Контакты</th><th>Статус</th><th>Объекты</th><th>Активные SLA</th><th class="actions"></th></tr></thead>
<tbody>{% for d in customers %}<tr>
@@ -59,7 +58,7 @@
<p style="color:var(--text-secondary);font-size:14px;margin-bottom:20px;">
<strong id="delete-customer-name"></strong> будет удалён навсегда. Это действие нельзя отменить.
</p>
<div class="modal-actions" style="justify-content:flex-end;">
<div class="form-actions" style="justify-content:flex-end;">
<button type="button" class="btn btn-danger" onclick="confirmDeleteCustomer()">Удалить</button>
<button type="button" class="btn" onclick="closeConfirmDeleteCustomer()">Отмена</button>
</div>
+1 -1
View File
@@ -205,7 +205,7 @@
<p style="color:var(--text-secondary);font-size:14px;margin-bottom:20px;">
<strong id="delete-formula-coeff-name"></strong> будет удалён навсегда. Это действие нельзя отменить.
</p>
<div class="modal-actions" style="justify-content:flex-end;">
<div class="form-actions" style="justify-content:flex-end;">
<button type="button" class="btn btn-danger" onclick="confirmDeleteFormulaCoeff()">Удалить</button>
<button type="button" class="btn" onclick="closeConfirmDeleteFormulaCoeff()">Отмена</button>
</div>
+1 -1
View File
@@ -44,7 +44,7 @@
<p style="color:var(--text-secondary);font-size:14px;margin-bottom:20px;">
<strong id="delete-idea-name"></strong> будет удалён навсегда. Это действие нельзя отменить.
</p>
<div class="modal-actions" style="justify-content:flex-end;">
<div class="form-actions" style="justify-content:flex-end;">
<button type="button" class="btn btn-danger" onclick="confirmDeleteIdea()">Удалить</button>
<button type="button" class="btn" onclick="closeConfirmDeleteIdea()">Отмена</button>
</div>
+1 -1
View File
@@ -54,7 +54,7 @@
<p style="color:var(--text-secondary);font-size:14px;margin-bottom:20px;">
<strong id="delete-object-name"></strong> будет удалён навсегда. Это действие нельзя отменить.
</p>
<div class="modal-actions" style="justify-content:flex-end;">
<div class="form-actions" style="justify-content:flex-end;">
<button type="button" class="btn btn-danger" onclick="confirmDeleteObject()">Удалить</button>
<button type="button" class="btn" onclick="closeConfirmDeleteObject()">Отмена</button>
</div>
@@ -46,7 +46,7 @@
<p style="color:var(--text-secondary);font-size:14px;margin-bottom:20px;">
<strong id="delete-qitem-name"></strong> будет удалён навсегда. Это действие нельзя отменить.
</p>
<div class="modal-actions" style="justify-content:flex-end;">
<div class="form-actions" style="justify-content:flex-end;">
<button type="button" class="btn btn-danger" onclick="confirmDeleteQitem()">Удалить</button>
<button type="button" class="btn" onclick="closeConfirmDeleteQitem()">Отмена</button>
</div>
+1 -1
View File
@@ -49,7 +49,7 @@
<p style="color:var(--text-secondary);font-size:14px;margin-bottom:20px;">
<strong id="delete-task-name"></strong> будет завершена. Это действие нельзя отменить.
</p>
<div class="modal-actions" style="justify-content:flex-end;">
<div class="form-actions" style="justify-content:flex-end;">
<button type="button" class="btn btn-success" onclick="confirmCloseTask()">Завершить</button>
<button type="button" class="btn" onclick="closeConfirmCloseTask()">Отмена</button>
</div>
+1 -1
View File
@@ -59,7 +59,7 @@
<p style="color:var(--text-secondary);font-size:14px;margin-bottom:20px;">
<strong id="delete-user-name"></strong> будет удалён навсегда. Это действие нельзя отменить.
</p>
<div class="modal-actions" style="justify-content:flex-end;">
<div class="form-actions" style="justify-content:flex-end;">
<button type="button" class="btn btn-danger" onclick="confirmDeleteUser()">Удалить</button>
<button type="button" class="btn" onclick="closeConfirmDeleteUser()">Отмена</button>
</div>
+1 -1
View File
@@ -1 +1 @@
1.8.5
1.8.6