diff --git a/site/app.py b/site/app.py index 59a5bfe..d9bfc4f 100644 --- a/site/app.py +++ b/site/app.py @@ -46,23 +46,28 @@ class ContractsApp: return render_template("upload.html") def _upload_files(self): - """Только сохранить файлы, без LLM.""" + """Сохранить файлы. ?cid=X — добавить к существующему договору.""" files = request.files.getlist("files") if not files or not any(f.filename for f in files): - return render_template("upload.html", error="Нет файлов") + return jsonify({"error": "Нет файлов"}), 400 - conn, err = db.connect() - if err: - return render_template("upload.html", error=f"БД: {err}") - cur = conn.cursor() - cur.execute( - "INSERT INTO contracts (number, client) VALUES (%s, %s) RETURNING id", - ("б/н " + __import__("datetime").datetime.now().strftime("%Y%m%d-%H%M"), ""), - ) - contract_id = cur.fetchone()[0] - conn.commit() - cur.close() - conn.close() + cid = request.args.get("cid") + + if cid: + contract_id = cid + else: + conn, err = db.connect() + if err: + return jsonify({"error": f"БД: {err}"}), 500 + cur = conn.cursor() + cur.execute( + "INSERT INTO contracts (number, client) VALUES (%s, %s) RETURNING id", + ("б/н " + __import__("datetime").datetime.now().strftime("%Y%m%d-%H%M"), ""), + ) + contract_id = cur.fetchone()[0] + conn.commit() + cur.close() + conn.close() for f in files: if not f.filename: diff --git a/site/templates/upload.html b/site/templates/upload.html index 767d260..4e0da6e 100644 --- a/site/templates/upload.html +++ b/site/templates/upload.html @@ -32,22 +32,22 @@ th { background: var(--brand-grey-light); text-transform: uppercase; padding: 6px 10px; border-right: 1px solid var(--brand-gray); text-align: left; font-weight: 600; font-size: 11px; color: var(--muted); } td { padding: 6px 10px; border-right: 1px solid var(--brand-gray); border-bottom: 1px solid var(--brand-gray); } tr:hover td { background: rgba(243,244,246,.5); } - .name-cell { max-width: 380px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } + .name-cell { max-width: 360px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } .zip-child .name-cell { padding-left: 28px; } .zip-child .name-cell::before { content: "└ "; color: var(--muted); } + .num-cell { text-align: right; white-space: nowrap; } .status-ok { color: var(--green); } .status-err { color: var(--destructive); } .summary-row td { background: rgba(34,197,94,.05); font-weight: 600; } .empty-row td { color: var(--muted); text-align: center; padding: 24px; } .remove-btn { cursor: pointer; color: var(--muted); background: none; border: none; padding: 2px 4px; font-size: 16px; line-height: 1; } .remove-btn:hover { 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); }
Nubes - Сверка договоров v1.5 + Сверка договоров v1.6
@@ -62,7 +62,7 @@
- +
ИмяИзменёнРазмерСтатус
ИмяИзменёнРазмерСтатус
@@ -85,6 +85,7 @@ let fileQueue = []; function formatSize(bytes) { + if (!bytes || bytes === 0) return '—'; if (bytes < 1024) return bytes + ' B'; if (bytes < 1048576) return (bytes / 1024).toFixed(1) + ' KB'; return (bytes / 1048576).toFixed(1) + ' MB'; @@ -107,7 +108,7 @@ return '' + '' + f.name + '' + '' + formatDate(f.lastModified) + '' + - '' + (f.size ? formatSize(f.size) : '—') + '' + + '' + formatSize(f.size) + '' + '' + '' + ''; @@ -118,7 +119,6 @@ function removeFile(index) { var f = fileQueue[index]; if (!f) return; - // Если удаляем ZIP — удалить и всех его детей if (!f.isZipChild && f.file && (f.file.type === 'application/zip' || f.name.toLowerCase().endsWith('.zip'))) { for (var i = fileQueue.length - 1; i >= 0; i--) { if (fileQueue[i].zipName === f.name) fileQueue.splice(i, 1); @@ -144,7 +144,6 @@ file: f }); - // ZIP: показать содержимое if (f.type === 'application/zip' || f.name.toLowerCase().endsWith('.zip')) { try { var zip = await JSZip.loadAsync(f); @@ -152,10 +151,11 @@ if (!zipEntry.dir) { var ext = relativePath.toLowerCase().split('.').pop(); if (['docx','doc','pdf'].indexOf(ext) === -1) return; + var zsize = (zipEntry._data && zipEntry._data.uncompressedSize) ? zipEntry._data.uncompressedSize : 0; fileQueue.push({ name: relativePath, lastModified: zipEntry.date ? zipEntry.date.getTime() : 0, - size: 0, + size: zsize, isZipChild: true, zipName: f.name }); @@ -171,6 +171,51 @@ renderTable(); }); + // ── Загрузка по одному файлу ────────────────────────────── + + function uploadOne(file, cid, row) { + return new Promise(function(resolve, reject) { + var xhr = new XMLHttpRequest(); + var url = '/' + (cid ? '?cid=' + cid : ''); + xhr.open('POST', url); + + xhr.upload.onprogress = function(e) { + if (e.lengthComputable && row) { + var pct = Math.round(e.loaded / e.total * 100); + row.querySelector('.status-cell').innerHTML = '↑ ' + pct + '%'; + } + }; + + xhr.onload = function() { + if (xhr.status === 200) { + try { + resolve(JSON.parse(xhr.responseText)); + } catch(e) { + reject(new Error('Bad JSON')); + } + } else { + reject(new Error('HTTP ' + xhr.status)); + } + }; + + xhr.onerror = function() { + reject(new Error('Сеть')); + }; + + xhr.ontimeout = function() { + reject(new Error('Таймаут')); + }; + + xhr.timeout = 60000; + + var fd = new FormData(); + fd.append('files', file); + xhr.send(fd); + }); + } + + // ── Парсинг ──────────────────────────────────────────────── + parseBtn.addEventListener('click', async function() { if (fileQueue.length === 0) return; @@ -180,21 +225,39 @@ parseBtn.disabled = true; parseBtn.innerHTML = ' Загрузка...'; - var formData = new FormData(); - realFiles.forEach(function(f) { formData.append('files', f.file); }); + var contractId = null; - var contractId; - try { - var uploadResp = await fetch('/', { method: 'POST', body: formData }); - var uploadJson = await uploadResp.json(); - if (uploadJson.error) { alert(uploadJson.error); resetBtn(); return; } - contractId = uploadJson.contract_id; - } catch(e) { - alert('Ошибка загрузки: ' + e.message); + // Загружаем каждый файл отдельно + for (var i = 0; i < realFiles.length; i++) { + var f = realFiles[i]; + var row = findRowByName(f.name); + + if (row) { + row.querySelector('.status-cell').innerHTML = '↑ 0%'; + } + + try { + var resp = await uploadOne(f.file, contractId, row); + contractId = resp.contract_id; + + if (row) { + row.querySelector('.status-cell').innerHTML = '✓ загружен'; + } + } catch(e) { + if (row) { + row.querySelector('.status-cell').innerHTML = '✗ ' + e.message + ''; + } + resetBtn(); + return; + } + } + + if (!contractId) { resetBtn(); return; } + // Фаза парсинга — SSE parseBtn.innerHTML = ' Парсинг...'; var es = new EventSource('/parse/' + contractId); @@ -213,6 +276,13 @@ var row = findRowByName(d.name); if (row) { row.querySelector('.status-cell').innerHTML = '⏳ 0с'; + // Обновить размер если был неизвестен + if (d.bytes > 0) { + var sizeCell = row.querySelector('.num-cell'); + if (sizeCell && sizeCell.textContent.trim() === '—') { + sizeCell.textContent = formatSize(d.bytes); + } + } } } @@ -246,7 +316,7 @@ summaryRow.className = 'summary-row'; summaryRow.innerHTML = '✓ Готово: ' + d.files_processed + ' файлов' + '' + - '' + formatSize(d.total_bytes) + '' + + '' + formatSize(d.total_bytes) + '' + '' + d.total_time_s + 'с' + ''; fileTable.appendChild(summaryRow);