442-sweep instructions #16 Dockerfile: runner (lean, 1.6 GB) + runner-with-chromium (2.9 GB). Compose + deploy.yml pushes 2 GHCR images. Backend saves 1.3 GB. #15 5 backend object endpoints + /analytics/objects/[id] page (gallery, POI table, sale chart) + sparkline-driven PrinzipObjectsTable. #14 prinzip_leads/deals + 2 MVs. Import script with PII-hashing & --inspect-only mode. 3 funnel endpoints + Sankey/Monthly charts. #13 README section with UI/CLI/SQL for 442-object sweep.
96 lines
2.7 KiB
TypeScript
96 lines
2.7 KiB
TypeScript
"use client";
|
|
|
|
import { useMemo } from "react";
|
|
|
|
import { usePrinzipFunnelMonthly } from "@/lib/analytics-api";
|
|
|
|
import { ChartShell } from "./ChartShell";
|
|
|
|
export function PrinzipFunnelMonthly({ months = 24 }: { months?: number }) {
|
|
const { data, isLoading } = usePrinzipFunnelMonthly(months);
|
|
|
|
const option = useMemo(() => {
|
|
const rows = data ?? [];
|
|
if (rows.length === 0) {
|
|
return {
|
|
title: {
|
|
text: "Нет данных CRM",
|
|
left: "center",
|
|
top: "middle",
|
|
textStyle: { color: "#9ca3af", fontWeight: 400, fontSize: 13 },
|
|
},
|
|
xAxis: { show: false },
|
|
yAxis: { show: false },
|
|
};
|
|
}
|
|
// Aggregate by month (sum across sources).
|
|
const byMonth: Record<
|
|
string,
|
|
{ leads: number; engaged: number; converted: number }
|
|
> = {};
|
|
for (const r of rows) {
|
|
const m = (r.month ?? "").slice(0, 7);
|
|
if (!byMonth[m]) byMonth[m] = { leads: 0, engaged: 0, converted: 0 };
|
|
byMonth[m].leads += r.leads;
|
|
byMonth[m].engaged += r.engaged;
|
|
byMonth[m].converted += r.converted;
|
|
}
|
|
const months = Object.keys(byMonth).sort();
|
|
const leads = months.map((m) => byMonth[m].leads);
|
|
const engaged = months.map((m) => byMonth[m].engaged);
|
|
const converted = months.map((m) => byMonth[m].converted);
|
|
const conv = months.map((m) =>
|
|
byMonth[m].leads > 0
|
|
? Math.round((byMonth[m].converted / byMonth[m].leads) * 1000) / 10
|
|
: null,
|
|
);
|
|
return {
|
|
tooltip: { trigger: "axis" },
|
|
legend: { data: ["Заявки", "Engaged", "Сделки", "Conversion %"] },
|
|
grid: { left: 56, right: 64, top: 40, bottom: 36 },
|
|
xAxis: { type: "category", data: months },
|
|
yAxis: [
|
|
{ type: "value", name: "шт", position: "left" },
|
|
{
|
|
type: "value",
|
|
name: "%",
|
|
position: "right",
|
|
axisLabel: { formatter: "{value}%" },
|
|
},
|
|
],
|
|
series: [
|
|
{
|
|
name: "Заявки",
|
|
type: "bar",
|
|
data: leads,
|
|
itemStyle: { color: "#9ca3af" },
|
|
},
|
|
{
|
|
name: "Engaged",
|
|
type: "bar",
|
|
data: engaged,
|
|
itemStyle: { color: "#3b82f6" },
|
|
},
|
|
{
|
|
name: "Сделки",
|
|
type: "bar",
|
|
data: converted,
|
|
itemStyle: { color: "#0a7a3a" },
|
|
},
|
|
{
|
|
name: "Conversion %",
|
|
type: "line",
|
|
yAxisIndex: 1,
|
|
smooth: true,
|
|
data: conv,
|
|
lineStyle: { color: "#c2410c", width: 2 },
|
|
itemStyle: { color: "#c2410c" },
|
|
},
|
|
],
|
|
};
|
|
}, [data, months]);
|
|
|
|
return (
|
|
<ChartShell option={option} loading={isLoading} height={320} notMerge />
|
|
);
|
|
}
|