feat: spending section — all providers breakdown, default period=all

This commit is contained in:
Nik (Claude)
2026-06-16 13:53:18 +03:00
parent 58e6092b7c
commit ef843768af
+51 -15
View File
@@ -266,7 +266,7 @@ const PERIODS = [
{ v: 'today', label: 'Сегодня' },
{ v: 'week', label: '7 дней' },
{ v: 'month', label: '30 дней' },
{ v: 'alltime', label: 'Всё время' },
{ v: 'all', label: 'Всё время' },
];
const TYPE_LABELS = {
@@ -281,7 +281,7 @@ function fmt(n) { return Number(n || 0).toFixed(2); }
function fmtI(n) { return Number(n || 0).toLocaleString('ru-RU'); }
function SpendingSection() {
const [period, setPeriod] = useState('month');
const [period, setPeriod] = useState('all');
const [data, setData] = useState(null);
const [byProv, setByProv] = useState(null);
const [loading, setLoading] = useState(false);
@@ -290,8 +290,8 @@ function SpendingSection() {
setLoading(true);
try {
const [r1, r2] = await Promise.all([
fetch(`/api/usage/summary?range=${p}&group_by=request_type`).then(r => r.json()),
fetch(`/api/usage/summary?range=${p}&group_by=provider`).then(r => r.json()),
fetch(`/api/usage/summary?range=${p === 'alltime' ? 'all' : p}&group_by=request_type`).then(r => r.json()),
fetch(`/api/usage/summary?range=${p === 'alltime' ? 'all' : p}&group_by=provider`).then(r => r.json()),
]);
setData(r1); setByProv(r2);
} catch {}
@@ -342,26 +342,62 @@ function SpendingSection() {
{/* По провайдерам */}
<div className="grid grid-cols-2 gap-3">
{[
{ key: 'aiprimetech', label: 'aiprimetech.io', icon: '💬', desc: 'Текст', data: aiprimetech },
{ key: 'routerai', label: 'routerai.ru', icon: '🖼', desc: 'Картинки', data: routerai },
].map(p => (
<div key={p.key} className="card p-4">
{ key: 'aiprimetech', label: 'aiprimetech.io', icon: '💬', desc: 'Текст' },
{ key: 'routerai', label: 'routerai.ru', icon: '🖼', desc: 'Картинки' },
].map(({ key, label, icon, desc }) => {
const d = byProv?.breakdown?.find(b => b.key === key);
return (
<div key={key} className="card p-4">
<div className="flex items-center gap-2 mb-2">
<span className="text-lg">{p.icon}</span>
<span className="text-lg">{icon}</span>
<div>
<div className="font-medium text-sm">{p.label}</div>
<div className="text-xs text-gray-500">{p.desc}</div>
<div className="font-medium text-sm">{label}</div>
<div className="text-xs text-gray-500">{desc}</div>
</div>
<div className="ml-auto text-accent font-bold"> {fmt(p.data?.cost_rub)}</div>
<div className="ml-auto text-accent font-bold"> {fmt(d?.cost_rub)}</div>
</div>
<div className="grid grid-cols-3 gap-1 text-xs text-gray-400">
<div><div className="font-medium text-gray-200">{fmtI(p.data?.calls)}</div>запросов</div>
<div><div className="font-medium text-red-400">{p.data?.failed||0}</div>ошибок</div>
<div><div className="font-medium text-gray-200">{fmtI(p.data?.image_count || (p.data?.prompt_tokens||0)+(p.data?.completion_tokens||0))}</div>{p.key==='routerai'?'картинок':'токенов'}</div>
<div><div className="font-medium text-gray-200">{fmtI(d?.calls)}</div>запросов</div>
<div><div className="font-medium text-red-400">{d?.failed||0}</div>ошибок</div>
<div><div className="font-medium text-gray-200">{fmtI(d?.image_count || (d?.prompt_tokens||0)+(d?.completion_tokens||0))}</div>{key==='routerai'?'картинок':'токенов'}</div>
</div>
</div>
);
})}
</div>
{/* Все провайдеры (включая fallback) */}
{byProv?.breakdown?.length > 2 && (
<div className="card overflow-hidden">
<div className="px-4 py-3 bg-surface2 text-xs text-gray-400 font-medium uppercase tracking-wide">
Все провайдеры
</div>
<table className="w-full text-sm">
<thead className="text-xs text-gray-500 bg-surface2/50">
<tr>
<th className="px-4 py-2 text-left">Провайдер</th>
<th className="px-4 py-2 text-right">Запросов</th>
<th className="px-4 py-2 text-right">Ошибок</th>
<th className="px-4 py-2 text-right">Объём</th>
<th className="px-4 py-2 text-right">Стоимость</th>
</tr>
</thead>
<tbody>
{byProv.breakdown.map((row, i) => (
<tr key={i} className="border-t border-border hover:bg-surface2/50">
<td className="px-4 py-2.5 font-medium">{row.key}</td>
<td className="px-4 py-2.5 text-right text-gray-300">{fmtI(row.calls)}</td>
<td className="px-4 py-2.5 text-right text-red-400">{row.failed||0}</td>
<td className="px-4 py-2.5 text-right text-gray-400 text-xs">
{row.image_count > 0 ? `${row.image_count} шт.` : fmtI((row.prompt_tokens||0)+(row.completion_tokens||0))}
</td>
<td className="px-4 py-2.5 text-right font-medium text-accent"> {fmt(row.cost_rub)}</td>
</tr>
))}
</tbody>
</table>
</div>
)}
{/* Таблица по операциям */}
<div className="card overflow-hidden">