From 8483c64fa4b9b233a2418f3e4cf5f7866ef30d3d Mon Sep 17 00:00:00 2001 From: jackrescuer-gif Date: Wed, 22 Apr 2026 21:14:37 +0300 Subject: [PATCH 1/2] =?UTF-8?q?feat:=20=D1=83=D0=BF=D1=80=D0=BE=D1=81?= =?UTF-8?q?=D1=82=D0=B8=D1=82=D1=8C=20=D1=80=D0=B5=D0=B3=D0=B8=D1=81=D1=82?= =?UTF-8?q?=D1=80=D0=B0=D1=86=D0=B8=D1=8E=20=D0=B8=20=D0=B4=D0=BE=D0=B1?= =?UTF-8?q?=D0=B0=D0=B2=D0=B8=D1=82=D1=8C=20=D1=81=D1=82=D0=B0=D1=82=D0=B8?= =?UTF-8?q?=D1=81=D1=82=D0=B8=D0=BA=D1=83=20=D0=BF=D0=BE=D0=BB=D1=8C=D0=B7?= =?UTF-8?q?=D0=BE=D0=B2=D0=B0=D1=82=D0=B5=D0=BB=D0=B5=D0=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Форма регистрации: - Разбить поле «Имя» на «Имя» и «Фамилию» (два отдельных инпута) - Email заполняется автоматически: имя.фамилия@flowtask.dev - Транслитерация кириллицы → латиница (иван.петров → ivan.petrov) - Домен flowtask.dev подставляется сразу (как у novak.pavel@flowtask.dev) - Предзаполненные профили отправляются в заявки как и раньше Раздел «Пользователи» (AdminUsersPage): - Добавить колонки: Спейсы, Доски, Задачи, Участники, Активность - Под именем показывать дату создания аккаунта Backend (admin.service.ts): - listUsers() дополнить агрегацией: кол-во спейсов, досок, задач и участников через Prisma _count + вложенные createdWorkspaces --- backend/src/modules/admin/admin.service.ts | 23 ++++- frontend/src/pages/AdminUsersPage.tsx | 29 ++++-- frontend/src/pages/LoginPage.tsx | 112 +++++++++++++-------- frontend/src/types/index.ts | 8 ++ 4 files changed, 119 insertions(+), 53 deletions(-) diff --git a/backend/src/modules/admin/admin.service.ts b/backend/src/modules/admin/admin.service.ts index 669b899..5cf9432 100644 --- a/backend/src/modules/admin/admin.service.ts +++ b/backend/src/modules/admin/admin.service.ts @@ -6,7 +6,7 @@ import { config } from '../../config.js'; import type { CreateUserDto, ReviewRequestDto } from './admin.dto.js'; export async function listUsers() { - return prisma.user.findMany({ + const users = await prisma.user.findMany({ select: { id: true, email: true, @@ -16,9 +16,30 @@ export async function listUsers() { lastLoginAt: true, createdAt: true, isSuperadmin: true, + _count: { + select: { + createdWorkspaces: true, + createdTasks: true, + }, + }, + createdWorkspaces: { + select: { + _count: { select: { boards: true, members: true } }, + }, + }, }, orderBy: { createdAt: 'desc' }, }); + + return users.map(({ createdWorkspaces, _count, ...u }) => ({ + ...u, + stats: { + workspaces: _count.createdWorkspaces, + boards: createdWorkspaces.reduce((s, ws) => s + ws._count.boards, 0), + tasks: _count.createdTasks, + members: createdWorkspaces.reduce((s, ws) => s + ws._count.members, 0), + }, + })); } export async function setUserSuperadmin(userId: string, isSuperadmin: boolean) { diff --git a/frontend/src/pages/AdminUsersPage.tsx b/frontend/src/pages/AdminUsersPage.tsx index 7bff9f4..a3dcc99 100644 --- a/frontend/src/pages/AdminUsersPage.tsx +++ b/frontend/src/pages/AdminUsersPage.tsx @@ -240,7 +240,7 @@ export default function AdminUsersPage() { return (
-
+
{/* Header */}
@@ -280,20 +280,23 @@ export default function AdminUsersPage() { <> {/* Header row */}
Пользователь Email Входов - Последний вход - Создан + Спейсы + Доски + Задачи + Участн. + Активность Роль
{users.map((u, i) => (
- - {u.name} - +
+
+ {u.name} +
+
+ создан {formatDate(u.createdAt)} +
+
{u.email} {u.loginCount} + {u.stats?.workspaces ?? '—'} + {u.stats?.boards ?? '—'} + {u.stats?.tasks ?? '—'} + {u.stats?.members ?? '—'} {formatDate(u.lastLoginAt)} - {formatDate(u.createdAt)}
{u.isSuperadmin && ( diff --git a/frontend/src/pages/LoginPage.tsx b/frontend/src/pages/LoginPage.tsx index 1c736c3..90677d6 100644 --- a/frontend/src/pages/LoginPage.tsx +++ b/frontend/src/pages/LoginPage.tsx @@ -7,6 +7,22 @@ import { useBreakpoint, useResponsiveValue } from '../utils/useBreakpoint'; import * as authApi from '../api/auth'; import api from '../api/client'; +// ─── Transliteration: Cyrillic → Latin for email auto-fill ─────────────────── +const CYR_MAP: Record = { + а:'a',б:'b',в:'v',г:'g',д:'d',е:'e',ё:'e',ж:'zh',з:'z',и:'i',й:'i', + к:'k',л:'l',м:'m',н:'n',о:'o',п:'p',р:'r',с:'s',т:'t',у:'u',ф:'f', + х:'kh',ц:'ts',ч:'ch',ш:'sh',щ:'sch',ъ:'',ы:'y',ь:'',э:'e',ю:'yu',я:'ya', +}; +function transliterate(s: string): string { + return s.toLowerCase().split('').map(c => CYR_MAP[c] ?? (/[a-z0-9]/.test(c) ? c : '')).join(''); +} +function buildEmailPrefix(first: string, last: string): string { + const f = transliterate(first.trim()); + const l = transliterate(last.trim()); + if (f && l) return `${f}.${l}`; + return f || l; +} + // ─── Design tokens (from Paper: artboards 2-0 dark, 3-0 light) ─────────────── type LoginTheme = Record; const DARK_C: LoginTheme = { @@ -304,10 +320,10 @@ export default function LoginPage() { // Existing logic — preserved as-is const [loading, setLoading] = useState(false); const [email, setEmail] = useState(''); - const [emailPrefix, setEmailPrefix] = useState(''); - const [registrationDomain, setRegistrationDomain] = useState(''); + const [registrationDomain, setRegistrationDomain] = useState('flowtask.dev'); const [password, setPassword] = useState(''); - const [name, setName] = useState(''); + const [firstName, setFirstName] = useState(''); + const [lastName, setLastName] = useState(''); const [showRegister, setShowRegister] = useState(false); const [forgotOpen, setForgotOpen] = useState(false); const [forgotEmail, setForgotEmail] = useState(''); @@ -325,14 +341,16 @@ export default function LoginPage() { setLoading(true); try { if (showRegister) { - const registerEmail = registrationDomain ? `${emailPrefix}@${registrationDomain}` : email; - const msg = await register(registerEmail, password, name); + const prefix = buildEmailPrefix(firstName, lastName); + const registerEmail = registrationDomain ? `${prefix}@${registrationDomain}` : email; + const fullName = `${firstName.trim()} ${lastName.trim()}`.trim(); + const msg = await register(registerEmail, password, fullName); message.success(msg); setShowRegister(false); setEmail(''); - setEmailPrefix(''); + setFirstName(''); + setLastName(''); setPassword(''); - setName(''); } else { await login(email, password); navigate('/'); @@ -395,49 +413,57 @@ export default function LoginPage() { {showRegister ? 'Создайте аккаунт FlowTask' : 'Войдите в свой аккаунт FlowTask'}
- {/* Name field (register only) */} + {/* First + Last name (register only) */} {showRegister && ( -
-
- Имя + <> +
+
+
+ Имя +
+ +
+
+
+ Фамилия +
+ +
- -
+ + {/* Auto-filled email preview */} +
+
+ Email (заполняется автоматически) +
+
+ + + + + {buildEmailPrefix(firstName, lastName) + ? `${buildEmailPrefix(firstName, lastName)}@${registrationDomain}` + : `ivan.petrov@${registrationDomain}`} + +
+
+ )} - {/* Email */} + {/* Email — shown for login OR registration without domain */} + {(!showRegister || (showRegister && !registrationDomain)) && (
Email
- {showRegister && registrationDomain ? ( -
- setEmailPrefix(e.target.value)} - placeholder="ivan.petrov" - autoComplete="email" - style={{ - flex: 1, background: 'transparent', border: 'none', outline: 'none', - padding: '11px 14px', fontFamily: '"Inter", system-ui, sans-serif', - fontSize: 14, color: C.inputText, - }} - /> - - @{registrationDomain} - -
- ) : ( - - )} +
+ )} {/* Password */}
@@ -459,7 +485,7 @@ export default function LoginPage() {
{/* Submit button */} -
Фамилия
- +
{/* Auto-filled email preview */}
-
- Email (заполняется автоматически) +
+ Email +
+
+ Генерируется из имени и фамилии
- + - - {buildEmailPrefix(firstName, lastName) - ? `${buildEmailPrefix(firstName, lastName)}@${registrationDomain}` - : `ivan.petrov@${registrationDomain}`} - +
)} {/* Email — shown for login OR registration without domain */} - {(!showRegister || (showRegister && !registrationDomain)) && ( + {(!showRegister || !registrationDomain) && (
Email @@ -485,7 +504,7 @@ export default function LoginPage() {
{/* Submit button */} -