feat: update permissions
This commit is contained in:
@@ -12,6 +12,7 @@ import {
|
||||
ArrowUpDown,
|
||||
ArrowUp,
|
||||
ArrowDown,
|
||||
Lock,
|
||||
} from "lucide-react";
|
||||
import { DashboardHeader } from "@/components/layout/dashboard-header";
|
||||
import { useWorkspace } from "@/components/providers/workspace-provider";
|
||||
@@ -74,14 +75,41 @@ type SortOrder = "asc" | "desc" | null;
|
||||
export default function ChannelsAnalyticsPage() {
|
||||
const params = useParams();
|
||||
const workspaceId = params?.workspaceId as string;
|
||||
const { getProjectFilterId } = useWorkspace();
|
||||
const { getProjectFilterId, canViewSubscriptions, hasAnyAnalyticsPermission } = useWorkspace();
|
||||
const projectFilterId = getProjectFilterId();
|
||||
const canViewSubs = canViewSubscriptions;
|
||||
const hasPermission = hasAnyAnalyticsPermission;
|
||||
|
||||
const [channels, setChannels] = useState<ChannelAnalyticsItem[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [sortField, setSortField] = useState<SortField | null>("cost");
|
||||
const [sortOrder, setSortOrder] = useState<SortOrder>("desc");
|
||||
|
||||
if (!hasPermission) {
|
||||
return (
|
||||
<>
|
||||
<DashboardHeader
|
||||
breadcrumbs={[
|
||||
{ label: "Главная", href: `/dashboard/${workspaceId}` },
|
||||
{ label: "Аналитика", href: `/dashboard/${workspaceId}/analytics` },
|
||||
{ label: "Каналы" },
|
||||
]}
|
||||
/>
|
||||
<div className="flex flex-1 flex-col gap-4 p-4 pt-0 mt-4">
|
||||
<Card>
|
||||
<CardContent className="flex flex-col items-center justify-center py-16">
|
||||
<Lock 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>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
loadData();
|
||||
}, [workspaceId, projectFilterId]);
|
||||
@@ -250,17 +278,19 @@ export default function ChannelsAnalyticsPage() {
|
||||
{getSortIcon("views")}
|
||||
</Button>
|
||||
</TableHead>
|
||||
<TableHead>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="-ml-3 h-8"
|
||||
onClick={() => handleSort("cps")}
|
||||
>
|
||||
Ср. CPS
|
||||
{getSortIcon("cps")}
|
||||
</Button>
|
||||
</TableHead>
|
||||
{canViewSubs && (
|
||||
<TableHead>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="-ml-3 h-8"
|
||||
onClick={() => handleSort("cps")}
|
||||
>
|
||||
Ср. CPS
|
||||
{getSortIcon("cps")}
|
||||
</Button>
|
||||
</TableHead>
|
||||
)}
|
||||
<TableHead>
|
||||
<Button
|
||||
variant="ghost"
|
||||
@@ -287,11 +317,11 @@ export default function ChannelsAnalyticsPage() {
|
||||
</TableCell>
|
||||
<TableCell>{channel.placements_count}</TableCell>
|
||||
<TableCell>{formatCurrency(channel.total_cost)}</TableCell>
|
||||
<TableCell>
|
||||
{formatNumber(channel.total_subscriptions)}
|
||||
</TableCell>
|
||||
<TableCell>{formatNumber(channel.total_views)}</TableCell>
|
||||
<TableCell>{formatCurrency(channel.avg_cps)}</TableCell>
|
||||
{canViewSubs && (
|
||||
<TableCell>
|
||||
{formatCurrency(channel.avg_cps)}
|
||||
</TableCell>
|
||||
)}
|
||||
<TableCell>{formatCurrency(channel.avg_cpm)}</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
|
||||
@@ -7,10 +7,40 @@
|
||||
import { DashboardHeader } from "@/components/layout/dashboard-header";
|
||||
import { useParams } from "next/navigation";
|
||||
import { Card, CardContent } from "@/components/ui/card";
|
||||
import { Lock } from "lucide-react";
|
||||
import { useWorkspace } from "@/components/providers/workspace-provider";
|
||||
|
||||
export default function CreativesAnalyticsPage() {
|
||||
const params = useParams();
|
||||
const workspaceId = params?.workspaceId as string;
|
||||
const { hasAnyAnalyticsPermission } = useWorkspace();
|
||||
const hasPermission = hasAnyAnalyticsPermission;
|
||||
|
||||
if (!hasPermission) {
|
||||
return (
|
||||
<>
|
||||
<DashboardHeader
|
||||
breadcrumbs={[
|
||||
{ label: "Главная", href: `/dashboard/${workspaceId}` },
|
||||
{ label: "Аналитика", href: `/dashboard/${workspaceId}/analytics` },
|
||||
{ label: "По креативам" },
|
||||
]}
|
||||
/>
|
||||
|
||||
<div className="flex flex-1 flex-col gap-4 p-4 pt-0 mt-4">
|
||||
<Card>
|
||||
<CardContent className="flex flex-col items-center justify-center py-16">
|
||||
<Lock 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>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
|
||||
@@ -136,7 +136,31 @@ const formatDate = (dateStr: string | undefined) => {
|
||||
export default function PlacementsAnalyticsPage() {
|
||||
const params = useParams();
|
||||
const workspaceId = params?.workspaceId as string;
|
||||
const { projects } = useWorkspace();
|
||||
const { projects, hasAnyAnalyticsPermission } = useWorkspace();
|
||||
|
||||
if (!hasAnyAnalyticsPermission) {
|
||||
return (
|
||||
<>
|
||||
<DashboardHeader
|
||||
breadcrumbs={[
|
||||
{ label: "Главная", href: `/dashboard/${workspaceId}` },
|
||||
{ label: "Аналитика", href: `/dashboard/${workspaceId}/analytics` },
|
||||
{ label: "Все размещения" },
|
||||
]}
|
||||
/>
|
||||
<div className="flex flex-1 flex-col gap-4 p-4 pt-0 mt-4">
|
||||
<Card className="flex flex-col items-center justify-center py-16">
|
||||
<CardContent className="text-center">
|
||||
<h2 className="text-xl font-semibold mb-2">Доступ запрещён</h2>
|
||||
<p className="text-muted-foreground">
|
||||
У вас нет прав для просмотра аналитики. Обратитесь к администратору workspace.
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
const [placements, setPlacements] = useState<PlacementAnalyticsItem[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
@@ -761,7 +761,31 @@ const SortablePanel: React.FC<SortablePanelProps> = ({
|
||||
export default function ProjectsAnalyticsPage() {
|
||||
const params = useParams();
|
||||
const workspaceId = params?.workspaceId as string;
|
||||
const { projects } = useWorkspace();
|
||||
const { projects, hasAnyAnalyticsPermission } = useWorkspace();
|
||||
|
||||
if (!hasAnyAnalyticsPermission) {
|
||||
return (
|
||||
<>
|
||||
<DashboardHeader
|
||||
breadcrumbs={[
|
||||
{ label: "Главная", href: `/dashboard/${workspaceId}` },
|
||||
{ label: "Аналитика", href: `/dashboard/${workspaceId}/analytics` },
|
||||
{ label: "По проектам" },
|
||||
]}
|
||||
/>
|
||||
<div className="flex flex-1 flex-col gap-4 p-4 pt-0 mt-4">
|
||||
<Card className="flex flex-col items-center justify-center py-16">
|
||||
<CardContent className="text-center">
|
||||
<h2 className="text-xl font-semibold mb-2">Доступ запрещён</h2>
|
||||
<p className="text-muted-foreground">
|
||||
У вас нет прав для просмотра аналитики. Обратитесь к администратору workspace.
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
const [panels, setPanels] = useState<ChartPanelConfig[]>([]);
|
||||
const [tableData, setTableData] = useState<ProjectsAnalyticsResponse | null>(null);
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
|
||||
import { useEffect, useState, useMemo } from "react";
|
||||
import { useParams } from "next/navigation";
|
||||
import { Loader2, TrendingUp, BarChart3, LineChart as LineChartIcon, Info } from "lucide-react";
|
||||
import { Loader2, TrendingUp, BarChart3, LineChart as LineChartIcon, Info, Lock } from "lucide-react";
|
||||
import {
|
||||
LineChart,
|
||||
Line,
|
||||
@@ -132,17 +132,47 @@ const chartConfig: ChartConfig = {
|
||||
export default function SpendingAnalyticsPage() {
|
||||
const params = useParams();
|
||||
const workspaceId = params?.workspaceId as string;
|
||||
const { selectedProjects, getProjectFilterId, allProjectsSelected } = useWorkspace();
|
||||
const { selectedProjects, getProjectFilterId, allProjectsSelected, canViewSubscriptions, hasAnyAnalyticsPermission } = useWorkspace();
|
||||
const projectFilterId = getProjectFilterId();
|
||||
const canViewSubs = canViewSubscriptions;
|
||||
const hasPermission = hasAnyAnalyticsPermission;
|
||||
|
||||
const [spending, setSpending] = useState<SpendingAnalytics | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [grouping, setGrouping] = useState<DateGrouping>("month");
|
||||
|
||||
if (!hasPermission) {
|
||||
return (
|
||||
<TooltipProvider>
|
||||
<DashboardHeader
|
||||
breadcrumbs={[
|
||||
{ label: "Главная", href: `/dashboard/${workspaceId}` },
|
||||
{ label: "Аналитика", href: `/dashboard/${workspaceId}/analytics` },
|
||||
{ label: "Расходы" },
|
||||
]}
|
||||
/>
|
||||
|
||||
<div className="flex flex-1 flex-col gap-4 p-4 pt-0 mt-4">
|
||||
<Card>
|
||||
<CardContent className="flex flex-col items-center justify-center py-16">
|
||||
<Lock 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>
|
||||
</div>
|
||||
</TooltipProvider>
|
||||
);
|
||||
}
|
||||
|
||||
// Chart settings
|
||||
const [chartType, setChartType] = useState<ChartType>("line");
|
||||
const [showLabels, setShowLabels] = useState(false);
|
||||
const [selectedMetrics, setSelectedMetrics] = useState<MetricKey[]>(["cost", "subscriptions"]);
|
||||
const [selectedMetrics, setSelectedMetrics] = useState<MetricKey[]>(
|
||||
canViewSubs ? ["cost", "subscriptions"] : ["cost"]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
loadData();
|
||||
@@ -320,16 +350,18 @@ export default function SpendingAnalyticsPage() {
|
||||
<div className="text-2xl font-bold">{formatCurrency(spending.total_cost)}</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card>
|
||||
<CardHeader className="pb-2">
|
||||
<CardTitle className="text-sm font-medium">Подписчиков</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-2xl font-bold">
|
||||
{formatNumber(spending.total_subscriptions)}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
{canViewSubs && (
|
||||
<Card>
|
||||
<CardHeader className="pb-2">
|
||||
<CardTitle className="text-sm font-medium">Подписчиков</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-2xl font-bold">
|
||||
{formatNumber(spending.total_subscriptions)}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
<Card>
|
||||
<CardHeader className="pb-2">
|
||||
<CardTitle className="text-sm font-medium">Просмотров</CardTitle>
|
||||
@@ -395,29 +427,48 @@ export default function SpendingAnalyticsPage() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Metrics Selection */}
|
||||
<div className="flex items-center gap-4 mt-3 pt-3 border-t">
|
||||
<span className="text-sm text-muted-foreground">Метрики:</span>
|
||||
{(["cost", "subscriptions", "views"] as MetricKey[]).map((metric) => (
|
||||
<div key={metric} className="flex items-center gap-2">
|
||||
<Checkbox
|
||||
id={`metric-${metric}`}
|
||||
checked={selectedMetrics.includes(metric)}
|
||||
onCheckedChange={() => handleToggleMetric(metric)}
|
||||
/>
|
||||
<Label
|
||||
htmlFor={`metric-${metric}`}
|
||||
className="text-sm cursor-pointer flex items-center gap-1.5"
|
||||
>
|
||||
<div
|
||||
className="h-2.5 w-2.5 rounded-full"
|
||||
style={{ backgroundColor: `var(--color-${metric})` }}
|
||||
{/* Metrics Selection */}
|
||||
<div className="flex items-center gap-4 mt-3 pt-3 border-t">
|
||||
<span className="text-sm text-muted-foreground">Метрики:</span>
|
||||
{(["cost", "views"] as MetricKey[]).map((metric) => (
|
||||
<div key={metric} className="flex items-center gap-2">
|
||||
<Checkbox
|
||||
id={`metric-${metric}`}
|
||||
checked={selectedMetrics.includes(metric)}
|
||||
onCheckedChange={() => handleToggleMetric(metric)}
|
||||
/>
|
||||
{chartConfig[metric].label}
|
||||
</Label>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<Label
|
||||
htmlFor={`metric-${metric}`}
|
||||
className="text-sm cursor-pointer flex items-center gap-1.5"
|
||||
>
|
||||
<div
|
||||
className="h-2.5 w-2.5 rounded-full"
|
||||
style={{ backgroundColor: `var(--color-${metric})` }}
|
||||
/>
|
||||
{chartConfig[metric].label}
|
||||
</Label>
|
||||
</div>
|
||||
))}
|
||||
{canViewSubs && (["subscriptions"] as MetricKey[]).map((metric) => (
|
||||
<div key={metric} className="flex items-center gap-2">
|
||||
<Checkbox
|
||||
id={`metric-${metric}`}
|
||||
checked={selectedMetrics.includes(metric)}
|
||||
onCheckedChange={() => handleToggleMetric(metric)}
|
||||
/>
|
||||
<Label
|
||||
htmlFor={`metric-${metric}`}
|
||||
className="text-sm cursor-pointer flex items-center gap-1.5"
|
||||
>
|
||||
<div
|
||||
className="h-2.5 w-2.5 rounded-full"
|
||||
style={{ backgroundColor: `var(--color-${metric})` }}
|
||||
/>
|
||||
{chartConfig[metric].label}
|
||||
</Label>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent className="px-2 sm:p-6">{renderChart()}</CardContent>
|
||||
</Card>
|
||||
@@ -435,19 +486,21 @@ export default function SpendingAnalyticsPage() {
|
||||
<TableRow>
|
||||
<TableHead>Период</TableHead>
|
||||
<TableHead className="text-right">Расходы</TableHead>
|
||||
<TableHead className="text-right">Подписчики</TableHead>
|
||||
{canViewSubs && <TableHead className="text-right">Подписчики</TableHead>}
|
||||
<TableHead className="text-right">Просмотры</TableHead>
|
||||
<TableHead className="text-right">
|
||||
<div className="flex items-center justify-end gap-1">
|
||||
CPS
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Info className="h-3.5 w-3.5 text-muted-foreground cursor-help" />
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>Cost Per Subscription</TooltipContent>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</TableHead>
|
||||
{canViewSubs && (
|
||||
<TableHead className="text-right">
|
||||
<div className="flex items-center justify-end gap-1">
|
||||
CPS
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Info className="h-3.5 w-3.5 text-muted-foreground cursor-help" />
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>Cost Per Subscription</TooltipContent>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</TableHead>
|
||||
)}
|
||||
<TableHead className="text-right">
|
||||
<div className="flex items-center justify-end gap-1">
|
||||
CPM
|
||||
@@ -470,13 +523,17 @@ export default function SpendingAnalyticsPage() {
|
||||
<TableRow key={index}>
|
||||
<TableCell className="font-medium">{item.period}</TableCell>
|
||||
<TableCell className="text-right">{formatCurrency(item.cost)}</TableCell>
|
||||
<TableCell className="text-right">
|
||||
{formatNumber(item.subscriptions)}
|
||||
</TableCell>
|
||||
{canViewSubs && (
|
||||
<TableCell className="text-right">
|
||||
{formatNumber(item.subscriptions)}
|
||||
</TableCell>
|
||||
)}
|
||||
<TableCell className="text-right">{formatNumber(item.views)}</TableCell>
|
||||
<TableCell className="text-right">
|
||||
{cps ? formatCurrency(cps) : "—"}
|
||||
</TableCell>
|
||||
{canViewSubs && (
|
||||
<TableCell className="text-right">
|
||||
{cps ? formatCurrency(cps) : "—"}
|
||||
</TableCell>
|
||||
)}
|
||||
<TableCell className="text-right">
|
||||
{cpm ? formatCurrency(cpm) : "—"}
|
||||
</TableCell>
|
||||
|
||||
Reference in New Issue
Block a user