feat: Add Markdown export service for DOCX and PDF formats
- Implemented MarkdownExportService to convert Markdown text into DOCX and PDF formats. - Supported Markdown elements include headings, bold, italic, blockquotes, tables, and specific emojis. - Added methods for generating DOCX and PDF documents with proper formatting and rendering. - Enhanced application properties to enable logging for production environment. - Introduced a test-run script to reset project state and trigger processing via API. Co-authored-by: Copilot <copilot@github.com>
This commit is contained in:
193
Scripts/DC Generate Report.sql
Normal file
193
Scripts/DC Generate Report.sql
Normal file
@@ -0,0 +1,193 @@
|
||||
-- =============================================================================
|
||||
-- dc_generate_report – Erstellt einen Markdown-Prüfbericht für ein Projekt
|
||||
--
|
||||
-- Aufruf:
|
||||
-- BEGIN dc_generate_report(p_project_id => 1); COMMIT; END;
|
||||
-- /
|
||||
--
|
||||
-- Ergebnis: dc_projects.report_markdown wird mit dem Markdown-Text befüllt.
|
||||
-- Wird automatisch beim POST /api/dc/projects/:id/complete aufgerufen.
|
||||
-- Kann manuell via POST /api/dc/projects/:id/report neu generiert werden.
|
||||
--
|
||||
-- Aufbau des Berichts:
|
||||
-- 1. Kopf (Projektname, Abschlussdatum)
|
||||
-- 2. Zusammenfassung (OK / Hinweise / Abweichungen)
|
||||
-- 3. Ergebnisse: je Dokument → Kategorie (order_nr) → Frage (order_nr)
|
||||
-- Icons: ✅ OK | ⚠️ Hinweis | ❌ Abweichung | ❓ Unklar
|
||||
-- =============================================================================
|
||||
|
||||
CREATE OR REPLACE PROCEDURE dc_generate_report(p_project_id IN NUMBER) IS
|
||||
|
||||
v_md CLOB;
|
||||
v_name dc_projects.name%TYPE;
|
||||
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;
|
||||
|
||||
-- Emoji-Konstanten via UNISTR (vermeidet Quellcode-Encoding-Probleme)
|
||||
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'); -- ⚠️
|
||||
C_UNKL CONSTANT VARCHAR2(10 CHAR) := UNISTR('\2753'); -- ❓
|
||||
C_DOC CONSTANT VARCHAR2(10 CHAR) := UNISTR('\D83D\DCC4'); -- 📄
|
||||
|
||||
CURSOR c_docs IS
|
||||
SELECT d.id, d.filename
|
||||
FROM dc_project_documents d
|
||||
WHERE d.project_id = p_project_id
|
||||
ORDER BY d.uploaded_at, d.id;
|
||||
|
||||
CURSOR c_cats(p_doc_id NUMBER) IS
|
||||
SELECT DISTINCT qc.id, qc.name, qc.order_nr
|
||||
FROM dc_question_categories qc
|
||||
JOIN dc_questions q ON q.category_id = qc.id
|
||||
JOIN dc_results r ON r.question_id = q.id
|
||||
WHERE r.project_id = p_project_id
|
||||
AND r.doc_id = p_doc_id
|
||||
ORDER BY qc.order_nr, qc.name;
|
||||
|
||||
CURSOR c_qs(p_doc_id NUMBER, p_cat_id NUMBER) IS
|
||||
SELECT q.question_text,
|
||||
r.result_type,
|
||||
r.score,
|
||||
r.deviation,
|
||||
r.warning,
|
||||
r.answer,
|
||||
r.the_comment
|
||||
FROM dc_questions q
|
||||
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 q.category_id = p_cat_id
|
||||
ORDER BY q.order_nr, q.id;
|
||||
|
||||
v_icon VARCHAR2(20 CHAR);
|
||||
|
||||
-- Fügt eine Zeile + Newline an den CLOB an. Akzeptiert CLOB damit kein
|
||||
-- implizites CLOB→VARCHAR2-Cast nötig ist (question_text etc. sind CLOBs).
|
||||
PROCEDURE ln(p_text IN CLOB DEFAULT NULL) IS
|
||||
BEGIN
|
||||
IF p_text IS NOT NULL AND DBMS_LOB.GETLENGTH(p_text) > 0 THEN
|
||||
DBMS_LOB.APPEND(v_md, p_text);
|
||||
END IF;
|
||||
DBMS_LOB.APPEND(v_md, TO_CLOB(CHR(10)));
|
||||
END ln;
|
||||
|
||||
BEGIN
|
||||
DBMS_LOB.CREATETEMPORARY(v_md, TRUE);
|
||||
|
||||
SELECT name, status, completed_at
|
||||
INTO v_name, v_status, v_done
|
||||
FROM dc_projects
|
||||
WHERE 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'
|
||||
AND deviation = 0
|
||||
AND warning = 0 THEN 1 ELSE 0 END),
|
||||
COUNT(*)
|
||||
INTO v_dev_cnt, v_warn_cnt, v_unkl_cnt, v_total
|
||||
FROM dc_results
|
||||
WHERE project_id = p_project_id;
|
||||
|
||||
v_ok_cnt := v_total - v_dev_cnt - v_warn_cnt - v_unkl_cnt;
|
||||
|
||||
-- =========================================================================
|
||||
-- Kopf
|
||||
-- =========================================================================
|
||||
ln('# ' || C_OK || ' Prüfbericht: ' || v_name);
|
||||
ln();
|
||||
IF v_done IS NOT NULL THEN
|
||||
ln('**Abgeschlossen:** ' || TO_CHAR(v_done, 'DD.MM.YYYY HH24:MI'));
|
||||
ELSE
|
||||
ln('**Erstellt:** ' || TO_CHAR(SYSDATE, 'DD.MM.YYYY HH24:MI'));
|
||||
END IF;
|
||||
ln();
|
||||
ln('---');
|
||||
ln();
|
||||
|
||||
-- =========================================================================
|
||||
-- Zusammenfassung (keine Tabelle – Plaintext für maximale Kompatibilität)
|
||||
-- =========================================================================
|
||||
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);
|
||||
END IF;
|
||||
ln('**Gesamt: ' || v_total || '**');
|
||||
ln();
|
||||
ln('---');
|
||||
ln();
|
||||
|
||||
-- =========================================================================
|
||||
-- Ergebnisse je Dokument → Kategorie (order_nr) → Frage (order_nr)
|
||||
-- =========================================================================
|
||||
ln('## Ergebnisse');
|
||||
ln();
|
||||
|
||||
FOR doc IN c_docs LOOP
|
||||
ln('## ' || C_DOC || ' ' || doc.filename);
|
||||
ln();
|
||||
|
||||
FOR cat IN c_cats(doc.id) LOOP
|
||||
ln();
|
||||
ln('### ' || cat.name);
|
||||
ln();
|
||||
|
||||
FOR q IN c_qs(doc.id, cat.id) LOOP
|
||||
|
||||
IF q.deviation = 1 THEN v_icon := C_NOK;
|
||||
ELSIF q.warning = 1 THEN v_icon := C_WARN;
|
||||
ELSIF q.result_type = 'UNKLAR' THEN v_icon := C_UNKL;
|
||||
ELSE v_icon := C_OK;
|
||||
END IF;
|
||||
|
||||
ln();
|
||||
ln(v_icon || ' **' || q.question_text || '**');
|
||||
ln();
|
||||
|
||||
IF q.answer IS NOT NULL AND DBMS_LOB.GETLENGTH(q.answer) > 0 THEN
|
||||
ln(q.answer);
|
||||
ln();
|
||||
END IF;
|
||||
|
||||
IF q.score IS NOT NULL THEN
|
||||
ln('*Score: ' || q.score || '%*');
|
||||
END IF;
|
||||
|
||||
IF q.the_comment IS NOT NULL
|
||||
AND DBMS_LOB.GETLENGTH(q.the_comment) > 0 THEN
|
||||
ln();
|
||||
ln('> ' || q.the_comment);
|
||||
END IF;
|
||||
|
||||
ln();
|
||||
END LOOP;
|
||||
|
||||
END LOOP;
|
||||
|
||||
ln('---');
|
||||
ln();
|
||||
END LOOP;
|
||||
|
||||
UPDATE dc_projects
|
||||
SET report_markdown = v_md
|
||||
WHERE id = p_project_id;
|
||||
|
||||
DBMS_LOB.FREETEMPORARY(v_md);
|
||||
|
||||
EXCEPTION
|
||||
WHEN OTHERS THEN
|
||||
DBMS_LOB.FREETEMPORARY(v_md);
|
||||
RAISE;
|
||||
END dc_generate_report;
|
||||
/
|
||||
@@ -96,6 +96,41 @@ CREATE OR REPLACE PACKAGE DC_BACKEND_PKG AS
|
||||
p_interval_sec IN NUMBER DEFAULT 5
|
||||
);
|
||||
|
||||
-- -------------------------------------------------------------------------
|
||||
-- Konvertiert einen Markdown-CLOB zu DOCX oder PDF.
|
||||
-- POST /export/markdown → BLOB (application/pdf oder .docx)
|
||||
--
|
||||
-- p_markdown Markdown-Text (z.B. dc_projects.report_markdown)
|
||||
-- p_format 'PDF' (Standard) oder 'DOCX'
|
||||
-- Rückgabe BLOB mit dem fertigen Dokument
|
||||
-- -------------------------------------------------------------------------
|
||||
FUNCTION convert_markdown(
|
||||
p_markdown IN CLOB,
|
||||
p_format IN VARCHAR2 DEFAULT 'PDF'
|
||||
) RETURN BLOB;
|
||||
|
||||
-- -------------------------------------------------------------------------
|
||||
-- Convenience: liest report_markdown des Projekts und konvertiert zu PDF/DOCX.
|
||||
--
|
||||
-- Beispiel:
|
||||
-- v_blob := DC_BACKEND_PKG.project_report(1, 'PDF');
|
||||
-- -------------------------------------------------------------------------
|
||||
FUNCTION project_report(
|
||||
p_project_id IN NUMBER,
|
||||
p_format IN VARCHAR2 DEFAULT 'PDF'
|
||||
) RETURN BLOB;
|
||||
|
||||
-- -------------------------------------------------------------------------
|
||||
-- Generiert den Markdown-Prüfbericht für ein Projekt neu und speichert ihn
|
||||
-- in dc_projects.report_markdown. Ruft intern dc_generate_report auf.
|
||||
--
|
||||
-- Beispiel:
|
||||
-- DC_BACKEND_PKG.regenerate_report(1);
|
||||
-- -------------------------------------------------------------------------
|
||||
PROCEDURE regenerate_report(
|
||||
p_project_id IN NUMBER
|
||||
);
|
||||
|
||||
END DC_BACKEND_PKG;
|
||||
/
|
||||
|
||||
@@ -262,6 +297,93 @@ CREATE OR REPLACE PACKAGE BODY DC_BACKEND_PKG AS
|
||||
END wait_for_completion;
|
||||
|
||||
|
||||
-- -------------------------------------------------------------------------
|
||||
FUNCTION convert_markdown(
|
||||
p_markdown IN CLOB,
|
||||
p_format IN VARCHAR2 DEFAULT 'PDF'
|
||||
) RETURN BLOB IS
|
||||
l_url VARCHAR2(500);
|
||||
l_body CLOB;
|
||||
l_result BLOB;
|
||||
l_status NUMBER;
|
||||
BEGIN
|
||||
l_url := c_base_url || '/export/markdown';
|
||||
|
||||
-- JSON-Body mit CLOB-Inhalt: APEX_JSON unterstützt CLOBs beliebiger Größe
|
||||
APEX_JSON.INITIALIZE_CLOB_OUTPUT;
|
||||
APEX_JSON.OPEN_OBJECT;
|
||||
APEX_JSON.WRITE('content', p_markdown);
|
||||
APEX_JSON.WRITE('format', UPPER(p_format));
|
||||
APEX_JSON.CLOSE_OBJECT;
|
||||
l_body := APEX_JSON.GET_CLOB_OUTPUT;
|
||||
APEX_JSON.FREE_OUTPUT;
|
||||
|
||||
set_headers();
|
||||
|
||||
l_result := APEX_WEB_SERVICE.MAKE_REST_REQUEST_B(
|
||||
p_url => l_url,
|
||||
p_http_method => 'POST',
|
||||
p_body => l_body
|
||||
);
|
||||
|
||||
l_status := APEX_WEB_SERVICE.G_STATUS_CODE;
|
||||
|
||||
IF l_status NOT IN (200, 201) THEN
|
||||
RAISE_APPLICATION_ERROR(-20200,
|
||||
'Export-Service Fehler: HTTP ' || l_status ||
|
||||
' (format=' || p_format || ')');
|
||||
END IF;
|
||||
|
||||
RETURN l_result;
|
||||
|
||||
EXCEPTION
|
||||
WHEN OTHERS THEN
|
||||
IF SQLCODE = -20200 THEN RAISE; END IF;
|
||||
RAISE_APPLICATION_ERROR(-20201,
|
||||
'Verbindungsfehler beim Export: ' || SQLERRM);
|
||||
END convert_markdown;
|
||||
|
||||
|
||||
-- -------------------------------------------------------------------------
|
||||
FUNCTION project_report(
|
||||
p_project_id IN NUMBER,
|
||||
p_format IN VARCHAR2 DEFAULT 'PDF'
|
||||
) RETURN BLOB IS
|
||||
l_markdown CLOB;
|
||||
BEGIN
|
||||
SELECT report_markdown
|
||||
INTO l_markdown
|
||||
FROM dc_projects
|
||||
WHERE id = p_project_id;
|
||||
|
||||
IF l_markdown IS NULL OR DBMS_LOB.GETLENGTH(l_markdown) = 0 THEN
|
||||
RAISE_APPLICATION_ERROR(-20202,
|
||||
'Projekt ' || p_project_id || ' hat noch keinen Markdown-Bericht. '
|
||||
|| 'Bitte zuerst POST /api/dc/projects/' || p_project_id || '/report aufrufen.');
|
||||
END IF;
|
||||
|
||||
RETURN convert_markdown(l_markdown, p_format);
|
||||
|
||||
EXCEPTION
|
||||
WHEN NO_DATA_FOUND THEN
|
||||
RAISE_APPLICATION_ERROR(-20110,
|
||||
'Projekt ' || p_project_id || ' nicht gefunden.');
|
||||
END project_report;
|
||||
|
||||
|
||||
-- -------------------------------------------------------------------------
|
||||
PROCEDURE regenerate_report(
|
||||
p_project_id IN NUMBER
|
||||
) IS
|
||||
BEGIN
|
||||
dc_generate_report(p_project_id => p_project_id);
|
||||
EXCEPTION
|
||||
WHEN NO_DATA_FOUND THEN
|
||||
RAISE_APPLICATION_ERROR(-20110,
|
||||
'Projekt ' || p_project_id || ' nicht gefunden.');
|
||||
END regenerate_report;
|
||||
|
||||
|
||||
END DC_BACKEND_PKG;
|
||||
/
|
||||
|
||||
@@ -294,4 +416,24 @@ BEGIN
|
||||
DBMS_OUTPUT.PUT_LINE('Response: ' || l_response);
|
||||
END;
|
||||
/
|
||||
|
||||
-- Bericht als PDF exportieren
|
||||
DECLARE
|
||||
l_blob BLOB;
|
||||
BEGIN
|
||||
DC_BACKEND_PKG.g_api_key := 'mein-api-key';
|
||||
l_blob := DC_BACKEND_PKG.project_report(p_project_id => 1, p_format => 'PDF');
|
||||
DBMS_OUTPUT.PUT_LINE('PDF-Größe: ' || DBMS_LOB.GETLENGTH(l_blob) || ' Bytes');
|
||||
END;
|
||||
/
|
||||
|
||||
-- Bericht als DOCX exportieren
|
||||
DECLARE
|
||||
l_blob BLOB;
|
||||
BEGIN
|
||||
DC_BACKEND_PKG.g_api_key := 'mein-api-key';
|
||||
l_blob := DC_BACKEND_PKG.project_report(p_project_id => 1, p_format => 'DOCX');
|
||||
DBMS_OUTPUT.PUT_LINE('DOCX-Größe: ' || DBMS_LOB.GETLENGTH(l_blob) || ' Bytes');
|
||||
END;
|
||||
/
|
||||
*/
|
||||
|
||||
@@ -16,14 +16,15 @@
|
||||
--
|
||||
-- Endpunkt-Übersicht:
|
||||
-- 1. GET /api/dc/catalogs/ Alle Fragenkataloge
|
||||
-- 2. GET /api/dc/catalogs/:id/questions/ Fragen eines Katalogs (flach)
|
||||
-- 3. GET /api/dc/projects/:id Projektdetail
|
||||
-- 4. POST /api/dc/projects/:id/start → IN_PROGRESS
|
||||
-- 5. PUT /api/dc/projects/:id/progress Fortschritt aktualisieren
|
||||
-- 6. POST /api/dc/projects/:id/complete → COMPLETED
|
||||
-- 2. GET /api/dc/catalogs/:id/questions/ Fragen eines Katalogs (flach, nach order_nr sortiert)
|
||||
-- 3. GET /api/dc/projects/:id Projektdetail (inkl. Detailstatus + ETA)
|
||||
-- 4. POST /api/dc/projects/:id/start → IN_PROGRESS (setzt processing_started_at)
|
||||
-- 5. PUT /api/dc/projects/:id/progress Fortschritt + Phase/Doc/Question/ETA
|
||||
-- 6. POST /api/dc/projects/:id/complete → COMPLETED (setzt current_phase=COMPLETED)
|
||||
-- 7. GET /api/dc/projects/:id/documents/ Dokument-Liste (kein BLOB)
|
||||
-- 8. GET /api/dc/documents/:id/file BLOB-Download
|
||||
-- 9. PUT /api/dc/documents/:id/texts OCR-Text + Übersetzung schreiben
|
||||
-- 8. GET /api/dc/documents/:id/file BLOB-Download (PLSQL-Workaround)
|
||||
-- 9. GET /api/dc/documents/:id/texts OCR-Text + Übersetzung lesen (für Resume)
|
||||
-- 9b. PUT /api/dc/documents/:id/texts OCR-Text + Übersetzung schreiben
|
||||
-- 10. POST /api/dc/projects/:id/results Prüfergebnis einfügen
|
||||
-- 11. DELETE /api/dc/projects/:id/results Alle Ergebnisse löschen
|
||||
-- =============================================================================
|
||||
@@ -115,17 +116,19 @@ BEGIN
|
||||
cat.id AS category_id,
|
||||
cat.name AS category_name,
|
||||
cat.description AS category_description,
|
||||
cat.order_nr AS category_order_nr,
|
||||
q.question_text,
|
||||
q.evaluation_type,
|
||||
q.threshold,
|
||||
q.result_handling,
|
||||
q.example_0_percent,
|
||||
q.example_100_percent,
|
||||
q.order_nr,
|
||||
q.row_version
|
||||
FROM dc_questions q
|
||||
JOIN dc_question_categories cat ON cat.id = q.category_id
|
||||
WHERE cat.catalog_id = :catalog_id
|
||||
ORDER BY cat.name, q.id
|
||||
ORDER BY cat.order_nr, q.order_nr, q.id
|
||||
]'
|
||||
);
|
||||
|
||||
@@ -148,7 +151,7 @@ BEGIN
|
||||
p_method => 'GET',
|
||||
p_source_type => ORDS.SOURCE_TYPE_COLLECTION_ITEM,
|
||||
p_items_per_page => 1,
|
||||
p_comments => 'Projektdetail als einzelnes JSON-Objekt; 404 wenn nicht gefunden',
|
||||
p_comments => 'Projektdetail mit Status-Details und ETA; 404 wenn nicht gefunden',
|
||||
p_source => q'[
|
||||
SELECT
|
||||
p.id,
|
||||
@@ -162,7 +165,12 @@ BEGIN
|
||||
p.notification_email,
|
||||
p.row_version,
|
||||
p.created,
|
||||
p.updated
|
||||
p.updated,
|
||||
p.processing_started_at,
|
||||
p.current_phase,
|
||||
p.current_doc_id,
|
||||
p.current_question_id,
|
||||
p.estimated_completion_at
|
||||
FROM dc_projects p
|
||||
WHERE p.id = :project_id
|
||||
]'
|
||||
@@ -185,7 +193,7 @@ BEGIN
|
||||
p_pattern => 'projects/:project_id/start',
|
||||
p_method => 'POST',
|
||||
p_source_type => ORDS.SOURCE_TYPE_PLSQL,
|
||||
p_comments => '200 OK; 409 wenn Status nicht PENDING; 404 wenn Projekt fehlt',
|
||||
p_comments => '200 OK; 409 wenn IN_PROGRESS/COMPLETED; 404 wenn Projekt fehlt',
|
||||
p_source => q'[
|
||||
DECLARE
|
||||
v_status dc_projects.status%TYPE;
|
||||
@@ -196,16 +204,21 @@ BEGIN
|
||||
WHERE id = :project_id
|
||||
FOR UPDATE NOWAIT;
|
||||
|
||||
IF v_status != 'PENDING' THEN
|
||||
IF v_status IN ('IN_PROGRESS', 'COMPLETED') THEN
|
||||
:status_code := 409;
|
||||
HTP.P('{"error":"Projekt ist nicht im Status PENDING",'
|
||||
HTP.P('{"error":"Projekt ist bereits ' || v_status || '",'
|
||||
|| '"current_status":"' || v_status || '"}');
|
||||
RETURN;
|
||||
END IF;
|
||||
|
||||
UPDATE dc_projects
|
||||
SET status = 'IN_PROGRESS',
|
||||
progress = 0
|
||||
SET status = 'IN_PROGRESS',
|
||||
progress = 0,
|
||||
processing_started_at = SYSTIMESTAMP,
|
||||
current_phase = 'OCR',
|
||||
current_doc_id = NULL,
|
||||
current_question_id = NULL,
|
||||
estimated_completion_at = NULL
|
||||
WHERE id = :project_id;
|
||||
|
||||
:status_code := 200;
|
||||
@@ -236,20 +249,30 @@ BEGIN
|
||||
);
|
||||
|
||||
-- 5. PUT /api/dc/projects/:project_id/progress
|
||||
-- Body: {"progress": 45}
|
||||
-- Body: {"progress":45,"current_phase":"QUESTIONS","current_doc_id":2,
|
||||
-- "current_question_id":15,"estimated_completion_at":"2026-04-27T16:30:00"}
|
||||
ORDS.DEFINE_HANDLER(
|
||||
p_module_name => 'frigosped.dc',
|
||||
p_pattern => 'projects/:project_id/progress',
|
||||
p_method => 'PUT',
|
||||
p_source_type => ORDS.SOURCE_TYPE_PLSQL,
|
||||
p_comments => 'Setzt progress (0-100); 400 bei ungueltigem Wert',
|
||||
p_comments => 'Setzt progress (0-100) + Phase/Dokument/Frage/ETA; 400 bei ungueltigem Wert',
|
||||
p_source => q'[
|
||||
DECLARE
|
||||
v_progress NUMBER;
|
||||
v_body CLOB;
|
||||
v_body CLOB;
|
||||
v_progress NUMBER;
|
||||
v_phase VARCHAR2(30);
|
||||
v_doc_id NUMBER;
|
||||
v_question_id NUMBER;
|
||||
v_eta_str VARCHAR2(30);
|
||||
v_eta TIMESTAMP;
|
||||
BEGIN
|
||||
v_body := :body_text;
|
||||
v_progress := TO_NUMBER(JSON_VALUE(v_body, '$.progress'));
|
||||
v_body := :body_text;
|
||||
v_progress := TO_NUMBER(JSON_VALUE(v_body, '$.progress'));
|
||||
v_phase := JSON_VALUE(v_body, '$.current_phase');
|
||||
v_doc_id := TO_NUMBER(JSON_VALUE(v_body, '$.current_doc_id'));
|
||||
v_question_id := TO_NUMBER(JSON_VALUE(v_body, '$.current_question_id'));
|
||||
v_eta_str := JSON_VALUE(v_body, '$.estimated_completion_at');
|
||||
|
||||
IF v_progress IS NULL OR v_progress < 0 OR v_progress > 100 THEN
|
||||
:status_code := 400;
|
||||
@@ -257,9 +280,21 @@ BEGIN
|
||||
RETURN;
|
||||
END IF;
|
||||
|
||||
IF v_eta_str IS NOT NULL THEN
|
||||
BEGIN
|
||||
v_eta := TO_TIMESTAMP(v_eta_str, 'YYYY-MM-DD"T"HH24:MI:SS');
|
||||
EXCEPTION WHEN OTHERS THEN
|
||||
v_eta := NULL;
|
||||
END;
|
||||
END IF;
|
||||
|
||||
UPDATE dc_projects
|
||||
SET progress = v_progress
|
||||
WHERE id = :project_id;
|
||||
SET progress = v_progress,
|
||||
current_phase = NVL(v_phase, current_phase),
|
||||
current_doc_id = v_doc_id,
|
||||
current_question_id = v_question_id,
|
||||
estimated_completion_at = NVL(v_eta, estimated_completion_at)
|
||||
WHERE id = :project_id;
|
||||
|
||||
IF SQL%ROWCOUNT = 0 THEN
|
||||
:status_code := 404;
|
||||
@@ -300,7 +335,7 @@ BEGIN
|
||||
p_pattern => 'projects/:project_id/complete',
|
||||
p_method => 'POST',
|
||||
p_source_type => ORDS.SOURCE_TYPE_PLSQL,
|
||||
p_comments => 'Setzt status=COMPLETED, completed_at=SYSDATE, progress=100',
|
||||
p_comments => 'Setzt status=COMPLETED und erstellt Markdown-Bericht',
|
||||
p_source => q'[
|
||||
DECLARE
|
||||
v_status dc_projects.status%TYPE;
|
||||
@@ -319,11 +354,21 @@ BEGIN
|
||||
END IF;
|
||||
|
||||
UPDATE dc_projects
|
||||
SET status = 'COMPLETED',
|
||||
completed_at = v_now,
|
||||
progress = 100
|
||||
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_generate_report(:project_id);
|
||||
EXCEPTION WHEN OTHERS THEN NULL;
|
||||
END;
|
||||
|
||||
:status_code := 200;
|
||||
HTP.P('{"project_id":' || :project_id
|
||||
|| ',"status":"COMPLETED"'
|
||||
@@ -342,6 +387,37 @@ BEGIN
|
||||
]'
|
||||
);
|
||||
|
||||
-- ===========================================================================
|
||||
-- TEMPLATE 12: projects/:project_id/report → Bericht generieren / abrufen
|
||||
-- ===========================================================================
|
||||
ORDS.DEFINE_TEMPLATE(
|
||||
p_module_name => 'frigosped.dc',
|
||||
p_pattern => 'projects/:project_id/report',
|
||||
p_priority => 0,
|
||||
p_comments => 'Markdown-Bericht eines Projekts'
|
||||
);
|
||||
|
||||
-- 12. POST /api/dc/projects/:project_id/report – (neu) generieren
|
||||
ORDS.DEFINE_HANDLER(
|
||||
p_module_name => 'frigosped.dc',
|
||||
p_pattern => 'projects/:project_id/report',
|
||||
p_method => 'POST',
|
||||
p_source_type => ORDS.SOURCE_TYPE_PLSQL,
|
||||
p_comments => 'Generiert report_markdown neu (z.B. nach Konfigurationsänderung)',
|
||||
p_source => q'[
|
||||
BEGIN
|
||||
dc_generate_report(:project_id);
|
||||
:status_code := 200;
|
||||
HTP.P('{"project_id":' || :project_id || ',"report_generated":true}');
|
||||
EXCEPTION
|
||||
WHEN OTHERS THEN
|
||||
ROLLBACK;
|
||||
:status_code := 500;
|
||||
HTP.P('{"error":"' || REPLACE(SQLERRM, '"', '''') || '"}');
|
||||
END;
|
||||
]'
|
||||
);
|
||||
|
||||
|
||||
-- ===========================================================================
|
||||
-- TEMPLATE 7: projects/:project_id/documents/
|
||||
@@ -393,36 +469,78 @@ BEGIN
|
||||
);
|
||||
|
||||
-- 8. GET /api/dc/documents/:doc_id/file
|
||||
-- SOURCE_TYPE_MEDIA: Pflicht-Aliase: content, content_type, filename
|
||||
-- PLSQL-Workaround: SOURCE_TYPE_MEDIA ruft getString() auf BLOB auf → ORA-17004 in dieser ORDS-Version
|
||||
ORDS.DEFINE_HANDLER(
|
||||
p_module_name => 'frigosped.dc',
|
||||
p_pattern => 'documents/:doc_id/file',
|
||||
p_method => 'GET',
|
||||
p_source_type => ORDS.SOURCE_TYPE_MEDIA,
|
||||
p_comments => 'Streamt original_file BLOB; ORDS setzt Content-Type aus mime_type-Spalte',
|
||||
p_source_type => ORDS.SOURCE_TYPE_PLSQL,
|
||||
p_comments => 'Streamt original_file BLOB mit korrektem Content-Type (PLSQL-Workaround fuer ORDS-Bug)',
|
||||
p_source => q'[
|
||||
SELECT
|
||||
d.original_file AS content,
|
||||
d.mime_type AS content_type,
|
||||
d.filename AS filename,
|
||||
d.updated AS last_modified
|
||||
FROM dc_project_documents d
|
||||
WHERE d.id = :doc_id
|
||||
DECLARE
|
||||
v_blob BLOB;
|
||||
v_mime VARCHAR2(200);
|
||||
v_filename VARCHAR2(500);
|
||||
BEGIN
|
||||
SELECT original_file, mime_type, filename
|
||||
INTO v_blob, v_mime, v_filename
|
||||
FROM dc_project_documents
|
||||
WHERE id = :doc_id;
|
||||
|
||||
IF v_blob IS NULL THEN
|
||||
:status_code := 404;
|
||||
HTP.P('{"error":"Datei nicht vorhanden","doc_id":' || :doc_id || '}');
|
||||
RETURN;
|
||||
END IF;
|
||||
|
||||
OWA_UTIL.MIME_HEADER(NVL(v_mime, 'application/octet-stream'), FALSE);
|
||||
HTP.P('Content-Disposition: attachment; filename="' || v_filename || '"');
|
||||
OWA_UTIL.HTTP_HEADER_CLOSE;
|
||||
WPG_DOCLOAD.DOWNLOAD_FILE(v_blob);
|
||||
:status_code := 200;
|
||||
EXCEPTION
|
||||
WHEN NO_DATA_FOUND THEN
|
||||
:status_code := 404;
|
||||
HTP.P('{"error":"Dokument nicht gefunden","doc_id":' || :doc_id || '}');
|
||||
WHEN OTHERS THEN
|
||||
:status_code := 500;
|
||||
HTP.P('{"error":"' || REPLACE(SQLERRM, '"', '''') || '"}');
|
||||
END;
|
||||
]'
|
||||
);
|
||||
|
||||
|
||||
-- ===========================================================================
|
||||
-- TEMPLATE 9: documents/:doc_id/texts
|
||||
-- TEMPLATE 9: documents/:doc_id/texts (GET + PUT)
|
||||
-- ===========================================================================
|
||||
ORDS.DEFINE_TEMPLATE(
|
||||
p_module_name => 'frigosped.dc',
|
||||
p_pattern => 'documents/:doc_id/texts',
|
||||
p_priority => 0,
|
||||
p_comments => 'OCR-Ergebnis und Uebersetzung zurueckschreiben'
|
||||
p_comments => 'OCR-Ergebnis und Uebersetzung lesen/schreiben'
|
||||
);
|
||||
|
||||
-- 9. PUT /api/dc/documents/:doc_id/texts
|
||||
-- 9a. GET /api/dc/documents/:doc_id/texts (fuer Resume nach Phase A)
|
||||
ORDS.DEFINE_HANDLER(
|
||||
p_module_name => 'frigosped.dc',
|
||||
p_pattern => 'documents/:doc_id/texts',
|
||||
p_method => 'GET',
|
||||
p_source_type => ORDS.SOURCE_TYPE_COLLECTION_ITEM,
|
||||
p_items_per_page => 1,
|
||||
p_comments => 'Gibt original_text und translated_text zurueck',
|
||||
p_source => q'[
|
||||
SELECT
|
||||
d.id,
|
||||
d.original_text,
|
||||
d.translated_text,
|
||||
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
|
||||
FROM dc_project_documents d
|
||||
WHERE d.id = :doc_id
|
||||
]'
|
||||
);
|
||||
|
||||
-- 9b. PUT /api/dc/documents/:doc_id/texts
|
||||
-- Body: {"original_text": "## ...", "translated_text": "## ..."}
|
||||
-- Beide Felder sind optional: NULL-Wert ueberschreibt nicht (nur wenn im Body vorhanden)
|
||||
ORDS.DEFINE_HANDLER(
|
||||
@@ -566,12 +684,6 @@ BEGIN
|
||||
)
|
||||
RETURNING id INTO v_new_id;
|
||||
|
||||
-- Location-Header gemaess REST-Konvention
|
||||
ORDS.SET_RESPONSE_HEADER(
|
||||
'Location',
|
||||
'/api/dc/projects/' || :project_id || '/results/' || v_new_id
|
||||
);
|
||||
|
||||
:status_code := 201;
|
||||
HTP.P('{"id":' || v_new_id
|
||||
|| ',"project_id":' || :project_id
|
||||
|
||||
Reference in New Issue
Block a user