diff --git a/src/app/providers/routing.tsx b/src/app/providers/routing.tsx index 7b1691e..0203765 100644 --- a/src/app/providers/routing.tsx +++ b/src/app/providers/routing.tsx @@ -4,6 +4,7 @@ import { HomePage } from "@/pages/home.page"; import { SecondaryPage } from "@/pages/secondary.page"; import { AuthPage } from "@/pages/AuthPage"; import { CreateOrganizationPage } from "@/pages/CreateOrganizationPage"; +import { OrganizationPage } from "@/pages/OrganizationPage"; import { ProtectedRoute } from "./helper/protected.route"; export const Routing = () => { @@ -29,6 +30,14 @@ export const Routing = () => { } /> + + + + } + /> { + const users = localStorage.getItem(MOCK_USERS_KEY); + if (!users) { + const defaultUsers = [ + { + id: 1, + username: "admin", + email: "admin@example.com", + password: "admin123", + first_name: "Admin", + last_name: "User", + }, + ]; + localStorage.setItem(MOCK_USERS_KEY, JSON.stringify(defaultUsers)); + return defaultUsers; + } + return JSON.parse(users); +}; + +const getMockOrganizations = () => { + const orgs = localStorage.getItem(MOCK_ORGS_KEY); + if (!orgs) { + return []; + } + return JSON.parse(orgs); +}; + +const getMockMembers = () => { + const members = localStorage.getItem(MOCK_MEMBERS_KEY); + if (!members) { + return []; + } + return JSON.parse(members); +}; + export const authService = { async login(credentials: LoginCredentials): Promise { - const formData = new URLSearchParams(); - formData.append("username", credentials.username); - formData.append("password", credentials.password); + return new Promise((resolve, reject) => { + setTimeout(() => { + const users = getMockUsers(); + const user = users.find( + (u: any) => + u.username === credentials.username && + u.password === credentials.password, + ); - return apiService.post("/auth/login", formData, { - headers: { - "Content-Type": "application/x-www-form-urlencoded", - }, + if (user) { + resolve({ + access_token: `mock_token_${user.id}_${Date.now()}`, + token_type: "bearer", + user: { + id: user.id, + username: user.username, + email: user.email, + first_name: user.first_name, + last_name: user.last_name, + }, + }); + } else { + reject({ + response: { + data: { message: "Неверное имя пользователя или пароль" }, + }, + }); + } + }, 500); }); }, async register( data: Omit, ): Promise { - return apiService.post("/auth/register", { - username: data.username, - email: data.email, - password: data.password, - first_name: data.first_name, - last_name: data.last_name, + return new Promise((resolve, reject) => { + setTimeout(() => { + const users = getMockUsers(); + const existingUser = users.find( + (u: any) => u.username === data.username, + ); + + if (existingUser) { + reject({ + response: { + data: { message: "Пользователь с таким именем уже существует" }, + }, + }); + return; + } + + const existingEmail = users.find((u: any) => u.email === data.email); + if (existingEmail) { + reject({ + response: { + data: { message: "Пользователь с таким email уже существует" }, + }, + }); + return; + } + + const newUser = { + id: users.length + 1, + username: data.username, + email: data.email, + password: data.password, + first_name: data.first_name, + last_name: data.last_name, + }; + + users.push(newUser); + localStorage.setItem(MOCK_USERS_KEY, JSON.stringify(users)); + + resolve({ + access_token: `mock_token_${newUser.id}_${Date.now()}`, + token_type: "bearer", + user: { + id: newUser.id, + username: newUser.username, + email: newUser.email, + first_name: newUser.first_name, + last_name: newUser.last_name, + }, + }); + }, 500); }); }, async createOrganization( data: OrganizationCreateData, ): Promise { - const formData = new FormData(); - formData.append("name", data.name); - if (data.logo) { - formData.append("logo", data.logo); - } + return new Promise((resolve) => { + setTimeout(() => { + const orgs = getMockOrganizations(); + const token = this.getToken(); + const userId = token ? parseInt(token.split("_")[1]) : 1; - return apiService.post("/organizations", formData, { - headers: { - "Content-Type": "multipart/form-data", - }, + const newOrg = { + id: orgs.length + 1, + name: data.name, + logo_url: data.logo ? URL.createObjectURL(data.logo) : undefined, + created_at: new Date().toISOString(), + owner_id: userId, + }; + + orgs.push(newOrg); + localStorage.setItem(MOCK_ORGS_KEY, JSON.stringify(orgs)); + + // Добавляем владельца в члены организации + const members = getMockMembers(); + const newMember = { + id: members.length + 1, + organization_id: newOrg.id, + user_id: userId, + username: "temp_username", + email: "temp_email", + first_name: "temp_first", + last_name: "temp_last", + role: "owner", + joined_at: new Date().toISOString(), + }; + + // Получаем данные пользователя + const users = getMockUsers(); + const user = users.find((u: any) => u.id === userId); + if (user) { + newMember.username = user.username; + newMember.email = user.email; + newMember.first_name = user.first_name; + newMember.last_name = user.last_name; + } + + members.push(newMember); + localStorage.setItem(MOCK_MEMBERS_KEY, JSON.stringify(members)); + + resolve(newOrg); + }, 500); + }); + }, + + async getOrganizations(): Promise { + return new Promise((resolve) => { + setTimeout(() => { + const orgs = getMockOrganizations(); + resolve(orgs); + }, 300); + }); + }, + + async getOrganizationMembers( + organizationId: number, + ): Promise { + return new Promise((resolve) => { + setTimeout(() => { + const members = getMockMembers(); + const orgMembers = members.filter( + (m: any) => m.organization_id === organizationId, + ); + resolve(orgMembers); + }, 300); + }); + }, + + async updateMemberRole( + organizationId: number, + userId: number, + newRole: string, + ): Promise { + return new Promise((resolve, reject) => { + setTimeout(() => { + const members = getMockMembers(); + const memberIndex = members.findIndex( + (m: any) => + m.organization_id === organizationId && m.user_id === userId, + ); + + if (memberIndex !== -1) { + members[memberIndex].role = newRole; + localStorage.setItem(MOCK_MEMBERS_KEY, JSON.stringify(members)); + resolve(); + } else { + reject({ response: { data: { message: "Участник не найден" } } }); + } + }, 500); + }); + }, + + async removeMember(organizationId: number, userId: number): Promise { + return new Promise((resolve, reject) => { + setTimeout(() => { + const members = getMockMembers(); + const filteredMembers = members.filter( + (m: any) => + !(m.organization_id === organizationId && m.user_id === userId), + ); + + if (filteredMembers.length !== members.length) { + localStorage.setItem( + MOCK_MEMBERS_KEY, + JSON.stringify(filteredMembers), + ); + resolve(); + } else { + reject({ response: { data: { message: "Участник не найден" } } }); + } + }, 500); + }); + }, + + async addMember( + organizationId: number, + email: string, + role: string, + ): Promise { + return new Promise((resolve, reject) => { + setTimeout(() => { + const users = getMockUsers(); + const user = users.find((u: any) => u.email === email); + + if (!user) { + reject({ + response: { + data: { message: "Пользователь с таким email не найден" }, + }, + }); + return; + } + + const members = getMockMembers(); + const existingMember = members.find( + (m: any) => + m.organization_id === organizationId && m.user_id === user.id, + ); + + if (existingMember) { + reject({ + response: { + data: { + message: "Пользователь уже является участником организации", + }, + }, + }); + return; + } + + const newMember = { + id: members.length + 1, + organization_id: organizationId, + user_id: user.id, + username: user.username, + email: user.email, + first_name: user.first_name, + last_name: user.last_name, + role: role, + joined_at: new Date().toISOString(), + }; + + members.push(newMember); + localStorage.setItem(MOCK_MEMBERS_KEY, JSON.stringify(members)); + + resolve(newMember); + }, 500); }); }, async getCurrentUser(): Promise { - return apiService.get("/users/me"); - }, + return new Promise((resolve, reject) => { + const token = this.getToken(); + if (!token) { + reject({ response: { data: { message: "Не авторизован" } } }); + return; + } - async getOrganizations(): Promise { - return apiService.get("/organizations"); + const userId = parseInt(token.split("_")[1]); + const users = getMockUsers(); + const user = users.find((u: any) => u.id === userId); + + if (user) { + resolve({ + id: user.id, + username: user.username, + email: user.email, + first_name: user.first_name, + last_name: user.last_name, + }); + } else { + reject({ response: { data: { message: "Пользователь не найден" } } }); + } + }); }, logout(): void { diff --git a/src/modules/auth/hooks/useAuth.ts b/src/modules/auth/hooks/useAuth.ts index 3d72584..a8c5df4 100644 --- a/src/modules/auth/hooks/useAuth.ts +++ b/src/modules/auth/hooks/useAuth.ts @@ -6,6 +6,7 @@ import type { LoginCredentials, RegisterData, OrganizationCreateData, + OrganizationMember, } from "../types/auth.types"; export const useAuth = () => { @@ -20,7 +21,6 @@ export const useAuth = () => { if (result) { authService.saveAuthData(result.access_token, result.user); - // Проверяем, есть ли уже организация const orgs = await request(() => authService.getOrganizations()); if (orgs && orgs.length > 0) { authService.saveOrganization(orgs[0]); diff --git a/src/modules/auth/types/auth.types.ts b/src/modules/auth/types/auth.types.ts index 713bdc1..3bf3e6d 100644 --- a/src/modules/auth/types/auth.types.ts +++ b/src/modules/auth/types/auth.types.ts @@ -36,3 +36,14 @@ export interface OrganizationResponse { created_at: string; owner_id: number; } + +export interface OrganizationMember { + id: number; + user_id: number; + username: string; + email: string; + first_name: string; + last_name: string; + role: "owner" | "admin" | "member"; + joined_at: string; +} diff --git a/src/modules/organization/components/OrganizationMembers.tsx b/src/modules/organization/components/OrganizationMembers.tsx new file mode 100644 index 0000000..65d5983 --- /dev/null +++ b/src/modules/organization/components/OrganizationMembers.tsx @@ -0,0 +1,307 @@ +import React, { useState } from "react"; +import { useOrganization } from "../hooks/useOrganization"; + +export const OrganizationMembers = () => { + const { members, isLoading, updateMemberRole, removeMember, addMember } = + useOrganization(); + const [showAddModal, setShowAddModal] = useState(false); + const [newMemberEmail, setNewMemberEmail] = useState(""); + const [newMemberRole, setNewMemberRole] = useState<"admin" | "member">( + "member", + ); + const [actionLoading, setActionLoading] = useState(null); + + const handleRoleChange = async (userId: number, newRole: string) => { + setActionLoading(userId); + await updateMemberRole(userId, newRole); + setActionLoading(null); + }; + + const handleRemoveMember = async (userId: number, memberName: string) => { + if ( + confirm( + `Вы уверены, что хотите удалить пользователя "${memberName}" из организации?`, + ) + ) { + setActionLoading(userId); + await removeMember(userId); + setActionLoading(null); + } + }; + + const handleAddMember = async (e: React.FormEvent) => { + e.preventDefault(); + const success = await addMember(newMemberEmail, newMemberRole); + if (success) { + setShowAddModal(false); + setNewMemberEmail(""); + setNewMemberRole("member"); + } + }; + + const getRoleLabel = (role: string) => { + switch (role) { + case "owner": + return "Владелец"; + case "admin": + return "Администратор"; + case "member": + return "Участник"; + default: + return role; + } + }; + + const getRoleColor = (role: string) => { + switch (role) { + case "owner": + return "text-yellow-600 dark:text-yellow-400"; + case "admin": + return "text-blue-600 dark:text-blue-400"; + case "member": + return "text-green-600 dark:text-green-400"; + default: + return ""; + } + }; + + if (isLoading && members.length === 0) { + return ( +
+
+ Загрузка участников... +
+
+ ); + } + + return ( +
+
+

+ Участники организации +

+ +
+ +
+ + + + + + + + + + + {members.map((member) => ( + + + + + + + ))} + +
+ Имя + + Email + + Роль + + Действия +
+ {member.first_name} {member.last_name} +
+ @{member.username} +
+
+ {member.email} + + {member.role === "owner" ? ( + + {getRoleLabel(member.role)} + + ) : ( + + )} + + {member.role !== "owner" && ( + + )} +
+
+ + {members.length === 0 && !isLoading && ( +
+ В организации пока нет участников +
+ )} + + {showAddModal && ( +
+
+

+ Добавить участника +

+
+
+ + setNewMemberEmail(e.target.value)} + className="w-full px-4 py-2 rounded-lg border focus:outline-none focus:ring-2" + style={{ + backgroundColor: "var(--input-bg)", + color: "var(--text-primary)", + borderColor: "var(--border)", + "--tw-ring-color": "var(--accent)", + }} + required + placeholder="user@example.com" + /> +
+
+ + +
+
+ + +
+
+
+
+ )} +
+ ); +}; diff --git a/src/modules/organization/hooks/useOrganization.ts b/src/modules/organization/hooks/useOrganization.ts new file mode 100644 index 0000000..46478e3 --- /dev/null +++ b/src/modules/organization/hooks/useOrganization.ts @@ -0,0 +1,82 @@ +import { useState, useEffect } from "react"; +import { authService } from "@/modules/auth/api/auth.service"; +import { useApi } from "@/shared/api/hooks/use.api"; +import type { OrganizationMember } from "@/modules/auth/types/auth.types"; + +export const useOrganization = () => { + const { isLoading, error, request } = useApi(); + const [members, setMembers] = useState([]); + const [organization, setOrganization] = useState( + authService.getCurrentOrganization(), + ); + + const loadMembers = async () => { + if (organization) { + const result = await request(() => + authService.getOrganizationMembers(organization.id), + ); + if (result) { + setMembers(result); + } + } + }; + + useEffect(() => { + loadMembers(); + }, [organization]); + + const updateMemberRole = async (userId: number, newRole: string) => { + if (organization) { + const result = await request(() => + authService.updateMemberRole(organization.id, userId, newRole), + ); + if (result !== undefined) { + await loadMembers(); + return true; + } + } + return false; + }; + + const removeMember = async (userId: number) => { + if (organization) { + const result = await request(() => + authService.removeMember(organization.id, userId), + ); + if (result !== undefined) { + await loadMembers(); + return true; + } + } + return false; + }; + + const addMember = async (email: string, role: string) => { + if (organization) { + const result = await request(() => + authService.addMember(organization.id, email, role), + ); + if (result) { + await loadMembers(); + return true; + } + } + return false; + }; + + const refreshOrganization = () => { + setOrganization(authService.getCurrentOrganization()); + }; + + return { + members, + organization, + isLoading, + error, + updateMemberRole, + removeMember, + addMember, + refreshOrganization, + loadMembers, + }; +}; diff --git a/src/pages/OrganizationPage.tsx b/src/pages/OrganizationPage.tsx new file mode 100644 index 0000000..35fee42 --- /dev/null +++ b/src/pages/OrganizationPage.tsx @@ -0,0 +1,74 @@ +import React from "react"; +import { ThemeToggle } from "@/modules/theme-changer/ui/Theme.toggle"; +import { OrganizationMembers } from "@/modules/organization/components/OrganizationMembers"; +import { useAuth } from "@/modules/auth/hooks/useAuth"; +import { Navigate, useNavigate } from "react-router-dom"; +import { authService } from "@/modules/auth/api/auth.service"; + +export const OrganizationPage = () => { + const { isAuthenticated, hasOrganization } = useAuth(); + const navigate = useNavigate(); + const organization = authService.getCurrentOrganization(); + + if (!isAuthenticated) { + return ; + } + + if (!hasOrganization) { + return ; + } + + return ( +
+
+ +
+ +
+
+ + +
+ {organization?.logo_url && ( + {organization.name} + )} +
+

+ {organization?.name} +

+

+ Управление участниками организации +

+
+
+
+ +
+ +
+
+
+ ); +}; diff --git a/src/pages/home.page.tsx b/src/pages/home.page.tsx index df87d99..90ab78e 100644 --- a/src/pages/home.page.tsx +++ b/src/pages/home.page.tsx @@ -1,13 +1,47 @@ import { ThemeToggle } from "@/modules/theme-changer/ui/Theme.toggle"; +import { useNavigate } from "react-router-dom"; +import { useAuth } from "@/modules/auth/hooks/useAuth"; export const HomePage = () => { + const navigate = useNavigate(); + const { logout } = useAuth(); + return (
- - Домашняя +
+ +
+ + +
+
+ +
+

+ Добро пожаловать в IPS Manager +

+
); };