v1.8.4: единая стилистика — замена confirm/alert/prompt на кастомные модалки, SERVICE_STYLE_GUIDE.md, обновлен AGENTS.md
Tests / test (push) Has been cancelled
Tests / test-max-bot (push) Has been cancelled

This commit is contained in:
2026-06-02 20:40:24 +03:00
parent df34048e2a
commit e184e5fe79
23 changed files with 777 additions and 70 deletions
+12
View File
@@ -223,3 +223,15 @@ docker logs aegisone-app --tail 10
- Следуй существующему стилю кода
- Удаляй неиспользуемый код при изменениях
- Не добавляй лишних зависимостей без необходимости
---
## 11. Стили UI (сервисный портал)
- **Единый гайд:** `py_service/SERVICE_STYLE_GUIDE.md` — читать перед работой с фронтендом
- **Нельзя** использовать `confirm()`, `alert()`, `prompt()` — только `showNotification()` и кастомные модалки
- **Удаление:** простая модалка (Variant A), кнопки: **Действие** (слева) → **Отмена** (справа)
- **Иконки:** `` = удалить, `` = редактировать, `+` = создать
- **Модалки:** `.modal-overlay` + `.modal`, открытие через `classList.add('open')`
- **Структура страницы:** `card` > `card-header` > `filter-bar` > `table-wrap` > `table`
- **CSS классы кнопок:** `.btn-primary` (создать), `.btn-secondary` (редактировать), `.btn-danger` (удалить), `.btn-success` (выполнено)
+1 -1
View File
@@ -1 +1 @@
1.8.3
1.8.4
+17
View File
@@ -1,5 +1,22 @@
# Changelog — AegisOne Service Portal
## 1.8.4 (02.06.2026)
### Новые функции
- **SERVICE_STYLE_GUIDE.md:** единый гайд стилей сервисного портала
- **AGENTS.md (раздел 11):** правила стилей UI — см. SERVICE_STYLE_GUIDE.md
- **CSS `.modal-actions`:** общий класс для кнопок в модалках
### Исправления
- **bot_dialogues.html:** скрыт `main-header` (дублирующий заголовок с датой)
- **bot_kb.html:** замена кастомных классов на `.modal-overlay`/`.modal`, `confirm()` → кастомная модалка, иконки ✎/✕
- **bot_unknown.html:** `.modal-box``.modal`, `confirm()` → кастомная модалка, иконки ✎/✕
- **bot_consent.html:** `confirm()` → кастомная модалка удаления пользователя
- **bot_test.html:** `confirm()` → кастомная модалка очистки тестовых данных
- **charts.html:** `prompt()` → кастомная модалка ввода имени графика, `confirm()` → кастомная модалка удаления
- **role_settings.html:** `alert()``showNotification()`
- **10 простых страниц:** blog, cases, coefficients, customers, formulas, ideas, objects, questionnaire_config, tasks, users — `confirm()` → кастомные модалки с уникальными ID
- **migrations_runner.py:** split SQL на `;` для совместимости с asyncpg
## 1.8.3 (02.06.2026)
### Новые функции
- **Docstrings:** добавлены docstrings и комментарии по AGENTS.md ко всем ключевым Python-файлам
+230
View File
@@ -0,0 +1,230 @@
# Сервисный портал AegisOne — Единый стиль
> Этот документ ОБЯЗАН прочитать каждый AI-агент перед изменением шаблонов/CSS сервисного портала.
---
## 1. Структура страницы
Каждая страница наследует `page.html` и реализует блок `page_content`.
### Порядок элементов в правой части (main):
```
page.html main-header:
left: h1 (заголовок) + breadcrumb
right: page_actions block + дата {{ now }}
page_content:
1. div.card — основной контент
├── div.card-header — заголовок + кнопка действия
├── div.filter-bar — фильтры (если нужны)
└── div.table-wrap > table — таблица данных
2. div.modal-overlay#modal-xxx — модалка создания/редактирования
3. div.modal-overlay#confirm-delete-xxx — модалка подтверждения удаления
```
### Card Header:
```html
<div class="card-header">
<h2>Название секции</h2>
<button class="btn btn-primary btn-sm" onclick="openModal()">+ Действие</button>
</div>
```
---
## 2. Модалки
### Единые CSS-классы
| Класс | Назначение |
|-------|-----------|
| `.modal-overlay` | Затемнённый фон (position: fixed, z-index: 200) |
| `.modal-overlay.open` | Показать модалку (display: flex) |
| `.modal` | Контейнер модалки (фон, рамка, скругление) |
| `.modal-close` | Кнопка закрытия (✕) в правом верхнем углу |
| `.form-actions` | Блок кнопок внизу модалки (display: flex, gap: 10px) |
### Открытие/закрытие
```javascript
// Открыть
document.getElementById('modal-xxx').classList.add('open');
// Закрыть
document.getElementById('modal-xxx').classList.remove('open');
// Или изнутри кнопки
this.closest('.modal-overlay').classList.remove('open');
```
**ЗАПРЕЩЕНО:** использовать `style.display = 'flex'` / `style.display = 'none'`
### Модалка создания/редактирования (стандарт)
```html
<div class="modal-overlay" id="modal-xxx">
<div class="modal">
<button type="button" class="modal-close"
onclick="this.closest('.modal-overlay').classList.remove('open')"></button>
<h2>Название</h2>
<form method="post" action="/service/xxx/create">
<div class="form-row">
<div class="form-group"><label>Поле</label><input type="text" name="field"></div>
</div>
<div class="form-actions">
<button type="submit" class="btn btn-primary">Сохранить</button>
<button type="button" class="btn"
onclick="this.closest('.modal-overlay').classList.remove('open')">Отмена</button>
</div>
</form>
</div>
</div>
```
### Модалка подтверждения удаления (единый шаблон)
**ЗАПРЕЩЕНЫ:** `confirm()`, `alert()`, `prompt()` — только кастомные модалки!
```html
<div class="modal-overlay" id="confirm-delete-xxx">
<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="delete-xxx-name"></strong> будет удалён навсегда.
Это действие нельзя отменить.
</p>
<div class="form-actions" style="justify-content:flex-end;">
<button type="button" class="btn btn-danger" onclick="confirmDeleteXxx()">Удалить</button>
<button type="button" class="btn" onclick="closeConfirmDeleteXxx()">Отмена</button>
</div>
</div>
</div>
```
### Порядок кнопок в модалках
```
[ Действие (btn-danger / btn-primary) ] [ Отмена (btn) ]
слева справа
```
---
## 3. Кнопки
### Иконки
| Иконка | Назначение | CSS-класс |
|--------|-----------|-----------|
| `+` | Создание (перед текстом) | `btn btn-primary btn-sm` |
| `✎` | Редактирование | `btn btn-secondary btn-sm` |
| `✕` | Удаление | `btn btn-danger btn-sm` |
| `✓` | Завершение/включение | `btn btn-success btn-sm` |
| `🗑` | Удаление (только в карточках) | `btn btn-danger btn-sm` |
### CSS-классы кнопок
| Класс | Назначение |
|-------|-----------|
| `.btn` | Базовая кнопка |
| `.btn-primary` | Основное действие (синий) |
| `.btn-secondary` | Вторичное действие (серый) |
| `.btn-danger` | Удаление/опасное действие (красный) |
| `.btn-success` | Успех/завершение (зелёный) |
| `.btn-warning` | Предупреждение (жёлтый) |
| `.btn-sm` | Маленький размер |
### Таблицы — колонка действий
```html
<th class="actions"></th>
<td class="actions">
<button class="btn btn-secondary btn-sm" onclick="editXxx(...)" title="Редактировать"></button>
<button class="btn btn-danger btn-sm" onclick="openConfirmDeleteXxx(...)" title="Удалить"></button>
</td>
```
---
## 4. Уведомления
### Показ уведомлений
```javascript
showNotification('Текст уведомления', 'success'); // зелёное
showNotification('Текст уведомления', 'error'); // красное
showNotification('Текст уведомления', 'warning'); // жёлтое
showNotification('Текст уведомления', 'info'); // синее
```
**ЗАПРЕЩЕНЫ:** `alert()`, `prompt()`, `confirm()` — только `showNotification()` и кастомные модалки!
---
## 5. Фильтры
```html
<div class="filter-bar">
<input type="text" id="search-input" placeholder="Поиск..." oninput="filterItems()">
<select id="status-filter" onchange="filterItems()" class="filter-select">
<option value="">Все статусы</option>
</select>
</div>
```
---
## 6. Таблицы
```html
<div class="table-wrap">
<table>
<thead>
<tr>
<th>Колонка 1</th>
<th>Колонка 2</th>
<th class="actions"></th>
</tr>
</thead>
<tbody>
{% for item in items %}
<tr>
<td>{{ item.field }}</td>
<td>{{ item.field2 }}</td>
<td class="actions">
<button class="btn btn-secondary btn-sm" onclick="editXxx(...)"></button>
<button class="btn btn-danger btn-sm" onclick="openConfirmDeleteXxx(...)"></button>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
```
---
## 7. Страницы с JS-рендерингом
Для страниц, где данные загружаются через `fetch()` (bot_consent, bot_dialogues, bot_kb, bot_tickets, bot_unknown, charts):
- Таблица рендерится через `renderXxx(data)` в JavaScript
- Фильтрация через `filterXxx()` на клиенте
- Модалки подтверждения удаления — те же HTML-шаблоны
---
## 8. Чек-лист перед коммитом
- [ ] Нет `confirm()`, `alert()`, `prompt()` в коде
- [ ] Модалки открываются через `classList.add('open')`
- [ ] Модалки закрываются через `classList.remove('open')`
- [ ] Кнопки удаления: `✕` (btn-danger)
- [ ] Кнопки редактирования: `✎` (btn-secondary)
- [ ] Порядок кнопок в модалках: Действие → Отмена
- [ ] Уведомления через `showNotification()`
- [ ] CSS-классы модалок: `.modal-overlay` / `.modal`
+6
View File
@@ -576,6 +576,12 @@ table .actions button {
cursor: pointer;
}
.modal-actions {
display: flex;
gap: 10px;
margin-top: 20px;
}
/* ---------- TABS ---------- */
.tabs {
display: flex;
+28 -1
View File
@@ -11,7 +11,7 @@
<td><small>{{ p.created_at.strftime('%d.%m.%Y') if p.created_at else '' }}</small></td>
<td class="actions">
<button class="btn btn-secondary btn-sm" onclick="editBlog({{ p.id }},'{{ p.title|e }}','{{ p.slug|e }}','{{ p.category|e }}','{{ p.status }}','{{ p.excerpt|e }}','{{ p.content|e }}')"></button>
<form method="post" action="/service/blog/delete" style="display:inline" onsubmit="return confirm('Удалить запись?')"><input type="hidden" name="id" value="{{ p.id }}"><button class="btn btn-danger btn-sm"></button></form>
<form method="post" action="/service/blog/delete" style="display:inline" id="delete-blog-form-{{ p.id }}"><input type="hidden" name="id" value="{{ p.id }}"><button type="button" class="btn btn-danger btn-sm" onclick="openConfirmDeleteBlog({{ p.id }},'{{ p.title|e }}')" title="Удалить"></button></form>
</td>
</tr>{% endfor %}</tbody></table>
</div>
@@ -33,7 +33,34 @@
<button type="button" class="btn btn-secondary" onclick="this.closest('.modal-overlay').classList.remove('open')">Отмена</button></div>
</form>
</div></div>
<div class="modal-overlay" id="confirm-delete-blog">
<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="delete-blog-name"></strong> будет удалён навсегда. Это действие нельзя отменить.
</p>
<div class="modal-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>
</div>
</div>
<script>
var deleteBlogId = null;
function openConfirmDeleteBlog(id, name){
deleteBlogId = id;
document.getElementById('delete-blog-name').textContent = name || '#' + id;
document.getElementById('confirm-delete-blog').classList.add('open');
}
function closeConfirmDeleteBlog(){
document.getElementById('confirm-delete-blog').classList.remove('open');
deleteBlogId = null;
}
function confirmDeleteBlog(){
if(!deleteBlogId) return;
document.getElementById('delete-blog-form-' + deleteBlogId).submit();
closeConfirmDeleteBlog();
}
function editBlog(id,title,slug,category,status,excerpt,content){
document.getElementById('modal-blog').classList.add('open');
document.getElementById('modal-blog-title').textContent = 'Редактировать запись';
@@ -31,6 +31,14 @@
.confirm-modal .confirm-actions { display:flex; gap:10px; justify-content:center; }
.confirm-modal .confirm-actions .btn { min-width:140px; }
.timer-danger { color:var(--danger) !important; }
.modal-overlay { display:none; position:fixed; top:0; left:0; right:0; bottom:0; background:rgba(0,0,0,0.5); z-index:1000; align-items:center; justify-content:center; }
.modal-overlay.open { display:flex; }
.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; }
.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>
<div class="card">
@@ -93,10 +101,27 @@
</div>
</div>
<div class="modal-overlay" id="confirm-delete-user">
<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="delete-user-name"></strong> (ID: <span id="delete-user-id"></span>) будет удалён навсегда.
Все его диалоги и сообщения будут удалены. Заявки останутся.
Это действие нельзя отменить.
</p>
<div class="modal-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>
</div>
</div>
<script>
var allConsents = [];
var confirmTimer = null;
var confirmSeconds = 10;
var pendingDeleteUserId = null;
var pendingDeleteUserName = null;
function loadConsents(){
var tbody = document.getElementById('consent-table-body');
@@ -135,7 +160,7 @@ function renderConsents(list){
'<td class="consent-yes">Да</td>' +
'<td style="font-size:12px;color:var(--text-muted);">' + dateStr + '</td>';
if(USER_ROLE === 'owner'){
html += '<td><button class="btn btn-danger btn-sm" onclick="confirmDeleteUser(' + u.id + ', \'' + escapeHtml(name).replace(/'/g, "\\'") + '\')" title="Удалить пользователя">🗑</button></td>';
html += '<td><button class="btn btn-danger btn-sm" onclick="openConfirmDeleteUser(' + u.id + ', \'' + escapeHtml(name).replace(/'/g, "\\'") + '\')" title="Удалить пользователя">🗑</button></td>';
}
html += '</tr>';
});
@@ -222,8 +247,24 @@ function escapeHtml(s){
return s.replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;');
}
function confirmDeleteUser(userId, userName){
if(!confirm('Удалить пользователя ' + userName + ' (ID: ' + userId + ')?\n\nВсе его диалоги и сообщения будут удалены навсегда. Заявки останутся.')) return;
function openConfirmDeleteUser(userId, userName){
pendingDeleteUserId = userId;
pendingDeleteUserName = userName;
document.getElementById('delete-user-name').textContent = userName;
document.getElementById('delete-user-id').textContent = userId;
document.getElementById('confirm-delete-user').classList.add('open');
}
function closeConfirmDeleteUser(){
document.getElementById('confirm-delete-user').classList.remove('open');
pendingDeleteUserId = null;
pendingDeleteUserName = null;
}
function confirmDeleteUser(){
var userId = pendingDeleteUserId;
var userName = pendingDeleteUserName;
closeConfirmDeleteUser();
fetch('/api/bot/users/' + userId, { method: 'DELETE' })
.then(function(r){ return r.json() })
.then(function(d){
@@ -24,7 +24,7 @@
<div class="loading"><div class="spinner"></div></div>
</div>
<div class="modal-overlay" id="dialog-modal" style="display:none;">
<div class="modal-overlay" id="dialog-modal">
<div class="modal" style="max-width:800px;width:90%;">
<button type="button" class="modal-close" onclick="closeDialog()"></button>
<h2 id="dialog-title">Диалог</h2>
@@ -45,8 +45,8 @@
Диалог с <strong id="delete-conv-name"></strong> будет удалён навсегда. Это действие нельзя отменить.
</p>
<div class="modal-actions" style="justify-content:flex-end;">
<button type="button" class="btn" onclick="closeDeleteModal()">Отмена</button>
<button type="button" class="btn btn-danger" onclick="confirmDelete()">Удалить</button>
<button type="button" class="btn" onclick="closeDeleteModal()">Отмена</button>
</div>
</div>
</div>
@@ -131,7 +131,7 @@ function renderConversations(data){
html += '<div class="conv-card" onclick="openDialog(' + c.id + ')">';
html += '<div class="conv-header"><span class="conv-user">' + escapeHtml(c.user_name) + '</span><span class="conv-id">ID: ' + c.id + '</span>';
if(USER_ROLE === 'owner'){
html += '<button class="conv-delete" onclick="event.stopPropagation();openDeleteModal(' + c.id + ', \'' + escapeHtml(c.user_name).replace(/'/g, "\\'") + '\')" title="Удалить диалог">🗑</button>';
html += '<button class="conv-delete" onclick="event.stopPropagation();openDeleteModal(' + c.id + ', \'' + escapeHtml(c.user_name).replace(/'/g, "\\'") + '\')" title="Удалить диалог"></button>';
}
html += '</div>';
html += '<div class="conv-meta">';
@@ -150,7 +150,7 @@ function filterConversations(){
function openDialog(convId){
currentConvId = convId;
document.getElementById('dialog-modal').style.display = 'flex';
document.getElementById('dialog-modal').classList.add('open');
var conv = conversations.find(function(c){ return c.id === convId; });
document.getElementById('dialog-title').textContent = 'Диалог с ' + (conv ? conv.user_name : '#' + convId);
showTab('messages');
@@ -158,7 +158,7 @@ function openDialog(convId){
}
function closeDialog(){
document.getElementById('dialog-modal').style.display = 'none';
document.getElementById('dialog-modal').classList.remove('open');
}
function showTab(tab){
+37 -23
View File
@@ -8,19 +8,6 @@
.kb-table .active-badge { display:inline-block; padding:2px 8px; border-radius:4px; font-size:11px; font-weight:600; }
.kb-table .active-badge.yes { background:var(--success-bg, #d4edda); color:var(--success, #155724); }
.kb-table .active-badge.no { background:var(--danger-bg, #f8d7da); color:var(--danger, #721c24); }
.kb-modal { display:none; position:fixed; top:0; left:0; right:0; bottom:0; background:rgba(0,0,0,0.4);
z-index:1000; align-items:center; justify-content:center; }
.kb-modal.open { display:flex; }
.kb-modal-content { background:var(--bg-card); border-radius:12px; padding:30px; width:600px; max-width:90vw;
max-height:80vh; overflow-y:auto; }
.kb-modal-content h3 { margin-bottom:20px; }
.form-group { margin-bottom:14px; }
.form-group label { display:block; font-weight:600; margin-bottom:4px; font-size:13px; color:var(--text-muted); }
.form-group input, .form-group textarea, .form-group select {
width:100%; padding:8px 12px; border:1px solid var(--border); border-radius:6px; font-size:14px;
}
.form-group textarea { min-height:100px; resize:vertical; }
.modal-actions { display:flex; gap:12px; margin-top:16px; }
.filter-bar { display:flex; gap:12px; margin-bottom:16px; align-items:center; flex-wrap:wrap; }
.filter-bar input, .filter-bar select { padding:8px 12px; border:1px solid var(--border); border-radius:6px; font-size:13px; }
</style>
@@ -60,9 +47,10 @@
</table>
</div>
<div class="kb-modal" id="kb-modal">
<div class="kb-modal-content">
<h3 id="modal-title">Новая карточка</h3>
<div class="modal-overlay" id="kb-modal">
<div class="modal" style="max-width:600px;">
<button type="button" class="modal-close" onclick="closeModal()"></button>
<h2 id="modal-title">Новая карточка</h2>
<form id="kb-form">
<div class="form-group">
<label>Категория</label>
@@ -84,10 +72,8 @@
<label>URL источника</label>
<input type="url" name="source_url" id="kb-source-url" placeholder="https://aegisone.ru/...">
</div>
<div class="form-group">
<label><input type="checkbox" name="is_active" id="kb-is-active" checked> Активная карточка</label>
</div>
<div class="modal-actions">
<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;">
<button type="submit" class="btn btn-primary">Сохранить</button>
<button type="button" class="btn" onclick="closeModal()">Отмена</button>
</div>
@@ -95,6 +81,19 @@
</div>
</div>
<div class="modal-overlay" id="confirm-delete-kb">
<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="delete-kb-name"></strong> будет удалена навсегда. Это действие нельзя отменить.
</p>
<div class="modal-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>
</div>
</div>
<script>
var categories = {};
var allCards = [];
@@ -126,7 +125,7 @@ function renderKB(cards){
'<td>' + catName + '</td>' +
'<td><span class="active-badge ' + activeClass + '">' + activeLabel + '</span></td>' +
'<td style="font-size:12px;">' + (c.source_url ? '<a href="' + c.source_url + '" target="_blank">ссылка</a>' : '—') + '</td>' +
'<td><button class="btn btn-sm" onclick="editCard(' + c.id + ')" title="Редактировать">✏️</button> <button class="btn btn-sm" onclick="deleteCard(' + c.id + ')" style="color:#dc3545;" title="Удалить">🗑</button></td>' +
'<td><button class="btn btn-secondary btn-sm" onclick="editCard(' + c.id + ')" title="Редактировать"></button> <button class="btn btn-danger btn-sm" onclick="openConfirmDeleteKB(' + c.id + ', \'' + escapeHtml(c.question).replace(/'/g, "\\'") + '\')" title="Удалить"></button></td>' +
'</tr>';
});
tbody.innerHTML = html;
@@ -173,8 +172,23 @@ function editCard(id){
document.getElementById('kb-modal').classList.add('open');
}
function deleteCard(id){
if(!confirm('Удалить карточку?')) return;
var deleteKBId = null;
function openConfirmDeleteKB(id, name){
deleteKBId = id;
document.getElementById('delete-kb-name').textContent = name || '#' + id;
document.getElementById('confirm-delete-kb').classList.add('open');
}
function closeConfirmDeleteKB(){
document.getElementById('confirm-delete-kb').classList.remove('open');
deleteKBId = null;
}
function confirmDeleteKB(){
if(!deleteKBId) return;
var id = deleteKBId;
closeConfirmDeleteKB();
fetch('/api/bot/kb/' + id, { method: 'DELETE' }).then(function(r){ return r.json() }).then(function(d){
if(d.ok) { showNotification('Карточка удалена', 'success'); loadKB(); }
else showNotification('Ошибка удаления', 'error');
+32 -3
View File
@@ -31,6 +31,14 @@ body { font-family:'Inter','Segoe UI',sans-serif; background:var(--bg-body, #1B2
.test-chat-messages::-webkit-scrollbar-track { background:var(--bg-body, #0D1B2A); border-radius:3px; }
.test-chat-messages::-webkit-scrollbar-thumb { background:var(--border, #253248); border-radius:3px; }
.test-chat-messages::-webkit-scrollbar-thumb:hover { background:var(--text-muted, #6B8299); }
.modal-overlay { display:none; position:fixed; top:0; left:0; right:0; bottom:0; background:rgba(0,0,0,0.5); z-index:1000; align-items:center; justify-content:center; }
.modal-overlay.open { display:flex; }
.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; }
.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>
<script>
(function(){function g(){return window.matchMedia('(prefers-color-scheme:light)').matches?'light':'dark'}var s=localStorage.getItem('aegisone_service_theme')||'system',r=s==='system'?g():s;document.documentElement.setAttribute('data-theme',r);})();
@@ -43,7 +51,7 @@ body { font-family:'Inter','Segoe UI',sans-serif; background:var(--bg-body, #1B2
<button onclick="runScenario('consent_no')">Отказ согласия</button>
<button onclick="runScenario('question')">Общий вопрос</button>
<button onclick="clearTest()">Очистить</button>
<button onclick="clearTestData()" style="background:var(--danger,#dc3545) !important;border-color:var(--danger,#dc3545) !important;">Сбросить все тестовые данные</button>
<button onclick="openConfirmClearTest()" style="background:var(--danger,#dc3545) !important;border-color:var(--danger,#dc3545) !important;">Сбросить все тестовые данные</button>
</div>
<div class="test-container">
<div class="test-chat">
@@ -56,6 +64,21 @@ body { font-family:'Inter','Segoe UI',sans-serif; background:var(--bg-body, #1B2
</div>
<div class="test-log" id="test-log"></div>
</div>
<div class="modal-overlay" id="confirm-clear-test">
<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;">
Удалить все тестовые диалоги, сообщения и логи обработки?
Это действие нельзя отменить.
</p>
<div class="modal-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>
</div>
</div>
<script>
var testUserId = 999999001;
function log(event, detail){
@@ -156,8 +179,14 @@ function clearTest(){
document.getElementById('test-input').value = '';
log('info', 'Тест очищен');
}
function clearTestData(){
if(!confirm('Удалить все тестовые диалоги, сообщения и логи обработки? Это действие нельзя отменить.')) return;
function openConfirmClearTest(){
document.getElementById('confirm-clear-test').classList.add('open');
}
function closeConfirmClearTest(){
document.getElementById('confirm-clear-test').classList.remove('open');
}
function confirmClearTest(){
closeConfirmClearTest();
log('info', 'Очистка тестовых данных...');
fetch('/api/bot/test/clear', {method: 'POST'}).then(function(r){ return r.json() }).then(function(d){
if(d && d.ok){
+36 -17
View File
@@ -10,16 +10,6 @@
.badge.auto_added { background:#cce5ff; color:#004085; }
.badge.reviewed { background:#d4edda; color:#155724; }
.badge.ignored { background:#f8f9fa; color:#6c757d; }
.modal-overlay { display:none; position:fixed; top:0; left:0; right:0; bottom:0; background:rgba(0,0,0,0.4); z-index:1000; align-items:center; justify-content:center; }
.modal-overlay.open { display:flex; }
.modal-box { background:var(--bg-card); border-radius:12px; padding:30px; width:650px; max-width:90vw; max-height:85vh; overflow-y:auto; }
.modal-box h3 { margin-bottom:20px; }
.form-group { margin-bottom:14px; }
.form-group label { display:block; font-weight:600; margin-bottom:4px; font-size:13px; color:var(--text-muted); }
.form-group input, .form-group textarea, .form-group select {
width:100%; padding:8px 12px; border:1px solid var(--border); border-radius:6px; font-size:14px; }
.form-group textarea { min-height:80px; resize:vertical; }
.modal-actions { display:flex; gap:12px; margin-top:16px; }
.filter-bar { display:flex; gap:12px; margin-bottom:16px; align-items:center; }
.filter-bar select { padding:8px 12px; border:1px solid var(--border); border-radius:6px; font-size:13px; }
</style>
@@ -55,8 +45,9 @@
</div>
<div class="modal-overlay" id="uq-modal">
<div class="modal-box">
<h3 id="uq-modal-title">Проверить вопрос</h3>
<div class="modal" style="max-width:650px;">
<button type="button" class="modal-close" onclick="closeModal()"></button>
<h2 id="uq-modal-title">Проверить вопрос</h2>
<form id="uq-form">
<input type="hidden" id="uq-edit-id">
<div class="form-group">
@@ -88,7 +79,7 @@
<option value="8">Отрасли и кейсы</option>
</select>
</div>
<div class="modal-actions">
<div class="modal-actions" style="justify-content:flex-end;">
<button type="submit" class="btn btn-primary">Опубликовать в базу знаний</button>
<button type="button" class="btn" onclick="closeModal()">Отмена</button>
</div>
@@ -96,6 +87,19 @@
</div>
</div>
<div class="modal-overlay" id="confirm-ignore-uq">
<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="ignore-uq-text"></strong> будет отмечен как игнорированный.
</p>
<div class="modal-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>
</div>
</div>
<script>
var allUQ = [];
@@ -132,8 +136,8 @@ function renderUQ(items){
'<td style="font-size:12px;">' + (q.created_at ? q.created_at.substring(0,10) : '—') + '</td>' +
'<td>';
if(q.status === 'new' || q.status === 'auto_added'){
html += '<button class="btn btn-sm" onclick="openReview(' + q.id + ')" title="Проверить">✏️</button> ';
html += '<button class="btn btn-sm" onclick="ignoreUQ(' + q.id + ')" style="color:#dc3545;" title="Игнорировать">🗑</button>';
html += '<button class="btn btn-secondary btn-sm" onclick="openReview(' + q.id + ')" title="Проверить"></button> ';
html += '<button class="btn btn-danger btn-sm" onclick="openConfirmIgnoreUQ(' + q.id + ', \'' + esc(q.question_text).replace(/'/g, "\\'") + '\')" title="Игнорировать"></button>';
}
html += '</td></tr>';
});
@@ -175,8 +179,23 @@ document.getElementById('uq-form').addEventListener('submit', function(e){
});
});
function ignoreUQ(id){
if(!confirm('Игнорировать вопрос?')) return;
var ignoreUQId = null;
function openConfirmIgnoreUQ(id, text){
ignoreUQId = id;
document.getElementById('ignore-uq-text').textContent = text || '#' + id;
document.getElementById('confirm-ignore-uq').classList.add('open');
}
function closeConfirmIgnoreUQ(){
document.getElementById('confirm-ignore-uq').classList.remove('open');
ignoreUQId = null;
}
function confirmIgnoreUQ(){
if(!ignoreUQId) return;
var id = ignoreUQId;
closeConfirmIgnoreUQ();
fetch('/api/bot/unknown-questions/' + id + '/ignore', { method: 'PUT' })
.then(function(r){ return r.json() }).then(function(d){
if(d.ok){ loadUQ(); showNotification('Игнорировано', 'success'); }
+28 -1
View File
@@ -10,7 +10,7 @@
<td><span class="badge badge-{{ 'active' if c.is_active else 'inactive' }}">{{ 'Да' if c.is_active else 'Нет' }}</span></td>
<td class="actions">
<button class="btn btn-secondary btn-sm" onclick="editCase({{ c.id }},'{{ c.title|e }}','{{ c.text|e }}','{{ c.effect|e }}',{{ c.sort_order }},{{ 'true' if c.is_active else 'false' }})"></button>
<form method="post" action="/service/cases/delete" style="display:inline" onsubmit="return confirm('Удалить?')"><input type="hidden" name="id" value="{{ c.id }}"><button class="btn btn-danger btn-sm"></button></form>
<form method="post" action="/service/cases/delete" style="display:inline" id="delete-case-form-{{ c.id }}"><input type="hidden" name="id" value="{{ c.id }}"><button type="button" class="btn btn-danger btn-sm" onclick="openConfirmDeleteCase({{ c.id }},'{{ c.title|e }}')" title="Удалить"></button></form>
</td>
</tr>{% endfor %}</tbody></table>
</div>
@@ -31,7 +31,34 @@
<button type="button" class="btn btn-secondary" onclick="this.closest('.modal-overlay').classList.remove('open')">Отмена</button></div>
</form>
</div></div>
<div class="modal-overlay" id="confirm-delete-case">
<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="delete-case-name"></strong> будет удалён навсегда. Это действие нельзя отменить.
</p>
<div class="modal-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>
</div>
</div>
<script>
var deleteCaseId = null;
function openConfirmDeleteCase(id, name){
deleteCaseId = id;
document.getElementById('delete-case-name').textContent = name || '#' + id;
document.getElementById('confirm-delete-case').classList.add('open');
}
function closeConfirmDeleteCase(){
document.getElementById('confirm-delete-case').classList.remove('open');
deleteCaseId = null;
}
function confirmDeleteCase(){
if(!deleteCaseId) return;
document.getElementById('delete-case-form-' + deleteCaseId).submit();
closeConfirmDeleteCase();
}
function editCase(id,title,text,effect,order,active){
document.getElementById('modal-case').classList.add('open');
document.getElementById('modal-case-title').textContent = 'Редактировать';
+67 -3
View File
@@ -1,5 +1,17 @@
{% extends "page.html" %}
{% block page_content %}
<style>
.modal-overlay { display:none; position:fixed; top:0; left:0; right:0; bottom:0; background:rgba(0,0,0,0.5); z-index:1000; align-items:center; justify-content:center; }
.modal-overlay.open { display:flex; }
.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; }
.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; }
.form-group { margin-bottom:0; }
</style>
<div class="card">
<div class="card-header"><h2>Графики и диаграммы</h2></div>
<div class="filter-bar" style="flex-wrap:wrap;gap:8px;">
@@ -41,10 +53,38 @@
</div>
</div>
<div class="modal-overlay" id="chart-name-modal">
<div class="modal" style="max-width:420px;">
<h2 style="font-size:16px;margin-bottom:8px;">Название графика</h2>
<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;">
<button type="button" class="btn btn-primary" onclick="confirmSaveChart()">Сохранить</button>
<button type="button" class="btn" onclick="closeChartNameModal()">Отмена</button>
</div>
</div>
</div>
<div class="modal-overlay" id="confirm-delete-chart">
<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="delete-chart-name"></strong> будет удалён навсегда.
Это действие нельзя отменить.
</p>
<div class="modal-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>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.7/dist/chart.umd.min.js"></script>
<script>
var chartInstance = null;
var currentChartConfig = null;
var pendingDeleteChartName = null;
function loadChart(){
var metric = document.getElementById('metric-select').value;
@@ -107,8 +147,19 @@ function exportChart(){
function saveCustomChart(){
if(!currentChartConfig) return;
var name = prompt('Название графика:');
if(!name) return;
document.getElementById('chart-name-input').value = '';
document.getElementById('chart-name-modal').classList.add('open');
document.getElementById('chart-name-input').focus();
}
function closeChartNameModal(){
document.getElementById('chart-name-modal').classList.remove('open');
}
function confirmSaveChart(){
var name = document.getElementById('chart-name-input').value.trim();
if(!name){ return; }
closeChartNameModal();
var saved = JSON.parse(localStorage.getItem('custom_charts') || '[]');
saved.push({ name: name, config: currentChartConfig });
localStorage.setItem('custom_charts', JSON.stringify(saved));
@@ -131,7 +182,20 @@ function deleteCustomChart(){
var sel = document.getElementById('custom-chart-select');
var name = sel.value;
if(!name) return;
if(!confirm('Удалить "' + name + '"?')) return;
pendingDeleteChartName = name;
document.getElementById('delete-chart-name').textContent = name;
document.getElementById('confirm-delete-chart').classList.add('open');
}
function closeConfirmDeleteChart(){
document.getElementById('confirm-delete-chart').classList.remove('open');
pendingDeleteChartName = null;
}
function confirmDeleteChart(){
var name = pendingDeleteChartName;
closeConfirmDeleteChart();
var sel = document.getElementById('custom-chart-select');
var saved = JSON.parse(localStorage.getItem('custom_charts') || '[]');
saved = saved.filter(function(c){ return c.name !== name; });
localStorage.setItem('custom_charts', JSON.stringify(saved));
@@ -33,7 +33,7 @@
<td><small>{{ c.formula_ref }}</small></td>
<td><form method="post" action="/service/coefficients/toggle"><input type="hidden" name="id" value="{{ c.id }}"><button class="btn btn-{{ 'success' if c.is_active else 'secondary' }} btn-sm">{{ '✓' if c.is_active else '✕' }}</button></form></td>
<td class="actions"><button class="btn btn-secondary btn-sm" onclick="editCoeff({{ c.id }},'{{ c.key|e }}','{{ c.value }}','{{ c.description|e }}','{{ c.formula_ref|e }}')"></button>
<form method="post" action="/service/coefficients/delete" style="display:inline" onsubmit="return confirm('Удалить коэффициент?')"><input type="hidden" name="id" value="{{ c.id }}"><button class="btn btn-danger btn-sm"></button></form>
<form method="post" action="/service/coefficients/delete" style="display:inline" id="delete-coeff-form-{{ c.id }}"><input type="hidden" name="id" value="{{ c.id }}"><button type="button" class="btn btn-danger btn-sm" onclick="openConfirmDeleteCoeff({{ c.id }},'{{ c.key|e }}')" title="Удалить"></button></form>
</td>
</tr>{% endfor %}</tbody></table>
</div>
@@ -65,7 +65,35 @@
<div id="formula-info-content" style="margin-top:12px"></div>
</div></div>
<div class="modal-overlay" id="confirm-delete-coeff">
<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="delete-coeff-name"></strong> будет удалён навсегда. Это действие нельзя отменить.
</p>
<div class="modal-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>
</div>
</div>
<script>
var deleteCoeffId = null;
function openConfirmDeleteCoeff(id, name){
deleteCoeffId = id;
document.getElementById('delete-coeff-name').textContent = name || '#' + id;
document.getElementById('confirm-delete-coeff').classList.add('open');
}
function closeConfirmDeleteCoeff(){
document.getElementById('confirm-delete-coeff').classList.remove('open');
deleteCoeffId = null;
}
function confirmDeleteCoeff(){
if(!deleteCoeffId) return;
document.getElementById('delete-coeff-form-' + deleteCoeffId).submit();
closeConfirmDeleteCoeff();
}
function toggleAccordion(el) {
var arrow = el.querySelector('.accordion-arrow');
var body = el.nextElementSibling;
+29 -2
View File
@@ -18,8 +18,8 @@
<td class="actions">
<button class="btn btn-secondary btn-sm" onclick="editCustomer({{ d.c.id }},'{{ d.c.name|e }}','{{ d.c.inn|e }}','{{ d.c.kpp|e }}','{{ d.c.legal_address|e }}','{{ d.c.contact_person|e }}','{{ d.c.contact_phone|e }}','{{ d.c.contact_email|e }}','{{ d.c.status }}','{{ d.c.notes|e }}')"></button>
{% if user.role == 'owner' %}
<form method="post" action="/service/customers/delete" style="display:inline" onsubmit="return confirm('Удалить клиента?')">
<input type="hidden" name="id" value="{{ d.c.id }}"><button class="btn btn-danger btn-sm"></button>
<form method="post" action="/service/customers/delete" style="display:inline" id="delete-customer-form-{{ d.c.id }}">
<input type="hidden" name="id" value="{{ d.c.id }}"><button type="button" class="btn btn-danger btn-sm" onclick="openConfirmDeleteCustomer({{ d.c.id }},'{{ d.c.name|e }}')" title="Удалить"></button>
</form>{% endif %}
</td>
</tr>{% endfor %}</tbody></table>
@@ -53,7 +53,34 @@
</div>
</form>
</div></div>
<div class="modal-overlay" id="confirm-delete-customer">
<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="delete-customer-name"></strong> будет удалён навсегда. Это действие нельзя отменить.
</p>
<div class="modal-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>
</div>
</div>
<script>
var deleteCustomerId = null;
function openConfirmDeleteCustomer(id, name){
deleteCustomerId = id;
document.getElementById('delete-customer-name').textContent = name || '#' + id;
document.getElementById('confirm-delete-customer').classList.add('open');
}
function closeConfirmDeleteCustomer(){
document.getElementById('confirm-delete-customer').classList.remove('open');
deleteCustomerId = null;
}
function confirmDeleteCustomer(){
if(!deleteCustomerId) return;
document.getElementById('delete-customer-form-' + deleteCustomerId).submit();
closeConfirmDeleteCustomer();
}
function phoneMask(input){
var x = input.value.replace(/\D/g, '').match(/(\d{0,1})(\d{0,3})(\d{0,3})(\d{0,2})(\d{0,2})/);
if (!x) return;
+29 -1
View File
@@ -161,7 +161,7 @@
<td><small>{{ c.formula_ref }}</small></td>
<td><form method="post" action="/service/coefficients/toggle"><input type="hidden" name="id" value="{{ c.id }}"><button class="btn btn-{{ 'success' if c.is_active else 'secondary' }} btn-sm">{{ '✓' if c.is_active else '✕' }}</button></form></td>
<td class="actions"><button class="btn btn-secondary btn-sm" onclick="editCoeff({{ c.id }},'{{ c.key|e }}','{{ c.value }}','{{ c.description|e }}','{{ c.formula_ref|e }}')"></button>
<form method="post" action="/service/coefficients/delete" style="display:inline" onsubmit="return confirm('Удалить коэффициент?')"><input type="hidden" name="id" value="{{ c.id }}"><button class="btn btn-danger btn-sm"></button></form>
<form method="post" action="/service/coefficients/delete" style="display:inline" id="delete-formula-coeff-form-{{ c.id }}"><input type="hidden" name="id" value="{{ c.id }}"><button type="button" class="btn btn-danger btn-sm" onclick="openConfirmDeleteFormulaCoeff({{ c.id }},'{{ c.key|e }}')" title="Удалить"></button></form>
</td>
</tr>{% endfor %}</tbody></table>
</div>
@@ -199,7 +199,35 @@
<div id="formula-info-content" style="margin-top:12px"></div>
</div></div>
<div class="modal-overlay" id="confirm-delete-formula-coeff">
<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="delete-formula-coeff-name"></strong> будет удалён навсегда. Это действие нельзя отменить.
</p>
<div class="modal-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>
</div>
</div>
<script>
var deleteFormulaCoeffId = null;
function openConfirmDeleteFormulaCoeff(id, name){
deleteFormulaCoeffId = id;
document.getElementById('delete-formula-coeff-name').textContent = name || '#' + id;
document.getElementById('confirm-delete-formula-coeff').classList.add('open');
}
function closeConfirmDeleteFormulaCoeff(){
document.getElementById('confirm-delete-formula-coeff').classList.remove('open');
deleteFormulaCoeffId = null;
}
function confirmDeleteFormulaCoeff(){
if(!deleteFormulaCoeffId) return;
document.getElementById('delete-formula-coeff-form-' + deleteFormulaCoeffId).submit();
closeConfirmDeleteFormulaCoeff();
}
function switchTab(name){
document.querySelectorAll('.tab-content').forEach(function(t){ t.classList.remove('active'); });
document.querySelectorAll('.tab').forEach(function(t){ t.classList.remove('active'); });
+30 -2
View File
@@ -17,9 +17,9 @@
<button class="btn btn-secondary btn-sm" title="Переключить статус"></button>
</form>
<button class="btn btn-secondary btn-sm" onclick="editIdea({{ idea.id }},{{ idea.title|tojson }},{{ idea.description|tojson }})"></button>
<form method="post" action="/service/ideas/delete" style="display:inline" onsubmit="return confirm('Удалить идею?')">
<form method="post" action="/service/ideas/delete" style="display:inline" id="delete-idea-form-{{ idea.id }}">
<input type="hidden" name="id" value="{{ idea.id }}">
<button class="btn btn-danger btn-sm"></button>
<button type="button" class="btn btn-danger btn-sm" onclick="openConfirmDeleteIdea({{ idea.id }},{{ idea.title|tojson }})" title="Удалить"></button>
</form>
</td>
</tr>{% endfor %}</tbody></table>
@@ -38,7 +38,35 @@
</form>
</div></div>
<div class="modal-overlay" id="confirm-delete-idea">
<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="delete-idea-name"></strong> будет удалён навсегда. Это действие нельзя отменить.
</p>
<div class="modal-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>
</div>
</div>
<script>
var deleteIdeaId = null;
function openConfirmDeleteIdea(id, name){
deleteIdeaId = id;
document.getElementById('delete-idea-name').textContent = name || '#' + id;
document.getElementById('confirm-delete-idea').classList.add('open');
}
function closeConfirmDeleteIdea(){
document.getElementById('confirm-delete-idea').classList.remove('open');
deleteIdeaId = null;
}
function confirmDeleteIdea(){
if(!deleteIdeaId) return;
document.getElementById('delete-idea-form-' + deleteIdeaId).submit();
closeConfirmDeleteIdea();
}
function editIdea(id,title,description){
document.getElementById('modal-idea').classList.add('open');
document.getElementById('modal-idea-title').textContent = 'Редактировать идею';
+28 -1
View File
@@ -17,7 +17,7 @@
<td class="actions">
<button class="btn btn-secondary btn-sm" onclick="editObject({{ o.id }},'{{ o.name|e }}','{{ o.customer_id or '' }}','{{ o.object_type|e }}','{{ o.address|e }}','{{ o.contact_person|e }}','{{ o.contact_phone|e }}','{{ o.status }}','{{ o.notes|e }}')"></button>
{% if user.role == 'owner' %}
<form method="post" action="/service/objects/delete" style="display:inline" onsubmit="return confirm('Удалить объект?')"><input type="hidden" name="id" value="{{ o.id }}"><button class="btn btn-danger btn-sm"></button></form>
<form method="post" action="/service/objects/delete" style="display:inline" id="delete-object-form-{{ o.id }}"><input type="hidden" name="id" value="{{ o.id }}"><button type="button" class="btn btn-danger btn-sm" onclick="openConfirmDeleteObject({{ o.id }},'{{ o.name|e }}')" title="Удалить"></button></form>
{% endif %}
</td>
</tr>{% endfor %}</tbody></table>
@@ -48,7 +48,34 @@
</div>
</form>
</div></div>
<div class="modal-overlay" id="confirm-delete-object">
<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="delete-object-name"></strong> будет удалён навсегда. Это действие нельзя отменить.
</p>
<div class="modal-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>
</div>
</div>
<script>
var deleteObjectId = null;
function openConfirmDeleteObject(id, name){
deleteObjectId = id;
document.getElementById('delete-object-name').textContent = name || '#' + id;
document.getElementById('confirm-delete-object').classList.add('open');
}
function closeConfirmDeleteObject(){
document.getElementById('confirm-delete-object').classList.remove('open');
deleteObjectId = null;
}
function confirmDeleteObject(){
if(!deleteObjectId) return;
document.getElementById('delete-object-form-' + deleteObjectId).submit();
closeConfirmDeleteObject();
}
function editObject(id,name,cid,type,addr,person,phone,status,notes){
document.getElementById('modal-object').classList.add('open');
document.getElementById('modal-object-title').textContent = 'Редактировать объект';
@@ -12,7 +12,7 @@
<td><form method="post" action="/service/questionnaire-config/toggle"><input type="hidden" name="id" value="{{ qi.id }}"><button class="btn btn-{{ 'success' if qi.is_active else 'secondary' }} btn-sm">{{ '✓' if qi.is_active else '✕' }}</button></form></td>
<td class="actions">
<button class="btn btn-secondary btn-sm" onclick="editQItem({{ qi.id }},{{ qi.step }},'{{ qi.section|e }}','{{ qi.question_key|e }}','{{ qi.label|e }}','{{ qi.type }}','{{ qi.required|lower }}','{{ qi.sort_order }}','{{ qi.help_text|e }}')"></button>
<form method="post" action="/service/questionnaire-config/delete" style="display:inline" onsubmit="return confirm('Удалить вопрос?')"><input type="hidden" name="id" value="{{ qi.id }}"><button class="btn btn-danger btn-sm"></button></form>
<form method="post" action="/service/questionnaire-config/delete" style="display:inline" id="delete-qitem-form-{{ qi.id }}"><input type="hidden" name="id" value="{{ qi.id }}"><button type="button" class="btn btn-danger btn-sm" onclick="openConfirmDeleteQitem({{ qi.id }},'{{ qi.label|e }}')" title="Удалить"></button></form>
</td>
</tr>{% endfor %}</tbody></table>
</div>
@@ -40,7 +40,34 @@
<button type="button" class="btn btn-secondary" onclick="this.closest('.modal-overlay').classList.remove('open')">Отмена</button></div>
</form>
</div></div>
<div class="modal-overlay" id="confirm-delete-qitem">
<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="delete-qitem-name"></strong> будет удалён навсегда. Это действие нельзя отменить.
</p>
<div class="modal-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>
</div>
</div>
<script>
var deleteQitemId = null;
function openConfirmDeleteQitem(id, name){
deleteQitemId = id;
document.getElementById('delete-qitem-name').textContent = name || '#' + id;
document.getElementById('confirm-delete-qitem').classList.add('open');
}
function closeConfirmDeleteQitem(){
document.getElementById('confirm-delete-qitem').classList.remove('open');
deleteQitemId = null;
}
function confirmDeleteQitem(){
if(!deleteQitemId) return;
document.getElementById('delete-qitem-form-' + deleteQitemId).submit();
closeConfirmDeleteQitem();
}
function editQItem(id,step,section,key,label,type,required,order,help){
document.getElementById('modal-qitem').classList.add('open');
document.getElementById('modal-qitem-title').textContent = 'Редактировать вопрос';
@@ -99,7 +99,7 @@ function loadPermissions(){
method: 'POST', headers: {'Content-Type': 'application/json'},
body: JSON.stringify({role: role, permissions: perms})
}).then(function(r){ return r.json() }).then(function(d){
if(d.ok) alert('Настройки роли "' + role + '" сохранены');
if(d.ok) showNotification('Настройки роли "' + role + '" сохранены', 'success');
});
});
});
+30 -1
View File
@@ -19,7 +19,7 @@
<td><span class="badge badge-{{ t.status }}">{{ {'open':'Открыта','in_progress':'В работе','completed':'Завершена','cancelled':'Отменена'}[t.status] }}</span></td>
<td><small>{{ t.deadline.strftime('%d.%m.%Y') if t.deadline else '' }}</small></td>
<td class="actions">{% if t.status in ['open','in_progress'] and (user.role in ['owner','engineer'] or t.assigned_to == user.id) %}
<form method="post" action="/service/tasks/close" style="display:inline" onsubmit="return confirm('Завершить задачу?')"><input type="hidden" name="id" value="{{ t.id }}"><button class="btn btn-success btn-sm">✓ Закрыть</button></form>
<form method="post" action="/service/tasks/close" style="display:inline" id="close-task-form-{{ t.id }}"><input type="hidden" name="id" value="{{ t.id }}"><button type="button" class="btn btn-success btn-sm" onclick="openConfirmCloseTask({{ t.id }},'{{ t.title|e }}')" title="Завершить">✓ Закрыть</button></form>
{% endif %}
</td>
</tr>{% endfor %}</tbody></table>
@@ -43,4 +43,33 @@
<button type="button" class="btn btn-secondary" onclick="this.closest('.modal-overlay').classList.remove('open')">Отмена</button></div>
</form>
</div></div>
<div class="modal-overlay" id="confirm-close-task">
<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="delete-task-name"></strong> будет завершена. Это действие нельзя отменить.
</p>
<div class="modal-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>
</div>
</div>
<script>
var closeTaskId = null;
function openConfirmCloseTask(id, name){
closeTaskId = id;
document.getElementById('delete-task-name').textContent = name || '#' + id;
document.getElementById('confirm-close-task').classList.add('open');
}
function closeConfirmCloseTask(){
document.getElementById('confirm-close-task').classList.remove('open');
closeTaskId = null;
}
function confirmCloseTask(){
if(!closeTaskId) return;
document.getElementById('close-task-form-' + closeTaskId).submit();
closeConfirmCloseTask();
}
</script>
{% endblock %}
+29 -2
View File
@@ -16,9 +16,9 @@
<td class="actions">
<button class="btn btn-secondary btn-sm" onclick="editUser({{ u.id }},'{{ u.login }}','{{ u.full_name }}','{{ u.role }}','{{ u.phone }}','{{ u.email }}','{{ u.max_user_id or '' }}',{{ 'true' if u.is_active else 'false' }})"></button>
{% if u.role != 'owner' %}
<form method="post" action="/service/users/delete" style="display:inline" onsubmit="return confirm('Удалить пользователя?')">
<form method="post" action="/service/users/delete" style="display:inline" id="delete-user-form-{{ u.id }}">
<input type="hidden" name="id" value="{{ u.id }}">
<button type="submit" class="btn btn-danger btn-sm"></button>
<button type="button" class="btn btn-danger btn-sm" onclick="openConfirmDeleteUser({{ u.id }},'{{ u.login|e }}')" title="Удалить"></button>
</form>{% endif %}
</td>
</tr>{% endfor %}</tbody></table>
@@ -53,7 +53,34 @@
</form>
</div>
</div>
<div class="modal-overlay" id="confirm-delete-user">
<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="delete-user-name"></strong> будет удалён навсегда. Это действие нельзя отменить.
</p>
<div class="modal-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>
</div>
</div>
<script>
var deleteUserId = null;
function openConfirmDeleteUser(id, name){
deleteUserId = id;
document.getElementById('delete-user-name').textContent = name || '#' + id;
document.getElementById('confirm-delete-user').classList.add('open');
}
function closeConfirmDeleteUser(){
document.getElementById('confirm-delete-user').classList.remove('open');
deleteUserId = null;
}
function confirmDeleteUser(){
if(!deleteUserId) return;
document.getElementById('delete-user-form-' + deleteUserId).submit();
closeConfirmDeleteUser();
}
function editUser(id,login,name,role,phone,email,maxId,active){
document.getElementById('modal-user').classList.add('open');
document.getElementById('modal-user-title').textContent = 'Редактировать сотрудника';
+1 -1
View File
@@ -1 +1 @@
1.8.3
1.8.4