# 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 скрипта.
|