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:
Wolf G. Beckmann
2026-04-28 14:32:45 +02:00
parent 1816168d1f
commit 522bc21439
24 changed files with 2011 additions and 127 deletions

View 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;
/

View File

@@ -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;
/
*/

View File

@@ -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