diff --git a/frontend/src/modules/admin/AdminPanel.tsx b/frontend/src/modules/admin/AdminPanel.tsx index 4ee11a7..0fd035a 100644 --- a/frontend/src/modules/admin/AdminPanel.tsx +++ b/frontend/src/modules/admin/AdminPanel.tsx @@ -1,10 +1,26 @@ -import React from "react"; -import { FaUsers, FaShieldAlt } from "react-icons/fa"; +import React, { useEffect, useState } from "react"; +import { + FaUsers, + FaShieldAlt, + FaSpinner, + FaExclamationCircle, + FaPlus, +} from "react-icons/fa"; import { useAdminStore } from "./store/useAdminStore"; import { UserCard } from "./components/UserCard"; +import { CreateUserModal } from "./components/CreateUserModal"; export const AdminPanel: React.FC = () => { const users = useAdminStore((s) => s.users); + const loading = useAdminStore((s) => s.loading); + const error = useAdminStore((s) => s.error); + const fetchUsers = useAdminStore((s) => s.fetchUsers); + + const [showCreateModal, setShowCreateModal] = useState(false); + + useEffect(() => { + fetchUsers(); + }, []); const activeCount = users.filter((u) => u.is_active).length; @@ -45,24 +61,106 @@ export const AdminPanel: React.FC = () => { Управление пользователями - {activeCount} / {users.length} активных + {loading + ? "Загрузка..." + : `${activeCount} / ${users.length} активных`} + + + {/* Error */} + {error && ( +
+ + {error} +
+ )} + + {/* Loading */} + {loading && users.length === 0 && ( +
+ +
+ )} + {/* Users list */} -
- {users.map((user) => ( - - ))} -
+ {!loading && ( +
+ {users.map((user) => ( + + ))} +
+ )} + + {/* Empty state */} + {!loading && users.length === 0 && ( +
+

+ Нет зарегистрированных пользователей +

+
+ )} + + {/* Create user modal */} + setShowCreateModal(false)} + /> ); }; diff --git a/frontend/src/modules/admin/api/admin.api.ts b/frontend/src/modules/admin/api/admin.api.ts new file mode 100644 index 0000000..3b7a697 --- /dev/null +++ b/frontend/src/modules/admin/api/admin.api.ts @@ -0,0 +1,88 @@ +import { apiClient } from "@/shared/api/axios.instance"; + +const getAuthHeader = () => { + const raw = localStorage.getItem("auth-storage"); + if (raw) { + try { + const parsed = JSON.parse(raw); + if (parsed?.state?.token) return `bearer ${parsed.state.token}`; + } catch {} + } + return ""; +}; + +export interface AdminUserDto { + id: number; + login: string; + name: string; + last_name: string; + is_active: boolean; + permission_admin: boolean; + permission_manage_agent: boolean; + permission_view: boolean; + token: string; +} + +export interface CreateUserPayload { + login: string; + name: string; + last_name: string; + password: string; + is_active: boolean; + permission_admin: boolean; + permission_manage_agent: boolean; + permission_view: boolean; +} + +export interface PermissionsPayload { + is_active: boolean; + permission_admin: boolean; + permission_manage_agent: boolean; + permission_view: boolean; +} + +export const adminApi = { + getUsers: async (): Promise => { + const res = await apiClient.get("/auth/tokens", { + headers: { Authorization: getAuthHeader() }, + }); + return res.data; + }, + + createUser: async (payload: CreateUserPayload): Promise => { + await apiClient.post("/auth/token", payload, { + headers: { Authorization: getAuthHeader() }, + }); + }, + + deleteUser: async (login: string): Promise => { + await apiClient.delete(`/auth/tokens/${login}`, { + headers: { Authorization: getAuthHeader() }, + }); + }, + + activateUser: async (login: string): Promise => { + await apiClient.post( + `/auth/users/${login}/activate`, + {}, + { headers: { Authorization: getAuthHeader() } }, + ); + }, + + deactivateUser: async (login: string): Promise => { + await apiClient.post( + `/auth/users/${login}/deactivate`, + {}, + { headers: { Authorization: getAuthHeader() } }, + ); + }, + + updatePermissions: async ( + login: string, + payload: PermissionsPayload, + ): Promise => { + await apiClient.put(`/auth/users/${login}/permissions`, payload, { + headers: { Authorization: getAuthHeader() }, + }); + }, +}; diff --git a/frontend/src/modules/admin/components/CreateUserModal.tsx b/frontend/src/modules/admin/components/CreateUserModal.tsx new file mode 100644 index 0000000..58eaca4 --- /dev/null +++ b/frontend/src/modules/admin/components/CreateUserModal.tsx @@ -0,0 +1,310 @@ +import React, { useState } from "react"; +import { FaTimes, FaPlus } from "react-icons/fa"; +import { useAdminStore } from "../store/useAdminStore"; + +interface CreateUserModalProps { + isOpen: boolean; + onClose: () => void; +} + +export const CreateUserModal: React.FC = ({ + isOpen, + onClose, +}) => { + const createUser = useAdminStore((s) => s.createUser); + + const [form, setForm] = useState({ + login: "", + name: "", + last_name: "", + password: "", + is_active: true, + permission_admin: false, + permission_manage_agent: false, + permission_view: true, + }); + + const [loading, setLoading] = useState(false); + + if (!isOpen) return null; + + const handleSubmit = async () => { + if (!form.login || !form.password) return; + setLoading(true); + await createUser(form); + setLoading(false); + setForm({ + login: "", + name: "", + last_name: "", + password: "", + is_active: true, + permission_admin: false, + permission_manage_agent: false, + permission_view: true, + }); + onClose(); + }; + + return ( +
+
e.stopPropagation()} + > +
+

+ Создать пользователя +

+ +
+ +
+ {/* Login */} +
+ + setForm({ ...form, login: e.target.value })} + style={{ + width: "100%", + padding: "8px", + backgroundColor: "var(--input-bg)", + border: "1px solid var(--border)", + borderRadius: "6px", + color: "var(--text-primary)", + fontSize: "13px", + outline: "none", + }} + /> +
+ + {/* Password */} +
+ + setForm({ ...form, password: e.target.value })} + style={{ + width: "100%", + padding: "8px", + backgroundColor: "var(--input-bg)", + border: "1px solid var(--border)", + borderRadius: "6px", + color: "var(--text-primary)", + fontSize: "13px", + outline: "none", + }} + /> +
+ + {/* Name + Last name */} +
+
+ + setForm({ ...form, name: e.target.value })} + style={{ + width: "100%", + padding: "8px", + backgroundColor: "var(--input-bg)", + border: "1px solid var(--border)", + borderRadius: "6px", + color: "var(--text-primary)", + fontSize: "13px", + outline: "none", + }} + /> +
+
+ + + setForm({ ...form, last_name: e.target.value }) + } + style={{ + width: "100%", + padding: "8px", + backgroundColor: "var(--input-bg)", + border: "1px solid var(--border)", + borderRadius: "6px", + color: "var(--text-primary)", + fontSize: "13px", + outline: "none", + }} + /> +
+
+ + {/* Permissions */} +
+ +
+ {[ + { key: "is_active", label: "Active" }, + { key: "permission_view", label: "View" }, + { key: "permission_manage_agent", label: "Manage Agent" }, + { key: "permission_admin", label: "Admin" }, + ].map(({ key, label }) => ( + + ))} +
+
+ + {/* Submit */} + +
+
+
+ ); +}; diff --git a/frontend/src/modules/admin/components/UserCard.tsx b/frontend/src/modules/admin/components/UserCard.tsx index d1ae490..8ffe552 100644 --- a/frontend/src/modules/admin/components/UserCard.tsx +++ b/frontend/src/modules/admin/components/UserCard.tsx @@ -1,5 +1,5 @@ import React from "react"; -import { FaUser, FaCheck, FaTimes } from "react-icons/fa"; +import { FaUser, FaCheck, FaTrash } from "react-icons/fa"; import type { AdminUser, PermissionKey } from "../types"; import { useAdminStore } from "../store/useAdminStore"; @@ -14,8 +14,10 @@ const permissions: { key: PermissionKey; label: string }[] = [ ]; export const UserCard: React.FC = ({ user }) => { + const users = useAdminStore((s) => s.users); const toggleActive = useAdminStore((s) => s.toggleActive); const togglePermission = useAdminStore((s) => s.togglePermission); + const deleteUser = useAdminStore((s) => s.deleteUser); return (
= ({ user }) => { opacity: user.is_active ? 1 : 0.6, }} > - {/* Header: User info + Active toggle */} + {/* Header: User info + Active toggle + Delete */}
= ({ user }) => {
- {/* Active toggle */} -
- - {user.is_active ? "Active" : "Inactive"} - - +
+ + {/* Delete button */} + @@ -144,7 +176,7 @@ export const UserCard: React.FC = ({ user }) => { }} >
togglePermission(user.id, key)} + onClick={() => togglePermission(user.id, user.login, key, users)} style={{ width: "18px", height: "18px", @@ -160,7 +192,10 @@ export const UserCard: React.FC = ({ user }) => { }} > {user[key] && ( - + )}
{label} diff --git a/frontend/src/modules/admin/index.ts b/frontend/src/modules/admin/index.ts index b997e10..5a039a3 100644 --- a/frontend/src/modules/admin/index.ts +++ b/frontend/src/modules/admin/index.ts @@ -1,3 +1,4 @@ export { AdminPanel } from "./AdminPanel"; export { useAdminStore } from "./store/useAdminStore"; +export { adminApi } from "./api/admin.api"; export type { AdminUser } from "./types"; diff --git a/frontend/src/modules/admin/store/useAdminStore.ts b/frontend/src/modules/admin/store/useAdminStore.ts index cda4279..34dcb21 100644 --- a/frontend/src/modules/admin/store/useAdminStore.ts +++ b/frontend/src/modules/admin/store/useAdminStore.ts @@ -1,69 +1,129 @@ import { create } from "zustand"; import type { AdminUser, PermissionKey } from "../types"; - -const mockUsers: AdminUser[] = [ - { - id: "1", - login: "admin", - name: "Иван", - last_name: "Петров", - is_active: true, - permission_admin: true, - permission_manage_agent: true, - permission_view: true, - }, - { - id: "2", - login: "operator", - name: "Анна", - last_name: "Сидорова", - is_active: true, - permission_admin: false, - permission_manage_agent: true, - permission_view: true, - }, - { - id: "3", - login: "viewer", - name: "Сергей", - last_name: "Козлов", - is_active: true, - permission_admin: false, - permission_manage_agent: false, - permission_view: true, - }, - { - id: "4", - login: "dev_user", - name: "Мария", - last_name: "Новикова", - is_active: false, - permission_admin: false, - permission_manage_agent: true, - permission_view: true, - }, -]; +import { adminApi } from "../api/admin.api"; +import type { CreateUserPayload } from "../api/admin.api"; interface AdminState { users: AdminUser[]; - toggleActive: (id: string) => void; - togglePermission: (id: string, permission: PermissionKey) => void; + loading: boolean; + error: string | null; + fetchUsers: () => Promise; + createUser: (payload: CreateUserPayload) => Promise; + deleteUser: (id: string, login: string) => Promise; + toggleActive: (id: string, login: string, current: boolean) => Promise; + togglePermission: ( + id: string, + login: string, + permission: PermissionKey, + users: AdminUser[], + ) => Promise; } -export const useAdminStore = create((set) => ({ - users: mockUsers, +export const useAdminStore = create((set, get) => ({ + users: [], + loading: false, + error: null, - toggleActive: (id: string) => - set((state) => ({ - users: state.users.map((u) => - u.id === id ? { ...u, is_active: !u.is_active } : u, - ), - })), + fetchUsers: async () => { + set({ loading: true, error: null }); + try { + const data = await adminApi.getUsers(); + set({ + users: data.map((u) => ({ + id: String(u.id), + login: u.login, + name: u.name, + last_name: u.last_name, + is_active: u.is_active, + permission_admin: u.permission_admin, + permission_manage_agent: u.permission_manage_agent, + permission_view: u.permission_view, + })), + loading: false, + }); + } catch (e) { + set({ + error: e instanceof Error ? e.message : "Failed to fetch users", + loading: false, + }); + } + }, - togglePermission: (id: string, permission: PermissionKey) => - set((state) => ({ - users: state.users.map((u) => - u.id === id ? { ...u, [permission]: !u[permission] } : u, - ), - })), + createUser: async (payload) => { + try { + await adminApi.createUser(payload); + await get().fetchUsers(); + } catch (e) { + set({ error: e instanceof Error ? e.message : "Failed to create user" }); + } + }, + + deleteUser: async (id, login) => { + try { + await adminApi.deleteUser(login); + set((state) => ({ + users: state.users.filter((u) => u.id !== id), + })); + } catch (e) { + set({ error: e instanceof Error ? e.message : "Failed to delete user" }); + } + }, + + toggleActive: async (id, login, current) => { + try { + if (current) { + await adminApi.deactivateUser(login); + } else { + await adminApi.activateUser(login); + } + set((state) => ({ + users: state.users.map((u) => + u.id === id ? { ...u, is_active: !current } : u, + ), + })); + } catch (e) { + set({ + error: e instanceof Error ? e.message : "Failed to toggle active", + }); + } + }, + + togglePermission: async (id, login, permission, users) => { + const user = users.find((u) => u.id === id); + if (!user) return; + + const newPermissions = { + is_active: user.is_active, + permission_admin: + permission === "permission_admin" + ? !user.permission_admin + : user.permission_admin, + permission_manage_agent: + permission === "permission_manage_agent" + ? !user.permission_manage_agent + : user.permission_manage_agent, + permission_view: + permission === "permission_view" + ? !user.permission_view + : user.permission_view, + }; + + try { + await adminApi.updatePermissions(login, newPermissions); + set((state) => ({ + users: state.users.map((u) => + u.id === id + ? { + ...u, + [permission]: !u[permission], + } + : u, + ), + })); + } catch (e) { + set({ + error: e instanceof Error ? e.message : "Failed to update permissions", + }); + } + }, }));