Programming

6. pager


{% extends "base.html" %}
{% comment %}
  Block names to match your project base:
    title / extra_css / content / extra_js
  Uses the Bootstrap 5 already loaded by base.html, plus a small scoped
  stylesheet. Everything is under .drp so it cannot touch other pages.
{% endcomment %}
 
{% block title %}Динамический отчёт по портфелю{% endblock %}
 
{% block extra_css %}
<style>
  .drp{--ink:#0B0B0C;--soft:#2A2A2E;--gold:#F2C230;--gd:#C9971A;--rule:#E4E1D8;
       --muted:#8A867C;--bad:#B3341F;--good:#2F6B4F;--cool:#4A6E8A;
       font-family:Cambria,Georgia,serif;color:var(--ink)}
  .drp .hero{background:var(--ink);color:#fff;border-radius:10px;
             padding:28px 30px;margin-bottom:22px;position:relative;overflow:hidden}
  .drp .hero::after{content:"";position:absolute;right:-60px;top:-60px;width:220px;
             height:220px;background:var(--gold);opacity:.10;border-radius:50%}
  .drp .hero .eyebrow{font-size:10.5px;letter-spacing:.22em;text-transform:uppercase;
             color:var(--gold)}
  .drp .hero h1{font-weight:400;font-size:30px;margin:8px 0 6px}
  .drp .hero p{color:#B9B5AB;font-size:13.5px;margin:0;max-width:72ch}
  .drp .facts{display:flex;gap:28px;flex-wrap:wrap;margin-top:18px;
             padding-top:16px;border-top:1px solid #2A2A2E}
  .drp .fact .n{font-size:22px;line-height:1.1}
  .drp .fact .l{font-size:10px;letter-spacing:.12em;text-transform:uppercase;
             color:#8E8A80;margin-top:3px}
 
  .drp .card{border:1px solid var(--rule);border-radius:10px;box-shadow:none}
  .drp .card-header{background:#FAF8F2;border-bottom:1px solid var(--rule);
             font-size:11px;letter-spacing:.14em;text-transform:uppercase;
             color:var(--gd);padding:11px 18px;border-radius:10px 10px 0 0}
  .drp .form-label{font-size:10.5px;letter-spacing:.12em;text-transform:uppercase;
             color:var(--muted);margin-bottom:5px}
  .drp .form-select{font-family:inherit;font-size:14.5px;border-color:#CFCBC0;
             border-radius:6px;padding:8px 11px}
  .drp .form-select:focus{border-color:var(--gd);box-shadow:0 0 0 .18rem rgba(242,194,48,.28)}
  .drp .btn-go{background:var(--gold);border:none;color:var(--ink);font-weight:600;
             font-family:inherit;font-size:15px;padding:10px 30px;border-radius:6px}
  .drp .btn-go:hover:not(:disabled){background:var(--gd)}
  .drp .btn-go:disabled{background:var(--rule);color:var(--muted)}
  .drp .preset{background:#fff;border:1px solid var(--rule);color:var(--soft);
             font-size:12.5px;padding:4px 13px;border-radius:999px;cursor:pointer;
             font-family:inherit}
  .drp .preset:hover{border-color:var(--gd);color:var(--gd)}
  .drp .hint{font-size:12.5px;color:var(--muted)}
 
  .drp #drp-panel{display:none;border-left:3px solid var(--gold);background:#FCFAF4;
             border-radius:0 8px 8px 0;padding:16px 20px;margin-top:18px}
  .drp #drp-panel.on{display:block}
  .drp #drp-panel.err{border-left-color:var(--bad);background:#FBF3F1}
  .drp #drp-panel.ok{border-left-color:var(--good);background:#F2F7F4}
  .drp .step{font-size:14.5px}
  .drp .bar{height:3px;background:var(--rule);margin-top:12px;overflow:hidden;
             border-radius:2px}
  .drp .bar i{display:block;height:100%;width:30%;background:var(--gd);
             animation:drp-slide 1.1s ease-in-out infinite}
  @keyframes drp-slide{0%{margin-left:-30%}100%{margin-left:100%}}
 
  .drp table{font-size:13.5px;margin:0}
  .drp thead th{font-size:10px;letter-spacing:.1em;text-transform:uppercase;
             color:var(--muted);border-bottom:1px solid var(--ink);font-weight:400;
             padding:9px 14px}
  .drp tbody td{padding:10px 14px;border-bottom:1px solid var(--rule);
             vertical-align:middle}
  .drp tbody tr:hover td{background:#FCFAF4}
  .drp .pill{font-size:11px;padding:3px 10px;border-radius:999px;
             display:inline-block;letter-spacing:.04em}
  .drp .p-done{background:#E7F0EA;color:var(--good)}
  .drp .p-failed{background:#F7E6E2;color:var(--bad)}
  .drp .p-running,.drp .p-pending{background:#FBF2D9;color:var(--gd)}
  .drp .go-link{color:var(--gd);font-weight:600;text-decoration:none}
  .drp .go-link:hover{text-decoration:underline}
  .drp .mono{font-variant-numeric:tabular-nums}
  @media (prefers-reduced-motion:reduce){.drp .bar i{animation:none;width:100%}}
</style>
{% endblock %}
 
{% block content %}
<div class="drp container-fluid py-4" style="max-width:1180px">
 
  <div class="hero">
    <div class="eyebrow">Kapitalbank · кредитный портфель</div>
    <h1>Динамический отчёт</h1>
    <p>Выберите период для анализа динамики и отчётную дату для структурного
       среза. Отчёт собирается из месячных выгрузок и открывается в новой вкладке.</p>
    <div class="facts">
      <div class="fact">
        <div class="n">{{ n_files }}</div>
        <div class="l">отчётных дат в базе</div>
      </div>
      <div class="fact">
        <div class="n">{{ periods.0.label|default:"—" }} — {{ latest_label|default:"—" }}</div>
        <div class="l">доступный диапазон</div>
      </div>
      <div class="fact">
        <div class="n">{{ n_done }}</div>
        <div class="l">готовых отчётов</div>
      </div>
    </div>
  </div>
 
  <div class="card mb-4">
    <div class="card-header">Параметры отчёта</div>
    <div class="card-body p-4">
      <form id="drp-form" method="post" action="{% url 'report-start' %}">
        {% csrf_token %}
        <input type="hidden" name="force" id="drp-force" value="0">
        <div class="row g-3 align-items-end">
          <div class="col-lg-3 col-md-6">
            <label class="form-label" for="pf">Динамика с</label>
            <select class="form-select" name="period_from" id="pf">
              <option value="">самая ранняя</option>
              {% for p in periods %}<option value="{{ p.value }}">{{ p.label }}</option>{% endfor %}
            </select>
          </div>
          <div class="col-lg-3 col-md-6">
            <label class="form-label" for="pt">Динамика по</label>
            <select class="form-select" name="period_to" id="pt">
              <option value="">самая поздняя</option>
              {% for p in periods %}<option value="{{ p.value }}">{{ p.label }}</option>{% endfor %}
            </select>
          </div>
          <div class="col-lg-3 col-md-6">
            <label class="form-label" for="sp">Структура на дату</label>
            <select class="form-select" name="selected_period" id="sp">
              <option value="">последняя доступная</option>
              {% for p in periods %}<option value="{{ p.value }}">{{ p.label }}</option>{% endfor %}
            </select>
          </div>
          <div class="col-lg-3 col-md-6 d-grid">
            <button type="submit" class="btn btn-go" id="drp-go">Построить отчёт</button>
          </div>
        </div>
 
        <div class="d-flex gap-2 flex-wrap mt-3">
          <button type="button" class="preset" data-months="12">Последние 12 месяцев</button>
          <button type="button" class="preset" data-months="24">Последние 24 месяца</button>
          <button type="button" class="preset" data-months="0">Весь период</button>
        </div>
 
        <div class="hint mt-3">
          Пустое поле — без ограничения. Структурные графики считаются
          <strong>только по выбранной дате</strong>: число клиентов, средние суммы
          и концентрация относятся к ней одной.
          Повторный запрос с теми же параметрами откроет уже готовый отчёт
          мгновенно — пересчёт запускается только после загрузки новых данных.
        </div>
      </form>
 
      <div id="drp-panel">
        <div class="step" id="drp-step">Запуск…</div>
        <div class="bar" id="drp-bar"><i></i></div>
      </div>
    </div>
  </div>
 
  <div class="card">
    <div class="card-header d-flex justify-content-between align-items-center">
      <span>Ранее построенные отчёты</span>
      <span class="text-muted" style="letter-spacing:0;text-transform:none;font-size:11.5px">
        открываются мгновенно</span>
    </div>
    <div class="table-responsive">
      <table class="table mb-0">
        <thead><tr>
          <th>#</th><th>Структура на</th><th>Период динамики</th>
          <th>Статус</th><th class="text-end">Графиков</th>
          <th class="text-end">Сек.</th><th>Создан</th><th></th>
        </tr></thead>
        <tbody>
          {% for r in runs %}
          <tr>
            <td class="mono text-muted">{{ r.pk }}</td>
            <td>{{ r.selected_period|default:"—" }}</td>
            <td class="mono">{{ r.period_from|default:"…" }} – {{ r.period_to|default:"…" }}</td>
            <td><span class="pill p-{{ r.status }}">{{ r.get_status_display }}</span></td>
            <td class="text-end mono">{{ r.stats.n_charts|default:"—" }}</td>
            <td class="text-end mono">{{ r.seconds }}</td>
            <td class="text-muted">{{ r.created_at|date:"d.m.Y H:i" }}</td>
            <td class="text-end">
              {% if r.status == "done" %}
                <a class="go-link" href="{% url 'report-view' r.pk %}" target="_blank"
                   rel="noopener">открыть</a>
                <span class="text-muted">·</span>
                <a class="go-link" href="{% url 'report-download' r.pk %}">скачать</a>
              {% elif r.status == "failed" %}
                <span class="text-muted" title="{{ r.error|truncatechars:200 }}">ошибка</span>
              {% endif %}
            </td>
          </tr>
          {% empty %}
          <tr><td colspan="8" class="text-muted py-4 text-center">
            Отчётов пока нет — постройте первый.</td></tr>
          {% endfor %}
        </tbody>
      </table>
    </div>
  </div>
</div>
{% endblock %}
 
Foydali bo'ldimi? Yoqmadi 0 Baholash uchun kiring