feat: global ui & functional update
This commit is contained in:
@@ -157,17 +157,17 @@ const exportToCSV = (placements: Placement[], filename: string) => {
|
||||
];
|
||||
|
||||
const rows = placements.map((p) => [
|
||||
p.placement_channel_title,
|
||||
p.channel.title,
|
||||
"",
|
||||
p.creative_name,
|
||||
new Date(p.placement_date).toISOString().split("T")[0],
|
||||
p.cost?.toString() || "",
|
||||
p.subscriptions_count.toString(),
|
||||
p.views_count?.toString() || "",
|
||||
calculateCPS(p.cost, p.subscriptions_count)?.toFixed(2) || "",
|
||||
calculateCPM(p.cost, p.views_count)?.toFixed(2) || "",
|
||||
"—", // creative_name not available in new structure
|
||||
p.details?.placement_at ? new Date(p.details.placement_at).toISOString().split("T")[0] : "",
|
||||
p.details?.cost?.value?.toString() || "",
|
||||
p.placement_post?.subscriptions_count.toString() || "0",
|
||||
p.placement_post?.views_count?.toString() || "",
|
||||
calculateCPS(p.details?.cost?.value ?? null, p.placement_post?.subscriptions_count ?? 0)?.toFixed(2) || "",
|
||||
calculateCPM(p.details?.cost?.value ?? null, p.placement_post?.views_count ?? null)?.toFixed(2) || "",
|
||||
p.status,
|
||||
p.ad_post_url || "",
|
||||
p.placement_post?.post?.url || "",
|
||||
p.invite_link,
|
||||
]);
|
||||
|
||||
@@ -192,15 +192,15 @@ const exportToExcel = (placements: Placement[], filename: string) => {
|
||||
];
|
||||
|
||||
const rows = placements.map((p) => [
|
||||
p.placement_channel_title,
|
||||
p.channel.title,
|
||||
"",
|
||||
p.creative_name,
|
||||
new Date(p.placement_date).toISOString().split("T")[0],
|
||||
p.cost?.toString() || "",
|
||||
p.subscriptions_count.toString(),
|
||||
p.views_count?.toString() || "",
|
||||
calculateCPS(p.cost, p.subscriptions_count)?.toFixed(2) || "",
|
||||
calculateCPM(p.cost, p.views_count)?.toFixed(2) || "",
|
||||
"—",
|
||||
p.details?.placement_at ? new Date(p.details.placement_at).toISOString().split("T")[0] : "",
|
||||
p.details?.cost?.value?.toString() || "",
|
||||
p.placement_post?.subscriptions_count.toString() || "0",
|
||||
p.placement_post?.views_count?.toString() || "",
|
||||
calculateCPS(p.details?.cost?.value ?? null, p.placement_post?.subscriptions_count ?? 0)?.toFixed(2) || "",
|
||||
calculateCPM(p.details?.cost?.value ?? null, p.placement_post?.views_count ?? null)?.toFixed(2) || "",
|
||||
p.status,
|
||||
]);
|
||||
|
||||
@@ -213,18 +213,21 @@ const exportToExcel = (placements: Placement[], filename: string) => {
|
||||
|
||||
const exportToTxt = (placements: Placement[], filename: string) => {
|
||||
const lines = placements.map((p) => {
|
||||
const cps = calculateCPS(p.cost, p.subscriptions_count);
|
||||
const cpm = calculateCPM(p.cost, p.views_count);
|
||||
const cost = p.details?.cost?.value ?? null;
|
||||
const subscriptions = p.placement_post?.subscriptions_count ?? 0;
|
||||
const views = p.placement_post?.views_count ?? null;
|
||||
const cps = calculateCPS(cost, subscriptions);
|
||||
const cpm = calculateCPM(cost, views);
|
||||
return [
|
||||
`Канал: ${p.placement_channel_title}`,
|
||||
`Креатив: ${p.creative_name}`,
|
||||
`Дата: ${formatDate(p.placement_date)}`,
|
||||
`Стоимость: ${formatCurrency(p.cost)}`,
|
||||
`Подписки: ${p.subscriptions_count}`,
|
||||
`Просмотры: ${p.views_count || "—"}`,
|
||||
`Канал: ${p.channel.title}`,
|
||||
`Креатив: —`,
|
||||
`Дата: ${p.details?.placement_at ? formatDate(p.details.placement_at) : "—"}`,
|
||||
`Стоимость: ${formatCurrency(cost)}`,
|
||||
`Подписки: ${p.placement_post?.subscriptions_count ?? "—"}`,
|
||||
`Просмотры: ${p.placement_post?.views_count || "—"}`,
|
||||
`CPS: ${cps ? formatCurrency(cps) : "—"}`,
|
||||
`CPM: ${cpm ? formatCurrency(cpm) : "—"}`,
|
||||
`Ссылка: ${p.invite_link}`,
|
||||
`Ссылка: ${p.invite_link || "—"}`,
|
||||
"---",
|
||||
].join("\n");
|
||||
});
|
||||
@@ -320,8 +323,8 @@ export default function PlacementsPage() {
|
||||
return placements
|
||||
.filter(
|
||||
(p) =>
|
||||
p.placement_channel_title.toLowerCase().includes(search.toLowerCase()) ||
|
||||
p.creative_name.toLowerCase().includes(search.toLowerCase())
|
||||
p.channel.title.toLowerCase().includes(search.toLowerCase()) ||
|
||||
(p.creative_id && p.creative_id.toString().toLowerCase().includes(search.toLowerCase()))
|
||||
)
|
||||
.sort((a, b) => {
|
||||
if (!sortField || !sortOrder) return 0;
|
||||
@@ -329,30 +332,39 @@ export default function PlacementsPage() {
|
||||
let aVal: number | string = 0;
|
||||
let bVal: number | string = 0;
|
||||
|
||||
const aCost = a.details?.cost?.value ?? null;
|
||||
const bCost = b.details?.cost?.value ?? null;
|
||||
const aSubs = a.placement_post?.subscriptions_count ?? 0;
|
||||
const bSubs = b.placement_post?.subscriptions_count ?? 0;
|
||||
const aViews = a.placement_post?.views_count ?? null;
|
||||
const bViews = b.placement_post?.views_count ?? null;
|
||||
|
||||
switch (sortField) {
|
||||
case "date":
|
||||
const aDate = a.details?.placement_at || "";
|
||||
const bDate = b.details?.placement_at || "";
|
||||
return sortOrder === "asc"
|
||||
? new Date(a.placement_date).getTime() - new Date(b.placement_date).getTime()
|
||||
: new Date(b.placement_date).getTime() - new Date(a.placement_date).getTime();
|
||||
? aDate.localeCompare(bDate)
|
||||
: bDate.localeCompare(aDate);
|
||||
case "cost":
|
||||
aVal = a.cost ?? 0;
|
||||
bVal = b.cost ?? 0;
|
||||
aVal = aCost ?? 0;
|
||||
bVal = bCost ?? 0;
|
||||
break;
|
||||
case "subscriptions":
|
||||
aVal = a.subscriptions_count;
|
||||
bVal = b.subscriptions_count;
|
||||
aVal = aSubs;
|
||||
bVal = bSubs;
|
||||
break;
|
||||
case "views":
|
||||
aVal = a.views_count ?? 0;
|
||||
bVal = b.views_count ?? 0;
|
||||
aVal = aViews ?? 0;
|
||||
bVal = bViews ?? 0;
|
||||
break;
|
||||
case "cps":
|
||||
aVal = calculateCPS(a.cost, a.subscriptions_count) ?? 0;
|
||||
bVal = calculateCPS(b.cost, b.subscriptions_count) ?? 0;
|
||||
aVal = calculateCPS(aCost, aSubs) ?? 0;
|
||||
bVal = calculateCPS(bCost, bSubs) ?? 0;
|
||||
break;
|
||||
case "cpm":
|
||||
aVal = calculateCPM(a.cost, a.views_count) ?? 0;
|
||||
bVal = calculateCPM(b.cost, b.views_count) ?? 0;
|
||||
aVal = calculateCPM(aCost, aViews) ?? 0;
|
||||
bVal = calculateCPM(bCost, bViews) ?? 0;
|
||||
break;
|
||||
case "status":
|
||||
aVal = a.status;
|
||||
@@ -370,14 +382,14 @@ export default function PlacementsPage() {
|
||||
|
||||
// Calculate totals with aggregation functions
|
||||
const totals = useMemo(() => {
|
||||
const costs = filteredPlacements.map((p) => p.cost).filter((c): c is number => c !== null);
|
||||
const subs = filteredPlacements.map((p) => p.subscriptions_count);
|
||||
const views = filteredPlacements.map((p) => p.views_count).filter((v): v is number => v !== null);
|
||||
const costs = filteredPlacements.map((p) => p.details?.cost?.value).filter((c): c is number => c !== null);
|
||||
const subs = filteredPlacements.map((p) => p.placement_post?.subscriptions_count ?? 0);
|
||||
const views = filteredPlacements.map((p) => p.placement_post?.views_count).filter((v): v is number => v !== null);
|
||||
const cpsValues = filteredPlacements
|
||||
.map((p) => calculateCPS(p.cost, p.subscriptions_count))
|
||||
.map((p) => calculateCPS(p.details?.cost?.value ?? null, p.placement_post?.subscriptions_count ?? 0))
|
||||
.filter((c): c is number => c !== null);
|
||||
const cpmValues = filteredPlacements
|
||||
.map((p) => calculateCPM(p.cost, p.views_count))
|
||||
.map((p) => calculateCPM(p.details?.cost?.value ?? null, p.placement_post?.views_count ?? null))
|
||||
.filter((c): c is number => c !== null);
|
||||
|
||||
return {
|
||||
@@ -639,28 +651,31 @@ export default function PlacementsPage() {
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{filteredPlacements.map((placement) => {
|
||||
const cps = calculateCPS(placement.cost, placement.subscriptions_count);
|
||||
const cpm = calculateCPM(placement.cost, placement.views_count);
|
||||
const cost = placement.details?.cost?.value ?? null;
|
||||
const subscriptions = placement.placement_post?.subscriptions_count ?? 0;
|
||||
const views = placement.placement_post?.views_count ?? null;
|
||||
const cps = calculateCPS(cost, subscriptions);
|
||||
const cpm = calculateCPM(cost, views);
|
||||
|
||||
return (
|
||||
<TableRow key={placement.id}>
|
||||
<TableCell>
|
||||
<div className="font-medium">{placement.placement_channel_title}</div>
|
||||
<div className="font-medium">{placement.channel.title}</div>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<div className="text-sm">{placement.creative_name}</div>
|
||||
<div className="text-sm">—</div>
|
||||
</TableCell>
|
||||
<TableCell>{formatDate(placement.placement_date)}</TableCell>
|
||||
<TableCell>{formatCurrency(placement.cost)}</TableCell>
|
||||
<TableCell>{formatNumber(placement.subscriptions_count)}</TableCell>
|
||||
<TableCell>{formatNumber(placement.views_count ?? null)}</TableCell>
|
||||
<TableCell>{placement.details?.placement_at ? formatDate(placement.details.placement_at) : "—"}</TableCell>
|
||||
<TableCell>{formatCurrency(cost)}</TableCell>
|
||||
<TableCell>{formatNumber(subscriptions)}</TableCell>
|
||||
<TableCell>{formatNumber(views)}</TableCell>
|
||||
<TableCell>{cps ? formatCurrency(cps) : "—"}</TableCell>
|
||||
<TableCell>{cpm ? formatCurrency(cpm) : "—"}</TableCell>
|
||||
<TableCell>
|
||||
<Badge
|
||||
variant={placement.status === "active" ? "default" : "secondary"}
|
||||
variant={placement.status === "Оплачено" ? "default" : "secondary"}
|
||||
>
|
||||
{placement.status === "active" ? "Активен" : "Архив"}
|
||||
{placement.status}
|
||||
</Badge>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
@@ -677,10 +692,10 @@ export default function PlacementsPage() {
|
||||
Подробнее
|
||||
</Link>
|
||||
</DropdownMenuItem>
|
||||
{placement.ad_post_url && (
|
||||
{placement.placement_post?.post?.url && (
|
||||
<DropdownMenuItem asChild>
|
||||
<a
|
||||
href={placement.ad_post_url}
|
||||
href={placement.placement_post.post.url}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
|
||||
Reference in New Issue
Block a user