201 lines
9.3 KiB
HTML
201 lines
9.3 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="ru">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||
<title>Сверка договоров</title>
|
||
<link rel="icon" type="image/png" href="{{ url_for('static', filename='favicon.png') }}">
|
||
<script src="https://unpkg.com/lucide@latest"></script>
|
||
<style>
|
||
:root {
|
||
--brand-primary: #2563eb; --brand-primary-dark: #1d4ed8;
|
||
--brand-gray: #d1d5db; --brand-grey-light: #f3f4f6;
|
||
--background: #ffffff; --foreground: #1a1a1a;
|
||
--muted: #6b7280; --destructive: #ef4444; --green: #22c55e;
|
||
}
|
||
* { box-sizing: border-box; margin: 0; padding: 0; }
|
||
body { font-family: system-ui, sans-serif; font-size: 14px; color: var(--foreground); background: var(--brand-grey-light); min-height: 100vh; }
|
||
.topbar { background: var(--background); border-bottom: 1px solid var(--brand-gray); padding: 0 24px; height: 56px; display: flex; align-items: center; gap: 16px; }
|
||
.topbar img { height: 28px; }
|
||
.topbar .title { font-weight: 600; font-size: 16px; }
|
||
.content { max-width: 800px; margin: 32px auto; padding: 0 16px; }
|
||
.card { background: var(--background); border-radius: 12px; border: 1px solid var(--brand-gray); box-shadow: 0 1px 2px rgba(0,0,0,.05); overflow: hidden; margin-bottom: 16px; }
|
||
.card-header { background: var(--brand-grey-light); padding: 12px 16px; font-weight: 600; font-size: 16px; display: flex; align-items: center; gap: 8px; }
|
||
.card-body { padding: 16px; }
|
||
.btn { height: 36px; border-radius: 6px; gap: 6px; padding: 0 14px; font-size: 14px; font-family: inherit; cursor: pointer; display: inline-flex; align-items: center; border: 1px solid var(--brand-gray); background: var(--background); }
|
||
.btn-primary { background: var(--brand-primary); border-color: var(--brand-primary); color: #fff; }
|
||
.btn-primary:hover { background: var(--brand-primary-dark); }
|
||
.queue-item { display: flex; align-items: center; gap: 8px; padding: 8px 12px; border-radius: 6px; border: 1px solid var(--brand-gray); background: var(--brand-grey-light); margin-bottom: 4px; }
|
||
.queue-item .name { flex: 1; font-weight: 500; }
|
||
.queue-item .size { color: var(--muted); font-size: 12px; }
|
||
.table-wrap { border-radius: 6px; border: 1px solid var(--brand-gray); overflow: hidden; margin-top: 12px; }
|
||
table { width: 100%; border-collapse: collapse; font-size: 13px; }
|
||
th { background: var(--brand-grey-light); text-transform: uppercase; padding: 4px 8px; border-right: 1px solid var(--brand-gray); text-align: left; font-weight: 600; }
|
||
td { padding: 4px 8px; border-right: 1px solid var(--brand-gray); border-bottom: 1px solid var(--brand-gray); }
|
||
tr:hover td { background: rgba(243,244,246,.5); }
|
||
.badge { display: inline-flex; align-items: center; gap: 4px; font-size: 12px; padding: 2px 8px; border-radius: 4px; }
|
||
.badge-ok { background: rgba(34,197,94,.1); color: var(--green); }
|
||
.badge-err { background: rgba(239,68,68,.1); color: var(--destructive); }
|
||
.error-box { margin-top: 12px; padding: 12px; border-radius: 6px; border: 1px solid var(--destructive); background: rgba(239,68,68,.05); color: var(--destructive); }
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<div class="topbar">
|
||
<img src="{{ url_for('static', filename='nubes-logo.svg') }}" alt="Nubes">
|
||
<span class="title">Сверка договоров <span style="font-weight:400;color:var(--muted);font-size:12px;">v1.1</span></span>
|
||
</div>
|
||
|
||
<div class="content">
|
||
<div class="card">
|
||
<div class="card-header">
|
||
<i data-lucide="folder-open" style="width:18px;height:18px;"></i>
|
||
Загрузка договоров / допников
|
||
</div>
|
||
<div class="card-body">
|
||
<form method="POST" enctype="multipart/form-data">
|
||
<input type="file" name="files" accept=".docx,.doc,.pdf,.zip" multiple style="margin-bottom:12px;width:100%;">
|
||
<button type="submit" class="btn btn-primary" style="width:100%;justify-content:center;">
|
||
<i data-lucide="upload" style="width:16px;height:16px;"></i> Загрузить
|
||
</button>
|
||
</form>
|
||
</div>
|
||
</div>
|
||
|
||
{% if contract %}
|
||
<div class="card">
|
||
<div class="card-header">
|
||
<i data-lucide="file-check" style="width:18px;height:18px;"></i>
|
||
Договор {{ contract.number }} — файлы загружены
|
||
</div>
|
||
<div class="card-body">
|
||
{% for s in supplements %}
|
||
<div class="queue-item">
|
||
<span class="badge {{ 'badge-ok' if s.status == 'extracted' else 'badge-err' }}">{{ '✅' if s.status == 'extracted' else '⏳' }}</span>
|
||
<span class="name">{{ s.filename }}</span>
|
||
<span class="size">{{ s.status }}</span>
|
||
</div>
|
||
{% endfor %}
|
||
{% if unprocessed %}
|
||
<div id="process-area">
|
||
<button class="btn btn-primary" onclick="startProcessing('{{ contract.id }}')" style="width:100%;justify-content:center;">
|
||
<i data-lucide="play" style="width:16px;height:16px;"></i> Обработать (LLM)
|
||
</button>
|
||
</div>
|
||
{% endif %}
|
||
</div>
|
||
</div>
|
||
{% endif %}
|
||
|
||
{% if all_rows %}
|
||
<div class="card">
|
||
<div class="card-header">
|
||
<i data-lucide="file-text" style="width:18px;height:18px;"></i>
|
||
Результаты — строк: {{ all_rows|length }}
|
||
</div>
|
||
<div class="card-body">
|
||
<div class="table-wrap">
|
||
<table>
|
||
<thead><tr><th>№</th><th>Услуга</th><th>Цена, руб</th><th>Кол-во</th><th>Сумма, руб</th><th>Дата</th></tr></thead>
|
||
<tbody>
|
||
{% for row in all_rows %}
|
||
<tr>
|
||
<td>{{ row.row_num }}</td>
|
||
<td>{{ row.name[:80] if row.name else '' }}</td>
|
||
<td style="text-align:right">{{ '{:,.2f}'.format(row.price) if row.price else '' }}</td>
|
||
<td style="text-align:center">{{ row.qty if row.qty else '' }}</td>
|
||
<td style="text-align:right">{{ '{:,.2f}'.format(row.sum) if row.sum else '' }}</td>
|
||
<td>{{ row.date_start if row.date_start else '' }}</td>
|
||
</tr>
|
||
{% endfor %}
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
{% endif %}
|
||
</div>
|
||
|
||
<script>
|
||
lucide.createIcons();
|
||
|
||
function formatBytes(b) {
|
||
if (!b) return '0 B';
|
||
var u = ['B','KB','MB','GB'], i = 0;
|
||
while (b >= 1024 && i < 3) { b /= 1024; i++; }
|
||
return (Math.round(b * 10) / 10) + ' ' + u[i];
|
||
}
|
||
|
||
function startProcessing(cid) {
|
||
var area = document.getElementById('process-area');
|
||
area.innerHTML = '<div style="padding:12px;" id="proc-list"></div>';
|
||
var list = document.getElementById('proc-list');
|
||
|
||
var timers = {};
|
||
var timerInterval = setInterval(function() {
|
||
var now = Date.now();
|
||
var els = list.querySelectorAll('.proc-timer');
|
||
els.forEach(function(el) {
|
||
var start = parseInt(el.dataset.start);
|
||
if (start) { el.textContent = Math.round((now - start) / 1000) + 'с'; }
|
||
});
|
||
}, 500);
|
||
|
||
var es = new EventSource('/process/' + cid);
|
||
|
||
es.addEventListener('error', function() {
|
||
clearInterval(timerInterval);
|
||
es.close();
|
||
});
|
||
|
||
es.onmessage = function(e) {
|
||
var d = JSON.parse(e.data);
|
||
|
||
if (d.type === 'file_start') {
|
||
var div = document.createElement('div');
|
||
div.className = 'queue-item';
|
||
div.id = 'f_' + d.name.replace(/[^a-zA-Z0-9]/g, '_');
|
||
div.innerHTML = '<span class="badge badge-err">⏳</span>' +
|
||
'<span class="name">' + d.name + '</span>' +
|
||
'<span class="size proc-timer" data-start="' + Date.now() + '">0с</span>';
|
||
list.appendChild(div);
|
||
lucide.createIcons();
|
||
}
|
||
|
||
else if (d.type === 'file_done') {
|
||
var el = document.getElementById('f_' + d.name.replace(/[^a-zA-Z0-9]/g, '_'));
|
||
if (el) {
|
||
el.querySelector('.badge').className = 'badge badge-ok';
|
||
el.querySelector('.badge').textContent = '✓';
|
||
el.querySelector('.proc-timer').textContent = d.time_s + 'с · ' + d.rows + ' строк';
|
||
}
|
||
}
|
||
|
||
else if (d.type === 'file_error') {
|
||
var el = document.getElementById('f_' + d.name.replace(/[^a-zA-Z0-9]/g, '_'));
|
||
if (el) {
|
||
el.querySelector('.badge').className = 'badge badge-err';
|
||
el.querySelector('.badge').textContent = '✗';
|
||
el.querySelector('.proc-timer').textContent = d.error;
|
||
}
|
||
}
|
||
|
||
else if (d.type === 'summary') {
|
||
clearInterval(timerInterval);
|
||
es.close();
|
||
var sum = document.createElement('div');
|
||
sum.style.cssText = 'margin-top:12px;padding:12px;border-radius:6px;background:rgba(34,197,94,.05);border:1px solid var(--green);';
|
||
sum.innerHTML = '<strong>✓ Готово:</strong> ' + d.files_processed + ' файлов, ' +
|
||
d.total_rows + ' строк, ' + formatBytes(d.total_bytes) + ', всего <strong>' + d.total_time_s + 'с</strong>';
|
||
list.appendChild(sum);
|
||
|
||
var btnDiv = document.createElement('div');
|
||
btnDiv.style.cssText = 'margin-top:12px;';
|
||
btnDiv.innerHTML = '<a href="/?id=' + cid + '" class="btn btn-primary" style="width:100%;justify-content:center;">Смотреть результаты</a>';
|
||
list.appendChild(btnDiv);
|
||
}
|
||
};
|
||
}
|
||
</script>
|
||
</body>
|
||
</html>
|