feat: Externe Dokument-Referenzen erkennen, herunterladen & mitbewerten
Nach der Transkription eines Dokuments werden referenzierte externe
Dokumente (AGB, Verhaltenskodex) erkannt, sicher heruntergeladen und ihr
Text dem Hauptdokument fuer die "Summen"-Bewertung angehaengt (Option D).
Die Referenzen werden zusaetzlich als eigene, herunterladbare Zeilen
gespeichert.
- DB: dc_project_documents um parent_document_id, is_reference, source_url
erweitert (DC_REFERENCES_ALTER.sql)
- ORDS: Dokumentenliste um die Felder erweitert; neue Endpunkte
POST /documents/:id/references und DELETE /projects/:id/references;
q-Quote-Parserfehler im ai-costs-Handler ('[]') behoben
- Backend: ReferenceDownloadService (SSRF-sicher via DNS-/Public-IP-
Validierung, Redirect-Pruefung, Groessen-/Timeout-Limits, jsoup fuer HTML),
EvaluationService.extractReferenceUrls (LLM-basierte URL-Erkennung),
Orchestrierung (Discovery in Phase A, Referenz-Skip + combinedText in Phase B)
- Config: dc.references.* in application.properties
Verifiziert an Projekt 144: AGB-PDF/-HTML heruntergeladen, verknuepft,
"Summen"-Bewertung zitiert AGB-Klauseln aus dem Referenzdokument.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -52,12 +52,28 @@ CREATE OR REPLACE PACKAGE DC_BACKEND_PKG AS
|
||||
-- -------------------------------------------------------------------------
|
||||
FUNCTION health_check RETURN BOOLEAN;
|
||||
|
||||
-- -------------------------------------------------------------------------
|
||||
-- Setzt Projekt auf PENDING zurück – eigene Transaktion (AUTONOMOUS COMMIT).
|
||||
-- dc_projects.status → PENDING, report_markdown → NULL
|
||||
-- dc_project_documents.original_text/translated_text → NULL, progress → 0
|
||||
-- dc_results löschen
|
||||
-- -------------------------------------------------------------------------
|
||||
PROCEDURE reset_project(
|
||||
p_project_id IN NUMBER
|
||||
);
|
||||
|
||||
-- -------------------------------------------------------------------------
|
||||
-- Startet die Prüfung und wirft eine Exception wenn nicht 202 zurückkommt.
|
||||
-- Convenience-Wrapper für direkte APEX-Button-Aufrufe.
|
||||
--
|
||||
-- p_reset TRUE = Projekt vor dem Start zurücksetzen (eigener COMMIT):
|
||||
-- dc_projects.status → PENDING, report_markdown → NULL
|
||||
-- dc_project_documents.original_text/translated_text → NULL, progress → 0
|
||||
-- dc_results löschen
|
||||
-- -------------------------------------------------------------------------
|
||||
PROCEDURE start_check_or_raise(
|
||||
p_project_id IN NUMBER
|
||||
p_project_id IN NUMBER,
|
||||
p_reset IN BOOLEAN DEFAULT FALSE
|
||||
);
|
||||
|
||||
-- -------------------------------------------------------------------------
|
||||
@@ -200,6 +216,31 @@ END DC_BACKEND_PKG;
|
||||
-- =============================================================================
|
||||
CREATE OR REPLACE PACKAGE BODY DC_BACKEND_PKG AS
|
||||
|
||||
-- -------------------------------------------------------------------------
|
||||
-- Setzt Projekt auf PENDING zurück – eigene Transaktion (AUTONOMOUS).
|
||||
-- -------------------------------------------------------------------------
|
||||
PROCEDURE reset_project(p_project_id IN NUMBER) IS
|
||||
PRAGMA AUTONOMOUS_TRANSACTION;
|
||||
BEGIN
|
||||
UPDATE dc_projects
|
||||
SET status = 'PENDING',
|
||||
report_markdown = NULL
|
||||
WHERE id = p_project_id;
|
||||
|
||||
UPDATE dc_project_documents
|
||||
SET original_text = NULL,
|
||||
translated_text = NULL,
|
||||
progress = 0,
|
||||
status = 'PENDING'
|
||||
WHERE project_id = p_project_id;
|
||||
|
||||
DELETE FROM dc_results
|
||||
WHERE project_id = p_project_id;
|
||||
|
||||
COMMIT;
|
||||
END reset_project;
|
||||
|
||||
|
||||
-- -------------------------------------------------------------------------
|
||||
-- Setzt gemeinsame Request-Header (Content-Type + optional API-Key)
|
||||
-- -------------------------------------------------------------------------
|
||||
@@ -274,11 +315,16 @@ CREATE OR REPLACE PACKAGE BODY DC_BACKEND_PKG AS
|
||||
|
||||
-- -------------------------------------------------------------------------
|
||||
PROCEDURE start_check_or_raise(
|
||||
p_project_id IN NUMBER
|
||||
p_project_id IN NUMBER,
|
||||
p_reset IN BOOLEAN DEFAULT FALSE
|
||||
) IS
|
||||
l_status NUMBER;
|
||||
l_response VARCHAR2(4000);
|
||||
BEGIN
|
||||
IF p_reset THEN
|
||||
reset_project(p_project_id);
|
||||
END IF;
|
||||
|
||||
start_check(
|
||||
p_project_id => p_project_id,
|
||||
p_status_code => l_status,
|
||||
|
||||
50
Scripts/DC_DOCUMENT_PROGRESS_ALTER.sql
Normal file
50
Scripts/DC_DOCUMENT_PROGRESS_ALTER.sql
Normal file
@@ -0,0 +1,50 @@
|
||||
-- =============================================================================
|
||||
-- DC_DOCUMENT_PROGRESS_ALTER.sql
|
||||
-- Ergänzt dc_project_documents um status und progress für Dokument-Fortschritt.
|
||||
-- Idempotent: Fehler "Spalte existiert bereits" wird ignoriert.
|
||||
-- =============================================================================
|
||||
|
||||
BEGIN
|
||||
EXECUTE IMMEDIATE '
|
||||
ALTER TABLE dc_project_documents ADD (
|
||||
status VARCHAR2(20) DEFAULT ''PENDING'',
|
||||
progress NUMBER DEFAULT 0
|
||||
)
|
||||
';
|
||||
DBMS_OUTPUT.PUT_LINE('Spalten status/progress angelegt.');
|
||||
EXCEPTION
|
||||
WHEN OTHERS THEN
|
||||
IF SQLCODE = -1430 THEN
|
||||
DBMS_OUTPUT.PUT_LINE('Spalten existieren bereits – kein Änderungsbedarf.');
|
||||
ELSE
|
||||
RAISE;
|
||||
END IF;
|
||||
END;
|
||||
/
|
||||
|
||||
-- Bestehende Zeilen initialisieren
|
||||
UPDATE dc_project_documents
|
||||
SET status = 'PENDING',
|
||||
progress = 0
|
||||
WHERE status IS NULL;
|
||||
|
||||
COMMIT;
|
||||
|
||||
-- Check-Constraint (idempotent)
|
||||
BEGIN
|
||||
EXECUTE IMMEDIATE '
|
||||
ALTER TABLE dc_project_documents
|
||||
ADD CONSTRAINT dc_project_documents_status_ck
|
||||
CHECK (status IN (''PENDING'', ''IN_PROGRESS'', ''COMPLETED'', ''ERROR''))
|
||||
';
|
||||
EXCEPTION
|
||||
WHEN OTHERS THEN
|
||||
IF SQLCODE IN (-2264, -2261) THEN NULL; -- Constraint existiert bereits
|
||||
ELSE RAISE;
|
||||
END IF;
|
||||
END;
|
||||
/
|
||||
|
||||
COMMIT;
|
||||
|
||||
EXIT;
|
||||
68
Scripts/DC_DOCUMENT_PROGRESS_ORDS.sql
Normal file
68
Scripts/DC_DOCUMENT_PROGRESS_ORDS.sql
Normal file
@@ -0,0 +1,68 @@
|
||||
-- =============================================================================
|
||||
-- DC_DOCUMENT_PROGRESS_ORDS.sql
|
||||
-- Registriert den ORDS-Endpunkt PUT /api/dc/documents/:doc_id/progress.
|
||||
-- Idempotent: bereits vorhandene Templates/Handler werden überschrieben.
|
||||
-- =============================================================================
|
||||
|
||||
BEGIN
|
||||
-- Template (idempotent)
|
||||
ORDS.DEFINE_TEMPLATE(
|
||||
p_module_name => 'frigosped.dc',
|
||||
p_pattern => 'documents/:doc_id/progress',
|
||||
p_priority => 0,
|
||||
p_comments => 'Dokument-Fortschritt und -Status schreiben'
|
||||
);
|
||||
|
||||
-- PUT /api/dc/documents/:doc_id/progress
|
||||
-- Body: {"progress":45,"status":"IN_PROGRESS"}
|
||||
ORDS.DEFINE_HANDLER(
|
||||
p_module_name => 'frigosped.dc',
|
||||
p_pattern => 'documents/:doc_id/progress',
|
||||
p_method => 'PUT',
|
||||
p_source_type => ORDS.SOURCE_TYPE_PLSQL,
|
||||
p_comments => 'Setzt progress (0-100) und/oder status des Dokuments',
|
||||
p_source =>
|
||||
'DECLARE
|
||||
v_body CLOB;
|
||||
v_progress NUMBER;
|
||||
v_status VARCHAR2(20);
|
||||
v_rows INTEGER;
|
||||
BEGIN
|
||||
v_body := :body_text;
|
||||
v_progress := TO_NUMBER(JSON_VALUE(v_body, ''$.progress''));
|
||||
v_status := JSON_VALUE(v_body, ''$.status'');
|
||||
|
||||
IF v_progress IS NOT NULL AND (v_progress < 0 OR v_progress > 100) THEN
|
||||
:status_code := 400;
|
||||
HTP.P(''{"error":"progress muss zwischen 0 und 100 liegen"}'');
|
||||
RETURN;
|
||||
END IF;
|
||||
|
||||
UPDATE dc_project_documents
|
||||
SET progress = CASE WHEN JSON_EXISTS(v_body, ''$.progress'') THEN v_progress ELSE progress END,
|
||||
status = CASE WHEN JSON_EXISTS(v_body, ''$.status'') THEN v_status ELSE status END
|
||||
WHERE id = :doc_id;
|
||||
|
||||
v_rows := SQL%ROWCOUNT;
|
||||
IF v_rows = 0 THEN
|
||||
:status_code := 404;
|
||||
HTP.P(''{"error":"Dokument nicht gefunden","doc_id":'' || :doc_id || ''}'');
|
||||
RETURN;
|
||||
END IF;
|
||||
|
||||
:status_code := 200;
|
||||
HTP.P(''{"doc_id":'' || :doc_id || '',"updated":true}'');
|
||||
|
||||
EXCEPTION
|
||||
WHEN OTHERS THEN
|
||||
ROLLBACK;
|
||||
:status_code := 500;
|
||||
HTP.P(''{"error":"'' || REPLACE(SQLERRM, ''"'', '''''''') || ''"}'');
|
||||
END;'
|
||||
);
|
||||
|
||||
COMMIT;
|
||||
END;
|
||||
/
|
||||
|
||||
EXIT;
|
||||
71
Scripts/DC_PROJECTS_NOTIFY_ALTER.sql
Normal file
71
Scripts/DC_PROJECTS_NOTIFY_ALTER.sql
Normal file
@@ -0,0 +1,71 @@
|
||||
-- =============================================================================
|
||||
-- DC_PROJECTS_NOTIFY_ALTER.sql
|
||||
-- Ergänzt dc_projects um drei Berichtstyp-Flags für die E-Mail-Benachrichtigung.
|
||||
-- Idempotent: bereits vorhandene Spalten werden ignoriert.
|
||||
-- =============================================================================
|
||||
|
||||
BEGIN
|
||||
EXECUTE IMMEDIATE '
|
||||
ALTER TABLE dc_projects ADD (
|
||||
notify_ok CHAR(1) DEFAULT ''Y'' NOT NULL,
|
||||
notify_hinweis CHAR(1) DEFAULT ''Y'' NOT NULL,
|
||||
notify_abweichend CHAR(1) DEFAULT ''Y'' NOT NULL
|
||||
)
|
||||
';
|
||||
DBMS_OUTPUT.PUT_LINE('Spalten notify_ok/notify_hinweis/notify_abweichend angelegt.');
|
||||
EXCEPTION
|
||||
WHEN OTHERS THEN
|
||||
IF SQLCODE = -1430 THEN
|
||||
DBMS_OUTPUT.PUT_LINE('Spalten existieren bereits – kein Änderungsbedarf.');
|
||||
ELSE
|
||||
RAISE;
|
||||
END IF;
|
||||
END;
|
||||
/
|
||||
|
||||
-- Check-Constraints (idempotent)
|
||||
BEGIN
|
||||
EXECUTE IMMEDIATE '
|
||||
ALTER TABLE dc_projects
|
||||
ADD CONSTRAINT dc_projects_notify_ok_ck
|
||||
CHECK (notify_ok IN (''Y'',''N''))
|
||||
';
|
||||
EXCEPTION
|
||||
WHEN OTHERS THEN
|
||||
IF SQLCODE IN (-2264, -2261) THEN NULL;
|
||||
ELSE RAISE;
|
||||
END IF;
|
||||
END;
|
||||
/
|
||||
|
||||
BEGIN
|
||||
EXECUTE IMMEDIATE '
|
||||
ALTER TABLE dc_projects
|
||||
ADD CONSTRAINT dc_projects_notify_hinweis_ck
|
||||
CHECK (notify_hinweis IN (''Y'',''N''))
|
||||
';
|
||||
EXCEPTION
|
||||
WHEN OTHERS THEN
|
||||
IF SQLCODE IN (-2264, -2261) THEN NULL;
|
||||
ELSE RAISE;
|
||||
END IF;
|
||||
END;
|
||||
/
|
||||
|
||||
BEGIN
|
||||
EXECUTE IMMEDIATE '
|
||||
ALTER TABLE dc_projects
|
||||
ADD CONSTRAINT dc_projects_notify_abweichend_ck
|
||||
CHECK (notify_abweichend IN (''Y'',''N''))
|
||||
';
|
||||
EXCEPTION
|
||||
WHEN OTHERS THEN
|
||||
IF SQLCODE IN (-2264, -2261) THEN NULL;
|
||||
ELSE RAISE;
|
||||
END IF;
|
||||
END;
|
||||
/
|
||||
|
||||
COMMIT;
|
||||
|
||||
EXIT;
|
||||
81
Scripts/DC_REFERENCES_ALTER.sql
Normal file
81
Scripts/DC_REFERENCES_ALTER.sql
Normal file
@@ -0,0 +1,81 @@
|
||||
-- =============================================================================
|
||||
-- DC_REFERENCES_ALTER.sql
|
||||
-- Ergänzt dc_project_documents um Felder für heruntergeladene externe Referenzen.
|
||||
-- parent_document_id → Self-FK auf das Originaldokument (NULL bei Originalen)
|
||||
-- is_reference → 0 = Originaldokument, 1 = heruntergeladene Referenz
|
||||
-- source_url → Ursprungs-URL der heruntergeladenen Referenz
|
||||
-- Idempotent: Fehler "Spalte/Constraint existiert bereits" wird ignoriert.
|
||||
-- =============================================================================
|
||||
|
||||
BEGIN
|
||||
EXECUTE IMMEDIATE '
|
||||
ALTER TABLE dc_project_documents ADD (
|
||||
parent_document_id NUMBER,
|
||||
is_reference NUMBER DEFAULT 0,
|
||||
source_url VARCHAR2(2000 CHAR)
|
||||
)
|
||||
';
|
||||
DBMS_OUTPUT.PUT_LINE('Spalten parent_document_id/is_reference/source_url angelegt.');
|
||||
EXCEPTION
|
||||
WHEN OTHERS THEN
|
||||
IF SQLCODE = -1430 THEN
|
||||
DBMS_OUTPUT.PUT_LINE('Spalten existieren bereits – kein Änderungsbedarf.');
|
||||
ELSE
|
||||
RAISE;
|
||||
END IF;
|
||||
END;
|
||||
/
|
||||
|
||||
-- Bestehende Zeilen initialisieren
|
||||
UPDATE dc_project_documents
|
||||
SET is_reference = 0
|
||||
WHERE is_reference IS NULL;
|
||||
|
||||
COMMIT;
|
||||
|
||||
-- Self-FK (idempotent)
|
||||
BEGIN
|
||||
EXECUTE IMMEDIATE '
|
||||
ALTER TABLE dc_project_documents
|
||||
ADD CONSTRAINT dc_project_documents_parent_fk
|
||||
FOREIGN KEY (parent_document_id) REFERENCES dc_project_documents (id)
|
||||
';
|
||||
EXCEPTION
|
||||
WHEN OTHERS THEN
|
||||
IF SQLCODE IN (-2264, -2261, -2275) THEN NULL; -- Constraint existiert bereits
|
||||
ELSE RAISE;
|
||||
END IF;
|
||||
END;
|
||||
/
|
||||
|
||||
-- Check-Constraint is_reference (idempotent)
|
||||
BEGIN
|
||||
EXECUTE IMMEDIATE '
|
||||
ALTER TABLE dc_project_documents
|
||||
ADD CONSTRAINT dc_project_documents_is_ref_ck
|
||||
CHECK (is_reference IN (0, 1))
|
||||
';
|
||||
EXCEPTION
|
||||
WHEN OTHERS THEN
|
||||
IF SQLCODE IN (-2264, -2261) THEN NULL; -- Constraint existiert bereits
|
||||
ELSE RAISE;
|
||||
END IF;
|
||||
END;
|
||||
/
|
||||
|
||||
-- Index auf parent_document_id (idempotent)
|
||||
BEGIN
|
||||
EXECUTE IMMEDIATE '
|
||||
CREATE INDEX dc_project_documents_parent_i ON dc_project_documents (parent_document_id)
|
||||
';
|
||||
EXCEPTION
|
||||
WHEN OTHERS THEN
|
||||
IF SQLCODE = -955 THEN NULL; -- Index existiert bereits
|
||||
ELSE RAISE;
|
||||
END IF;
|
||||
END;
|
||||
/
|
||||
|
||||
COMMIT;
|
||||
|
||||
EXIT;
|
||||
51
Scripts/DC_STRIP_TRANSLATED_FENCE.sql
Normal file
51
Scripts/DC_STRIP_TRANSLATED_FENCE.sql
Normal file
@@ -0,0 +1,51 @@
|
||||
-- =============================================================================
|
||||
-- Bereinigt umschließende Markdown-Code-Fences (```markdown ... ```) aus der
|
||||
-- Spalte TRANSLATED_TEXT in DC_PROJECT_DOCUMENTS.
|
||||
--
|
||||
-- Hintergrund: Das Übersetzungs-Modell hat den Text mal mit, mal ohne Fence
|
||||
-- gespeichert. Der Backend-Code strippt den Fence ab jetzt beim Schreiben;
|
||||
-- dieses Skript bereinigt die bereits vorhandenen Altdatensätze.
|
||||
--
|
||||
-- Idempotent: Datensätze ohne führenden ``` bleiben unverändert.
|
||||
-- =============================================================================
|
||||
|
||||
SET SERVEROUTPUT ON
|
||||
|
||||
DECLARE
|
||||
v_count PLS_INTEGER := 0;
|
||||
v_text CLOB;
|
||||
v_nl PLS_INTEGER;
|
||||
BEGIN
|
||||
FOR rec IN (
|
||||
SELECT id, translated_text
|
||||
FROM dc_project_documents
|
||||
WHERE translated_text IS NOT NULL
|
||||
AND LTRIM(translated_text) LIKE '```%'
|
||||
) LOOP
|
||||
v_text := LTRIM(rec.translated_text);
|
||||
|
||||
-- Erste Zeile (z. B. ```markdown) entfernen
|
||||
v_nl := INSTR(v_text, CHR(10));
|
||||
IF v_nl > 0 THEN
|
||||
v_text := SUBSTR(v_text, v_nl + 1);
|
||||
END IF;
|
||||
|
||||
-- Schließendes ``` am Ende entfernen
|
||||
v_text := RTRIM(v_text);
|
||||
IF SUBSTR(v_text, -3) = '```' THEN
|
||||
v_text := SUBSTR(v_text, 1, LENGTH(v_text) - 3);
|
||||
END IF;
|
||||
|
||||
v_text := TRIM(v_text);
|
||||
|
||||
UPDATE dc_project_documents
|
||||
SET translated_text = v_text
|
||||
WHERE id = rec.id;
|
||||
|
||||
v_count := v_count + 1;
|
||||
END LOOP;
|
||||
|
||||
COMMIT;
|
||||
DBMS_OUTPUT.PUT_LINE('Bereinigte Datensätze: ' || v_count);
|
||||
END;
|
||||
/
|
||||
@@ -3,7 +3,7 @@
|
||||
-- Hilfsprozeduren und -funktionen für den Dokumenten-Check
|
||||
--
|
||||
-- Deployment:
|
||||
-- sqlplus user/pass@db @"Scripts/DC_UTILS_PKG.sql"
|
||||
-- sql user/pass@db @"Scripts/DC_UTILS_PKG.sql"
|
||||
-- =============================================================================
|
||||
|
||||
|
||||
@@ -12,13 +12,24 @@
|
||||
-- =============================================================================
|
||||
CREATE OR REPLACE PACKAGE DC_UTILS_PKG AS
|
||||
|
||||
-- Absender-Adresse für Berichts-Mails (anpassen falls nötig)
|
||||
c_mail_from CONSTANT VARCHAR2(100) := 'express-o@frigosped.de';
|
||||
|
||||
-- Basis-URL der APEX-App für Direktlinks in Mails (Friendly URL, ohne
|
||||
-- Session-ID -> APEX Deep-Linking leitet bei Bedarf über den Login).
|
||||
-- Die Projekt-ID wird angehängt: ...?p7_project_id=<id>
|
||||
c_app_link_base CONSTANT VARCHAR2(300) :=
|
||||
'https://apex-ai.express-o.org/ords/r/ai_dev/dokumentencheck/' ||
|
||||
'projekt-bearbeiten?p7_project_id=';
|
||||
|
||||
-- -------------------------------------------------------------------------
|
||||
-- Erstellt einen Markdown-Prüfbericht für ein Projekt und speichert ihn
|
||||
-- in dc_projects.report_markdown.
|
||||
-- in dc_projects.report_markdown. Anschließend wird der Bericht per Mail
|
||||
-- an notification_email versendet (sofern gesetzt).
|
||||
--
|
||||
-- Aufbau des Berichts:
|
||||
-- 1. Kopf (Projektname, Abschlussdatum)
|
||||
-- 2. Zusammenfassung (OK / Hinweise / Abweichungen)
|
||||
-- 2. Zusammenfassung (OK / Hinweise / Abweichungen) – gefiltert nach notify_*
|
||||
-- 3. Ergebnisse: je Dokument → Kategorie (order_nr) → Frage (order_nr)
|
||||
-- Icons: ✅ OK | ⚠️ Hinweis | ❌ Abweichung | ❓ Unklar
|
||||
--
|
||||
@@ -27,6 +38,17 @@ CREATE OR REPLACE PACKAGE DC_UTILS_PKG AS
|
||||
-- -------------------------------------------------------------------------
|
||||
PROCEDURE generate_report(p_project_id IN NUMBER);
|
||||
|
||||
-- -------------------------------------------------------------------------
|
||||
-- Versendet den gespeicherten Markdown-Bericht per E-Mail als PDF-Anhang.
|
||||
-- Wird automatisch von generate_report aufgerufen; kann aber auch
|
||||
-- manuell ausgelöst werden (z.B. für erneuten Versand).
|
||||
-- Tut nichts wenn notification_email oder report_markdown leer ist.
|
||||
-- p_skip_pdf = TRUE: kein HTTP-Rückruf ans Java-Backend (nur Text-Mail).
|
||||
-- -------------------------------------------------------------------------
|
||||
PROCEDURE send_report_email(p_project_id IN NUMBER,
|
||||
p_skip_pdf IN BOOLEAN DEFAULT FALSE,
|
||||
p_pdf IN BLOB DEFAULT NULL);
|
||||
|
||||
END DC_UTILS_PKG;
|
||||
/
|
||||
|
||||
@@ -44,11 +66,24 @@ CREATE OR REPLACE PACKAGE BODY DC_UTILS_PKG AS
|
||||
v_status dc_projects.status%TYPE;
|
||||
v_done dc_projects.completed_at%TYPE;
|
||||
|
||||
v_ok_cnt NUMBER := 0;
|
||||
v_warn_cnt NUMBER := 0;
|
||||
v_dev_cnt NUMBER := 0;
|
||||
v_unkl_cnt NUMBER := 0;
|
||||
v_total NUMBER := 0;
|
||||
v_ok_cnt NUMBER := 0;
|
||||
v_warn_cnt NUMBER := 0;
|
||||
v_dev_cnt NUMBER := 0;
|
||||
v_unkl_cnt NUMBER := 0;
|
||||
v_total NUMBER := 0;
|
||||
v_total_all NUMBER := 0;
|
||||
|
||||
-- Zähler pro Dokument
|
||||
v_doc_ok_cnt NUMBER := 0;
|
||||
v_doc_warn_cnt NUMBER := 0;
|
||||
v_doc_dev_cnt NUMBER := 0;
|
||||
v_doc_unkl_cnt NUMBER := 0;
|
||||
v_doc_total NUMBER := 0; -- gefiltert (für ok_cnt-Berechnung)
|
||||
v_doc_total_all NUMBER := 0; -- ungefiltert (für Gesamt-Zeile)
|
||||
|
||||
v_notify_ok dc_projects.notify_ok%TYPE;
|
||||
v_notify_hinweis dc_projects.notify_hinweis%TYPE;
|
||||
v_notify_abweichend dc_projects.notify_abweichend%TYPE;
|
||||
|
||||
-- Emoji-Konstanten via UNISTR (vermeidet Quellcode-Encoding-Probleme)
|
||||
C_OK CONSTANT VARCHAR2(10 CHAR) := UNISTR('\2705'); -- ✅
|
||||
@@ -70,6 +105,10 @@ CREATE OR REPLACE PACKAGE BODY DC_UTILS_PKG AS
|
||||
JOIN dc_results r ON r.question_id = q.id
|
||||
WHERE r.project_id = p_project_id
|
||||
AND r.doc_id = p_doc_id
|
||||
AND ( (r.deviation = 1 AND v_notify_abweichend = 'Y')
|
||||
OR (r.warning = 1 AND v_notify_hinweis = 'Y')
|
||||
OR (r.deviation = 0 AND r.warning = 0 AND v_notify_ok = 'Y')
|
||||
)
|
||||
ORDER BY qc.order_nr, qc.name;
|
||||
|
||||
CURSOR c_qs(p_doc_id NUMBER, p_cat_id NUMBER) IS
|
||||
@@ -85,6 +124,10 @@ CREATE OR REPLACE PACKAGE BODY DC_UTILS_PKG AS
|
||||
WHERE r.project_id = p_project_id
|
||||
AND r.doc_id = p_doc_id
|
||||
AND q.category_id = p_cat_id
|
||||
AND ( (r.deviation = 1 AND v_notify_abweichend = 'Y')
|
||||
OR (r.warning = 1 AND v_notify_hinweis = 'Y')
|
||||
OR (r.deviation = 0 AND r.warning = 0 AND v_notify_ok = 'Y')
|
||||
)
|
||||
ORDER BY q.order_nr, q.id;
|
||||
|
||||
v_icon VARCHAR2(20 CHAR);
|
||||
@@ -102,11 +145,18 @@ CREATE OR REPLACE PACKAGE BODY DC_UTILS_PKG AS
|
||||
BEGIN
|
||||
DBMS_LOB.CREATETEMPORARY(v_md, TRUE);
|
||||
|
||||
SELECT name, status, completed_at
|
||||
INTO v_name, v_status, v_done
|
||||
SELECT name, status, completed_at,
|
||||
notify_ok, notify_hinweis, notify_abweichend
|
||||
INTO v_name, v_status, v_done,
|
||||
v_notify_ok, v_notify_hinweis, v_notify_abweichend
|
||||
FROM dc_projects
|
||||
WHERE id = p_project_id;
|
||||
|
||||
SELECT COUNT(*)
|
||||
INTO v_total_all
|
||||
FROM dc_results
|
||||
WHERE project_id = p_project_id;
|
||||
|
||||
SELECT SUM(CASE WHEN deviation = 1 THEN 1 ELSE 0 END),
|
||||
SUM(CASE WHEN warning = 1 THEN 1 ELSE 0 END),
|
||||
SUM(CASE WHEN result_type = 'UNKLAR'
|
||||
@@ -115,7 +165,11 @@ CREATE OR REPLACE PACKAGE BODY DC_UTILS_PKG AS
|
||||
COUNT(*)
|
||||
INTO v_dev_cnt, v_warn_cnt, v_unkl_cnt, v_total
|
||||
FROM dc_results
|
||||
WHERE project_id = p_project_id;
|
||||
WHERE project_id = p_project_id
|
||||
AND ( (deviation = 1 AND v_notify_abweichend = 'Y')
|
||||
OR (warning = 1 AND v_notify_hinweis = 'Y')
|
||||
OR (deviation = 0 AND warning = 0 AND v_notify_ok = 'Y')
|
||||
);
|
||||
|
||||
v_ok_cnt := v_total - v_dev_cnt - v_warn_cnt - v_unkl_cnt;
|
||||
|
||||
@@ -138,13 +192,19 @@ CREATE OR REPLACE PACKAGE BODY DC_UTILS_PKG AS
|
||||
-- =====================================================================
|
||||
ln('## Zusammenfassung');
|
||||
ln();
|
||||
ln(C_OK || ' OK: ' || v_ok_cnt);
|
||||
ln(C_WARN || ' Hinweise: ' || v_warn_cnt);
|
||||
ln(C_NOK || ' Abweichungen: ' || v_dev_cnt);
|
||||
IF v_unkl_cnt > 0 THEN
|
||||
ln(C_UNKL || ' Unklar: ' || v_unkl_cnt);
|
||||
IF v_notify_ok = 'Y' THEN
|
||||
ln(C_OK || ' OK: ' || v_ok_cnt);
|
||||
IF v_unkl_cnt > 0 THEN
|
||||
ln(C_UNKL || ' Unklar: ' || v_unkl_cnt);
|
||||
END IF;
|
||||
END IF;
|
||||
ln('**Gesamt: ' || v_total || '**');
|
||||
IF v_notify_hinweis = 'Y' THEN
|
||||
ln(C_WARN || ' Hinweise: ' || v_warn_cnt);
|
||||
END IF;
|
||||
IF v_notify_abweichend = 'Y' THEN
|
||||
ln(C_NOK || ' Abweichungen: ' || v_dev_cnt);
|
||||
END IF;
|
||||
ln('**Gesamt: ' || v_total_all || '**');
|
||||
ln();
|
||||
ln('---');
|
||||
ln();
|
||||
@@ -159,6 +219,42 @@ CREATE OR REPLACE PACKAGE BODY DC_UTILS_PKG AS
|
||||
ln('## ' || C_DOC || ' ' || doc.filename);
|
||||
ln();
|
||||
|
||||
-- Zusammenfassung je Dokument (gefiltert nach notify_*, analog zur Gesamt-Zusammenfassung)
|
||||
SELECT SUM(CASE WHEN deviation = 1
|
||||
AND v_notify_abweichend = 'Y' THEN 1 ELSE 0 END),
|
||||
SUM(CASE WHEN warning = 1 AND deviation = 0
|
||||
AND v_notify_hinweis = 'Y' THEN 1 ELSE 0 END),
|
||||
SUM(CASE WHEN result_type = 'UNKLAR' AND deviation = 0 AND warning = 0
|
||||
AND v_notify_ok = 'Y' THEN 1 ELSE 0 END),
|
||||
SUM(CASE WHEN (deviation = 1 AND v_notify_abweichend = 'Y')
|
||||
OR (warning = 1 AND v_notify_hinweis = 'Y')
|
||||
OR (deviation = 0 AND warning = 0
|
||||
AND v_notify_ok = 'Y') THEN 1 ELSE 0 END),
|
||||
COUNT(*)
|
||||
INTO v_doc_dev_cnt, v_doc_warn_cnt, v_doc_unkl_cnt, v_doc_total, v_doc_total_all
|
||||
FROM dc_results
|
||||
WHERE project_id = p_project_id
|
||||
AND doc_id = doc.id;
|
||||
|
||||
v_doc_ok_cnt := v_doc_total - v_doc_dev_cnt - v_doc_warn_cnt - v_doc_unkl_cnt;
|
||||
|
||||
IF v_notify_ok = 'Y' THEN
|
||||
ln(C_OK || ' OK: ' || v_doc_ok_cnt);
|
||||
IF v_doc_unkl_cnt > 0 THEN
|
||||
ln(C_UNKL || ' Unklar: ' || v_doc_unkl_cnt);
|
||||
END IF;
|
||||
END IF;
|
||||
IF v_notify_hinweis = 'Y' THEN
|
||||
ln(C_WARN || ' Hinweise: ' || v_doc_warn_cnt);
|
||||
END IF;
|
||||
IF v_notify_abweichend = 'Y' THEN
|
||||
ln(C_NOK || ' Abweichungen: ' || v_doc_dev_cnt);
|
||||
END IF;
|
||||
ln('**Gesamt: ' || v_doc_total_all || '**');
|
||||
ln();
|
||||
ln('---');
|
||||
ln();
|
||||
|
||||
FOR cat IN c_cats(doc.id) LOOP
|
||||
ln();
|
||||
ln('### ' || cat.name);
|
||||
@@ -172,7 +268,7 @@ CREATE OR REPLACE PACKAGE BODY DC_UTILS_PKG AS
|
||||
ELSE v_icon := C_OK;
|
||||
END IF;
|
||||
|
||||
|
||||
|
||||
ln();
|
||||
ln();
|
||||
ln(v_icon || ' **' || q.question_text || '**');
|
||||
@@ -208,12 +304,137 @@ CREATE OR REPLACE PACKAGE BODY DC_UTILS_PKG AS
|
||||
|
||||
DBMS_LOB.FREETEMPORARY(v_md);
|
||||
|
||||
-- Mail-Versand (Fehler blockieren nie den Report-Build)
|
||||
BEGIN
|
||||
send_report_email(p_project_id);
|
||||
EXCEPTION WHEN OTHERS THEN NULL;
|
||||
END;
|
||||
|
||||
EXCEPTION
|
||||
WHEN OTHERS THEN
|
||||
DBMS_LOB.FREETEMPORARY(v_md);
|
||||
RAISE;
|
||||
END generate_report;
|
||||
|
||||
|
||||
-- -------------------------------------------------------------------------
|
||||
PROCEDURE send_report_email(p_project_id IN NUMBER,
|
||||
p_skip_pdf IN BOOLEAN DEFAULT FALSE,
|
||||
p_pdf IN BLOB DEFAULT NULL) IS
|
||||
|
||||
v_email dc_projects.notification_email%TYPE;
|
||||
v_name dc_projects.name%TYPE;
|
||||
v_markdown dc_projects.report_markdown%TYPE;
|
||||
v_done dc_projects.completed_at%TYPE;
|
||||
v_mail_id NUMBER;
|
||||
v_pdf BLOB;
|
||||
v_body VARCHAR2(4000);
|
||||
|
||||
v_ok_cnt NUMBER;
|
||||
v_warn_cnt NUMBER;
|
||||
v_dev_cnt NUMBER;
|
||||
v_total_all NUMBER;
|
||||
|
||||
C_OK CONSTANT VARCHAR2(10 CHAR) := UNISTR('\2705'); -- ✅
|
||||
C_NOK CONSTANT VARCHAR2(10 CHAR) := UNISTR('\274C'); -- ❌
|
||||
C_WARN CONSTANT VARCHAR2(10 CHAR) := UNISTR('\26A0\FE0F'); -- ⚠️
|
||||
|
||||
CRLF CONSTANT VARCHAR2(2) := CHR(13) || CHR(10);
|
||||
|
||||
BEGIN
|
||||
SELECT notification_email, name, report_markdown, completed_at
|
||||
INTO v_email, v_name, v_markdown, v_done
|
||||
FROM dc_projects
|
||||
WHERE id = p_project_id;
|
||||
|
||||
IF v_email IS NULL OR TRIM(v_email) IS NULL THEN RETURN; END IF;
|
||||
IF v_markdown IS NULL OR DBMS_LOB.GETLENGTH(v_markdown) = 0 THEN RETURN; END IF;
|
||||
|
||||
-- Zusammenfassung aus dc_results (alle Ergebnisse, ungefiltert)
|
||||
SELECT COUNT(*),
|
||||
SUM(CASE WHEN deviation = 1 THEN 1 ELSE 0 END),
|
||||
SUM(CASE WHEN warning = 1 AND deviation = 0 THEN 1 ELSE 0 END)
|
||||
INTO v_total_all, v_dev_cnt, v_warn_cnt
|
||||
FROM dc_results
|
||||
WHERE project_id = p_project_id;
|
||||
|
||||
v_ok_cnt := v_total_all - v_dev_cnt - v_warn_cnt;
|
||||
|
||||
-- =====================================================================
|
||||
-- E-Mail-Beschreibungstext
|
||||
-- =====================================================================
|
||||
v_body :=
|
||||
'Sehr geehrte Damen und Herren,' || CRLF ||
|
||||
CRLF ||
|
||||
'die automatische Dokumentenprüfung für das Projekt' || CRLF ||
|
||||
'„' || v_name || '"' || CRLF ||
|
||||
'wurde am ' || TO_CHAR(NVL(v_done, SYSDATE), 'DD.MM.YYYY') ||
|
||||
' erfolgreich abgeschlossen.' || CRLF ||
|
||||
CRLF ||
|
||||
'Zusammenfassung der Prüfergebnisse:' || CRLF ||
|
||||
C_OK || ' OK: ' || v_ok_cnt || CRLF ||
|
||||
C_WARN || ' Hinweise: ' || v_warn_cnt || CRLF ||
|
||||
C_NOK || ' Abweichungen: ' || v_dev_cnt || CRLF ||
|
||||
' Gesamt: ' || v_total_all || ' Prüfpunkte' || CRLF ||
|
||||
CRLF ||
|
||||
'Den vollständigen Prüfbericht finden Sie als PDF im Anhang.' || CRLF ||
|
||||
CRLF ||
|
||||
'Direkt zur Prüfung in der Anwendung:' || CRLF ||
|
||||
c_app_link_base || p_project_id || CRLF ||
|
||||
CRLF ||
|
||||
'Mit freundlichen Grüßen' || CRLF ||
|
||||
'Ihr Dokumenten-Check-System' || CRLF ||
|
||||
'Frigosped Express GmbH';
|
||||
|
||||
-- =====================================================================
|
||||
-- PDF bestimmen: Priorität p_pdf (von Java mitgeliefert) >
|
||||
-- convert_markdown HTTP-Callback > kein PDF (skip_pdf oder Fehler)
|
||||
-- =====================================================================
|
||||
IF p_pdf IS NOT NULL AND DBMS_LOB.GETLENGTH(p_pdf) > 0 THEN
|
||||
-- Java hat das PDF schon fertig mitgeschickt – direkt verwenden
|
||||
v_pdf := p_pdf;
|
||||
ELSIF NOT p_skip_pdf THEN
|
||||
BEGIN
|
||||
v_pdf := DC_BACKEND_PKG.convert_markdown(v_markdown, 'PDF');
|
||||
EXCEPTION WHEN OTHERS THEN
|
||||
DBMS_OUTPUT.PUT_LINE('PDF-Generierung fehlgeschlagen: ' || SQLERRM);
|
||||
v_pdf := NULL;
|
||||
END;
|
||||
END IF;
|
||||
|
||||
IF v_pdf IS NOT NULL THEN
|
||||
-- Mail mit PDF-Anhang
|
||||
v_mail_id := APEX_MAIL.SEND(
|
||||
p_to => v_email,
|
||||
p_from => c_mail_from,
|
||||
p_subj => 'Prüfbericht: ' || v_name,
|
||||
p_body => v_body
|
||||
);
|
||||
|
||||
APEX_MAIL.ADD_ATTACHMENT(
|
||||
p_mail_id => v_mail_id,
|
||||
p_attachment => v_pdf,
|
||||
p_filename => 'Pruefbericht_' ||
|
||||
REGEXP_REPLACE(v_name, '[^A-Za-z0-9_äöüÄÖÜß-]', '_') ||
|
||||
'.pdf',
|
||||
p_mime_type => 'application/pdf'
|
||||
);
|
||||
ELSE
|
||||
-- Fallback: Backend nicht erreichbar → Markdown direkt im Body
|
||||
APEX_MAIL.SEND(
|
||||
p_to => v_email,
|
||||
p_from => c_mail_from,
|
||||
p_subj => 'Prüfbericht: ' || v_name,
|
||||
p_body => v_body || CRLF || CRLF ||
|
||||
'--- Detaillierter Bericht ---' || CRLF || CRLF ||
|
||||
v_markdown
|
||||
);
|
||||
END IF;
|
||||
|
||||
EXCEPTION
|
||||
WHEN NO_DATA_FOUND THEN NULL;
|
||||
END send_report_email;
|
||||
|
||||
END DC_UTILS_PKG;
|
||||
/
|
||||
|
||||
|
||||
@@ -169,6 +169,15 @@ create table dc_projects (
|
||||
progress number default on null 0,
|
||||
completed_at date,
|
||||
notification_email varchar2(100 char),
|
||||
notify_ok char(1 char) default 'Y' not null
|
||||
constraint dc_projects_notify_ok_ck
|
||||
check (notify_ok in ('Y','N')),
|
||||
notify_hinweis char(1 char) default 'Y' not null
|
||||
constraint dc_projects_notify_hinweis_ck
|
||||
check (notify_hinweis in ('Y','N')),
|
||||
notify_abweichend char(1 char) default 'Y' not null
|
||||
constraint dc_projects_notify_abweichend_ck
|
||||
check (notify_abweichend in ('Y','N')),
|
||||
row_version integer not null,
|
||||
created date not null,
|
||||
created_by varchar2(255 char) not null,
|
||||
@@ -192,6 +201,10 @@ create table dc_project_documents (
|
||||
references dc_document_types,
|
||||
catalog_id number constraint dc_project_documents_catalog_id_fk
|
||||
references dc_question_catalogs,
|
||||
status varchar2(20 char) default on null 'PENDING'
|
||||
constraint dc_project_documents_status_ck
|
||||
check (status in ('PENDING','IN_PROGRESS','COMPLETED','ERROR')),
|
||||
progress number default on null 0,
|
||||
original_file blob,
|
||||
original_text clob,
|
||||
translated_text clob,
|
||||
|
||||
@@ -170,7 +170,10 @@ BEGIN
|
||||
p.current_phase,
|
||||
p.current_doc_id,
|
||||
p.current_question_id,
|
||||
p.estimated_completion_at
|
||||
p.estimated_completion_at,
|
||||
p.notify_ok,
|
||||
p.notify_hinweis,
|
||||
p.notify_abweichend
|
||||
FROM dc_projects p
|
||||
WHERE p.id = :project_id
|
||||
]'
|
||||
@@ -335,12 +338,67 @@ BEGIN
|
||||
p_pattern => 'projects/:project_id/complete',
|
||||
p_method => 'POST',
|
||||
p_source_type => ORDS.SOURCE_TYPE_PLSQL,
|
||||
p_comments => 'Setzt status=COMPLETED und erstellt Markdown-Bericht',
|
||||
p_comments => 'Setzt status=COMPLETED, speichert Java-Markdown, sendet pro Dokument ein PDF per E-Mail',
|
||||
p_source => q'[
|
||||
DECLARE
|
||||
v_status dc_projects.status%TYPE;
|
||||
v_now DATE := SYSDATE;
|
||||
v_status dc_projects.status%TYPE;
|
||||
v_now DATE := SYSDATE;
|
||||
v_report_md CLOB;
|
||||
v_doc_pdfs JSON_ARRAY_T;
|
||||
|
||||
-- Konvertiert den BLOB-Request-Body (UTF-8 JSON) in einen CLOB
|
||||
FUNCTION body_to_clob(p_blob IN BLOB) RETURN CLOB IS
|
||||
v_clob CLOB;
|
||||
v_dest INTEGER := 1;
|
||||
v_src INTEGER := 1;
|
||||
v_lang INTEGER := DBMS_LOB.DEFAULT_LANG_CTX;
|
||||
v_warning INTEGER;
|
||||
BEGIN
|
||||
IF p_blob IS NULL OR DBMS_LOB.GETLENGTH(p_blob) = 0 THEN RETURN NULL; END IF;
|
||||
DBMS_LOB.CREATETEMPORARY(v_clob, TRUE);
|
||||
DBMS_LOB.CONVERTTOCLOB(v_clob, p_blob, DBMS_LOB.LOBMAXSIZE,
|
||||
v_dest, v_src, NLS_CHARSET_ID('AL32UTF8'), v_lang, v_warning);
|
||||
RETURN v_clob;
|
||||
END body_to_clob;
|
||||
|
||||
-- Dekodiert einen Base64-CLOB in einen BLOB (chunk-weise, 24000 Zeichen pro Schritt)
|
||||
FUNCTION base64_to_blob(p_b64 IN CLOB) RETURN BLOB IS
|
||||
v_blob BLOB;
|
||||
v_raw RAW(32767);
|
||||
v_len INTEGER;
|
||||
v_chunk INTEGER := 24000;
|
||||
v_offset INTEGER := 1;
|
||||
v_sub VARCHAR2(32767);
|
||||
BEGIN
|
||||
DBMS_LOB.CREATETEMPORARY(v_blob, TRUE);
|
||||
v_len := DBMS_LOB.GETLENGTH(p_b64);
|
||||
WHILE v_offset <= v_len LOOP
|
||||
v_sub := DBMS_LOB.SUBSTR(p_b64, v_chunk, v_offset);
|
||||
v_raw := UTL_ENCODE.BASE64_DECODE(UTL_RAW.CAST_TO_RAW(v_sub));
|
||||
DBMS_LOB.WRITEAPPEND(v_blob, UTL_RAW.LENGTH(v_raw), v_raw);
|
||||
v_offset := v_offset + v_chunk;
|
||||
END LOOP;
|
||||
RETURN v_blob;
|
||||
END base64_to_blob;
|
||||
|
||||
BEGIN
|
||||
-- JSON-Body parsen: report_markdown + document_pdfs (Array)
|
||||
DECLARE
|
||||
v_body_clob CLOB;
|
||||
v_json JSON_OBJECT_T;
|
||||
BEGIN
|
||||
v_body_clob := body_to_clob(:body);
|
||||
IF v_body_clob IS NOT NULL THEN
|
||||
v_json := JSON_OBJECT_T.parse(v_body_clob);
|
||||
v_report_md := v_json.get_clob('report_markdown');
|
||||
BEGIN
|
||||
v_doc_pdfs := v_json.get_array('document_pdfs');
|
||||
EXCEPTION WHEN OTHERS THEN v_doc_pdfs := NULL; END;
|
||||
DBMS_LOB.FREETEMPORARY(v_body_clob);
|
||||
END IF;
|
||||
EXCEPTION WHEN OTHERS THEN NULL;
|
||||
END;
|
||||
|
||||
SELECT status
|
||||
INTO v_status
|
||||
FROM dc_projects
|
||||
@@ -354,20 +412,114 @@ BEGIN
|
||||
END IF;
|
||||
|
||||
UPDATE dc_projects
|
||||
SET status = 'COMPLETED',
|
||||
completed_at = v_now,
|
||||
progress = 100,
|
||||
current_phase = 'COMPLETED',
|
||||
current_doc_id = NULL,
|
||||
current_question_id = NULL,
|
||||
SET status = 'COMPLETED',
|
||||
completed_at = v_now,
|
||||
progress = 100,
|
||||
current_phase = 'COMPLETED',
|
||||
current_doc_id = NULL,
|
||||
current_question_id = NULL,
|
||||
estimated_completion_at = NULL
|
||||
WHERE id = :project_id;
|
||||
|
||||
-- Markdown-Bericht generieren (Fehler ignoriert, Projekt bleibt COMPLETED)
|
||||
BEGIN
|
||||
DC_UTILS_PKG.generate_report(:project_id);
|
||||
EXCEPTION WHEN OTHERS THEN NULL;
|
||||
END;
|
||||
IF v_report_md IS NOT NULL AND DBMS_LOB.GETLENGTH(v_report_md) > 0 THEN
|
||||
-- Kombinierten Markdown-Bericht speichern
|
||||
UPDATE dc_projects SET report_markdown = v_report_md WHERE id = :project_id;
|
||||
|
||||
-- E-Mail mit je einem PDF-Anhang pro Dokument versenden
|
||||
BEGIN
|
||||
DECLARE
|
||||
v_email dc_projects.notification_email%TYPE;
|
||||
v_name dc_projects.name%TYPE;
|
||||
v_done dc_projects.completed_at%TYPE;
|
||||
v_mail_id NUMBER;
|
||||
v_ok_cnt NUMBER := 0;
|
||||
v_warn_cnt NUMBER := 0;
|
||||
v_dev_cnt NUMBER := 0;
|
||||
v_total NUMBER := 0;
|
||||
C_OK CONSTANT VARCHAR2(10 CHAR) := UNISTR('\2705');
|
||||
C_NOK CONSTANT VARCHAR2(10 CHAR) := UNISTR('\274C');
|
||||
C_WARN CONSTANT VARCHAR2(10 CHAR) := UNISTR('\26A0\FE0F');
|
||||
CRLF CONSTANT VARCHAR2(2) := CHR(13)||CHR(10);
|
||||
v_body VARCHAR2(4000);
|
||||
v_doc_obj JSON_OBJECT_T;
|
||||
v_pdf_b64 CLOB;
|
||||
v_pdf_blob BLOB;
|
||||
v_fname VARCHAR2(500);
|
||||
BEGIN
|
||||
SELECT notification_email, name, completed_at
|
||||
INTO v_email, v_name, v_done
|
||||
FROM dc_projects WHERE id = :project_id;
|
||||
|
||||
IF v_email IS NULL OR TRIM(v_email) IS NULL THEN
|
||||
NULL; -- keine Empfängeradresse → abbrechen
|
||||
ELSE
|
||||
SELECT COUNT(*),
|
||||
SUM(CASE WHEN deviation = 1 THEN 1 ELSE 0 END),
|
||||
SUM(CASE WHEN warning = 1 AND deviation = 0 THEN 1 ELSE 0 END)
|
||||
INTO v_total, v_dev_cnt, v_warn_cnt
|
||||
FROM dc_results WHERE project_id = :project_id;
|
||||
v_ok_cnt := v_total - v_dev_cnt - v_warn_cnt;
|
||||
|
||||
v_body :=
|
||||
'Sehr geehrte Damen und Herren,' || CRLF ||
|
||||
CRLF ||
|
||||
'die automatische Dokumentenprüfung für das Projekt' || CRLF ||
|
||||
'„' || v_name || '"' || CRLF ||
|
||||
'wurde am ' || TO_CHAR(NVL(v_done, SYSDATE), 'DD.MM.YYYY') ||
|
||||
' erfolgreich abgeschlossen.' || CRLF ||
|
||||
CRLF ||
|
||||
'Zusammenfassung der Prüfergebnisse:' || CRLF ||
|
||||
C_OK || ' OK: ' || v_ok_cnt || CRLF ||
|
||||
C_WARN || ' Hinweise: ' || v_warn_cnt || CRLF ||
|
||||
C_NOK || ' Abweichungen: ' || v_dev_cnt || CRLF ||
|
||||
' Gesamt: ' || v_total || ' Prüfpunkte' || CRLF ||
|
||||
CRLF ||
|
||||
'Die Prüfberichte der einzelnen Dokumente finden Sie als PDF-Anhänge.' || CRLF ||
|
||||
CRLF ||
|
||||
'Mit freundlichen Grüßen' || CRLF ||
|
||||
'Ihr Dokumenten-Check-System' || CRLF ||
|
||||
'Frigosped Express GmbH';
|
||||
|
||||
v_mail_id := APEX_MAIL.SEND(
|
||||
p_to => v_email,
|
||||
p_from => DC_UTILS_PKG.c_mail_from,
|
||||
p_subj => 'Prüfbericht: ' || v_name,
|
||||
p_body => v_body
|
||||
);
|
||||
|
||||
-- Einen PDF-Anhang pro Dokument hinzufügen
|
||||
IF v_doc_pdfs IS NOT NULL AND v_doc_pdfs.get_size > 0 THEN
|
||||
FOR i IN 0 .. v_doc_pdfs.get_size - 1 LOOP
|
||||
BEGIN
|
||||
v_doc_obj := TREAT(v_doc_pdfs.get(i) AS JSON_OBJECT_T);
|
||||
v_fname := v_doc_obj.get_string('filename');
|
||||
v_pdf_b64 := v_doc_obj.get_clob('pdf_base64');
|
||||
IF v_pdf_b64 IS NOT NULL AND DBMS_LOB.GETLENGTH(v_pdf_b64) > 0 THEN
|
||||
v_pdf_blob := base64_to_blob(v_pdf_b64);
|
||||
APEX_MAIL.ADD_ATTACHMENT(
|
||||
p_mail_id => v_mail_id,
|
||||
p_attachment => v_pdf_blob,
|
||||
p_filename => NVL(v_fname, 'Pruefbericht.pdf'),
|
||||
p_mime_type => 'application/pdf'
|
||||
);
|
||||
DBMS_LOB.FREETEMPORARY(v_pdf_blob);
|
||||
DBMS_LOB.FREETEMPORARY(v_pdf_b64);
|
||||
END IF;
|
||||
EXCEPTION WHEN OTHERS THEN NULL;
|
||||
END;
|
||||
END LOOP;
|
||||
END IF;
|
||||
END IF;
|
||||
EXCEPTION WHEN OTHERS THEN NULL;
|
||||
END;
|
||||
END;
|
||||
ELSE
|
||||
-- Fallback: Bericht in DB generieren (z.B. bei leerem Body)
|
||||
BEGIN
|
||||
DC_UTILS_PKG.generate_report(:project_id);
|
||||
EXCEPTION WHEN OTHERS THEN NULL;
|
||||
END;
|
||||
END IF;
|
||||
|
||||
:status_code := 200;
|
||||
HTP.P('{"project_id":' || :project_id
|
||||
@@ -446,6 +598,11 @@ BEGIN
|
||||
d.document_type_id,
|
||||
d.catalog_id,
|
||||
d.uploaded_at,
|
||||
d.status,
|
||||
d.progress,
|
||||
d.parent_document_id,
|
||||
NVL(d.is_reference, 0) AS is_reference,
|
||||
d.source_url,
|
||||
CASE WHEN d.original_text IS NOT NULL THEN 1 ELSE 0 END AS has_original_text,
|
||||
CASE WHEN d.translated_text IS NOT NULL THEN 1 ELSE 0 END AS has_translated_text,
|
||||
d.row_version,
|
||||
@@ -594,6 +751,67 @@ BEGIN
|
||||
);
|
||||
|
||||
|
||||
-- ===========================================================================
|
||||
-- TEMPLATE 9c: documents/:doc_id/progress (PUT)
|
||||
-- ===========================================================================
|
||||
ORDS.DEFINE_TEMPLATE(
|
||||
p_module_name => 'frigosped.dc',
|
||||
p_pattern => 'documents/:doc_id/progress',
|
||||
p_priority => 0,
|
||||
p_comments => 'Dokument-Fortschritt und -Status schreiben'
|
||||
);
|
||||
|
||||
-- 9c. PUT /api/dc/documents/:doc_id/progress
|
||||
-- Body: {"progress":45,"status":"IN_PROGRESS"}
|
||||
ORDS.DEFINE_HANDLER(
|
||||
p_module_name => 'frigosped.dc',
|
||||
p_pattern => 'documents/:doc_id/progress',
|
||||
p_method => 'PUT',
|
||||
p_source_type => ORDS.SOURCE_TYPE_PLSQL,
|
||||
p_comments => 'Setzt progress (0-100) und/oder status des Dokuments',
|
||||
p_source => q'[
|
||||
DECLARE
|
||||
v_body CLOB;
|
||||
v_progress NUMBER;
|
||||
v_status VARCHAR2(20);
|
||||
v_rows INTEGER;
|
||||
BEGIN
|
||||
v_body := :body_text;
|
||||
v_progress := TO_NUMBER(JSON_VALUE(v_body, '$.progress'));
|
||||
v_status := JSON_VALUE(v_body, '$.status');
|
||||
|
||||
IF v_progress IS NOT NULL AND (v_progress < 0 OR v_progress > 100) THEN
|
||||
:status_code := 400;
|
||||
HTP.P('{"error":"progress muss zwischen 0 und 100 liegen"}');
|
||||
RETURN;
|
||||
END IF;
|
||||
|
||||
UPDATE dc_project_documents
|
||||
SET progress = CASE WHEN JSON_EXISTS(v_body, '$.progress') THEN v_progress ELSE progress END,
|
||||
status = CASE WHEN JSON_EXISTS(v_body, '$.status') THEN v_status ELSE status END
|
||||
WHERE id = :doc_id;
|
||||
|
||||
v_rows := SQL%ROWCOUNT;
|
||||
IF v_rows = 0 THEN
|
||||
:status_code := 404;
|
||||
HTP.P('{"error":"Dokument nicht gefunden","doc_id":' || :doc_id || '}');
|
||||
RETURN;
|
||||
END IF;
|
||||
|
||||
:status_code := 200;
|
||||
HTP.P('{"doc_id":' || :doc_id || ',"progress":' || NVL(v_progress, -1)
|
||||
|| ',"status":"' || NVL(v_status, '') || '"}');
|
||||
|
||||
EXCEPTION
|
||||
WHEN OTHERS THEN
|
||||
ROLLBACK;
|
||||
:status_code := 500;
|
||||
HTP.P('{"error":"' || REPLACE(SQLERRM, '"', '''') || '"}');
|
||||
END;
|
||||
]'
|
||||
);
|
||||
|
||||
|
||||
-- ===========================================================================
|
||||
-- TEMPLATE 10+11: projects/:project_id/results
|
||||
-- (POST = Einfuegen, DELETE = Alle loeschen)
|
||||
@@ -1143,7 +1361,7 @@ BEGIN
|
||||
|| ',"cost_eur":' || ROUND(NVL(v_month_eur, 0), 6)
|
||||
|| ',"total_tokens":'|| NVL(v_month_tokens, 0)
|
||||
|| ',"call_count":' || NVL(v_month_calls, 0)
|
||||
|| '},"monthly":' || NVL(v_monthly_json, '[]')
|
||||
|| '},"monthly":' || NVL(v_monthly_json, '[' || CHR(93))
|
||||
|| '}'
|
||||
);
|
||||
|
||||
@@ -1156,6 +1374,196 @@ BEGIN
|
||||
);
|
||||
|
||||
|
||||
-- ===========================================================================
|
||||
-- TEMPLATE 18: documents/:parent_id/references (POST)
|
||||
-- Legt eine heruntergeladene externe Referenz als neue dc_project_documents-
|
||||
-- Zeile an (is_reference=1, verknuepft mit dem Originaldokument).
|
||||
-- ===========================================================================
|
||||
ORDS.DEFINE_TEMPLATE(
|
||||
p_module_name => 'frigosped.dc',
|
||||
p_pattern => 'documents/:parent_id/references',
|
||||
p_priority => 0,
|
||||
p_comments => 'Heruntergeladene externe Referenz zu einem Dokument speichern'
|
||||
);
|
||||
|
||||
-- 18. POST /api/dc/documents/:parent_id/references
|
||||
-- Body: {"source_url":"https://...","filename":"AGB.pdf",
|
||||
-- "mime_type":"application/pdf","file_base64":"JVBERi0x..."}
|
||||
ORDS.DEFINE_HANDLER(
|
||||
p_module_name => 'frigosped.dc',
|
||||
p_pattern => 'documents/:parent_id/references',
|
||||
p_method => 'POST',
|
||||
p_source_type => ORDS.SOURCE_TYPE_PLSQL,
|
||||
p_comments => '201 Created mit neuer doc-id; legt is_reference=1-Zeile mit BLOB an',
|
||||
p_source => q'[
|
||||
DECLARE
|
||||
v_body_clob CLOB;
|
||||
v_json JSON_OBJECT_T;
|
||||
v_source_url VARCHAR2(2000);
|
||||
v_filename VARCHAR2(255);
|
||||
v_mime VARCHAR2(100);
|
||||
v_file_b64 CLOB;
|
||||
v_blob BLOB;
|
||||
v_project_id NUMBER;
|
||||
v_doc_type_id NUMBER;
|
||||
v_new_id NUMBER;
|
||||
|
||||
-- Wandelt den (ggf. grossen) Request-BLOB in einen CLOB
|
||||
FUNCTION body_to_clob(p_body IN BLOB) RETURN CLOB IS
|
||||
v_clob CLOB;
|
||||
v_dest INTEGER := 1;
|
||||
v_src INTEGER := 1;
|
||||
v_lang INTEGER := 0;
|
||||
v_warning INTEGER := 0;
|
||||
BEGIN
|
||||
IF p_body IS NULL OR DBMS_LOB.GETLENGTH(p_body) = 0 THEN
|
||||
RETURN NULL;
|
||||
END IF;
|
||||
DBMS_LOB.CREATETEMPORARY(v_clob, TRUE);
|
||||
DBMS_LOB.CONVERTTOCLOB(v_clob, p_body, DBMS_LOB.LOBMAXSIZE,
|
||||
v_dest, v_src, NLS_CHARSET_ID('AL32UTF8'), v_lang, v_warning);
|
||||
RETURN v_clob;
|
||||
END body_to_clob;
|
||||
|
||||
-- Dekodiert einen Base64-CLOB in einen BLOB (chunk-weise)
|
||||
FUNCTION base64_to_blob(p_b64 IN CLOB) RETURN BLOB IS
|
||||
v_blob BLOB;
|
||||
v_raw RAW(32767);
|
||||
v_chunk INTEGER := 24000;
|
||||
v_offset INTEGER := 1;
|
||||
v_len INTEGER;
|
||||
v_sub VARCHAR2(32767);
|
||||
BEGIN
|
||||
DBMS_LOB.CREATETEMPORARY(v_blob, TRUE);
|
||||
v_len := DBMS_LOB.GETLENGTH(p_b64);
|
||||
WHILE v_offset <= v_len LOOP
|
||||
v_sub := DBMS_LOB.SUBSTR(p_b64, v_chunk, v_offset);
|
||||
v_raw := UTL_ENCODE.BASE64_DECODE(UTL_RAW.CAST_TO_RAW(v_sub));
|
||||
DBMS_LOB.WRITEAPPEND(v_blob, UTL_RAW.LENGTH(v_raw), v_raw);
|
||||
v_offset := v_offset + v_chunk;
|
||||
END LOOP;
|
||||
RETURN v_blob;
|
||||
END base64_to_blob;
|
||||
|
||||
BEGIN
|
||||
v_body_clob := body_to_clob(:body);
|
||||
IF v_body_clob IS NULL THEN
|
||||
:status_code := 400;
|
||||
HTP.P('{"error":"Leerer Request-Body"}');
|
||||
RETURN;
|
||||
END IF;
|
||||
|
||||
v_json := JSON_OBJECT_T.parse(v_body_clob);
|
||||
v_source_url := v_json.get_string('source_url');
|
||||
v_filename := v_json.get_string('filename');
|
||||
v_mime := v_json.get_string('mime_type');
|
||||
BEGIN
|
||||
v_file_b64 := v_json.get_clob('file_base64');
|
||||
EXCEPTION WHEN OTHERS THEN v_file_b64 := NULL; END;
|
||||
DBMS_LOB.FREETEMPORARY(v_body_clob);
|
||||
|
||||
IF v_file_b64 IS NULL OR DBMS_LOB.GETLENGTH(v_file_b64) = 0 THEN
|
||||
:status_code := 400;
|
||||
HTP.P('{"error":"file_base64 ist Pflichtfeld"}');
|
||||
RETURN;
|
||||
END IF;
|
||||
|
||||
-- Projekt + Dokumenttyp vom Originaldokument uebernehmen
|
||||
SELECT project_id, document_type_id
|
||||
INTO v_project_id, v_doc_type_id
|
||||
FROM dc_project_documents
|
||||
WHERE id = :parent_id;
|
||||
|
||||
v_blob := base64_to_blob(v_file_b64);
|
||||
|
||||
INSERT INTO dc_project_documents (
|
||||
project_id,
|
||||
document_type_id,
|
||||
parent_document_id,
|
||||
is_reference,
|
||||
source_url,
|
||||
original_file,
|
||||
mime_type,
|
||||
filename,
|
||||
status,
|
||||
progress,
|
||||
uploaded_at
|
||||
) VALUES (
|
||||
v_project_id,
|
||||
v_doc_type_id,
|
||||
:parent_id,
|
||||
1,
|
||||
v_source_url,
|
||||
v_blob,
|
||||
NVL(v_mime, 'application/octet-stream'),
|
||||
NVL(v_filename, 'referenz'),
|
||||
'IN_PROGRESS',
|
||||
0,
|
||||
SYSDATE
|
||||
)
|
||||
RETURNING id INTO v_new_id;
|
||||
|
||||
:status_code := 201;
|
||||
HTP.P('{"id":' || v_new_id
|
||||
|| ',"parent_document_id":' || :parent_id
|
||||
|| ',"project_id":' || v_project_id || '}');
|
||||
|
||||
EXCEPTION
|
||||
WHEN NO_DATA_FOUND THEN
|
||||
ROLLBACK;
|
||||
:status_code := 404;
|
||||
HTP.P('{"error":"Originaldokument nicht gefunden","parent_id":' || :parent_id || '}');
|
||||
WHEN OTHERS THEN
|
||||
ROLLBACK;
|
||||
:status_code := 500;
|
||||
HTP.P('{"error":"' || REPLACE(SQLERRM, '"', '''') || '"}');
|
||||
END;
|
||||
]'
|
||||
);
|
||||
|
||||
|
||||
-- ===========================================================================
|
||||
-- TEMPLATE 19: projects/:project_id/references (DELETE)
|
||||
-- Loescht alle heruntergeladenen Referenzen eines Projekts (Re-Processing).
|
||||
-- ===========================================================================
|
||||
ORDS.DEFINE_TEMPLATE(
|
||||
p_module_name => 'frigosped.dc',
|
||||
p_pattern => 'projects/:project_id/references',
|
||||
p_priority => 0,
|
||||
p_comments => 'Heruntergeladene Referenzen eines Projekts verwalten'
|
||||
);
|
||||
|
||||
-- 19. DELETE /api/dc/projects/:project_id/references
|
||||
ORDS.DEFINE_HANDLER(
|
||||
p_module_name => 'frigosped.dc',
|
||||
p_pattern => 'projects/:project_id/references',
|
||||
p_method => 'DELETE',
|
||||
p_source_type => ORDS.SOURCE_TYPE_PLSQL,
|
||||
p_comments => 'Loescht alle is_reference=1-Zeilen eines Projekts (fuer Re-Processing)',
|
||||
p_source => q'[
|
||||
DECLARE
|
||||
v_deleted INTEGER;
|
||||
BEGIN
|
||||
DELETE FROM dc_project_documents
|
||||
WHERE project_id = :project_id
|
||||
AND is_reference = 1;
|
||||
|
||||
v_deleted := SQL%ROWCOUNT;
|
||||
|
||||
:status_code := 200;
|
||||
HTP.P('{"project_id":' || :project_id
|
||||
|| ',"deleted_count":' || v_deleted || '}');
|
||||
|
||||
EXCEPTION
|
||||
WHEN OTHERS THEN
|
||||
ROLLBACK;
|
||||
:status_code := 500;
|
||||
HTP.P('{"error":"' || REPLACE(SQLERRM, '"', '''') || '"}');
|
||||
END;
|
||||
]'
|
||||
);
|
||||
|
||||
|
||||
COMMIT;
|
||||
END;
|
||||
/
|
||||
|
||||
217
Scripts/deploy_complete_handler.sql
Normal file
217
Scripts/deploy_complete_handler.sql
Normal file
@@ -0,0 +1,217 @@
|
||||
-- =============================================================================
|
||||
-- Deploys only the POST /api/dc/projects/:project_id/complete handler.
|
||||
-- Uses CLOB construction to avoid SQLcl bind-variable pre-processing of
|
||||
-- :status_code / :body / :project_id inside the handler source string.
|
||||
-- =============================================================================
|
||||
DECLARE
|
||||
v_src CLOB;
|
||||
|
||||
PROCEDURE a(p IN VARCHAR2) IS
|
||||
BEGIN DBMS_LOB.APPEND(v_src, TO_CLOB(p)); END;
|
||||
|
||||
BEGIN
|
||||
DBMS_LOB.CREATETEMPORARY(v_src, TRUE);
|
||||
|
||||
a(' DECLARE');
|
||||
a(CHR(10)||' v_status dc_projects.status%TYPE;');
|
||||
a(CHR(10)||' v_now DATE := SYSDATE;');
|
||||
a(CHR(10)||' v_report_md CLOB;');
|
||||
a(CHR(10)||' v_doc_pdfs JSON_ARRAY_T;');
|
||||
a(CHR(10));
|
||||
a(CHR(10)||' FUNCTION body_to_clob(p_blob IN BLOB) RETURN CLOB IS');
|
||||
a(CHR(10)||' v_clob CLOB;');
|
||||
a(CHR(10)||' v_dest INTEGER := 1;');
|
||||
a(CHR(10)||' v_src INTEGER := 1;');
|
||||
a(CHR(10)||' v_lang INTEGER := DBMS_LOB.DEFAULT_LANG_CTX;');
|
||||
a(CHR(10)||' v_warning INTEGER;');
|
||||
a(CHR(10)||' BEGIN');
|
||||
a(CHR(10)||' IF p_blob IS NULL OR DBMS_LOB.GETLENGTH(p_blob) = 0 THEN RETURN NULL; END IF;');
|
||||
a(CHR(10)||' DBMS_LOB.CREATETEMPORARY(v_clob, TRUE);');
|
||||
a(CHR(10)||' DBMS_LOB.CONVERTTOCLOB(v_clob, p_blob, DBMS_LOB.LOBMAXSIZE,');
|
||||
a(CHR(10)||' v_dest, v_src, NLS_CHARSET_ID(''AL32UTF8''), v_lang, v_warning);');
|
||||
a(CHR(10)||' RETURN v_clob;');
|
||||
a(CHR(10)||' END body_to_clob;');
|
||||
a(CHR(10));
|
||||
a(CHR(10)||' FUNCTION base64_to_blob(p_b64 IN CLOB) RETURN BLOB IS');
|
||||
a(CHR(10)||' v_blob BLOB;');
|
||||
a(CHR(10)||' v_raw RAW(32767);');
|
||||
a(CHR(10)||' v_len INTEGER;');
|
||||
a(CHR(10)||' v_chunk INTEGER := 24000;');
|
||||
a(CHR(10)||' v_offset INTEGER := 1;');
|
||||
a(CHR(10)||' v_sub VARCHAR2(32767);');
|
||||
a(CHR(10)||' BEGIN');
|
||||
a(CHR(10)||' DBMS_LOB.CREATETEMPORARY(v_blob, TRUE);');
|
||||
a(CHR(10)||' v_len := DBMS_LOB.GETLENGTH(p_b64);');
|
||||
a(CHR(10)||' WHILE v_offset <= v_len LOOP');
|
||||
a(CHR(10)||' v_sub := DBMS_LOB.SUBSTR(p_b64, v_chunk, v_offset);');
|
||||
a(CHR(10)||' v_raw := UTL_ENCODE.BASE64_DECODE(UTL_RAW.CAST_TO_RAW(v_sub));');
|
||||
a(CHR(10)||' DBMS_LOB.WRITEAPPEND(v_blob, UTL_RAW.LENGTH(v_raw), v_raw);');
|
||||
a(CHR(10)||' v_offset := v_offset + v_chunk;');
|
||||
a(CHR(10)||' END LOOP;');
|
||||
a(CHR(10)||' RETURN v_blob;');
|
||||
a(CHR(10)||' END base64_to_blob;');
|
||||
a(CHR(10));
|
||||
a(CHR(10)||' BEGIN');
|
||||
a(CHR(10)||' DECLARE');
|
||||
a(CHR(10)||' v_body_clob CLOB;');
|
||||
a(CHR(10)||' v_json JSON_OBJECT_T;');
|
||||
a(CHR(10)||' BEGIN');
|
||||
-- Use concatenation to embed the bind variable names so SQLcl doesn't pre-process them
|
||||
a(CHR(10)||' v_body_clob := body_to_clob(' || CHR(58) || 'body);');
|
||||
a(CHR(10)||' IF v_body_clob IS NOT NULL THEN');
|
||||
a(CHR(10)||' v_json := JSON_OBJECT_T.parse(v_body_clob);');
|
||||
a(CHR(10)||' v_report_md := v_json.get_clob(''report_markdown'');');
|
||||
a(CHR(10)||' BEGIN');
|
||||
a(CHR(10)||' v_doc_pdfs := v_json.get_array(''document_pdfs'');');
|
||||
a(CHR(10)||' EXCEPTION WHEN OTHERS THEN v_doc_pdfs := NULL; END;');
|
||||
a(CHR(10)||' DBMS_LOB.FREETEMPORARY(v_body_clob);');
|
||||
a(CHR(10)||' END IF;');
|
||||
a(CHR(10)||' EXCEPTION WHEN OTHERS THEN NULL;');
|
||||
a(CHR(10)||' END;');
|
||||
a(CHR(10));
|
||||
a(CHR(10)||' SELECT status');
|
||||
a(CHR(10)||' INTO v_status');
|
||||
a(CHR(10)||' FROM dc_projects');
|
||||
a(CHR(10)||' WHERE id = ' || CHR(58) || 'project_id');
|
||||
a(CHR(10)||' FOR UPDATE NOWAIT;');
|
||||
a(CHR(10));
|
||||
a(CHR(10)||' IF v_status = ''COMPLETED'' THEN');
|
||||
a(CHR(10)||' ' || CHR(58) || 'status_code := 409;');
|
||||
a(CHR(10)||' HTP.P(''{"error":"Projekt ist bereits COMPLETED","project_id":'' || ' || CHR(58) || 'project_id || ''}'');');
|
||||
a(CHR(10)||' RETURN;');
|
||||
a(CHR(10)||' END IF;');
|
||||
a(CHR(10));
|
||||
a(CHR(10)||' UPDATE dc_projects');
|
||||
a(CHR(10)||' SET status = ''COMPLETED'',');
|
||||
a(CHR(10)||' completed_at = v_now,');
|
||||
a(CHR(10)||' progress = 100,');
|
||||
a(CHR(10)||' current_phase = ''COMPLETED'',');
|
||||
a(CHR(10)||' current_doc_id = NULL,');
|
||||
a(CHR(10)||' current_question_id = NULL,');
|
||||
a(CHR(10)||' estimated_completion_at = NULL');
|
||||
a(CHR(10)||' WHERE id = ' || CHR(58) || 'project_id;');
|
||||
a(CHR(10));
|
||||
a(CHR(10)||' IF v_report_md IS NOT NULL AND DBMS_LOB.GETLENGTH(v_report_md) > 0 THEN');
|
||||
a(CHR(10)||' UPDATE dc_projects SET report_markdown = v_report_md WHERE id = ' || CHR(58) || 'project_id;');
|
||||
a(CHR(10));
|
||||
a(CHR(10)||' BEGIN');
|
||||
a(CHR(10)||' DECLARE');
|
||||
a(CHR(10)||' v_email dc_projects.notification_email%TYPE;');
|
||||
a(CHR(10)||' v_name dc_projects.name%TYPE;');
|
||||
a(CHR(10)||' v_done dc_projects.completed_at%TYPE;');
|
||||
a(CHR(10)||' v_mail_id NUMBER;');
|
||||
a(CHR(10)||' v_ok_cnt NUMBER := 0;');
|
||||
a(CHR(10)||' v_warn_cnt NUMBER := 0;');
|
||||
a(CHR(10)||' v_dev_cnt NUMBER := 0;');
|
||||
a(CHR(10)||' v_total NUMBER := 0;');
|
||||
a(CHR(10)||' C_OK CONSTANT VARCHAR2(10 CHAR) := UNISTR(''\2705'');');
|
||||
a(CHR(10)||' C_NOK CONSTANT VARCHAR2(10 CHAR) := UNISTR(''\274C'');');
|
||||
a(CHR(10)||' C_WARN CONSTANT VARCHAR2(10 CHAR) := UNISTR(''\26A0\FE0F'');');
|
||||
a(CHR(10)||' CRLF CONSTANT VARCHAR2(2) := CHR(13)||CHR(10);');
|
||||
a(CHR(10)||' v_body VARCHAR2(4000);');
|
||||
a(CHR(10)||' v_doc_obj JSON_OBJECT_T;');
|
||||
a(CHR(10)||' v_pdf_b64 CLOB;');
|
||||
a(CHR(10)||' v_pdf_blob BLOB;');
|
||||
a(CHR(10)||' v_fname VARCHAR2(500);');
|
||||
a(CHR(10)||' BEGIN');
|
||||
a(CHR(10)||' SELECT notification_email, name, completed_at');
|
||||
a(CHR(10)||' INTO v_email, v_name, v_done');
|
||||
a(CHR(10)||' FROM dc_projects WHERE id = ' || CHR(58) || 'project_id;');
|
||||
a(CHR(10));
|
||||
a(CHR(10)||' IF v_email IS NOT NULL AND TRIM(v_email) IS NOT NULL THEN');
|
||||
a(CHR(10)||' SELECT COUNT(*),');
|
||||
a(CHR(10)||' SUM(CASE WHEN deviation = 1 THEN 1 ELSE 0 END),');
|
||||
a(CHR(10)||' SUM(CASE WHEN warning = 1 AND deviation = 0 THEN 1 ELSE 0 END)');
|
||||
a(CHR(10)||' INTO v_total, v_dev_cnt, v_warn_cnt');
|
||||
a(CHR(10)||' FROM dc_results WHERE project_id = ' || CHR(58) || 'project_id;');
|
||||
a(CHR(10)||' v_ok_cnt := v_total - v_dev_cnt - v_warn_cnt;');
|
||||
a(CHR(10));
|
||||
a(CHR(10)||' v_body :=');
|
||||
a(CHR(10)||' ''Sehr geehrte Damen und Herren,'' || CRLF ||');
|
||||
a(CHR(10)||' CRLF ||');
|
||||
a(CHR(10)||' ''die automatische Dokumentenprüfung für das Projekt'' || CRLF ||');
|
||||
a(CHR(10)||' ''„'' || v_name || ''"'' || CRLF ||');
|
||||
a(CHR(10)||' ''wurde am '' || TO_CHAR(NVL(v_done, SYSDATE), ''DD.MM.YYYY'') ||');
|
||||
a(CHR(10)||' '' erfolgreich abgeschlossen.'' || CRLF ||');
|
||||
a(CHR(10)||' CRLF ||');
|
||||
a(CHR(10)||' ''Zusammenfassung der Prüfergebnisse:'' || CRLF ||');
|
||||
a(CHR(10)||' C_OK || '' OK: '' || v_ok_cnt || CRLF ||');
|
||||
a(CHR(10)||' C_WARN || '' Hinweise: '' || v_warn_cnt || CRLF ||');
|
||||
a(CHR(10)||' C_NOK || '' Abweichungen: '' || v_dev_cnt || CRLF ||');
|
||||
a(CHR(10)||' '' Gesamt: '' || v_total || '' Prüfpunkte'' || CRLF ||');
|
||||
a(CHR(10)||' CRLF ||');
|
||||
a(CHR(10)||' ''Die Prüfberichte der einzelnen Dokumente finden Sie als PDF-Anhänge.'' || CRLF ||');
|
||||
a(CHR(10)||' CRLF ||');
|
||||
a(CHR(10)||' ''Mit freundlichen Grüßen'' || CRLF ||');
|
||||
a(CHR(10)||' ''Ihr Dokumenten-Check-System'' || CRLF ||');
|
||||
a(CHR(10)||' ''Frigosped Express GmbH'';');
|
||||
a(CHR(10));
|
||||
a(CHR(10)||' v_mail_id := APEX_MAIL.SEND(');
|
||||
a(CHR(10)||' p_to => v_email,');
|
||||
a(CHR(10)||' p_from => DC_UTILS_PKG.c_mail_from,');
|
||||
a(CHR(10)||' p_subj => ''Prüfbericht: '' || v_name,');
|
||||
a(CHR(10)||' p_body => v_body');
|
||||
a(CHR(10)||' );');
|
||||
a(CHR(10));
|
||||
a(CHR(10)||' IF v_doc_pdfs IS NOT NULL AND v_doc_pdfs.get_size > 0 THEN');
|
||||
a(CHR(10)||' FOR i IN 0 .. v_doc_pdfs.get_size - 1 LOOP');
|
||||
a(CHR(10)||' BEGIN');
|
||||
a(CHR(10)||' v_doc_obj := TREAT(v_doc_pdfs.get(i) AS JSON_OBJECT_T);');
|
||||
a(CHR(10)||' v_fname := v_doc_obj.get_string(''filename'');');
|
||||
a(CHR(10)||' v_pdf_b64 := v_doc_obj.get_clob(''pdf_base64'');');
|
||||
a(CHR(10)||' IF v_pdf_b64 IS NOT NULL AND DBMS_LOB.GETLENGTH(v_pdf_b64) > 0 THEN');
|
||||
a(CHR(10)||' v_pdf_blob := base64_to_blob(v_pdf_b64);');
|
||||
a(CHR(10)||' APEX_MAIL.ADD_ATTACHMENT(');
|
||||
a(CHR(10)||' p_mail_id => v_mail_id,');
|
||||
a(CHR(10)||' p_attachment => v_pdf_blob,');
|
||||
a(CHR(10)||' p_filename => NVL(v_fname, ''Pruefbericht.pdf''),');
|
||||
a(CHR(10)||' p_mime_type => ''application/pdf''');
|
||||
a(CHR(10)||' );');
|
||||
a(CHR(10)||' DBMS_LOB.FREETEMPORARY(v_pdf_blob);');
|
||||
a(CHR(10)||' DBMS_LOB.FREETEMPORARY(v_pdf_b64);');
|
||||
a(CHR(10)||' END IF;');
|
||||
a(CHR(10)||' EXCEPTION WHEN OTHERS THEN NULL;');
|
||||
a(CHR(10)||' END;');
|
||||
a(CHR(10)||' END LOOP;');
|
||||
a(CHR(10)||' END IF;');
|
||||
a(CHR(10)||' END IF;');
|
||||
a(CHR(10)||' EXCEPTION WHEN OTHERS THEN NULL;');
|
||||
a(CHR(10)||' END;');
|
||||
a(CHR(10)||' END;');
|
||||
a(CHR(10)||' ELSE');
|
||||
a(CHR(10)||' BEGIN');
|
||||
a(CHR(10)||' DC_UTILS_PKG.generate_report(' || CHR(58) || 'project_id);');
|
||||
a(CHR(10)||' EXCEPTION WHEN OTHERS THEN NULL;');
|
||||
a(CHR(10)||' END;');
|
||||
a(CHR(10)||' END IF;');
|
||||
a(CHR(10));
|
||||
a(CHR(10)||' ' || CHR(58) || 'status_code := 200;');
|
||||
a(CHR(10)||' HTP.P(''{"project_id":'' || ' || CHR(58) || 'project_id');
|
||||
a(CHR(10)||' || '',\"status\":\"COMPLETED\"''');
|
||||
a(CHR(10)||' || '',\"completed_at\":\"'' || TO_CHAR(v_now, ''YYYY-MM-DD"T"HH24:MI:SS'') || ''\"''');
|
||||
a(CHR(10)||' || ''}'');');
|
||||
a(CHR(10));
|
||||
a(CHR(10)||' EXCEPTION');
|
||||
a(CHR(10)||' WHEN NO_DATA_FOUND THEN');
|
||||
a(CHR(10)||' ' || CHR(58) || 'status_code := 404;');
|
||||
a(CHR(10)||' HTP.P(''{"error":"Projekt nicht gefunden","project_id":'' || ' || CHR(58) || 'project_id || ''}'');');
|
||||
a(CHR(10)||' WHEN OTHERS THEN');
|
||||
a(CHR(10)||' ROLLBACK;');
|
||||
a(CHR(10)||' ' || CHR(58) || 'status_code := 500;');
|
||||
a(CHR(10)||' HTP.P(''{"error":"'' || REPLACE(SQLERRM, ''"'', '''''''') || ''"}'');');
|
||||
a(CHR(10)||' END;');
|
||||
|
||||
ORDS.DEFINE_HANDLER(
|
||||
p_module_name => 'frigosped.dc',
|
||||
p_pattern => 'projects/:project_id/complete',
|
||||
p_method => 'POST',
|
||||
p_source_type => ORDS.SOURCE_TYPE_PLSQL,
|
||||
p_comments => 'Setzt status=COMPLETED, sendet pro Dokument ein PDF per E-Mail',
|
||||
p_source => v_src
|
||||
);
|
||||
|
||||
DBMS_LOB.FREETEMPORARY(v_src);
|
||||
COMMIT;
|
||||
DBMS_OUTPUT.PUT_LINE('Handler erfolgreich deployed.');
|
||||
END;
|
||||
/
|
||||
Reference in New Issue
Block a user