feat: global platform v2 update

This commit is contained in:
ivannoskov
2025-12-29 10:12:13 +03:00
parent 8e6a1fa83f
commit f64cf02100
84 changed files with 11023 additions and 11486 deletions

View File

@@ -0,0 +1,281 @@
"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: "Участники" },
]}
showProjectSelector={false}
/>
<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>
</>
);
}