diff --git a/deploy/app.js b/deploy/app.js index 28650b1..e93965a 100644 --- a/deploy/app.js +++ b/deploy/app.js @@ -49,6 +49,22 @@ fileInput.addEventListener('change', async function() { fileInput.disabled = true; + // Удалить из очереди файлы, которых нет в новом выборе + var newNames = new Set(newFiles.map(function(f) { return f.name; })); + var removed = []; + fileQueue = fileQueue.filter(function(f) { + if (!newNames.has(f.name)) { removed.push(f); return false; } + return true; + }); + // Удалить с сервера то, что убрали из таблицы + for (var ri = 0; ri < removed.length; ri++) { + var rf = removed[ri]; + if (rf.doc_id) { + fetch(VM_API + '/api/documents/' + rf.doc_id + '?contract_id=' + (contractId||''), { method: 'DELETE' }).catch(function(){}); + } + } + renderTable(); + for (var i = 0; i < newFiles.length; i++) { var f = newFiles[i]; var isZip = f.name.toLowerCase().endsWith('.zip'); @@ -168,6 +184,16 @@ fileInput.addEventListener('change', async function() { await refreshSupps(); + // Удалить с сервера мусор — документы этого батча, которых нет в таблице + if (batchId) { + var keepIds = fileQueue.map(function(f) { return f.doc_id; }).filter(Boolean); + fetch(VM_API + '/api/sync-batch', { + method: 'POST', + headers: {'Content-Type': 'application/json'}, + body: JSON.stringify({batch_id: batchId, keep_ids: keepIds}) + }).catch(function(){}); + } + fileInput.value = ''; fileInput.disabled = false; // Показать кнопки после авто-парсинга diff --git a/deploy/convert_server.py b/deploy/convert_server.py index 386a8c3..70130e3 100755 --- a/deploy/convert_server.py +++ b/deploy/convert_server.py @@ -78,6 +78,15 @@ class Handler(BaseHTTPRequestHandler): self._handle_api_prompts_activate() elif self.path == "/api/prompts/delete": self._handle_api_prompts_delete() + elif self.path == "/api/sync-batch": + self._handle_api_sync_batch() + else: + self.send_error(404) + + def do_DELETE(self): + parsed = urlparse(self.path) + if parsed.path.startswith("/api/documents/"): + self._handle_api_document_delete(parsed) else: self.send_error(404) @@ -139,6 +148,44 @@ class Handler(BaseHTTPRequestHandler): "elements_json": doc.get("elements_json"), }) + def _handle_api_document_delete(self, parsed): + """DELETE /api/documents/ — cascade: spec_current, spec_events, supplements, document.""" + from db.connection import execute, query + doc_id = parsed.path.split("/")[-1] + supps = query("SELECT id, contract_id FROM supplements WHERE document_id = %s", (doc_id,)) + for s in (supps or []): + execute("DELETE FROM spec_current WHERE contract_id = %s", (s["contract_id"],)) + execute("DELETE FROM spec_events WHERE supplement_id = %s", (s["id"],)) + execute("DELETE FROM supplements WHERE id = %s", (s["id"],)) + execute("DELETE FROM documents WHERE id = %s", (doc_id,)) + self._json({"ok": True}) + + # ── POST /api/sync-batch ──────────────────────────────────────────── + + def _handle_api_sync_batch(self): + """Удалить все документы батча, НЕ входящие в keep_ids.""" + from db.connection import execute, query + length = int(self.headers.get("Content-Length", 0)) + body = json.loads(self.rfile.read(length)) if length > 0 else {} + batch_id = body.get("batch_id", "") + keep_ids = set(body.get("keep_ids", [])) + if not batch_id: + self._json({"ok": False, "error": "batch_id required"}, 400) + return + docs = query("SELECT id FROM documents WHERE batch_id = %s", (batch_id,)) + deleted = 0 + for d in (docs or []): + if d["id"] in keep_ids: + continue + supps = query("SELECT id, contract_id FROM supplements WHERE document_id = %s", (d["id"],)) + for s in (supps or []): + execute("DELETE FROM spec_current WHERE contract_id = %s", (s["contract_id"],)) + execute("DELETE FROM spec_events WHERE supplement_id = %s", (s["id"],)) + execute("DELETE FROM supplements WHERE id = %s", (s["id"],)) + execute("DELETE FROM documents WHERE id = %s", (d["id"],)) + deleted += 1 + self._json({"ok": True, "deleted": deleted}) + # ── /api/groups ?batch=X ───────────────────────────────────────────── def _handle_api_groups(self, parsed):