feat: add plug and organization

This commit is contained in:
2026-06-12 19:25:23 +03:00
parent 444bc05f9d
commit d348e0c347
8 changed files with 832 additions and 31 deletions
@@ -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<number | null>(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 (
<div className="flex justify-center py-12">
<div style={{ color: "var(--text-secondary)" }}>
Загрузка участников...
</div>
</div>
);
}
return (
<div>
<div className="flex justify-between items-center mb-6">
<h2
className="text-2xl font-bold"
style={{ color: "var(--text-primary)" }}
>
Участники организации
</h2>
<button
onClick={() => setShowAddModal(true)}
className="px-4 py-2 rounded-lg font-medium transition-all transform hover:scale-[1.02] active:scale-[0.98]"
style={{
backgroundColor: "var(--accent)",
color: "var(--button-primary-text)",
}}
>
+ Добавить участника
</button>
</div>
<div className="overflow-x-auto">
<table className="w-full">
<thead>
<tr className="border-b" style={{ borderColor: "var(--border)" }}>
<th
className="text-left py-3 px-4"
style={{ color: "var(--text-secondary)" }}
>
Имя
</th>
<th
className="text-left py-3 px-4"
style={{ color: "var(--text-secondary)" }}
>
Email
</th>
<th
className="text-left py-3 px-4"
style={{ color: "var(--text-secondary)" }}
>
Роль
</th>
<th
className="text-left py-3 px-4"
style={{ color: "var(--text-secondary)" }}
>
Действия
</th>
</tr>
</thead>
<tbody>
{members.map((member) => (
<tr
key={member.id}
className="border-b"
style={{ borderColor: "var(--border)" }}
>
<td
className="py-3 px-4"
style={{ color: "var(--text-primary)" }}
>
{member.first_name} {member.last_name}
<div
className="text-xs"
style={{ color: "var(--text-muted)" }}
>
@{member.username}
</div>
</td>
<td
className="py-3 px-4"
style={{ color: "var(--text-primary)" }}
>
{member.email}
</td>
<td className="py-3 px-4">
{member.role === "owner" ? (
<span
className={`font-medium ${getRoleColor(member.role)}`}
>
{getRoleLabel(member.role)}
</span>
) : (
<select
value={member.role}
onChange={(e) =>
handleRoleChange(member.user_id, e.target.value)
}
disabled={actionLoading === member.user_id}
className="px-2 py-1 rounded border focus:outline-none focus:ring-2"
style={{
backgroundColor: "var(--input-bg)",
color: "var(--text-primary)",
borderColor: "var(--border)",
"--tw-ring-color": "var(--accent)",
}}
>
<option value="admin">{getRoleLabel("admin")}</option>
<option value="member">{getRoleLabel("member")}</option>
</select>
)}
</td>
<td className="py-3 px-4">
{member.role !== "owner" && (
<button
onClick={() =>
handleRemoveMember(
member.user_id,
`${member.first_name} ${member.last_name}`,
)
}
disabled={actionLoading === member.user_id}
className="px-3 py-1 rounded text-sm transition-all hover:scale-105 disabled:opacity-50"
style={{
backgroundColor: "var(--button-danger)",
color: "var(--button-danger-text)",
}}
>
{actionLoading === member.user_id ? "..." : "Удалить"}
</button>
)}
</td>
</tr>
))}
</tbody>
</table>
</div>
{members.length === 0 && !isLoading && (
<div
className="text-center py-12"
style={{ color: "var(--text-secondary)" }}
>
В организации пока нет участников
</div>
)}
{showAddModal && (
<div className="fixed inset-0 bg-[#00000055] bg-opacity-50 flex items-center justify-center z-50">
<div
className="rounded-2xl p-6 max-w-md w-full mx-4"
style={{
backgroundColor: "var(--card-bg)",
boxShadow: `0 20px 25px -5px var(--shadow-color)`,
}}
>
<h3
className="text-xl font-bold mb-4"
style={{ color: "var(--text-primary)" }}
>
Добавить участника
</h3>
<form onSubmit={handleAddMember}>
<div className="mb-4">
<label
className="block text-sm font-medium mb-2"
style={{ color: "var(--text-secondary)" }}
>
Email пользователя
</label>
<input
type="email"
value={newMemberEmail}
onChange={(e) => 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"
/>
</div>
<div className="mb-6">
<label
className="block text-sm font-medium mb-2"
style={{ color: "var(--text-secondary)" }}
>
Роль
</label>
<select
value={newMemberRole}
onChange={(e) =>
setNewMemberRole(e.target.value as "admin" | "member")
}
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)",
}}
>
<option value="admin">Администратор</option>
<option value="member">Участник</option>
</select>
</div>
<div className="flex gap-3">
<button
type="button"
onClick={() => setShowAddModal(false)}
className="flex-1 py-2 px-4 rounded-lg font-medium transition-all"
style={{
backgroundColor: "var(--bg-secondary)",
color: "var(--text-secondary)",
}}
>
Отмена
</button>
<button
type="submit"
className="flex-1 py-2 px-4 rounded-lg font-medium transition-all transform hover:scale-[1.02]"
style={{
backgroundColor: "var(--accent)",
color: "var(--button-primary-text)",
}}
>
Добавить
</button>
</div>
</form>
</div>
</div>
)}
</div>
);
};