Files
tgex-frontend/app/dashboard/[workspaceId]/purchase-plans/page.tsx
2025-12-29 10:12:13 +03:00

235 lines
8.6 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";
// ============================================================================
// Purchase Plans Overview Page
// ============================================================================
import { useEffect, useState } from "react";
import { useParams, useRouter } from "next/navigation";
import Link from "next/link";
import {
Loader2,
ClipboardList,
ChevronRight,
Tv,
Calendar,
DollarSign,
CheckCircle,
Clock,
XCircle,
} from "lucide-react";
import { DashboardHeader } from "@/components/layout/dashboard-header";
import { useWorkspace } from "@/components/providers/workspace-provider";
import { demoAwarePurchasePlanApi } from "@/lib/demo";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import type { Project, PurchasePlanChannel } from "@/lib/types/api";
// ============================================================================
// Helpers
// ============================================================================
const formatCurrency = (value: number | null) => {
if (value === null) return "—";
return new Intl.NumberFormat("ru-RU", {
style: "currency",
currency: "RUB",
minimumFractionDigits: 0,
maximumFractionDigits: 0,
}).format(value);
};
interface ProjectPlanStats {
totalChannels: number;
plannedChannels: number;
completedChannels: number;
cancelledChannels: number;
totalPlannedCost: number;
}
// ============================================================================
// Component
// ============================================================================
export default function PurchasePlansPage() {
const params = useParams();
const router = useRouter();
const workspaceId = params?.workspaceId as string;
const { projects, isLoadingProjects } = useWorkspace();
const [projectStats, setProjectStats] = useState<Record<string, ProjectPlanStats>>({});
const [loadingStats, setLoadingStats] = useState(true);
useEffect(() => {
if (projects.length > 0) {
loadAllStats();
} else {
setLoadingStats(false);
}
}, [projects, workspaceId]);
const loadAllStats = async () => {
setLoadingStats(true);
const stats: Record<string, ProjectPlanStats> = {};
for (const project of projects) {
try {
const response = await demoAwarePurchasePlanApi.list(workspaceId, project.id, { size: 100 });
const channels = response.items;
stats[project.id] = {
totalChannels: channels.length,
plannedChannels: channels.filter((c) => c.status === "planned").length,
completedChannels: channels.filter((c) => c.status === "completed").length,
cancelledChannels: channels.filter((c) => c.status === "cancelled").length,
totalPlannedCost: channels
.filter((c) => c.status === "planned")
.reduce((sum, c) => sum + (c.planned_cost || 0), 0),
};
} catch (err) {
console.error(`Failed to load plan for project ${project.id}:`, err);
stats[project.id] = {
totalChannels: 0,
plannedChannels: 0,
completedChannels: 0,
cancelledChannels: 0,
totalPlannedCost: 0,
};
}
}
setProjectStats(stats);
setLoadingStats(false);
};
const isLoading = isLoadingProjects || loadingStats;
return (
<>
<DashboardHeader
breadcrumbs={[
{ label: "Главная", href: `/dashboard/${workspaceId}` },
{ 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>
</div>
{isLoading ? (
<div className="flex items-center justify-center py-12">
<Loader2 className="h-8 w-8 animate-spin text-muted-foreground" />
</div>
) : projects.length === 0 ? (
<Card>
<CardContent className="flex flex-col items-center justify-center py-12">
<ClipboardList 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 mb-4">
Добавьте проект через Telegram бота для создания плана закупок
</p>
<Button asChild>
<Link href={`/dashboard/${workspaceId}/projects`}>
Перейти к проектам
</Link>
</Button>
</CardContent>
</Card>
) : (
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
{projects.map((project) => {
const stats = projectStats[project.id];
return (
<Card
key={project.id}
className="cursor-pointer hover:bg-muted/50 transition-colors"
onClick={() =>
router.push(`/dashboard/${workspaceId}/purchase-plans/${project.id}`)
}
>
<CardHeader className="pb-3">
<div className="flex items-start justify-between">
<div className="flex items-center gap-3">
<div className="flex h-10 w-10 items-center justify-center rounded-lg bg-primary/10">
<Tv className="h-5 w-5 text-primary" />
</div>
<div>
<CardTitle className="text-lg">{project.title}</CardTitle>
{project.username && (
<CardDescription>@{project.username}</CardDescription>
)}
</div>
</div>
<ChevronRight className="h-5 w-5 text-muted-foreground" />
</div>
</CardHeader>
<CardContent>
{stats ? (
<div className="space-y-3">
<div className="flex items-center justify-between text-sm">
<span className="text-muted-foreground">Всего каналов:</span>
<span className="font-medium">{stats.totalChannels}</span>
</div>
<div className="flex gap-2 flex-wrap">
{stats.plannedChannels > 0 && (
<Badge variant="outline" className="gap-1">
<Clock className="h-3 w-3" />
{stats.plannedChannels} план.
</Badge>
)}
{stats.completedChannels > 0 && (
<Badge variant="default" className="gap-1">
<CheckCircle className="h-3 w-3" />
{stats.completedChannels} заверш.
</Badge>
)}
{stats.cancelledChannels > 0 && (
<Badge variant="secondary" className="gap-1">
<XCircle className="h-3 w-3" />
{stats.cancelledChannels} отмен.
</Badge>
)}
</div>
{stats.totalPlannedCost > 0 && (
<div className="flex items-center gap-2 text-sm pt-2 border-t">
<DollarSign className="h-4 w-4 text-muted-foreground" />
<span className="text-muted-foreground">План. бюджет:</span>
<span className="font-medium ml-auto">
{formatCurrency(stats.totalPlannedCost)}
</span>
</div>
)}
</div>
) : (
<div className="text-sm text-muted-foreground">Загрузка...</div>
)}
</CardContent>
</Card>
);
})}
</div>
)}
</div>
</>
);
}