- Страница перенесена с /upload на / (корень) - Верхняя панель с логотипом Nubes (nubes-logo.svg) - favicon.png из ipwhitelist-app - Поля номера/клиента/даты убраны — договор создаётся авто - Исправлен JS (относительные URL)
535 lines
24 KiB
HTML
535 lines
24 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, -apple-system, "Segoe UI", Roboto, 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; }
|
||
.form-row { display: flex; gap: 12px; align-items: flex-end; flex-wrap: wrap; }
|
||
.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); }
|
||
.btn:disabled { opacity: .5; cursor: not-allowed; }
|
||
.queue { margin-top: 12px; }
|
||
.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; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||
.queue-item .size { color: var(--muted); font-size: 12px; }
|
||
.queue-item .remove { cursor: pointer; color: var(--muted); padding: 4px; }
|
||
.queue-item .remove:hover { color: var(--destructive); }
|
||
.queue-empty { color: var(--muted); padding: 12px; text-align: center; }
|
||
.progress-bar {
|
||
height: 4px; background: var(--brand-grey-light);
|
||
border-radius: 2px; margin-top: 12px; overflow: hidden;
|
||
}
|
||
.progress-fill { height: 100%; background: var(--brand-primary); border-radius: 2px; transition: width .3s; }
|
||
.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); }
|
||
.summary { margin-top: 12px; font-size: 14px; display: flex; gap: 16px; flex-wrap: wrap; }
|
||
.loader { display: inline-block; width: 16px; height: 16px; border: 2px solid rgba(255,255,255,.3); border-top-color: #fff; border-radius: 50%; animation: spin .6s linear infinite; }
|
||
@keyframes spin { to { transform: rotate(360deg); } }
|
||
.error-box { margin-top: 12px; padding: 12px; border-radius: 6px; border: 1px solid var(--destructive); background: rgba(239,68,68,.05); color: var(--destructive); font-size: 14px; }
|
||
.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); }
|
||
</style>
|
||
</head>
|
||
<body>
|
||
|
||
<!-- Верхняя панель -->
|
||
<div class="topbar">
|
||
<img src="{{ url_for('static', filename='nubes-logo.svg') }}" alt="Nubes">
|
||
<span class="title">Сверка договоров</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">
|
||
<div class="form-row">
|
||
<input type="file" id="fileInput" accept=".docx,.doc,.pdf,.zip" multiple style="height:36px;">
|
||
<button class="btn btn-primary" onclick="addFiles()">
|
||
<i data-lucide="plus" style="width:16px;height:16px;"></i> Добавить
|
||
</button>
|
||
</div>
|
||
<div class="queue" id="queue"></div>
|
||
<div class="queue-empty" id="queueEmpty">Нет добавленных файлов</div>
|
||
<div style="margin-top:12px;">
|
||
<button class="btn btn-primary" id="processBtn" onclick="processAll()" style="width:100%;justify-content:center;">
|
||
<i data-lucide="play" style="width:16px;height:16px;"></i> Обработать все файлы
|
||
</button>
|
||
</div>
|
||
<div class="progress-bar" id="progressBar" style="display:none;">
|
||
<div class="progress-fill" id="progressFill" style="width:0%;"></div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Карточка: Результаты -->
|
||
<div class="card" id="resultsCard" style="display:none;">
|
||
<div class="card-header">
|
||
<i data-lucide="file-text" style="width:18px;height:18px;"></i>
|
||
Результаты обработки
|
||
</div>
|
||
<div class="card-body" id="resultsBody"></div>
|
||
</div>
|
||
|
||
</div>
|
||
|
||
<script>
|
||
lucide.createIcons();
|
||
let fileQueue = [];
|
||
|
||
function addFiles() {
|
||
const input = document.getElementById('fileInput');
|
||
for (const f of input.files) {
|
||
if (!fileQueue.find(q => q.name === f.name && q.size === f.size)) {
|
||
fileQueue.push(f);
|
||
}
|
||
}
|
||
input.value = '';
|
||
renderQueue();
|
||
}
|
||
|
||
function removeFile(index) { fileQueue.splice(index, 1); renderQueue(); }
|
||
|
||
function renderQueue() {
|
||
const q = document.getElementById('queue');
|
||
const empty = document.getElementById('queueEmpty');
|
||
if (fileQueue.length === 0) {
|
||
q.innerHTML = ''; empty.style.display = 'block';
|
||
} else {
|
||
empty.style.display = 'none';
|
||
q.innerHTML = fileQueue.map((f, i) =>
|
||
'<div class="queue-item"><i data-lucide="file-text" style="width:16px;height:16px;color:var(--muted);"></i>' +
|
||
'<span class="name">' + f.name + '</span>' +
|
||
'<span class="size">' + formatSize(f.size) + '</span>' +
|
||
'<span class="remove" onclick="removeFile('+i+')"><i data-lucide="x" style="width:14px;height:14px;"></i></span></div>'
|
||
).join('');
|
||
lucide.createIcons();
|
||
}
|
||
}
|
||
|
||
function formatSize(bytes) {
|
||
if (bytes < 1024) return bytes + ' B';
|
||
if (bytes < 1048576) return (bytes / 1024).toFixed(1) + ' KB';
|
||
return (bytes / 1048576).toFixed(1) + ' MB';
|
||
}
|
||
|
||
async function processAll() {
|
||
if (fileQueue.length === 0) return;
|
||
const processBtn = document.getElementById('processBtn');
|
||
const progressBar = document.getElementById('progressBar');
|
||
const progressFill = document.getElementById('progressFill');
|
||
const resultsCard = document.getElementById('resultsCard');
|
||
const resultsBody = document.getElementById('resultsBody');
|
||
|
||
processBtn.disabled = true;
|
||
processBtn.innerHTML = '<span class="loader"></span> Обработка...';
|
||
progressBar.style.display = 'block';
|
||
resultsCard.style.display = 'block';
|
||
resultsBody.innerHTML = '';
|
||
|
||
// Создаём договор
|
||
let contractId;
|
||
try {
|
||
const resp = await fetch('/api/contracts', {
|
||
method: 'POST', headers: {'Content-Type': 'application/json'},
|
||
body: JSON.stringify({number: 'б/н ' + new Date().toISOString().slice(0,10)}),
|
||
});
|
||
const listResp = await fetch('/api/contracts');
|
||
contractId = (await listResp.json()).contracts[0].id;
|
||
} catch (e) {
|
||
resultsBody.innerHTML = '<div class="error-box">❌ ' + e.message + '</div>';
|
||
resetBtn(); return;
|
||
}
|
||
|
||
let totalRows = 0;
|
||
let results = [];
|
||
|
||
for (let i = 0; i < fileQueue.length; i++) {
|
||
const file = fileQueue[i];
|
||
progressFill.style.width = Math.round((i / fileQueue.length) * 100) + '%';
|
||
const formData = new FormData();
|
||
formData.append('file', file);
|
||
formData.append('type', 'initial');
|
||
try {
|
||
const resp = await fetch('/api/contracts/' + contractId + '/supplements', { method: 'POST', body: formData });
|
||
const data = await resp.json();
|
||
if (resp.ok) {
|
||
totalRows += data.rows_extracted || 0;
|
||
results.push({name: file.name, status: 'ok', rows: data.rows_extracted || 0, summary: data.diff_summary || {}});
|
||
} else {
|
||
results.push({name: file.name, status: 'error', error: data.error || data.details?.error || 'Ошибка'});
|
||
}
|
||
} catch (e) {
|
||
results.push({name: file.name, status: 'error', error: e.message});
|
||
}
|
||
}
|
||
progressFill.style.width = '100%';
|
||
|
||
resultsBody.innerHTML =
|
||
'<div class="summary" style="margin-bottom:12px;">' +
|
||
'<span>Файлов: ' + fileQueue.length + '</span>' +
|
||
'<span>Строк извлечено: ' + totalRows + '</span></div>' +
|
||
results.map(r =>
|
||
'<div class="queue-item">' +
|
||
'<span class="badge ' + (r.status === 'ok' ? 'badge-ok' : 'badge-err') + '">' +
|
||
(r.status === 'ok' ? '✅ OK' : '❌ Ошибка') + '</span>' +
|
||
'<span class="name">' + r.name + '</span>' +
|
||
(r.status === 'ok'
|
||
? '<span class="size">строк: ' + r.rows + ', +' + (r.summary.added||0) + ' ~' + (r.summary.changed||0) + ' -' + (r.summary.deleted||0) + '</span>'
|
||
: '<span class="size" style="color:var(--destructive);">' + r.error + '</span>') +
|
||
'</div>'
|
||
).join('');
|
||
|
||
// История
|
||
try {
|
||
const histResp = await fetch('/api/contracts/' + contractId + '/history');
|
||
const hist = await histResp.json();
|
||
const rows = (hist.history || []).slice(-totalRows);
|
||
if (rows.length > 0) {
|
||
resultsBody.innerHTML +=
|
||
'<div class="table-wrap" style="margin-top:12px;"><table><thead><tr>' +
|
||
'<th>№</th><th>Услуга</th><th>Цена, руб</th><th>Кол-во</th><th>Сумма, руб</th><th>Дата</th></tr></thead><tbody>' +
|
||
rows.map(r => {
|
||
const v = r.new_values || {};
|
||
return '<tr><td>' + (r.row_num||'') + '</td><td>' + (v.name||'').substring(0,80) + '</td>' +
|
||
'<td style="text-align:right">' + (v.price ? Number(v.price).toLocaleString() : '') + '</td>' +
|
||
'<td style="text-align:center">' + (v.qty||'') + '</td>' +
|
||
'<td style="text-align:right">' + (v.sum ? Number(v.sum).toLocaleString() : '') + '</td>' +
|
||
'<td>' + (v.date_start||'') + '</td></tr>';
|
||
}).join('') + '</tbody></table></div>';
|
||
}
|
||
} catch(e) {}
|
||
|
||
resetBtn(); lucide.createIcons();
|
||
}
|
||
|
||
function resetBtn() {
|
||
const btn = document.getElementById('processBtn');
|
||
btn.disabled = false;
|
||
btn.innerHTML = '<i data-lucide="play" style="width:16px;height:16px;"></i> Обработать все файлы';
|
||
lucide.createIcons();
|
||
}
|
||
</script>
|
||
</body>
|
||
</html>
|
||
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; }
|
||
.form-row { display: flex; gap: 12px; align-items: flex-end; flex-wrap: wrap; }
|
||
.form-group { display: flex; flex-direction: column; gap: 4px; }
|
||
.form-group label { font-size: 14px; line-height: 1; margin-left: 4px; }
|
||
.form-group input, .form-group select {
|
||
height: 36px; border-radius: 6px; border: 1px solid var(--brand-gray);
|
||
padding: 0 12px; font-size: 14px; font-family: inherit;
|
||
}
|
||
.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); }
|
||
.btn-destructive { background: var(--destructive); border-color: var(--destructive); color: #fff; }
|
||
.btn:disabled { opacity: .5; cursor: not-allowed; }
|
||
.queue { margin-top: 12px; }
|
||
.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; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||
.queue-item .size { color: var(--muted); font-size: 12px; }
|
||
.queue-item .remove { cursor: pointer; color: var(--muted); padding: 4px; }
|
||
.queue-item .remove:hover { color: var(--destructive); }
|
||
.queue-empty { color: var(--muted); padding: 12px; text-align: center; }
|
||
.progress-bar {
|
||
height: 4px; background: var(--brand-grey-light);
|
||
border-radius: 2px; margin-top: 12px; overflow: hidden;
|
||
}
|
||
.progress-fill { height: 100%; background: var(--brand-primary); border-radius: 2px; transition: width .3s; }
|
||
.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); }
|
||
.summary { margin-top: 12px; font-size: 14px; display: flex; gap: 16px; flex-wrap: wrap; }
|
||
.summary span { display: flex; align-items: center; gap: 4px; }
|
||
.loader { display: inline-block; width: 16px; height: 16px; border: 2px solid rgba(255,255,255,.3); border-top-color: #fff; border-radius: 50%; animation: spin .6s linear infinite; }
|
||
@keyframes spin { to { transform: rotate(360deg); } }
|
||
.error-box { margin-top: 12px; padding: 12px; border-radius: 6px; border: 1px solid var(--destructive); background: rgba(239,68,68,.05); color: var(--destructive); font-size: 14px; }
|
||
.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); }
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<div class="container">
|
||
<div class="card">
|
||
<div class="card-header">
|
||
<i data-lucide="folder-open" style="width:18px;height:18px;"></i>
|
||
Добавление договоров / допников
|
||
</div>
|
||
<div class="card-body">
|
||
<div class="form-row" style="margin-bottom:12px;">
|
||
<div class="form-group">
|
||
<label>Номер договора</label>
|
||
<input type="text" id="contractNumber" placeholder="03700_1" style="width:160px;">
|
||
</div>
|
||
<div class="form-group">
|
||
<label>Клиент</label>
|
||
<input type="text" id="contractClient" placeholder="ЗАО XXX001" style="width:200px;">
|
||
</div>
|
||
<div class="form-group">
|
||
<label>Дата</label>
|
||
<input type="date" id="contractDate" style="width:150px;">
|
||
</div>
|
||
</div>
|
||
<div class="form-row">
|
||
<input type="file" id="fileInput" accept=".docx,.doc,.pdf,.zip" multiple style="height:36px;">
|
||
<button class="btn btn-primary" onclick="addFiles()">
|
||
<i data-lucide="plus" style="width:16px;height:16px;"></i> Добавить
|
||
</button>
|
||
</div>
|
||
<div class="queue" id="queue"></div>
|
||
<div class="queue-empty" id="queueEmpty">Нет добавленных файлов</div>
|
||
<div style="margin-top:12px;">
|
||
<button class="btn btn-primary" id="processBtn" onclick="processAll()" style="width:100%;justify-content:center;">
|
||
<i data-lucide="play" style="width:16px;height:16px;"></i> Обработать все файлы
|
||
</button>
|
||
</div>
|
||
<div class="progress-bar" id="progressBar" style="display:none;">
|
||
<div class="progress-fill" id="progressFill" style="width:0%;"></div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="card" id="resultsCard" style="display:none;">
|
||
<div class="card-header">
|
||
<i data-lucide="file-text" style="width:18px;height:18px;"></i>
|
||
Результаты обработки
|
||
</div>
|
||
<div class="card-body" id="resultsBody"></div>
|
||
</div>
|
||
</div>
|
||
|
||
<script>
|
||
lucide.createIcons();
|
||
let fileQueue = [];
|
||
|
||
function addFiles() {
|
||
const input = document.getElementById('fileInput');
|
||
for (const f of input.files) {
|
||
if (!fileQueue.find(q => q.name === f.name && q.size === f.size)) {
|
||
fileQueue.push(f);
|
||
}
|
||
}
|
||
input.value = '';
|
||
renderQueue();
|
||
}
|
||
|
||
function removeFile(index) { fileQueue.splice(index, 1); renderQueue(); }
|
||
|
||
function renderQueue() {
|
||
const q = document.getElementById('queue');
|
||
const empty = document.getElementById('queueEmpty');
|
||
if (fileQueue.length === 0) {
|
||
q.innerHTML = ''; empty.style.display = 'block';
|
||
} else {
|
||
empty.style.display = 'none';
|
||
q.innerHTML = fileQueue.map((f, i) =>
|
||
'<div class="queue-item"><i data-lucide="file-text" style="width:16px;height:16px;color:var(--muted);"></i>' +
|
||
'<span class="name">' + f.name + '</span>' +
|
||
'<span class="size">' + formatSize(f.size) + '</span>' +
|
||
'<span class="remove" onclick="removeFile('+i+')"><i data-lucide="x" style="width:14px;height:14px;"></i></span></div>'
|
||
).join('');
|
||
lucide.createIcons();
|
||
}
|
||
}
|
||
|
||
function formatSize(bytes) {
|
||
if (bytes < 1024) return bytes + ' B';
|
||
if (bytes < 1048576) return (bytes / 1024).toFixed(1) + ' KB';
|
||
return (bytes / 1048576).toFixed(1) + ' MB';
|
||
}
|
||
|
||
async function processAll() {
|
||
if (fileQueue.length === 0) return;
|
||
const number = document.getElementById('contractNumber').value.trim();
|
||
const client = document.getElementById('contractClient').value.trim();
|
||
const dateSigned = document.getElementById('contractDate').value;
|
||
const processBtn = document.getElementById('processBtn');
|
||
const progressBar = document.getElementById('progressBar');
|
||
const progressFill = document.getElementById('progressFill');
|
||
const resultsCard = document.getElementById('resultsCard');
|
||
const resultsBody = document.getElementById('resultsBody');
|
||
|
||
processBtn.disabled = true;
|
||
processBtn.innerHTML = '<span class="loader"></span> Обработка...';
|
||
progressBar.style.display = 'block';
|
||
resultsCard.style.display = 'block';
|
||
resultsBody.innerHTML = '';
|
||
|
||
let contractId;
|
||
try {
|
||
if (number) {
|
||
const listResp = await fetch('/api/contracts');
|
||
const list = await listResp.json();
|
||
const existing = (list.contracts || []).find(c => c.number === number);
|
||
contractId = existing ? existing.id : null;
|
||
}
|
||
if (!contractId) {
|
||
await fetch('/api/contracts', {
|
||
method: 'POST', headers: {'Content-Type': 'application/json'},
|
||
body: JSON.stringify({number: number || ('б/н ' + Date.now()), client, date_signed: dateSigned || undefined}),
|
||
});
|
||
const listResp = await fetch('/api/contracts');
|
||
contractId = (await listResp.json()).contracts[0].id;
|
||
}
|
||
} catch (e) {
|
||
resultsBody.innerHTML = '<div class="error-box">❌ ' + e.message + '</div>';
|
||
resetBtn(); return;
|
||
}
|
||
|
||
let totalRows = 0;
|
||
let results = [];
|
||
|
||
for (let i = 0; i < fileQueue.length; i++) {
|
||
const file = fileQueue[i];
|
||
progressFill.style.width = Math.round((i / fileQueue.length) * 100) + '%';
|
||
const formData = new FormData();
|
||
formData.append('file', file);
|
||
formData.append('type', 'amendment');
|
||
try {
|
||
const resp = await fetch('/api/contracts/' + contractId + '/supplements', { method: 'POST', body: formData });
|
||
const data = await resp.json();
|
||
if (resp.ok) {
|
||
totalRows += data.rows_extracted || 0;
|
||
results.push({name: file.name, status: 'ok', rows: data.rows_extracted || 0, summary: data.diff_summary || {}});
|
||
} else {
|
||
results.push({name: file.name, status: 'error', error: data.error || data.details?.error || 'Ошибка'});
|
||
}
|
||
} catch (e) {
|
||
results.push({name: file.name, status: 'error', error: e.message});
|
||
}
|
||
}
|
||
progressFill.style.width = '100%';
|
||
|
||
resultsBody.innerHTML =
|
||
'<div class="summary" style="margin-bottom:12px;">' +
|
||
'<span>Файлов: ' + fileQueue.length + '</span>' +
|
||
'<span>Строк извлечено: ' + totalRows + '</span></div>' +
|
||
results.map(r =>
|
||
'<div class="queue-item">' +
|
||
'<span class="badge ' + (r.status === 'ok' ? 'badge-ok' : 'badge-err') + '">' +
|
||
(r.status === 'ok' ? '✅ OK' : '❌ Ошибка') + '</span>' +
|
||
'<span class="name">' + r.name + '</span>' +
|
||
(r.status === 'ok'
|
||
? '<span class="size">строк: ' + r.rows + ', +' + (r.summary.added||0) + ' ~' + (r.summary.changed||0) + ' -' + (r.summary.deleted||0) + '</span>'
|
||
: '<span class="size" style="color:var(--destructive);">' + r.error + '</span>') +
|
||
'</div>'
|
||
).join('');
|
||
|
||
// История
|
||
try {
|
||
const histResp = await fetch('/api/contracts/' + contractId + '/history');
|
||
const hist = await histResp.json();
|
||
const rows = (hist.history || []).slice(-totalRows);
|
||
if (rows.length > 0) {
|
||
resultsBody.innerHTML +=
|
||
'<div class="table-wrap" style="margin-top:12px;"><table><thead><tr>' +
|
||
'<th>№</th><th>Услуга</th><th>Цена, руб</th><th>Кол-во</th><th>Сумма, руб</th><th>Дата</th></tr></thead><tbody>' +
|
||
rows.map(r => {
|
||
const v = r.new_values || {};
|
||
return '<tr><td>' + (r.row_num||'') + '</td><td>' + (v.name||'').substring(0,80) + '</td>' +
|
||
'<td style="text-align:right">' + (v.price ? Number(v.price).toLocaleString() : '') + '</td>' +
|
||
'<td style="text-align:center">' + (v.qty||'') + '</td>' +
|
||
'<td style="text-align:right">' + (v.sum ? Number(v.sum).toLocaleString() : '') + '</td>' +
|
||
'<td>' + (v.date_start||'') + '</td></tr>';
|
||
}).join('') + '</tbody></table></div>';
|
||
}
|
||
} catch(e) {}
|
||
|
||
resetBtn(); lucide.createIcons();
|
||
}
|
||
|
||
function resetBtn() {
|
||
const btn = document.getElementById('processBtn');
|
||
btn.disabled = false;
|
||
btn.innerHTML = '<i data-lucide="play" style="width:16px;height:16px;"></i> Обработать все файлы';
|
||
lucide.createIcons();
|
||
}
|
||
</script>
|
||
</body>
|
||
</html>
|