Files
contractor/index.cfm
T

282 lines
12 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<cfsetting showdebugoutput="no" enablecfoutputonly="no">
<cfheader name="Content-Type" value="text/html; charset=utf-8">
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Сверка договоров — Lucee v1.0.18</title>
<style>
:root { --brand: #2563eb; --brand-dark: #1d4ed8; --gray: #d1d5db; --gray-light: #f3f4f6; --bg: #fff; --fg: #1a1a1a; --muted: #6b7280; --red: #ef4444; --green: #22c55e; --yellow: #eab308; }
* { box-sizing: border-box; margin: 0; padding: 0; }
body { font-family: system-ui, sans-serif; font-size: 14px; color: var(--fg); background: var(--gray-light); min-height: 100vh; }
.topbar { background: var(--bg); border-bottom: 1px solid var(--gray); padding: 0 24px; height: 56px; display: flex; align-items: center; gap: 12px; }
.topbar .title { font-weight: 600; font-size: 16px; }
.content { max-width: 900px; margin: 32px auto; padding: 0 16px; }
.card { background: var(--bg); border-radius: 12px; border: 1px solid var(--gray); box-shadow: 0 1px 2px rgba(0,0,0,.05); margin-bottom: 16px; }
.card-header { background: var(--gray-light); padding: 12px 16px; font-weight: 600; font-size: 15px; }
.card-body { padding: 16px; }
.btn { height: 32px; border-radius: 6px; padding: 0 12px; font-size: 13px; font-family: inherit; cursor: pointer; display: inline-flex; align-items: center; gap: 4px; border: 1px solid var(--gray); background: var(--bg); }
.btn-primary { background: var(--brand); border-color: var(--brand); color: #fff; }
.btn-primary:hover { background: var(--brand-dark); }
.btn:disabled { opacity: .5; cursor: not-allowed; }
.btn-sm { height: 26px; font-size: 12px; padding: 0 8px; }
table { width: 100%; border-collapse: collapse; font-size: 13px; }
th { background: var(--gray-light); padding: 6px 8px; border: 1px solid var(--gray); text-align: left; font-weight: 600; font-size: 11px; color: var(--muted); }
td { padding: 6px 8px; border: 1px solid var(--gray); }
.diff-added td { background: rgba(34,197,94,.08); }
.diff-deleted td { background: rgba(239,68,68,.08); text-decoration: line-through; color: var(--red); }
.diff-changed td { background: rgba(234,179,8,.08); }
input[type=file] { width: 100%; margin-bottom: 8px; }
input[type=text] { width: 100%; height: 32px; border: 1px solid var(--gray); border-radius: 6px; padding: 0 8px; font-size: 13px; }
.flex-row { display: flex; gap: 8px; align-items: center; }
.flex-grow { flex: 1; }
.msg { font-size: 13px; padding: 4px 0; }
.msg-green { color: var(--green); }
.msg-red { color: var(--red); }
#log { font-size: 12px; color: var(--muted); max-height: 200px; overflow-y: auto; background: var(--gray-light); padding: 8px; border-radius: 6px; margin-top: 8px; white-space: pre-wrap; }
</style>
</head>
<body>
<div class="topbar">
<span class="title">Сверка договоров</span>
<span style="color:var(--muted);font-size:12px;">v1.0.18 — Lucee</span>
</div>
<div class="content">
<!-- Загрузка -->
<div class="card">
<div class="card-header">📁 Загрузка</div>
<div class="card-body">
<input type="file" id="fileInput" accept=".docx,.doc,.pdf,.zip" multiple>
<div id="fileList" style="margin-bottom:8px;"></div>
<div class="flex-row">
<button class="btn btn-primary" id="btnUpload">Загрузить</button>
<span id="uploadStatus" class="msg"></span>
</div>
</div>
</div>
<!-- Файлы -->
<div class="card" id="filesCard" style="display:none;">
<div class="card-header">📋 Файлы договора <span id="contractLabel" style="color:var(--muted);font-weight:400;"></span></div>
<div class="card-body">
<div id="filesTable"></div>
<div class="flex-row" style="margin-top:8px;">
<button class="btn btn-primary btn-sm" id="btnParseAll">Парсинг</button>
<button class="btn btn-primary btn-sm" id="btnExtractAll">LLM-извлечение</button>
<button class="btn btn-primary btn-sm" id="btnDiffer">Сравнить</button>
<span id="actionStatus" class="msg"></span>
</div>
</div>
</div>
<!-- Результаты сравнения -->
<div class="card" id="diffCard" style="display:none;">
<div class="card-header">🔍 Сравнение</div>
<div class="card-body" id="diffBody"></div>
</div>
<!-- Чат -->
<div class="card" id="chatCard" style="display:none;">
<div class="card-header">💬 Чат с договором</div>
<div class="card-body">
<div id="chatMessages" style="margin-bottom:8px;font-size:13px;"></div>
<div class="flex-row">
<input type="text" id="chatInput" placeholder="Какая общая сумма? Что изменилось? ..." class="flex-grow">
<button class="btn btn-primary" id="chatSend">→</button>
</div>
</div>
</div>
<div id="log"></div>
</div>
<script>
var contractId = '';
var supplements = [];
function log(msg) {
var el = document.getElementById('log');
el.textContent += msg + '\n';
el.scrollTop = el.scrollHeight;
}
// Загрузка
document.getElementById('btnUpload').addEventListener('click', async function() {
var files = document.getElementById('fileInput').files;
if (!files.length) return;
document.getElementById('btnUpload').disabled = true;
document.getElementById('uploadStatus').textContent = 'Загрузка...';
log('Загрузка ' + files.length + ' файлов...');
// Загружаем первый файл (или все)
for (var i = 0; i < files.length; i++) {
var fd = new FormData();
fd.append('files', files[i]);
if (contractId) fd.append('contract_id', contractId);
try {
var resp = await fetch('/upload.cfm', { method: 'POST', body: fd });
var data = await resp.json();
if (data.OK) {
contractId = data.CONTRACT_ID;
log('✅ ' + data.FILENAME + ' (' + data.SIZE + ' bytes)');
if (!contractId) contractId = data.CONTRACT_ID;
} else {
log('❌ ' + files[i].name + ': ' + data.ERROR);
}
} catch(e) {
log('❌ ' + files[i].name + ': ' + e.message);
}
}
document.getElementById('btnUpload').disabled = false;
document.getElementById('uploadStatus').textContent = '';
if (contractId) {
document.getElementById('contractLabel').textContent = contractId.substring(0,8) + '...';
loadFiles();
}
});
async function loadFiles() {
if (!contractId) return;
try {
var resp = await fetch('/api.cfm?action=query&sql=' + encodeURIComponent(
"SELECT s.id as supp_id,d.id as doc_id,d.filename,d.mime_type,d.status,s.type FROM supplements s JOIN documents d ON s.document_id=d.id WHERE s.contract_id='" + contractId + "' ORDER BY s.created_at"
));
var data = await resp.json();
if (data.OK && data.ROWS) {
supplements = data.ROWS;
renderFiles();
document.getElementById('filesCard').style.display = 'block';
document.getElementById('chatCard').style.display = 'block';
}
} catch(e) { log('Ошибка загрузки файлов: ' + e.message); }
}
function renderFiles() {
var html = '<table><tr><th>Файл</th><th>Тип</th><th>Статус</th><th>Действия</th></tr>';
for (var i = 0; i < supplements.length; i++) {
var s = supplements[i];
var statusClass = s.status === 'parsed' ? 'msg-green' : (s.status === 'error' ? 'msg-red' : '');
html += '<tr><td>' + s.filename + '</td><td>' + s.type + '</td><td class="' + statusClass + '">' + (s.status||'') + '</td>';
html += '<td><button class="btn btn-sm" onclick="parseOne(\'' + s.doc_id + '\',this)">Парс</button> ';
html += '<button class="btn btn-sm" onclick="extractOne(\'' + s.supp_id + '\',this)">LLM</button></td></tr>';
}
html += '</table>';
document.getElementById('filesTable').innerHTML = html;
}
// Парсинг
document.getElementById('btnParseAll').addEventListener('click', async function() {
document.getElementById('actionStatus').textContent = 'Парсинг...';
for (var i = 0; i < supplements.length; i++) {
var s = supplements[i];
if (s.status === 'parsed') continue;
try {
var resp = await fetch('/parser.cfm?doc_id=' + s.doc_id);
var data = await resp.json();
if (data.OK) {
s.status = data.STATUS;
log('📄 ' + s.filename + ': ' + data.ELEMENT_COUNT + ' элементов');
}
} catch(e) { log('❌ Парсинг ' + s.filename + ': ' + e.message); }
}
renderFiles();
document.getElementById('actionStatus').textContent = 'Готово';
});
// Извлечение
document.getElementById('btnExtractAll').addEventListener('click', async function() {
document.getElementById('actionStatus').textContent = 'LLM-извлечение...';
for (var i = 0; i < supplements.length; i++) {
var s = supplements[i];
try {
var resp = await fetch('/extractor.cfm?supplement_id=' + s.supp_id);
var data = await resp.json();
if (data.OK) {
log('🤖 ' + s.filename + ': ' + data.ROWS_SAVED + ' строк');
} else {
log('❌ LLM ' + s.filename + ': ' + (data.ERROR||''));
}
} catch(e) { log('❌ LLM ' + s.filename + ': ' + e.message); }
}
document.getElementById('actionStatus').textContent = 'Готово';
});
// Сравнение
document.getElementById('btnDiffer').addEventListener('click', async function() {
document.getElementById('actionStatus').textContent = 'Сравнение...';
try {
var resp = await fetch('/differ.cfm?contract_id=' + contractId);
var data = await resp.json();
if (data.OK) {
renderDiff(data);
document.getElementById('diffCard').style.display = 'block';
} else {
log('❌ Сравнение: ' + (data.ERROR||''));
}
} catch(e) { log('❌ Сравнение: ' + e.message); }
document.getElementById('actionStatus').textContent = 'Готово';
});
function renderDiff(data) {
var s = data.SUMMARY;
var html = '<p>Изменено: ' + (s.CHANGED||0) + ', Добавлено: ' + (s.ADDED||0) + ', Удалено: ' + (s.DELETED||0) + ', Без изменений: ' + (s.UNCHANGED||0) + '</p>';
html += '<table><tr><th>#</th><th>Тип</th><th>Услуга</th><th>Цена</th><th>Кол-во</th><th>Сумма</th></tr>';
for (var i = 0; i < data.CHANGES.length; i++) {
var c = data.CHANGES[i];
var cls = c.CHANGE_TYPE === 'added' ? 'diff-added' : (c.CHANGE_TYPE === 'deleted' ? 'diff-deleted' : (c.CHANGE_TYPE === 'changed' ? 'diff-changed' : ''));
var vals = c.NEW_VALUES && Object.keys(c.NEW_VALUES).length > 0 ? c.NEW_VALUES : c.OLD_VALUES;
html += '<tr class="' + cls + '"><td>' + c.ROW_NUM + '</td><td>' + c.CHANGE_TYPE + '</td><td>' + (vals.NAME||'') + '</td><td>' + (vals.PRICE||'') + '</td><td>' + (vals.QTY||'') + '</td><td>' + (vals.SUM||'') + '</td></tr>';
}
html += '</table>';
document.getElementById('diffBody').innerHTML = html;
}
// Чат
document.getElementById('chatSend').addEventListener('click', async function() {
var input = document.getElementById('chatInput');
var q = input.value.trim();
if (!q || !contractId) return;
var msgs = document.getElementById('chatMessages');
msgs.innerHTML += '<div><b>Вы:</b> ' + q + '</div>';
input.value = '';
document.getElementById('chatSend').disabled = true;
try {
var fd = new FormData();
fd.append('question', q);
var resp = await fetch('/chat.cfm?contract_id=' + contractId, { method: 'POST', body: fd });
var data = await resp.json();
if (data.OK) {
msgs.innerHTML += '<div><b>AI:</b> ' + data.ANSWER + '</div>';
} else {
msgs.innerHTML += '<div style="color:var(--red)">Ошибка: ' + (data.ERROR||'') + '</div>';
}
} catch(e) {
msgs.innerHTML += '<div style="color:var(--red)">Сеть: ' + e.message + '</div>';
}
document.getElementById('chatSend').disabled = false;
});
document.getElementById('chatInput').addEventListener('keydown', function(e) {
if (e.key === 'Enter') document.getElementById('chatSend').click();
});
// Превью файлов
document.getElementById('fileInput').addEventListener('change', function() {
var files = this.files;
var html = '';
for (var i = 0; i < files.length; i++) {
html += '<div style="font-size:13px;">📄 ' + files[i].name + ' (' + (files[i].size/1024).toFixed(1) + ' KB)</div>';
}
document.getElementById('fileList').innerHTML = html;
});
</script>
</body>
</html>