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

8.7 KiB
Raw Blame History

Phase 5: Per-row quick actions

Status: Done Parent plan: PLAN.md Domain: frontend Parallelizable with: Phase 3, Phase 4

Objective

На hover-строке user / session показывать кнопки частых action прямо в таблице — без открытия overlay-панели. Сокращает 2-3 клика до 1 для типичных задач (бан, выдача монет, удаление сессии).

Tasks

  • 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 / UnbanquickToggleBan(uid, isBanned, btn)LS.confirmLS.adminBanUser
      • Award coinsquickAwardCoins(uid, name)LS.modal (sm) с inputs amount+reason → LS.adminShopAwardCoins
      • SessionsquickOpenUserSessions(uid)AdminRouter.navigate('#sessions') (fallback на switchTab)
      • DeletequickDeleteUser(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 на старый
  • Sessions table (frontend/js/admin/sections/sessions.js):
    • View (eye icon)toggleDrawer(id) (тот же flow что и row-click)
    • Delete (trash, danger)quickDeleteSession(id, btn)LS.confirmLS.adminDeleteSessionload() (refresh)
  • 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
  • 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 (для активных строк)
  • title="…" на каждой кнопке (tooltip)
  • 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.jsdeleteSession если отсутствует
  • backend/src/routes/admin.jsDELETE /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

  • Кнопки не сдвигают layout — opacity: 0 → 1 без display swap, занимают слот старого
  • Имя пользователя в onclick экранируется через esc() + replace(/'/g, "\\'") для безопасности SQL/HTML-injection в строковых литералах
  • No emoji — только inline SVG (Lucide-style outline-stroke, viewBox 24x24)
  • event.stopPropagation() на каждой кнопке + на родительском .row-actions div (defence in depth)
  • Confirm через LS.confirm для destructive (delete user, delete session, ban/unban)
  • title атрибут есть на каждой кнопке
  • Mobile (≤768px): .row-actions { display: none } — row-click overlay по-прежнему работает как fallback
  • node --check all modified files OK
  • 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 скрипта.