feat: global ui & functional update
This commit is contained in:
@@ -13,7 +13,6 @@ import {
|
||||
ExternalLink,
|
||||
Copy,
|
||||
Check,
|
||||
Archive,
|
||||
Eye,
|
||||
Users,
|
||||
TrendingUp,
|
||||
@@ -30,17 +29,6 @@ import {
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/components/ui/card";
|
||||
import {
|
||||
AlertDialog,
|
||||
AlertDialogAction,
|
||||
AlertDialogCancel,
|
||||
AlertDialogContent,
|
||||
AlertDialogDescription,
|
||||
AlertDialogFooter,
|
||||
AlertDialogHeader,
|
||||
AlertDialogTitle,
|
||||
AlertDialogTrigger,
|
||||
} from "@/components/ui/alert-dialog";
|
||||
import type { Placement } from "@/lib/types/api";
|
||||
|
||||
// ============================================================================
|
||||
@@ -114,15 +102,6 @@ export default function PlacementDetailPage() {
|
||||
}
|
||||
};
|
||||
|
||||
const handleArchive = async () => {
|
||||
try {
|
||||
await placementsApi.delete(workspaceId, placementId);
|
||||
router.push(`/dashboard/${workspaceId}/placements`);
|
||||
} catch (err) {
|
||||
console.error("Failed to archive:", err);
|
||||
}
|
||||
};
|
||||
|
||||
const handleCopyLink = () => {
|
||||
if (placement?.invite_link) {
|
||||
navigator.clipboard.writeText(placement.invite_link);
|
||||
@@ -159,14 +138,11 @@ export default function PlacementDetailPage() {
|
||||
}
|
||||
|
||||
// Calculate metrics
|
||||
const cps =
|
||||
placement.cost && placement.subscriptions_count > 0
|
||||
? placement.cost / placement.subscriptions_count
|
||||
: null;
|
||||
const cpm =
|
||||
placement.cost && placement.views_count
|
||||
? (placement.cost / placement.views_count) * 1000
|
||||
: null;
|
||||
const cost = placement.details?.cost?.value;
|
||||
const subscriptionsCount = placement.placement_post?.subscriptions_count ?? 0;
|
||||
const viewsCount = placement.placement_post?.views_count ?? 0;
|
||||
const cps = cost && subscriptionsCount > 0 ? cost / subscriptionsCount : null;
|
||||
const cpm = cost && viewsCount > 0 ? (cost / viewsCount) * 1000 : null;
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -174,7 +150,7 @@ export default function PlacementDetailPage() {
|
||||
breadcrumbs={[
|
||||
{ label: "Главная", href: `/dashboard/${workspaceId}` },
|
||||
{ label: "Размещения", href: `/dashboard/${workspaceId}/placements` },
|
||||
{ label: placement.placement_channel_title },
|
||||
{ label: placement.channel.title },
|
||||
]}
|
||||
/>
|
||||
|
||||
@@ -192,46 +168,21 @@ export default function PlacementDetailPage() {
|
||||
<div>
|
||||
<div className="flex items-center gap-2">
|
||||
<h1 className="text-2xl font-bold tracking-tight">
|
||||
{placement.placement_channel_title}
|
||||
{placement.channel.title}
|
||||
</h1>
|
||||
<Badge
|
||||
variant={
|
||||
placement.status === "active" ? "default" : "secondary"
|
||||
placement.status === "Оплачено" ? "default" : "secondary"
|
||||
}
|
||||
>
|
||||
{placement.status === "active" ? "Активно" : "Архив"}
|
||||
{placement.status === "Оплачено" ? "Активно" : placement.status}
|
||||
</Badge>
|
||||
</div>
|
||||
<p className="text-muted-foreground">
|
||||
{formatDate(placement.placement_date)}
|
||||
{placement.details?.placement_at ? formatDate(placement.details.placement_at) : "—"}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{canWrite && placement.status === "active" && (
|
||||
<AlertDialog>
|
||||
<AlertDialogTrigger asChild>
|
||||
<Button variant="outline">
|
||||
<Archive className="h-4 w-4 mr-2" />
|
||||
В архив
|
||||
</Button>
|
||||
</AlertDialogTrigger>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>Архивировать размещение?</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
Размещение будет перемещено в архив. Статистика сохранится.
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>Отмена</AlertDialogCancel>
|
||||
<AlertDialogAction onClick={handleArchive}>
|
||||
Архивировать
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Stats Cards */}
|
||||
@@ -241,11 +192,11 @@ export default function PlacementDetailPage() {
|
||||
<CardTitle className="text-sm font-medium">Стоимость</CardTitle>
|
||||
<TrendingUp className="h-4 w-4 text-muted-foreground" />
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-2xl font-bold">
|
||||
{formatCurrency(placement.cost)}
|
||||
</div>
|
||||
</CardContent>
|
||||
<CardContent>
|
||||
<div className="text-2xl font-bold">
|
||||
{formatCurrency(cost)}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
@@ -253,14 +204,14 @@ export default function PlacementDetailPage() {
|
||||
<CardTitle className="text-sm font-medium">Подписчиков</CardTitle>
|
||||
<Users className="h-4 w-4 text-muted-foreground" />
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-2xl font-bold">
|
||||
{formatNumber(placement.subscriptions_count)}
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
CPS: {cps ? formatCurrency(cps) : "—"}
|
||||
</p>
|
||||
</CardContent>
|
||||
<CardContent>
|
||||
<div className="text-2xl font-bold">
|
||||
{formatNumber(subscriptionsCount)}
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
CPS: {cps ? formatCurrency(cps) : "—"}
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
@@ -268,14 +219,14 @@ export default function PlacementDetailPage() {
|
||||
<CardTitle className="text-sm font-medium">Просмотров</CardTitle>
|
||||
<Eye className="h-4 w-4 text-muted-foreground" />
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-2xl font-bold">
|
||||
{formatNumber(placement.views_count)}
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
CPM: {cpm ? formatCurrency(cpm) : "—"}
|
||||
</p>
|
||||
</CardContent>
|
||||
<CardContent>
|
||||
<div className="text-2xl font-bold">
|
||||
{formatNumber(viewsCount)}
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
CPM: {cpm ? formatCurrency(cpm) : "—"}
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
@@ -308,24 +259,31 @@ export default function PlacementDetailPage() {
|
||||
<CardContent>
|
||||
<div className="flex items-center gap-2">
|
||||
<code className="flex-1 bg-muted px-3 py-2 rounded text-sm truncate">
|
||||
{placement.invite_link}
|
||||
{placement.invite_link || "—"}
|
||||
</code>
|
||||
<Button variant="outline" size="icon" onClick={handleCopyLink}>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="icon"
|
||||
onClick={handleCopyLink}
|
||||
disabled={!placement.invite_link}
|
||||
>
|
||||
{copiedLink ? (
|
||||
<Check className="h-4 w-4" />
|
||||
) : (
|
||||
<Copy className="h-4 w-4" />
|
||||
)}
|
||||
</Button>
|
||||
<Button variant="outline" size="icon" asChild>
|
||||
<a
|
||||
href={placement.invite_link}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<ExternalLink className="h-4 w-4" />
|
||||
</a>
|
||||
</Button>
|
||||
{placement.invite_link && (
|
||||
<Button variant="outline" size="icon" asChild>
|
||||
<a
|
||||
href={placement.invite_link}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<ExternalLink className="h-4 w-4" />
|
||||
</a>
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
@@ -334,16 +292,18 @@ export default function PlacementDetailPage() {
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Креатив</CardTitle>
|
||||
<CardDescription>{placement.creative_name}</CardDescription>
|
||||
<CardDescription>ID: {placement.creative_id}</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<Button variant="outline" asChild>
|
||||
<Link
|
||||
href={`/dashboard/${workspaceId}/creatives/${placement.creative_id}`}
|
||||
>
|
||||
Просмотреть креатив
|
||||
</Link>
|
||||
</Button>
|
||||
{placement.creative_id && (
|
||||
<Button variant="outline" asChild>
|
||||
<Link
|
||||
href={`/dashboard/${workspaceId}/creatives/${placement.creative_id}`}
|
||||
>
|
||||
Просмотреть креатив
|
||||
</Link>
|
||||
</Button>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
@@ -357,57 +317,45 @@ export default function PlacementDetailPage() {
|
||||
<dl className="grid gap-4 sm:grid-cols-2">
|
||||
<div>
|
||||
<dt className="text-sm font-medium text-muted-foreground">
|
||||
Проект
|
||||
Канал размещения
|
||||
</dt>
|
||||
<dd className="text-sm">{placement.project_channel_title}</dd>
|
||||
<dd className="text-sm">{placement.channel.title}</dd>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<dt className="text-sm font-medium text-muted-foreground">
|
||||
Дата размещения
|
||||
</dt>
|
||||
<dd className="text-sm">{formatDate(placement.placement_date)}</dd>
|
||||
<dd className="text-sm">{placement.details?.placement_at ? formatDate(placement.details.placement_at) : "—"}</dd>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<dt className="text-sm font-medium text-muted-foreground">
|
||||
Статус просмотров
|
||||
Дата оплаты
|
||||
</dt>
|
||||
<dd className="text-sm">
|
||||
<Badge variant="outline">
|
||||
{placement.views_availability === "available"
|
||||
? "Автообновление"
|
||||
: placement.views_availability === "manual"
|
||||
? "Ручной ввод"
|
||||
: placement.views_availability === "unavailable"
|
||||
? "Недоступно"
|
||||
: "Не проверено"}
|
||||
</Badge>
|
||||
</dd>
|
||||
<dd className="text-sm">{placement.details?.payment_at ? formatDate(placement.details.payment_at) : "—"}</dd>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<dt className="text-sm font-medium text-muted-foreground">
|
||||
Последнее обновление просмотров
|
||||
Формат
|
||||
</dt>
|
||||
<dd className="text-sm">
|
||||
{formatDateTime(placement.last_views_fetch_at)}
|
||||
</dd>
|
||||
<dd className="text-sm">{placement.details?.format || "—"}</dd>
|
||||
</div>
|
||||
|
||||
{placement.ad_post_url && (
|
||||
{placement.placement_post?.post?.url && (
|
||||
<div className="sm:col-span-2">
|
||||
<dt className="text-sm font-medium text-muted-foreground">
|
||||
Ссылка на пост
|
||||
</dt>
|
||||
<dd className="text-sm">
|
||||
<a
|
||||
href={placement.ad_post_url}
|
||||
href={placement.placement_post.post.url}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-primary hover:underline inline-flex items-center gap-1"
|
||||
>
|
||||
{placement.ad_post_url}
|
||||
{placement.placement_post.post.url}
|
||||
<ExternalLink className="h-3 w-3" />
|
||||
</a>
|
||||
</dd>
|
||||
|
||||
Reference in New Issue
Block a user