improvement for the backend api

This commit is contained in:
ivannoskov
2025-11-19 12:56:04 +03:00
parent b0a9934220
commit d4672ea32b
40 changed files with 2383 additions and 3313 deletions

View File

@@ -73,10 +73,10 @@ export default function PurchasesPage() {
setLoading(true);
const [purchasesRes, channelsRes] = await Promise.all([
purchasesApi.list(),
channelsApi.list({}),
channelsApi.list(),
]);
setPurchases(purchasesRes.data);
setChannels(channelsRes.data);
setPurchases(purchasesRes.placements);
setChannels(channelsRes.target_channels);
} catch (err: any) {
setError(err?.error?.message || "Ошибка загрузки данных");
} finally {
@@ -87,26 +87,24 @@ export default function PurchasesPage() {
const filteredPurchases = purchases.filter((purchase) => {
// Поиск
const matchesSearch =
purchase.external_channel.title
purchase.external_channel_title
.toLowerCase()
.includes(searchQuery.toLowerCase()) ||
purchase.target_channel.title
purchase.target_channel_title
.toLowerCase()
.includes(searchQuery.toLowerCase()) ||
purchase.creative.name.toLowerCase().includes(searchQuery.toLowerCase());
purchase.creative_name.toLowerCase().includes(searchQuery.toLowerCase());
// Фильтр по каналу
const matchesChannel =
filterChannel === "all" || purchase.target_channel.id === filterChannel;
filterChannel === "all" || purchase.target_channel_id === filterChannel;
// Фильтр по статусу
let matchesStatus = true;
if (filterStatus === "completed") {
matchesStatus = !!purchase.actual_date;
} else if (filterStatus === "scheduled") {
matchesStatus = !purchase.actual_date;
} else if (filterStatus === "archived") {
matchesStatus = purchase.is_archived;
if (filterStatus === "archived") {
matchesStatus = purchase.status === "archived";
} else if (filterStatus === "active") {
matchesStatus = purchase.status === "active";
}
return matchesSearch && matchesChannel && matchesStatus;
@@ -115,17 +113,13 @@ export default function PurchasesPage() {
// Статистика
const stats = {
total: purchases.length,
completed: purchases.filter((p) => p.actual_date).length,
scheduled: purchases.filter((p) => !p.actual_date).length,
active: purchases.filter((p) => p.status === "active").length,
archived: purchases.filter((p) => p.status === "archived").length,
totalCost: purchases.reduce((sum, p) => sum + (p.cost || 0), 0),
totalSubscriptions: purchases.reduce(
(sum, p) => sum + p.subscriptions_count,
0
),
avgCpf:
purchases.length > 0
? purchases.reduce((sum, p) => sum + (p.cpf || 0), 0) / purchases.length
: 0,
};
return (
@@ -169,7 +163,7 @@ export default function PurchasesPage() {
{formatNumber(stats.total)}
</div>
<p className="text-xs text-muted-foreground mt-1">
{stats.completed} завершено, {stats.scheduled} запланировано
{stats.active} активных, {stats.archived} в архиве
</p>
</CardContent>
</Card>
@@ -203,20 +197,6 @@ export default function PurchasesPage() {
<p className="text-xs text-muted-foreground mt-1">Всего</p>
</CardContent>
</Card>
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">Средний CPF</CardTitle>
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">
{formatMetric(stats.avgCpf)}
</div>
<p className="text-xs text-muted-foreground mt-1">
По всем закупам
</p>
</CardContent>
</Card>
</div>
<Card>
@@ -308,9 +288,8 @@ export default function PurchasesPage() {
<TableHead>Креатив</TableHead>
<TableHead>Дата</TableHead>
<TableHead className="text-right">Стоимость</TableHead>
<TableHead className="text-right">Подписчики</TableHead>
<TableHead className="text-right">CPF</TableHead>
<TableHead className="text-right">Конверсия</TableHead>
<TableHead className="text-right">Подписки</TableHead>
<TableHead className="text-right">Просмотры</TableHead>
<TableHead></TableHead>
</TableRow>
</TableHeader>
@@ -318,34 +297,27 @@ export default function PurchasesPage() {
{filteredPurchases.map((purchase) => (
<TableRow key={purchase.id}>
<TableCell>
{purchase.is_archived ? (
{purchase.status === "archived" ? (
<Badge variant="secondary">
<Archive className="h-3 w-3 mr-1" />
Архив
</Badge>
) : purchase.actual_date ? (
) : (
<Badge variant="default">
<CheckCircle className="h-3 w-3 mr-1" />
Завершено
</Badge>
) : (
<Badge variant="outline">
<Clock className="h-3 w-3 mr-1" />
Запланировано
Активно
</Badge>
)}
</TableCell>
<TableCell className="font-medium">
{purchase.target_channel.title}
{purchase.target_channel_title}
</TableCell>
<TableCell>{purchase.external_channel.title}</TableCell>
<TableCell>{purchase.external_channel_title}</TableCell>
<TableCell className="max-w-[200px] truncate">
{purchase.creative.name}
{purchase.creative_name}
</TableCell>
<TableCell>
{formatDate(
purchase.actual_date || purchase.scheduled_date
)}
{formatDate(purchase.placement_date)}
</TableCell>
<TableCell className="text-right">
{formatCurrency(purchase.cost)}
@@ -354,11 +326,8 @@ export default function PurchasesPage() {
{formatNumber(purchase.subscriptions_count)}
</TableCell>
<TableCell className="text-right">
{purchase.cpf ? `${formatMetric(purchase.cpf)}` : "—"}
</TableCell>
<TableCell className="text-right">
{purchase.conversion_rate
? formatPercent(purchase.conversion_rate)
{purchase.views_count
? formatNumber(purchase.views_count)
: "—"}
</TableCell>
<TableCell className="text-right">