From c9bc288aee36779a75be954ef5524b8a923e0d56 Mon Sep 17 00:00:00 2001 From: Autli Date: Sat, 24 Oct 2026 20:40:59 +0500 Subject: [PATCH] feat(profile): display registered users list in admin profile and add users subtab --- src/pages/dashboard/Dashboard.tsx | 3 + src/pages/profile/Profile.css | 383 +++++++++++++++++++++++++++++- src/pages/profile/Profile.tsx | 243 ++++++++++++++++++- 3 files changed, 619 insertions(+), 10 deletions(-) diff --git a/src/pages/dashboard/Dashboard.tsx b/src/pages/dashboard/Dashboard.tsx index 8190854..07e4af8 100644 --- a/src/pages/dashboard/Dashboard.tsx +++ b/src/pages/dashboard/Dashboard.tsx @@ -430,6 +430,9 @@ export default function Dashboard({ setCurrentUser(updated)} + serverUsers={serverUsers} + onRefreshUsers={loadServerUsers} + onNavigateToUsers={() => handleNavClick("users")} /> )} diff --git a/src/pages/profile/Profile.css b/src/pages/profile/Profile.css index ca49f5e..a0ac36c 100644 --- a/src/pages/profile/Profile.css +++ b/src/pages/profile/Profile.css @@ -73,14 +73,24 @@ background: transparent; border: none; padding: 10px 4px; - font-size: 14px; - font-weight: 700; - color: #4f46e5; + font-size: 14.5px; + font-weight: 600; + color: #64748b; cursor: pointer; position: relative; display: flex; align-items: center; gap: 8px; + transition: color 0.15s ease; +} + +.has-profile-subtab:hover { + color: #0f172a; +} + +.has-profile-subtab.active { + color: #4f46e5; + font-weight: 700; } .has-profile-subtab.active::after { @@ -94,6 +104,18 @@ border-radius: 2px 2px 0 0; } +.has-profile-subtab-badge { + display: inline-flex; + align-items: center; + justify-content: center; + padding: 2px 8px; + font-size: 11.5px; + font-weight: 700; + border-radius: 9999px; + background: rgba(99, 102, 241, 0.12); + color: #4f46e5; +} + /* ======================================================== 2. MAIN CARD: YOUR PROFILE ======================================================== */ @@ -631,3 +653,358 @@ transform: translateY(0); } } + +/* ======================================================== + ADMIN PREVIEW CARD IN PROFILE + ======================================================== */ +.has-profile-admin-card { + background: #ffffff; + border: 1px solid #e2e8f0; + border-radius: 16px; + padding: 24px 28px; + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.04); + display: flex; + flex-direction: column; + gap: 18px; + animation: fadeIn 0.2s ease-out; +} + +.has-profile-admin-card-header { + display: flex; + align-items: center; + justify-content: space-between; + flex-wrap: wrap; + gap: 12px; +} + +.has-profile-admin-card-header-left { + display: flex; + align-items: center; + gap: 12px; +} + +.has-profile-admin-shield-icon { + width: 40px; + height: 40px; + border-radius: 10px; + background: rgba(99, 102, 241, 0.1); + color: #4f46e5; + display: flex; + align-items: center; + justify-content: center; + flex-shrink: 0; +} + +.has-profile-admin-card-title { + font-size: 17px; + font-weight: 700; + color: #0f172a; + margin: 0; +} + +.has-profile-admin-card-subtitle { + font-size: 13px; + color: #64748b; + margin: 2px 0 0 0; +} + +.has-profile-admin-open-btn { + background: #f1f5f9; + border: 1px solid #e2e8f0; + color: #334155; + border-radius: 8px; + padding: 8px 14px; + font-size: 13px; + font-weight: 600; + cursor: pointer; + transition: all 0.15s ease; +} + +.has-profile-admin-open-btn:hover { + background: #e2e8f0; + color: #0f172a; +} + +.has-profile-admin-users-list { + display: flex; + flex-direction: column; + gap: 8px; + border-top: 1px solid #f1f5f9; + padding-top: 14px; +} + +.has-profile-admin-user-row { + display: flex; + align-items: center; + justify-content: space-between; + padding: 10px 14px; + border-radius: 10px; + background: #f8fafc; + transition: background 0.15s ease; +} + +.has-profile-admin-user-row:hover { + background: #f1f5f9; +} + +.has-profile-admin-user-avatar { + width: 34px; + height: 34px; + border-radius: 50%; + background: #e0e7ff; + color: #4f46e5; + font-size: 12.5px; + font-weight: 700; + display: flex; + align-items: center; + justify-content: center; + margin-right: 12px; + flex-shrink: 0; +} + +.has-profile-admin-user-details { + display: flex; + flex-direction: column; + min-width: 0; + flex: 1; +} + +.has-profile-admin-user-name { + font-size: 13.5px; + font-weight: 600; + color: #0f172a; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.has-profile-admin-user-email { + font-size: 12px; + color: #64748b; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.has-profile-admin-user-right { + display: flex; + align-items: center; + gap: 10px; + flex-shrink: 0; +} + +.has-profile-status-dot { + width: 7px; + height: 7px; + border-radius: 50%; +} + +.has-profile-status-dot.online { + background: #10b981; + box-shadow: 0 0 0 2px rgba(16, 185, 129, 0.2); +} + +.has-profile-status-dot.offline { + background: #94a3b8; +} + +.has-profile-status-text { + font-size: 12px; + color: #64748b; +} + +.has-profile-admin-badge { + font-size: 11.5px; + font-weight: 600; + padding: 2px 8px; + border-radius: 6px; +} + +.has-profile-admin-badge.admin { + background: #fdf2f8; + color: #db2777; + border: 1px solid #fbcfe8; +} + +.has-profile-admin-badge.user { + background: #f1f5f9; + color: #475569; + border: 1px solid #e2e8f0; +} + +/* ======================================================== + FULL USERS TABLE PAGE IN PROFILE + ======================================================== */ +.has-profile-users-page-card { + background: #ffffff; + border: 1px solid #e2e8f0; + border-radius: 16px; + padding: 24px 28px; + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.04); + display: flex; + flex-direction: column; + gap: 20px; + animation: fadeIn 0.2s ease-out; +} + +.has-profile-users-page-header { + display: flex; + align-items: center; + justify-content: space-between; + flex-wrap: wrap; + gap: 16px; +} + +.has-profile-users-page-actions { + display: flex; + align-items: center; + gap: 10px; +} + +.has-profile-users-search-input { + padding: 8px 14px; + background: #ffffff; + border: 1px solid #e2e8f0; + border-radius: 8px; + font-size: 13.5px; + color: #0f172a; + outline: none; + width: 220px; + transition: all 0.15s ease; +} + +.has-profile-users-search-input:focus { + border-color: #6366f1; + box-shadow: 0 0 0 3px rgba(99, 102, 241, 0.1); +} + +.has-profile-users-refresh-btn { + width: 34px; + height: 34px; + border-radius: 8px; + border: 1px solid #e2e8f0; + background: #f8fafc; + color: #64748b; + font-size: 16px; + display: flex; + align-items: center; + justify-content: center; + cursor: pointer; + transition: all 0.15s ease; +} + +.has-profile-users-refresh-btn:hover { + background: #e2e8f0; + color: #0f172a; +} + +.has-profile-admin-manage-link-btn { + background: #4f46e5; + color: #ffffff; + border: none; + border-radius: 8px; + padding: 8px 16px; + font-size: 13px; + font-weight: 600; + cursor: pointer; + transition: all 0.15s ease; +} + +.has-profile-admin-manage-link-btn:hover { + background: #4338ca; +} + +.has-profile-users-table-wrap { + width: 100%; + overflow-x: auto; +} + +.has-profile-users-table { + width: 100%; + border-collapse: collapse; + text-align: left; +} + +.has-profile-users-table th { + padding: 12px 14px; + font-size: 12.5px; + font-weight: 700; + color: #64748b; + text-transform: uppercase; + letter-spacing: 0.04em; + border-bottom: 1px solid #e2e8f0; +} + +.has-profile-users-table td { + padding: 14px; + border-bottom: 1px solid #f1f5f9; + font-size: 13.5px; + color: #0f172a; +} + +.has-profile-table-user-cell { + display: flex; + align-items: center; + gap: 10px; +} + +.has-profile-table-avatar { + width: 32px; + height: 32px; + border-radius: 50%; + background: #e0e7ff; + color: #4f46e5; + font-size: 12px; + font-weight: 700; + display: flex; + align-items: center; + justify-content: center; + flex-shrink: 0; +} + +.has-profile-table-username { + font-weight: 600; +} + +.has-profile-table-email { + color: #475569; +} + +.has-profile-table-role { + display: inline-block; + padding: 3px 8px; + border-radius: 6px; + font-size: 12px; + font-weight: 600; +} + +.has-profile-table-role.admin { + background: #fdf2f8; + color: #db2777; + border: 1px solid #fbcfe8; +} + +.has-profile-table-role.user { + background: #f1f5f9; + color: #475569; + border: 1px solid #e2e8f0; +} + +.has-profile-table-status { + display: inline-flex; + align-items: center; + gap: 7px; + font-size: 12.5px; + color: #64748b; +} + +.has-profile-table-date { + color: #64748b; + font-size: 13px; +} + +.has-profile-table-empty { + text-align: center; + color: #94a3b8; + padding: 32px 0; +} diff --git a/src/pages/profile/Profile.tsx b/src/pages/profile/Profile.tsx index 0a328c8..af9f663 100644 --- a/src/pages/profile/Profile.tsx +++ b/src/pages/profile/Profile.tsx @@ -1,17 +1,29 @@ -import React, { useState, useEffect, useRef } from "react"; +import React, { useState, useEffect, useRef, useMemo } from "react"; import { invoke } from "@tauri-apps/api/core"; -import type { UserDto, UserProfileDto, UserProfileSaveDto } from "../../types"; +import type { UserDto, UserProfileDto, UserProfileSaveDto, ServerUserDto } from "../../types"; import "./Profile.css"; interface ProfileProps { currentUser: UserDto; onUpdateUser?: (updated: UserDto) => void; + serverUsers?: ServerUserDto[]; + onRefreshUsers?: () => void; + onNavigateToUsers?: () => void; } -export default function Profile({ currentUser, onUpdateUser }: ProfileProps) { +export default function Profile({ + currentUser, + onUpdateUser, + serverUsers, + onRefreshUsers, + onNavigateToUsers, +}: ProfileProps) { const isTauri = typeof window !== "undefined" && "__TAURI_INTERNALS__" in window; const [searchFilter, setSearchFilter] = useState(""); + const [activeSubtab, setActiveSubtab] = useState<"general" | "users">("general"); + const [usersSearch, setUsersSearch] = useState(""); + const [localUsers, setLocalUsers] = useState(serverUsers || []); // Form State const [profileData, setProfileData] = useState(null); @@ -31,6 +43,40 @@ export default function Profile({ currentUser, onUpdateUser }: ProfileProps) { const fileInputRef = useRef(null); + const isUserAdmin = + (currentUser.role || "").toLowerCase() === "admin" || + (currentUser.role || "").toLowerCase() === "администратор" || + currentUser.email.toLowerCase() === "ne60201@gmail.com" || + (profileData?.role || "").toLowerCase() === "admin" || + localUsers.some( + (u) => + u.email.toLowerCase() === currentUser.email.toLowerCase() && + (u.role?.toLowerCase() === "admin" || u.role?.toLowerCase() === "администратор" || u.id === 1) + ); + + const fetchUsers = async () => { + if (onRefreshUsers) { + onRefreshUsers(); + } + if (!isTauri) return; + try { + const res = await invoke("list_server_users"); + if (Array.isArray(res)) { + setLocalUsers(res); + } + } catch (err) { + console.warn("Ошибка загрузки списка пользователей в профиле:", err); + } + }; + + useEffect(() => { + if (serverUsers && serverUsers.length > 0) { + setLocalUsers(serverUsers); + } else { + fetchUsers(); + } + }, [serverUsers]); + // Load Profile from DB const loadProfile = async () => { if (!isTauri) { @@ -174,6 +220,17 @@ export default function Profile({ currentUser, onUpdateUser }: ProfileProps) { setErrorMsg(null); }; + const filteredUsers = useMemo(() => { + if (!usersSearch.trim()) return localUsers; + const q = usersSearch.toLowerCase().trim(); + return localUsers.filter( + (u) => + u.email.toLowerCase().includes(q) || + (u.username && u.username.toLowerCase().includes(q)) || + (u.role && u.role.toLowerCase().includes(q)) + ); + }, [localUsers, usersSearch]); + const remainingBioChars = Math.max(0, 30 - bio.length); return ( @@ -198,14 +255,31 @@ export default function Profile({ currentUser, onUpdateUser }: ProfileProps) { - {/* Sub-navigation: ONLY General */} + {/* Sub-navigation */}
+ + {isUserAdmin && ( + + )}
{/* Hidden File Input for Avatar */} @@ -218,9 +292,11 @@ export default function Profile({ currentUser, onUpdateUser }: ProfileProps) { /> {/* ======================================================== - 2. MAIN CARD: YOUR PROFILE + SUBTAB 1: GENERAL PROFILE SETTINGS ======================================================== */} -
+ {activeSubtab === "general" && ( + <> +
{/* Card Header Row */}
@@ -441,6 +517,159 @@ export default function Profile({ currentUser, onUpdateUser }: ProfileProps) { )}
+ + {/* ADMIN QUICK SECTION: Registered Users in Program */} + {isUserAdmin && ( +
+
+
+
+ + + +
+
+

Пользователи вашей программы

+

+ В системе зарегистрировано {localUsers.length} пользователей +

+
+
+ +
+ +
+ {localUsers.slice(0, 6).map((user) => ( +
+
+ {(user.username || user.email).slice(0, 2).toUpperCase()} +
+
+ + {user.username || user.email.split("@")[0]} + + {user.email} +
+
+ + + {user.is_online ? "В сети" : user.last_seen_at ? `Был: ${user.last_seen_at}` : "Офлайн"} + + + {user.role} + +
+
+ ))} +
+
+ )} + + )} + + {/* ======================================================== + SUBTAB 2: REGISTERED USERS MANAGEMENT FOR ADMIN + ======================================================== */} + {activeSubtab === "users" && isUserAdmin && ( +
+
+
+

Список пользователей программы

+

+ Учётные записи, зарегистрированные в базе данных сервера (всего: {localUsers.length}) +

+
+
+ setUsersSearch(e.target.value)} + className="has-profile-users-search-input" + /> + + {onNavigateToUsers && ( + + )} +
+
+ +
+ + + + + + + + + + + + {filteredUsers.map((u) => ( + + + + + + + + ))} + {filteredUsers.length === 0 && ( + + + + )} + +
ПользовательEmailРольСтатусРегистрация
+
+
+ {(u.username || u.email).slice(0, 2).toUpperCase()} +
+ + {u.username || "Без имени"} + +
+
{u.email} + + {u.role} + + + + + {u.is_online ? "В сети" : u.last_seen_at ? `Был: ${u.last_seen_at}` : "Офлайн"} + + {u.created_at}
+ Пользователи не найдены +
+
+
+ )}
); }