308 lines
10 KiB
TypeScript
308 lines
10 KiB
TypeScript
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>
|
||
);
|
||
};
|