forked from admin/zeropost-tool
feat: spending section — all providers breakdown, default period=all
This commit is contained in:
+57
-21
@@ -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,27 +342,63 @@ 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">
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<span className="text-lg">{p.icon}</span>
|
||||
<div>
|
||||
<div className="font-medium text-sm">{p.label}</div>
|
||||
<div className="text-xs text-gray-500">{p.desc}</div>
|
||||
{ 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">{icon}</span>
|
||||
<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(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(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 className="ml-auto text-accent font-bold">₽ {fmt(p.data?.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>
|
||||
))}
|
||||
);
|
||||
})}
|
||||
</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">
|
||||
<table className="w-full text-sm">
|
||||
|
||||
Reference in New Issue
Block a user