Files
Learn_System/plans/admin-redesign/phase-5-quick-actions.md
T
Maxim Dolgolyov 69113ab35e feat(admin): phase 5 — per-row quick actions for users + sessions
Hover-only action buttons (right-aligned, opacity transition, hidden on mobile).

- users.js: 4 actions (ban/unban, award coins, sessions, delete) — replaces `>` glyph cell, falls back to glyph for non-admin / self

- sessions.js: 2 actions (view, delete)

- DELETE /api/admin/sessions/:id (NEW): transactional (assignment_sessions=NULL, user_answers, session_questions, test_sessions), audit-logged, admin-only

- event.stopPropagation defence-in-depth (each button + parent .row-actions)

- LS.confirm for destructive ops; LS.modal for award-coins amount/reason

- CSS injected once via #row-actions-style id-dedup (same content in both sections)

Existing user-panel overlay + session toggle-drawer flows untouched (Phase 6 removes overlay).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-16 23:53:19 +03:00

114 lines
8.7 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Phase 5: Per-row quick actions
**Status:** ✅ Done
**Parent plan:** [PLAN.md](./PLAN.md)
**Domain:** frontend
**Parallelizable with:** Phase 3, Phase 4
## Objective
На hover-строке user / session показывать кнопки частых action прямо в таблице — без открытия overlay-панели. Сокращает 2-3 клика до 1 для типичных задач (бан, выдача монет, удаление сессии).
## Tasks
- [x] **Users table** (`frontend/js/admin/sections/users.js`):
- Добавлена `<td class="row-actions-cell">` с inline-flex блоком `.row-actions` (заменяет старый `` индикатор)
- Visible: только на `:hover` строки (CSS opacity transition)
- Кнопки (inline SVG, Lucide-style):
- **Ban / Unban** — `quickToggleBan(uid, isBanned, btn)``LS.confirm``LS.adminBanUser`
- **Award coins** — `quickAwardCoins(uid, name)``LS.modal` (sm) с inputs amount+reason → `LS.adminShopAwardCoins`
- **Sessions** — `quickOpenUserSessions(uid)``AdminRouter.navigate('#sessions')` (fallback на `switchTab`)
- **Delete** — `quickDeleteUser(uid, name, btn)``LS.confirm` (destructive) → `LS.adminDeleteUser`
- SVG-иконки (inline, Lucide outline-style), НЕТ эмоджи
- `event.stopPropagation()` на каждой кнопке + на родительском `.row-actions` (чтобы не открывать user-panel overlay)
- Hidden для self (`u.id !== user.id`) и для non-admin — fallback на старый ``
- [x] **Sessions table** (`frontend/js/admin/sections/sessions.js`):
- **View (eye icon)** — `toggleDrawer(id)` (тот же flow что и row-click)
- **Delete (trash, danger)** — `quickDeleteSession(id, btn)``LS.confirm``LS.adminDeleteSession``load()` (refresh)
- [x] **Backend `DELETE /api/admin/sessions/:id`** — endpoint отсутствовал, добавлен:
- Route: `backend/src/routes/admin.js` (внутри `requireRole('admin')` блока)
- Controller: `deleteSession(req, res, next)` в `adminController.js` — транзакция:
1. `UPDATE assignment_sessions SET session_id = NULL WHERE session_id = ?` (explicit null, hoarded slot stays)
2. `DELETE FROM user_answers WHERE session_id = ?` (FK has `ON DELETE CASCADE`, но делаем явно)
3. `DELETE FROM session_questions WHERE session_id = ?` (то же)
4. `DELETE FROM test_sessions WHERE id = ?`
- Audit: `audit(req, 'session.delete', 'session:${sid}', 'user:N mode:X')`
- Validates `Number.isInteger(sid) && sid > 0`; 404 if not found
- API helper: `LS.adminDeleteSession(id)``DELETE /admin/sessions/:id`
- [x] **CSS** (`#row-actions-style`):
- Inject ONCE из обеих секций (de-dup по element id) — оба `ensureRowActionsStyles()` проверяют `getElementById('row-actions-style')` перед добавлением
- Стили: `.row-actions`, `.row-action-btn` (default + .danger), `.row-actions-cell`, `@media (max-width: 768px) { display: none }`
- Также handle `tr.selected .row-actions` и `.sess-tl-item.open .row-actions` → opacity 1 (для активных строк)
- [x] `title="…"` на каждой кнопке (tooltip)
- [x] `LS.confirm(message, { title, confirmText })` использован везде (signature: `lsConfirm(message, { title, confirmText, danger=true })``danger:true` default, gradient pink→violet)
## Files to Modify/Create
- `frontend/js/admin/sections/users.js` — модификация renderRow + action handlers (~50-100L добавления)
- `frontend/js/admin/sections/sessions.js` — same (~30-50L)
- `frontend/admin.html` — стили для `.row-actions` (~30L)
- `backend/src/controllers/adminController.js``deleteSession` если отсутствует
- `backend/src/routes/admin.js``DELETE /sessions/:id` если отсутствует
## Acceptance Criteria
- Hover на user row → видны 4 кнопки справа без раздвигания layout
- Hover на session row → видны 2 кнопки
- Каждая кнопка работает (ban / coins / sessions / delete)
- Click на кнопку НЕ открывает user-panel overlay (stopPropagation)
- Tooltip на hover каждой кнопки
- Confirm для деструктивных action (delete, ban)
- LS.toast после success
- Auth check — все action available только admin
- Mobile: actions hidden (tap-only context), либо альтернативный UI (long-press → menu) — пока минимум скрыть на ≤768px
## Notes
### Существующие helpers использовать
- `LS.confirm(message, { okText, danger })` для подтверждений
- `LS.modal(...)` если нужна форма (например award coins amount)
- `LS.toast` для feedback
- Существующие admin* функции (toggleBanUser, awardCoins, etc.) — не дублировать
### Визуальный паттерн
Inspired by Linear / Vercel admin: actions visible on row hover, positioned right-aligned, ghost-style buttons (transparent bg, border on hover). Иконки только.
### Что НЕ делать в этой фазе
- Не делать bulk-actions (select multiple → action) — это после merge
- Не делать undo (toast с "отменить" внутри) — Phase 6+
- Не менять структуру таблицы radically
## Review Checklist
- [x] Кнопки не сдвигают layout — `opacity: 0 → 1` без display swap, занимают слот старого ``
- [x] Имя пользователя в onclick экранируется через `esc()` + `replace(/'/g, "\\'")` для безопасности SQL/HTML-injection в строковых литералах
- [x] No emoji — только inline SVG (Lucide-style outline-stroke, viewBox 24x24)
- [x] `event.stopPropagation()` на каждой кнопке + на родительском `.row-actions` div (defence in depth)
- [x] Confirm через `LS.confirm` для destructive (delete user, delete session, ban/unban)
- [x] `title` атрибут есть на каждой кнопке
- [x] Mobile (≤768px): `.row-actions { display: none }` — row-click overlay по-прежнему работает как fallback
- [x] `node --check` all modified files OK
- [x] Tests: 32/35 pass (3 pre-existing auth-test failures, unrelated)
## Handoff to Next Phase
**Phase 6 (deep entity pages) рекомендации:**
1. **`quickOpenUserSessions(uid)`** сейчас просто навигирует на `#sessions` без фильтра. Phase 6 должна:
- Расширить router до `#sessions?user=N` (или новый формат `#sessions/user/N`)
- В `sessions.js` `load()` читать query param и передавать `user_id` в `LS.adminGetSessions({ user_id })` (backend уже поддерживает `user_id` query param — см. `getAllSessions` controller)
- Обновить хелпер: `AdminRouter.navigate('#sessions?user=' + uid)` (когда router научится parse'ить query)
2. **User-panel overlay vs hover actions:** Phase 6 удалит старую `.user-panel` overlay. Когда это произойдёт, row-click больше не будет открывать панель. Hover-actions останутся как primary UX. Рекомендация: при удалении overlay row-click сделать `onclick="AdminRouter.navigate('#users/' + uid)"` (deep page).
3. **Mobile UX gap:** на ≤768px actions сейчас полностью скрыты. Когда Phase 6 добавит deep page, mobile-row-click станет переходом на deep page → primary actions доступны там. До тех пор mobile = read-only browse.
4. **Backend `DELETE /admin/sessions/:id`** уже там, готов для Phase 6 deep session page (где будет кнопка "Удалить эту сессию" в header).
5. **Award coins modal pattern** (используем `LS.modal` с body=DOM Node + actions с `onClick({close, setError})`) — может быть полезен Phase 6 для inline-edit flow на deep user page.
6. **Linter note:** `npm run lint:routes` показывает FAIL (65 unprotected vs baseline 56) — pre-existing проблема, my new admin-protected `DELETE /sessions/:id` добавил +1 false-positive (роут защищён через `router.use(requireRole('admin'))` блок, который linter не видит). Не требует действий — это known limitation скрипта.