feat(profile): display registered users list in admin profile and add users subtab
This commit is contained in:
parent
d1c2d4e62f
commit
c9bc288aee
3 changed files with 619 additions and 10 deletions
|
|
@ -430,6 +430,9 @@ export default function Dashboard({
|
||||||
<Profile
|
<Profile
|
||||||
currentUser={currentUser}
|
currentUser={currentUser}
|
||||||
onUpdateUser={(updated) => setCurrentUser(updated)}
|
onUpdateUser={(updated) => setCurrentUser(updated)}
|
||||||
|
serverUsers={serverUsers}
|
||||||
|
onRefreshUsers={loadServerUsers}
|
||||||
|
onNavigateToUsers={() => handleNavClick("users")}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -73,14 +73,24 @@
|
||||||
background: transparent;
|
background: transparent;
|
||||||
border: none;
|
border: none;
|
||||||
padding: 10px 4px;
|
padding: 10px 4px;
|
||||||
font-size: 14px;
|
font-size: 14.5px;
|
||||||
font-weight: 700;
|
font-weight: 600;
|
||||||
color: #4f46e5;
|
color: #64748b;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
position: relative;
|
position: relative;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 8px;
|
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 {
|
.has-profile-subtab.active::after {
|
||||||
|
|
@ -94,6 +104,18 @@
|
||||||
border-radius: 2px 2px 0 0;
|
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
|
2. MAIN CARD: YOUR PROFILE
|
||||||
======================================================== */
|
======================================================== */
|
||||||
|
|
@ -631,3 +653,358 @@
|
||||||
transform: translateY(0);
|
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;
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -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 { invoke } from "@tauri-apps/api/core";
|
||||||
import type { UserDto, UserProfileDto, UserProfileSaveDto } from "../../types";
|
import type { UserDto, UserProfileDto, UserProfileSaveDto, ServerUserDto } from "../../types";
|
||||||
import "./Profile.css";
|
import "./Profile.css";
|
||||||
|
|
||||||
interface ProfileProps {
|
interface ProfileProps {
|
||||||
currentUser: UserDto;
|
currentUser: UserDto;
|
||||||
onUpdateUser?: (updated: UserDto) => void;
|
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 isTauri = typeof window !== "undefined" && "__TAURI_INTERNALS__" in window;
|
||||||
|
|
||||||
const [searchFilter, setSearchFilter] = useState("");
|
const [searchFilter, setSearchFilter] = useState("");
|
||||||
|
const [activeSubtab, setActiveSubtab] = useState<"general" | "users">("general");
|
||||||
|
const [usersSearch, setUsersSearch] = useState("");
|
||||||
|
const [localUsers, setLocalUsers] = useState<ServerUserDto[]>(serverUsers || []);
|
||||||
|
|
||||||
// Form State
|
// Form State
|
||||||
const [profileData, setProfileData] = useState<UserProfileDto | null>(null);
|
const [profileData, setProfileData] = useState<UserProfileDto | null>(null);
|
||||||
|
|
@ -31,6 +43,40 @@ export default function Profile({ currentUser, onUpdateUser }: ProfileProps) {
|
||||||
|
|
||||||
const fileInputRef = useRef<HTMLInputElement | null>(null);
|
const fileInputRef = useRef<HTMLInputElement | null>(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<ServerUserDto[]>("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
|
// Load Profile from DB
|
||||||
const loadProfile = async () => {
|
const loadProfile = async () => {
|
||||||
if (!isTauri) {
|
if (!isTauri) {
|
||||||
|
|
@ -174,6 +220,17 @@ export default function Profile({ currentUser, onUpdateUser }: ProfileProps) {
|
||||||
setErrorMsg(null);
|
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);
|
const remainingBioChars = Math.max(0, 30 - bio.length);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
@ -198,14 +255,31 @@ export default function Profile({ currentUser, onUpdateUser }: ProfileProps) {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Sub-navigation: ONLY General */}
|
{/* Sub-navigation */}
|
||||||
<div className="has-profile-subtabs-row">
|
<div className="has-profile-subtabs-row">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="has-profile-subtab active"
|
className={`has-profile-subtab ${activeSubtab === "general" ? "active" : ""}`}
|
||||||
|
onClick={() => setActiveSubtab("general")}
|
||||||
>
|
>
|
||||||
Основные
|
Основные
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
|
{isUserAdmin && (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className={`has-profile-subtab ${activeSubtab === "users" ? "active" : ""}`}
|
||||||
|
onClick={() => {
|
||||||
|
setActiveSubtab("users");
|
||||||
|
fetchUsers();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span>Пользователи программы</span>
|
||||||
|
{localUsers.length > 0 && (
|
||||||
|
<span className="has-profile-subtab-badge">{localUsers.length}</span>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Hidden File Input for Avatar */}
|
{/* 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
|
||||||
======================================================== */}
|
======================================================== */}
|
||||||
<div className="has-profile-card">
|
{activeSubtab === "general" && (
|
||||||
|
<>
|
||||||
|
<div className="has-profile-card">
|
||||||
{/* Card Header Row */}
|
{/* Card Header Row */}
|
||||||
<div className="has-profile-card-header">
|
<div className="has-profile-card-header">
|
||||||
<div>
|
<div>
|
||||||
|
|
@ -441,6 +517,159 @@ export default function Profile({ currentUser, onUpdateUser }: ProfileProps) {
|
||||||
</form>
|
</form>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* ADMIN QUICK SECTION: Registered Users in Program */}
|
||||||
|
{isUserAdmin && (
|
||||||
|
<div className="has-profile-admin-card">
|
||||||
|
<div className="has-profile-admin-card-header">
|
||||||
|
<div className="has-profile-admin-card-header-left">
|
||||||
|
<div className="has-profile-admin-shield-icon">
|
||||||
|
<svg width={22} height={22} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={2}>
|
||||||
|
<path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h3 className="has-profile-admin-card-title">Пользователи вашей программы</h3>
|
||||||
|
<p className="has-profile-admin-card-subtitle">
|
||||||
|
В системе зарегистрировано <strong>{localUsers.length}</strong> пользователей
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="has-profile-admin-open-btn"
|
||||||
|
onClick={() => {
|
||||||
|
if (onNavigateToUsers) {
|
||||||
|
onNavigateToUsers();
|
||||||
|
} else {
|
||||||
|
setActiveSubtab("users");
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Все пользователи ({localUsers.length}) →
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="has-profile-admin-users-list">
|
||||||
|
{localUsers.slice(0, 6).map((user) => (
|
||||||
|
<div key={user.id} className="has-profile-admin-user-row">
|
||||||
|
<div className="has-profile-admin-user-avatar">
|
||||||
|
{(user.username || user.email).slice(0, 2).toUpperCase()}
|
||||||
|
</div>
|
||||||
|
<div className="has-profile-admin-user-details">
|
||||||
|
<span className="has-profile-admin-user-name">
|
||||||
|
{user.username || user.email.split("@")[0]}
|
||||||
|
</span>
|
||||||
|
<span className="has-profile-admin-user-email">{user.email}</span>
|
||||||
|
</div>
|
||||||
|
<div className="has-profile-admin-user-right">
|
||||||
|
<span className={`has-profile-status-dot ${user.is_online ? "online" : "offline"}`} />
|
||||||
|
<span className="has-profile-status-text">
|
||||||
|
{user.is_online ? "В сети" : user.last_seen_at ? `Был: ${user.last_seen_at}` : "Офлайн"}
|
||||||
|
</span>
|
||||||
|
<span className={`has-profile-admin-badge ${user.role === "Администратор" ? "admin" : "user"}`}>
|
||||||
|
{user.role}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* ========================================================
|
||||||
|
SUBTAB 2: REGISTERED USERS MANAGEMENT FOR ADMIN
|
||||||
|
======================================================== */}
|
||||||
|
{activeSubtab === "users" && isUserAdmin && (
|
||||||
|
<div className="has-profile-users-page-card">
|
||||||
|
<div className="has-profile-users-page-header">
|
||||||
|
<div>
|
||||||
|
<h2 className="has-profile-card-title">Список пользователей программы</h2>
|
||||||
|
<p className="has-profile-card-subtitle">
|
||||||
|
Учётные записи, зарегистрированные в базе данных сервера (всего: {localUsers.length})
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="has-profile-users-page-actions">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
placeholder="Поиск по имени или email..."
|
||||||
|
value={usersSearch}
|
||||||
|
onChange={(e) => setUsersSearch(e.target.value)}
|
||||||
|
className="has-profile-users-search-input"
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="has-profile-users-refresh-btn"
|
||||||
|
onClick={fetchUsers}
|
||||||
|
title="Обновить список"
|
||||||
|
>
|
||||||
|
↻
|
||||||
|
</button>
|
||||||
|
{onNavigateToUsers && (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="has-profile-admin-manage-link-btn"
|
||||||
|
onClick={onNavigateToUsers}
|
||||||
|
>
|
||||||
|
Управление и права →
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="has-profile-users-table-wrap">
|
||||||
|
<table className="has-profile-users-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Пользователь</th>
|
||||||
|
<th>Email</th>
|
||||||
|
<th>Роль</th>
|
||||||
|
<th>Статус</th>
|
||||||
|
<th>Регистрация</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{filteredUsers.map((u) => (
|
||||||
|
<tr key={u.id}>
|
||||||
|
<td>
|
||||||
|
<div className="has-profile-table-user-cell">
|
||||||
|
<div className="has-profile-table-avatar">
|
||||||
|
{(u.username || u.email).slice(0, 2).toUpperCase()}
|
||||||
|
</div>
|
||||||
|
<span className="has-profile-table-username">
|
||||||
|
{u.username || "Без имени"}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td className="has-profile-table-email">{u.email}</td>
|
||||||
|
<td>
|
||||||
|
<span className={`has-profile-table-role ${u.role === "Администратор" ? "admin" : "user"}`}>
|
||||||
|
{u.role}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<span className="has-profile-table-status">
|
||||||
|
<span className={`has-profile-status-dot ${u.is_online ? "online" : "offline"}`} />
|
||||||
|
{u.is_online ? "В сети" : u.last_seen_at ? `Был: ${u.last_seen_at}` : "Офлайн"}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td className="has-profile-table-date">{u.created_at}</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
{filteredUsers.length === 0 && (
|
||||||
|
<tr>
|
||||||
|
<td colSpan={5} className="has-profile-table-empty">
|
||||||
|
Пользователи не найдены
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
)}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue