Files
tgex-frontend/app/dashboard/[workspaceId]/settings/members/page.tsx
2026-01-07 16:08:26 +03:00

281 lines
9.9 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"use client";
// ============================================================================
// Workspace Members Page
// ============================================================================
import { useEffect, useState } from "react";
import { useParams } from "next/navigation";
import { Loader2, Users, Shield, ChevronDown } from "lucide-react";
import { DashboardHeader } from "@/components/layout/dashboard-header";
import { membersApi } from "@/lib/api";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import {
Card,
CardContent,
} from "@/components/ui/card";
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuCheckboxItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import type { WorkspaceMember, PermissionKey } from "@/lib/types/api";
// ============================================================================
// Constants
// ============================================================================
const PERMISSION_LABELS: Record<PermissionKey, string> = {
admin_full: "Полный доступ",
projects_read: "Просмотр проектов",
projects_write: "Редактирование проектов",
placements_read: "Просмотр размещений",
placements_write: "Редактирование размещений",
analytics_read: "Просмотр аналитики",
};
const ALL_PERMISSIONS: PermissionKey[] = [
"admin_full",
"projects_read",
"projects_write",
"placements_read",
"placements_write",
"analytics_read",
];
// ============================================================================
// Component
// ============================================================================
export default function MembersPage() {
const params = useParams();
const workspaceId = params?.workspaceId as string;
const [members, setMembers] = useState<WorkspaceMember[]>([]);
const [loading, setLoading] = useState(true);
const [updating, setUpdating] = useState<string | null>(null);
useEffect(() => {
const loadMembers = async () => {
try {
setLoading(true);
const response = await membersApi.list(workspaceId, { size: 100 });
setMembers(response.items);
} catch (err) {
console.error("Failed to load members:", err);
} finally {
setLoading(false);
}
};
loadMembers();
}, [workspaceId]);
const handlePermissionChange = async (
memberId: string,
userId: string,
permission: PermissionKey,
enabled: boolean
) => {
try {
setUpdating(memberId);
const member = members.find((m) => m.id === memberId);
if (!member) return;
let newPermissions = [...member.permissions];
if (enabled) {
// Add permission
if (!newPermissions.some((p) => p.key === permission)) {
newPermissions.push({ key: permission });
}
} else {
// Remove permission
newPermissions = newPermissions.filter((p) => p.key !== permission);
}
await membersApi.updatePermissions(workspaceId, userId, {
permissions: newPermissions,
});
// Update local state
setMembers((prev) =>
prev.map((m) =>
m.id === memberId ? { ...m, permissions: newPermissions } : m
)
);
} catch (err) {
console.error("Failed to update permissions:", err);
} finally {
setUpdating(null);
}
};
const hasPermission = (member: WorkspaceMember, key: PermissionKey) => {
return member.permissions.some((p) => p.key === key);
};
return (
<>
<DashboardHeader
breadcrumbs={[
{ label: "Главная", href: `/dashboard/${workspaceId}` },
{ label: "Настройки", href: `/dashboard/${workspaceId}/settings` },
{ label: "Участники" },
]}
/>
<div className="flex flex-1 flex-col gap-4 p-4 pt-0 mt-4">
<div className="flex items-center justify-between">
<div>
<h1 className="text-2xl font-bold tracking-tight">Участники</h1>
<p className="text-muted-foreground">
Управление участниками и правами доступа
</p>
</div>
<Button asChild>
<a href={`/dashboard/${workspaceId}/settings/invites`}>
Пригласить участника
</a>
</Button>
</div>
{loading ? (
<div className="flex items-center justify-center py-12">
<Loader2 className="h-8 w-8 animate-spin text-muted-foreground" />
</div>
) : members.length === 0 ? (
<Card>
<CardContent className="flex flex-col items-center justify-center py-12">
<Users className="h-12 w-12 text-muted-foreground mb-4" />
<h3 className="text-lg font-semibold mb-2">Нет участников</h3>
<p className="text-muted-foreground text-center">
Пригласите участников в воркспейс
</p>
</CardContent>
</Card>
) : (
<Card>
<Table>
<TableHeader>
<TableRow>
<TableHead>Пользователь</TableHead>
<TableHead>Статус</TableHead>
<TableHead>Права доступа</TableHead>
<TableHead className="w-[150px]"></TableHead>
</TableRow>
</TableHeader>
<TableBody>
{members.map((member) => (
<TableRow key={member.id}>
<TableCell>
<div className="flex items-center gap-3">
<div className="flex h-9 w-9 items-center justify-center rounded-full bg-primary/10">
<Users className="h-4 w-4 text-primary" />
</div>
<div>
<div className="font-medium">
{member.user.username || `User ${member.user.telegram_id}`}
</div>
{member.user.username && (
<div className="text-xs text-muted-foreground">
@{member.user.username}
</div>
)}
</div>
</div>
</TableCell>
<TableCell>
<Badge
variant={
member.status === "active" ? "default" : "secondary"
}
>
{member.status === "active" ? "Активен" : "Приглашён"}
</Badge>
</TableCell>
<TableCell>
<div className="flex flex-wrap gap-1">
{hasPermission(member, "admin_full") ? (
<Badge variant="destructive" className="gap-1">
<Shield className="h-3 w-3" />
Админ
</Badge>
) : (
member.permissions.map((p) => (
<Badge key={p.key} variant="outline">
{PERMISSION_LABELS[p.key]}
</Badge>
))
)}
{member.permissions.length === 0 && (
<span className="text-sm text-muted-foreground">
Нет прав
</span>
)}
</div>
</TableCell>
<TableCell>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
variant="outline"
size="sm"
disabled={updating === member.id}
>
{updating === member.id ? (
<Loader2 className="h-4 w-4 animate-spin" />
) : (
<>
Права
<ChevronDown className="h-4 w-4 ml-1" />
</>
)}
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="w-56">
<DropdownMenuLabel>Права доступа</DropdownMenuLabel>
<DropdownMenuSeparator />
{ALL_PERMISSIONS.map((perm) => (
<DropdownMenuCheckboxItem
key={perm}
checked={hasPermission(member, perm)}
onCheckedChange={(checked) =>
handlePermissionChange(
member.id,
member.user.id,
perm,
checked
)
}
>
{PERMISSION_LABELS[perm]}
</DropdownMenuCheckboxItem>
))}
</DropdownMenuContent>
</DropdownMenu>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</Card>
)}
</div>
</>
);
}