# 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`): - Добавлена `` с 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 скрипта.