nordabiz/templates/admin/ai_usage_dashboard.html
Maciej Pienczyn 954cd02975
Some checks are pending
NordaBiz Tests / Unit & Integration Tests (push) Waiting to run
NordaBiz Tests / E2E Tests (Playwright) (push) Blocked by required conditions
NordaBiz Tests / Smoke Tests (Production) (push) Blocked by required conditions
NordaBiz Tests / Send Failure Notification (push) Blocked by required conditions
fix: convert Decimal to float for PLN cost calculation in template
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-11 04:24:25 +01:00

383 lines
14 KiB
HTML

{% extends "base.html" %}
{% block title %}Monitoring AI - Panel Admina{% endblock %}
{% block extra_css %}
<style>
.page-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: var(--spacing-xl);
}
.page-header h1 { font-size: var(--font-size-2xl); }
.period-filters {
display: flex;
gap: var(--spacing-sm);
margin-bottom: var(--spacing-xl);
flex-wrap: wrap;
align-items: center;
}
.period-btn {
padding: var(--spacing-sm) var(--spacing-md);
border-radius: var(--radius);
border: 1px solid var(--border);
background: var(--surface);
text-decoration: none;
color: var(--text-secondary);
font-size: var(--font-size-sm);
transition: var(--transition);
}
.period-btn:hover { background: var(--background); color: var(--text-primary); }
.period-btn.active { background: var(--primary); border-color: var(--primary); color: white; }
/* Koszty - 3 karty */
.cost-cards {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: var(--spacing-lg);
margin-bottom: var(--spacing-2xl);
}
.cost-card {
background: var(--surface);
padding: var(--spacing-xl);
border-radius: var(--radius-lg);
box-shadow: var(--shadow);
text-align: center;
}
.cost-card.today { border-left: 4px solid var(--primary); }
.cost-card.month { border-left: 4px solid var(--warning); }
.cost-card.total { border-left: 4px solid #6366f1; }
.cost-amount {
font-size: var(--font-size-2xl);
font-weight: 700;
}
.cost-amount.today-val { color: var(--primary); }
.cost-amount.month-val { color: var(--warning); }
.cost-amount.total-val { color: #6366f1; }
.cost-label {
color: var(--text-secondary);
font-size: var(--font-size-sm);
margin-top: var(--spacing-xs);
}
.cost-requests {
color: var(--text-muted);
font-size: var(--font-size-xs);
margin-top: 2px;
}
.section {
background: var(--surface);
padding: var(--spacing-xl);
border-radius: var(--radius-lg);
box-shadow: var(--shadow);
margin-bottom: var(--spacing-xl);
}
.section h2 {
font-size: var(--font-size-lg);
margin-bottom: var(--spacing-lg);
color: var(--text-primary);
border-bottom: 2px solid var(--border);
padding-bottom: var(--spacing-sm);
}
.sections-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(400px, 1fr));
gap: var(--spacing-xl);
margin-bottom: var(--spacing-xl);
}
/* Paski wykorzystania */
.usage-breakdown { display: flex; flex-direction: column; gap: var(--spacing-md); }
.usage-row { display: flex; align-items: center; gap: var(--spacing-md); }
.usage-label {
width: 200px; min-width: 200px;
font-size: var(--font-size-sm);
color: var(--text-secondary);
}
.usage-bar-container {
flex: 1; height: 24px;
background: var(--background);
border-radius: var(--radius);
overflow: hidden;
}
.usage-bar {
height: 100%; border-radius: var(--radius);
min-width: 4px; transition: width 0.3s ease;
}
.usage-bar.chat { background: #3b82f6; }
.usage-bar.news { background: #10b981; }
.usage-bar.user { background: #f59e0b; }
.usage-bar.image { background: #8b5cf6; }
.usage-bar.other { background: #6b7280; }
.usage-stats {
display: flex; gap: var(--spacing-md); align-items: center;
min-width: 140px; justify-content: flex-end;
}
.usage-count { font-weight: 600; font-size: var(--font-size-sm); }
.usage-cost {
font-size: var(--font-size-sm);
color: var(--text-secondary);
min-width: 70px; text-align: right;
}
/* Ranking */
.ranking-table { width: 100%; border-collapse: collapse; }
.ranking-table th, .ranking-table td {
padding: var(--spacing-md);
text-align: left;
border-bottom: 1px solid var(--border);
}
.ranking-table th {
background: var(--background);
font-weight: 600;
font-size: var(--font-size-sm);
color: var(--text-secondary);
}
.ranking-table tr:hover { background: var(--background); }
.rank-num { width: 40px; text-align: center; font-weight: 700; color: var(--text-muted); }
.rank-num.r1 { color: #f59e0b; }
.rank-num.r2 { color: #94a3b8; }
.rank-num.r3 { color: #b45309; }
.user-info { display: flex; flex-direction: column; }
.user-name { font-weight: 600; color: var(--text-primary); }
.user-company { font-size: var(--font-size-xs); color: var(--text-secondary); }
.user-link {
text-decoration: none; display: block;
padding: var(--spacing-xs); margin: calc(-1 * var(--spacing-xs));
border-radius: var(--radius-sm); transition: var(--transition);
}
.user-link:hover { background: var(--primary-light, #eff6ff); }
.user-link:hover .user-name { color: var(--primary); }
.cost-badge {
display: inline-block; padding: 2px 8px;
border-radius: var(--radius-sm);
font-size: var(--font-size-sm); font-weight: 600;
background: #f0fdf4; color: #166534;
}
.cost-badge.medium { background: #fef3c7; color: #92400e; }
.cost-badge.high { background: #fee2e2; color: #991b1b; }
/* Historia dzienna */
.daily-table { width: 100%; border-collapse: collapse; font-size: var(--font-size-sm); }
.daily-table th, .daily-table td {
padding: var(--spacing-sm) var(--spacing-md);
text-align: left;
border-bottom: 1px solid var(--border);
}
.daily-table th { background: var(--background); font-weight: 600; color: var(--text-secondary); }
.daily-table tr:hover { background: var(--background); }
.empty-state { text-align: center; padding: var(--spacing-2xl); color: var(--text-secondary); }
.export-link {
font-size: var(--font-size-sm);
color: var(--text-secondary);
text-decoration: none;
padding: 6px 14px;
border: 1px solid var(--border);
border-radius: var(--radius);
transition: var(--transition);
}
.export-link:hover { background: var(--background); color: var(--text-primary); }
@media (max-width: 768px) {
.cost-cards { grid-template-columns: 1fr; }
.sections-grid { grid-template-columns: 1fr; }
.usage-label { width: 120px; min-width: 120px; }
.ranking-table { font-size: var(--font-size-sm); }
}
</style>
{% endblock %}
{% block content %}
<div class="page-header">
<div>
<h1>Monitoring AI</h1>
<p class="text-muted">Koszty i wykorzystanie sztucznej inteligencji</p>
</div>
<div style="display: flex; gap: var(--spacing-sm);">
<a href="{{ url_for('admin.admin_ai_usage_export', period=current_period) }}" class="export-link">Eksport CSV</a>
<a href="{{ url_for('public.dashboard') }}" class="btn btn-secondary">Powrot</a>
</div>
</div>
<!-- Okres -->
<div class="period-filters">
<span class="text-muted" style="padding: var(--spacing-sm) 0;">Okres:</span>
<a href="{{ url_for('admin.admin_ai_usage', period='day') }}" class="period-btn {% if current_period == 'day' %}active{% endif %}">Dzisiaj</a>
<a href="{{ url_for('admin.admin_ai_usage', period='week') }}" class="period-btn {% if current_period == 'week' %}active{% endif %}">Tydzien</a>
<a href="{{ url_for('admin.admin_ai_usage', period='month') }}" class="period-btn {% if current_period == 'month' %}active{% endif %}">Miesiac</a>
<a href="{{ url_for('admin.admin_ai_usage', period='all') }}" class="period-btn {% if current_period == 'all' %}active{% endif %}">Od poczatku</a>
</div>
<!-- 3 karty kosztow -->
<div class="cost-cards">
<div class="cost-card today">
<div class="cost-amount today-val">{{ "%.2f"|format(stats.today_cost_pln) }} zl</div>
<div class="cost-label">Dzisiaj</div>
<div class="cost-requests">{{ stats.today_requests }} zapytan</div>
</div>
<div class="cost-card month">
<div class="cost-amount month-val">{{ "%.2f"|format(stats.month_cost_pln) }} zl</div>
<div class="cost-label">Ten miesiac</div>
<div class="cost-requests">{{ stats.month_requests }} zapytan</div>
</div>
<div class="cost-card total">
<div class="cost-amount total-val">{{ "%.2f"|format(stats.all_cost_pln) }} zl</div>
<div class="cost-label">Od poczatku</div>
<div class="cost-requests">{{ stats.all_requests }} zapytan</div>
</div>
</div>
<!-- Do czego AI + Kto korzysta -->
<div class="sections-grid">
<!-- Wykorzystanie wg typu -->
<div class="section">
<h2>Do czego uzywany jest AI</h2>
{% if usage_by_type %}
<div class="usage-breakdown">
{% for item in usage_by_type %}
<div class="usage-row">
<div class="usage-label">{{ item.type_label }}</div>
<div class="usage-bar-container">
<div class="usage-bar {{ item.type_class }}" style="width: {{ item.percentage }}%"></div>
</div>
<div class="usage-stats">
<span class="usage-count">{{ item.count }}x</span>
<span class="usage-cost">{{ "%.2f"|format(item.cost_pln) }} zl</span>
</div>
</div>
{% endfor %}
</div>
{% else %}
<div class="empty-state"><p>Brak danych</p></div>
{% endif %}
</div>
<!-- Ranking uzytkownikow -->
<div class="section">
<h2>Kto najczesciej korzysta</h2>
{% if user_rankings %}
<table class="ranking-table">
<thead>
<tr>
<th>#</th>
<th>Uzytkownik</th>
<th>Zapytania</th>
<th>Koszt</th>
</tr>
</thead>
<tbody>
{% for user in user_rankings[:10] %}
<tr>
<td class="rank-num {% if loop.index == 1 %}r1{% elif loop.index == 2 %}r2{% elif loop.index == 3 %}r3{% endif %}">{{ loop.index }}</td>
<td>
<a href="{{ url_for('admin.admin_ai_usage_user', user_id=user.id) }}" class="user-link">
<div class="user-info">
<span class="user-name">{{ user.name }}</span>
<span class="user-company">{{ user.company }}</span>
</div>
</a>
</td>
<td>{{ user.requests }}</td>
<td>
<span class="cost-badge {% if user.cost_pln > 1.0 %}high{% elif user.cost_pln > 0.2 %}medium{% endif %}">
{{ "%.2f"|format(user.cost_pln) }} zl
</span>
</td>
</tr>
{% endfor %}
</tbody>
</table>
{% else %}
<div class="empty-state"><p>Brak danych</p></div>
{% endif %}
</div>
</div>
<!-- Ranking firm -->
<div class="section">
<h2>Ktore firmy korzystaja z AI</h2>
{% if company_rankings %}
<table class="ranking-table">
<thead>
<tr>
<th>#</th>
<th>Firma</th>
<th>Uzytkownicy</th>
<th>Zapytania</th>
<th>Koszt</th>
</tr>
</thead>
<tbody>
{% for company in company_rankings %}
<tr>
<td class="rank-num {% if loop.index == 1 %}r1{% elif loop.index == 2 %}r2{% elif loop.index == 3 %}r3{% endif %}">{{ loop.index }}</td>
<td>
<a href="{{ url_for('admin.admin_ai_usage_company', company_id=company.id) }}" class="user-link">
<span class="user-name">{{ company.name }}</span>
</a>
</td>
<td>{{ company.unique_users }}</td>
<td>{{ company.requests }}</td>
<td>
<span class="cost-badge {% if company.cost_pln > 1.0 %}high{% elif company.cost_pln > 0.2 %}medium{% endif %}">
{{ "%.2f"|format(company.cost_pln) }} zl
</span>
</td>
</tr>
{% endfor %}
</tbody>
</table>
{% else %}
<div class="empty-state"><p>Brak danych o firmach</p></div>
{% endif %}
</div>
<!-- Historia dzienna -->
<div class="section">
<h2>Historia dzienna (ostatnie 14 dni)</h2>
{% if daily_history %}
<table class="daily-table">
<thead>
<tr>
<th>Data</th>
<th>Zapytania</th>
<th>Chat</th>
<th>Newsy</th>
<th>Koszt</th>
</tr>
</thead>
<tbody>
{% for day in daily_history %}
<tr>
<td>{{ day.date.strftime('%d.%m.%Y') }}</td>
<td><strong>{{ day.total_requests }}</strong></td>
<td>{{ day.chat_requests }}</td>
<td>{{ day.news_evaluation_requests }}</td>
<td>
{% set day_cost_pln = (((day.total_cost_cents or 0)|float) / 100) * stats.usd_to_pln %}
<span class="cost-badge {% if day_cost_pln > 1.0 %}high{% elif day_cost_pln > 0.2 %}medium{% endif %}">
{{ "%.2f"|format(day_cost_pln) }} zl
</span>
</td>
</tr>
{% endfor %}
</tbody>
</table>
{% else %}
<div class="empty-state"><p>Brak historii - dane pojawia sie po pierwszym uzyciu AI</p></div>
{% endif %}
</div>
{% endblock %}