From e1117e46c86558de193cce7dc6ac8ff05dad5ad4 Mon Sep 17 00:00:00 2001
From: ivannoskov
Date: Tue, 10 Feb 2026 17:15:38 +0300
Subject: [PATCH] feat: update permissions
---
.../[workspaceId]/creatives/page.tsx | 217 +++++++----
app/dashboard/[workspaceId]/projects/page.tsx | 363 ++++++++++--------
.../purchase-plans/[projectId]/page.tsx | 52 ++-
.../[workspaceId]/purchase-plans/page.tsx | 14 +-
.../[workspaceId]/settings/invites/page.tsx | 89 ++---
app/dashboard/[workspaceId]/settings/page.tsx | 206 +++++++---
app/providers.tsx | 6 +-
components/project-selector.tsx | 92 +++--
components/providers/workspace-provider.tsx | 34 ++
components/ui/dialog.tsx | 2 +-
components/ui/permission-modal.tsx | 324 ++++++++++------
lib/api/members.ts | 13 +-
lib/utils/error-handler.ts | 72 ++++
13 files changed, 961 insertions(+), 523 deletions(-)
create mode 100644 lib/utils/error-handler.ts
diff --git a/app/dashboard/[workspaceId]/creatives/page.tsx b/app/dashboard/[workspaceId]/creatives/page.tsx
index 32aa487..9d25e00 100644
--- a/app/dashboard/[workspaceId]/creatives/page.tsx
+++ b/app/dashboard/[workspaceId]/creatives/page.tsx
@@ -74,6 +74,8 @@ import {
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
+import { toast } from "sonner";
+import { handleApiError } from "@/lib/utils/error-handler";
import { BOT_USERNAME } from "@/lib/utils/constants";
import { ProjectSelector } from "@/components/project-selector";
import type { Creative, CreativeAnalyticsItem } from "@/lib/types/api";
@@ -157,17 +159,21 @@ export default function CreativesPage() {
const [includeArchived, setIncludeArchived] = useState(false);
const [viewMode, setViewMode] = useState<"table" | "grid">("table");
- const canWrite = hasPermission("placements_write");
+ // Permission checks
+ const canReadCreatives = hasPermission("creatives_read");
+ const canWriteCreatives = hasPermission("creatives_write");
+ const canViewAnalytics = hasPermission("analytics_read") || hasPermission("analytics_without_clicks") || hasPermission("analytics_own_creatives");
+ const canViewSubscriptions = hasPermission("analytics_read") || hasPermission("analytics_own_creatives");
useEffect(() => {
loadCreatives();
}, [workspaceId, projectFilterId, includeArchived]);
useEffect(() => {
- if (creatives.length > 0) {
+ if (creatives.length > 0 && canViewAnalytics) {
loadAnalytics();
}
- }, [creatives, workspaceId, projectFilterId]);
+ }, [creatives, workspaceId, projectFilterId, canViewAnalytics]);
const loadCreatives = async () => {
try {
@@ -179,13 +185,18 @@ export default function CreativesPage() {
});
setCreatives(response.items);
} catch (err) {
- console.error("Failed to load creatives:", err);
+ handleApiError(err);
} finally {
setLoading(false);
}
};
const loadAnalytics = async () => {
+ if (!canViewAnalytics) {
+ setAnalyticsData([]);
+ return;
+ }
+
try {
setLoadingAnalytics(true);
const response = await demoAwareAnalyticsApi.creatives(workspaceId, {
@@ -209,24 +220,42 @@ export default function CreativesPage() {
}, [creatives, analyticsData]);
const handleArchive = async (creative: Creative) => {
- try {
- const newStatus = creative.status === "active" ? "archived" : "active";
- await creativesApi.update(workspaceId, creative.id, { status: newStatus });
- loadCreatives();
- } catch (err) {
- console.error("Failed to archive creative:", err);
- }
+ await toast.promise(
+ (async () => {
+ const newStatus = creative.status === "active" ? "archived" : "active";
+ await creativesApi.update(workspaceId, creative.id, { status: newStatus });
+ await loadCreatives();
+ })(),
+ {
+ loading: creative.status === "active" ? "Архивирование..." : "Восстановление...",
+ success: () => {
+ return creative.status === "active"
+ ? `Креатив «${creative.name}» архивирован`
+ : `Креатив «${creative.name}» восстановлен`;
+ },
+ error: (err) => {
+ handleApiError(err);
+ return "Не удалось изменить статус";
+ },
+ }
+ );
};
- const handleDelete = async (creativeId: string) => {
- if (!confirm("Удалить креатив? Это действие необратимо.")) return;
-
- try {
- await creativesApi.delete(workspaceId, creativeId);
- loadCreatives();
- } catch (err) {
- console.error("Failed to delete creative:", err);
- }
+ const handleDelete = async (creativeId: string, creativeName: string) => {
+ await toast.promise(
+ (async () => {
+ await creativesApi.delete(workspaceId, creativeId);
+ await loadCreatives();
+ })(),
+ {
+ loading: "Удаление креатива...",
+ success: () => `Креатив «${creativeName}» удалён`,
+ error: (err) => {
+ handleApiError(err);
+ return "Не удалось удалить креатив";
+ },
+ }
+ );
};
@@ -254,14 +283,16 @@ export default function CreativesPage() {
// Totals
const totals = useMemo(() => {
- const data = filteredCreatives.filter((c) => c.analytics);
+ const data = filteredCreatives.filter((c) => canViewAnalytics && c.analytics);
return {
placements: data.reduce((sum, c) => sum + (c.analytics?.placements_count || 0), 0),
cost: data.reduce((sum, c) => sum + (c.analytics?.total_cost || 0), 0),
- subscriptions: data.reduce((sum, c) => sum + (c.analytics?.total_subscriptions || 0), 0),
+ subscriptions: canViewSubscriptions
+ ? data.reduce((sum, c) => sum + (c.analytics?.total_subscriptions || 0), 0)
+ : 0,
views: data.reduce((sum, c) => sum + (c.analytics?.total_views || 0), 0),
};
- }, [filteredCreatives]);
+ }, [filteredCreatives, canViewAnalytics, canViewSubscriptions]);
return (
@@ -300,7 +331,7 @@ export default function CreativesPage() {
- {canWrite && (
+ {canWriteCreatives && (
- {canWrite && !search && (
+ {canWriteCreatives && !search && (