feat: Rollenkonzept backend + SOP template (Phase 1-3)
- Migration 111: 3 new tables (org_roles, document_reviews, document_role_mapping) with seed data mapping all 71 doc types to 7 compliance roles - org_role_routes.py: CRUD for roles, seed defaults, test email, mapping API - document_review_routes.py: Review lifecycle (create→send→approve/reject) with approval notification to all affected roles - Migration 112: SOP template (ISO 9001 structure, 21 placeholders) - Added standard_operating_procedure to TemplateType, doc-labels, presets [migration-approved] Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -107,6 +107,9 @@ export const DOC_LABELS: Record<string, { label: string; category: string }> = {
|
|||||||
ai_usage_policy: { label: 'KI-Nutzungsrichtlinie', category: 'KI & Cyber' },
|
ai_usage_policy: { label: 'KI-Nutzungsrichtlinie', category: 'KI & Cyber' },
|
||||||
cybersecurity_policy: { label: 'Cybersecurity-Richtlinie (CRA)', category: 'KI & Cyber' },
|
cybersecurity_policy: { label: 'Cybersecurity-Richtlinie (CRA)', category: 'KI & Cyber' },
|
||||||
byod_policy: { label: 'BYOD-Richtlinie', category: 'KI & Cyber' },
|
byod_policy: { label: 'BYOD-Richtlinie', category: 'KI & Cyber' },
|
||||||
|
|
||||||
|
// ── SOP ──────────────────────────────────────────────────────────
|
||||||
|
standard_operating_procedure: { label: 'Standard Operating Procedure', category: 'Prozesse' },
|
||||||
}
|
}
|
||||||
|
|
||||||
export const CATEGORY_COLORS: Record<string, string> = {
|
export const CATEGORY_COLORS: Record<string, string> = {
|
||||||
@@ -124,4 +127,5 @@ export const CATEGORY_COLORS: Record<string, string> = {
|
|||||||
BCM: 'bg-yellow-50 text-yellow-700',
|
BCM: 'bg-yellow-50 text-yellow-700',
|
||||||
'KI & Cyber': 'bg-cyan-50 text-cyan-700',
|
'KI & Cyber': 'bg-cyan-50 text-cyan-700',
|
||||||
Marketing: 'bg-pink-50 text-pink-700',
|
Marketing: 'bg-pink-50 text-pink-700',
|
||||||
|
Prozesse: 'bg-teal-50 text-teal-700',
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -224,7 +224,7 @@ export const COMPANY_PROFILE_PRESETS: CompanyProfilePreset[] = [
|
|||||||
'transfer_impact_assessment', 'scc_companion',
|
'transfer_impact_assessment', 'scc_companion',
|
||||||
'vendor_risk_management_policy', 'third_party_security_policy',
|
'vendor_risk_management_policy', 'third_party_security_policy',
|
||||||
'business_continuity_policy', 'disaster_recovery_policy', 'crisis_management_policy',
|
'business_continuity_policy', 'disaster_recovery_policy', 'crisis_management_policy',
|
||||||
'ai_usage_policy',
|
'ai_usage_policy', 'standard_operating_procedure',
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -262,6 +262,7 @@ export const COMPANY_PROFILE_PRESETS: CompanyProfilePreset[] = [
|
|||||||
'vendor_risk_management_policy', 'third_party_security_policy',
|
'vendor_risk_management_policy', 'third_party_security_policy',
|
||||||
'business_continuity_policy', 'disaster_recovery_policy',
|
'business_continuity_policy', 'disaster_recovery_policy',
|
||||||
'ai_usage_policy', 'cybersecurity_policy', 'byod_policy',
|
'ai_usage_policy', 'cybersecurity_policy', 'byod_policy',
|
||||||
|
'standard_operating_procedure',
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -296,6 +297,7 @@ export const COMPANY_PROFILE_PRESETS: CompanyProfilePreset[] = [
|
|||||||
'transfer_impact_assessment', 'vendor_risk_management_policy',
|
'transfer_impact_assessment', 'vendor_risk_management_policy',
|
||||||
'supplier_security_policy',
|
'supplier_security_policy',
|
||||||
'business_continuity_policy', 'disaster_recovery_policy', 'crisis_management_policy',
|
'business_continuity_policy', 'disaster_recovery_policy', 'crisis_management_policy',
|
||||||
|
'standard_operating_procedure',
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -85,6 +85,8 @@ export type TemplateType =
|
|||||||
| 'tom_documentation'
|
| 'tom_documentation'
|
||||||
| 'loeschkonzept'
|
| 'loeschkonzept'
|
||||||
| 'pflichtenregister'
|
| 'pflichtenregister'
|
||||||
|
// SOP (Migration 112)
|
||||||
|
| 'standard_operating_procedure'
|
||||||
|
|
||||||
export type Jurisdiction = 'DE' | 'AT' | 'CH' | 'EU' | 'US' | 'INTL'
|
export type Jurisdiction = 'DE' | 'AT' | 'CH' | 'EU' | 'US' | 'INTL'
|
||||||
|
|
||||||
|
|||||||
@@ -63,6 +63,8 @@ _ROUTER_MODULES = [
|
|||||||
"tom_mapping_routes",
|
"tom_mapping_routes",
|
||||||
"llm_audit_routes",
|
"llm_audit_routes",
|
||||||
"assertion_routes",
|
"assertion_routes",
|
||||||
|
"org_role_routes",
|
||||||
|
"document_review_routes",
|
||||||
]
|
]
|
||||||
|
|
||||||
_loaded_count = 0
|
_loaded_count = 0
|
||||||
|
|||||||
@@ -0,0 +1,312 @@
|
|||||||
|
"""
|
||||||
|
FastAPI routes for Document Review Workflow.
|
||||||
|
|
||||||
|
Tracks which compliance documents have been sent for review, their status,
|
||||||
|
and handles email notifications to reviewers.
|
||||||
|
|
||||||
|
Endpoints:
|
||||||
|
GET /document-reviews — list reviews with filters
|
||||||
|
GET /document-reviews/stats — counts by status
|
||||||
|
POST /document-reviews — create review (auto-assign from mapping)
|
||||||
|
GET /document-reviews/{id} — single review
|
||||||
|
POST /document-reviews/{id}/send — send notification email
|
||||||
|
POST /document-reviews/{id}/approve — mark as approved
|
||||||
|
POST /document-reviews/{id}/reject — mark as rejected
|
||||||
|
GET /document-reviews/for-document — reviews for a specific doc type
|
||||||
|
"""
|
||||||
|
|
||||||
|
import hashlib
|
||||||
|
import logging
|
||||||
|
from datetime import datetime
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
from fastapi import APIRouter, Depends, HTTPException, Query
|
||||||
|
from pydantic import BaseModel
|
||||||
|
from sqlalchemy import text
|
||||||
|
from sqlalchemy.orm import Session
|
||||||
|
|
||||||
|
from classroom_engine.database import get_db
|
||||||
|
from .tenant_utils import get_tenant_id as _get_tenant_id
|
||||||
|
from .db_utils import row_to_dict as _row_to_dict
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
router = APIRouter(prefix="/document-reviews", tags=["document-reviews"])
|
||||||
|
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# Schemas
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
|
||||||
|
class ReviewCreate(BaseModel):
|
||||||
|
document_type: str
|
||||||
|
document_title: str
|
||||||
|
document_content: Optional[str] = None
|
||||||
|
project_id: Optional[str] = None
|
||||||
|
submitted_by: Optional[str] = None
|
||||||
|
review_link: Optional[str] = None
|
||||||
|
|
||||||
|
|
||||||
|
class ReviewReject(BaseModel):
|
||||||
|
comment: str
|
||||||
|
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# Routes
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("")
|
||||||
|
def list_reviews(
|
||||||
|
project_id: Optional[str] = Query(None),
|
||||||
|
status: Optional[str] = Query(None),
|
||||||
|
document_type: Optional[str] = Query(None),
|
||||||
|
reviewer_role_key: Optional[str] = Query(None),
|
||||||
|
limit: int = Query(50, le=200),
|
||||||
|
offset: int = Query(0),
|
||||||
|
db: Session = Depends(get_db),
|
||||||
|
tenant_id: str = Depends(_get_tenant_id),
|
||||||
|
):
|
||||||
|
where = ["tenant_id = :tid"]
|
||||||
|
params = {"tid": tenant_id, "lim": limit, "off": offset}
|
||||||
|
if project_id:
|
||||||
|
where.append("project_id = :pid")
|
||||||
|
params["pid"] = project_id
|
||||||
|
if status:
|
||||||
|
where.append("status = :status")
|
||||||
|
params["status"] = status
|
||||||
|
if document_type:
|
||||||
|
where.append("document_type = :dt")
|
||||||
|
params["dt"] = document_type
|
||||||
|
if reviewer_role_key:
|
||||||
|
where.append("reviewer_role_key = :rrk")
|
||||||
|
params["rrk"] = reviewer_role_key
|
||||||
|
|
||||||
|
q = text(f"""
|
||||||
|
SELECT * FROM compliance_document_reviews
|
||||||
|
WHERE {' AND '.join(where)}
|
||||||
|
ORDER BY created_at DESC LIMIT :lim OFFSET :off
|
||||||
|
""")
|
||||||
|
rows = db.execute(q, params).fetchall()
|
||||||
|
return [_row_to_dict(r) for r in rows]
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/stats")
|
||||||
|
def review_stats(
|
||||||
|
project_id: Optional[str] = Query(None),
|
||||||
|
db: Session = Depends(get_db),
|
||||||
|
tenant_id: str = Depends(_get_tenant_id),
|
||||||
|
):
|
||||||
|
where = "tenant_id = :tid"
|
||||||
|
params = {"tid": tenant_id}
|
||||||
|
if project_id:
|
||||||
|
where += " AND project_id = :pid"
|
||||||
|
params["pid"] = project_id
|
||||||
|
q = text(f"SELECT status, COUNT(*) as count FROM compliance_document_reviews WHERE {where} GROUP BY status")
|
||||||
|
rows = db.execute(q, params).fetchall()
|
||||||
|
return {r.status: r.count for r in rows}
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/for-document")
|
||||||
|
def reviews_for_document(
|
||||||
|
document_type: str = Query(...),
|
||||||
|
project_id: Optional[str] = Query(None),
|
||||||
|
db: Session = Depends(get_db),
|
||||||
|
tenant_id: str = Depends(_get_tenant_id),
|
||||||
|
):
|
||||||
|
where = "tenant_id = :tid AND document_type = :dt"
|
||||||
|
params = {"tid": tenant_id, "dt": document_type}
|
||||||
|
if project_id:
|
||||||
|
where += " AND project_id = :pid"
|
||||||
|
params["pid"] = project_id
|
||||||
|
q = text(f"SELECT * FROM compliance_document_reviews WHERE {where} ORDER BY created_at DESC LIMIT 10")
|
||||||
|
rows = db.execute(q, params).fetchall()
|
||||||
|
return [_row_to_dict(r) for r in rows]
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("")
|
||||||
|
def create_review(
|
||||||
|
body: ReviewCreate,
|
||||||
|
db: Session = Depends(get_db),
|
||||||
|
tenant_id: str = Depends(_get_tenant_id),
|
||||||
|
):
|
||||||
|
# Find reviewer(s) from mapping + org_roles
|
||||||
|
q = text("""
|
||||||
|
SELECT m.role_key, m.is_primary, r.person_name, r.person_email, r.role_label
|
||||||
|
FROM compliance_document_role_mapping m
|
||||||
|
LEFT JOIN compliance_org_roles r
|
||||||
|
ON r.tenant_id = m.tenant_id AND r.role_key = m.role_key
|
||||||
|
AND (r.project_id = :pid OR r.project_id IS NULL)
|
||||||
|
WHERE m.tenant_id = :tid AND m.document_type = :dt
|
||||||
|
ORDER BY m.is_primary DESC
|
||||||
|
""")
|
||||||
|
mappings = db.execute(q, {"tid": tenant_id, "dt": body.document_type, "pid": body.project_id}).fetchall()
|
||||||
|
|
||||||
|
if not mappings:
|
||||||
|
raise HTTPException(404, f"No reviewer mapping found for document type '{body.document_type}'")
|
||||||
|
|
||||||
|
content_hash = hashlib.sha256(body.document_content.encode()).hexdigest() if body.document_content else None
|
||||||
|
created = []
|
||||||
|
for m in mappings:
|
||||||
|
m_dict = _row_to_dict(m)
|
||||||
|
ins = text("""
|
||||||
|
INSERT INTO compliance_document_reviews
|
||||||
|
(tenant_id, project_id, document_type, document_title, document_content_hash,
|
||||||
|
reviewer_role_key, reviewer_name, reviewer_email, submitted_by, review_link, submitted_at)
|
||||||
|
VALUES (:tid, :pid, :dt, :title, :hash, :rrk, :rn, :re, :sb, :rl, NOW())
|
||||||
|
RETURNING *
|
||||||
|
""")
|
||||||
|
row = db.execute(ins, {
|
||||||
|
"tid": tenant_id, "pid": body.project_id, "dt": body.document_type,
|
||||||
|
"title": body.document_title, "hash": content_hash,
|
||||||
|
"rrk": m_dict["role_key"], "rn": m_dict.get("person_name"),
|
||||||
|
"re": m_dict.get("person_email"), "sb": body.submitted_by,
|
||||||
|
"rl": body.review_link,
|
||||||
|
}).fetchone()
|
||||||
|
created.append(_row_to_dict(row))
|
||||||
|
db.commit()
|
||||||
|
return created
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/{review_id}")
|
||||||
|
def get_review(
|
||||||
|
review_id: str,
|
||||||
|
db: Session = Depends(get_db),
|
||||||
|
tenant_id: str = Depends(_get_tenant_id),
|
||||||
|
):
|
||||||
|
q = text("SELECT * FROM compliance_document_reviews WHERE id = :rid AND tenant_id = :tid")
|
||||||
|
row = db.execute(q, {"rid": review_id, "tid": tenant_id}).fetchone()
|
||||||
|
if not row:
|
||||||
|
raise HTTPException(404, "Review not found")
|
||||||
|
return _row_to_dict(row)
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/{review_id}/send")
|
||||||
|
def send_notification(
|
||||||
|
review_id: str,
|
||||||
|
db: Session = Depends(get_db),
|
||||||
|
tenant_id: str = Depends(_get_tenant_id),
|
||||||
|
):
|
||||||
|
q = text("SELECT * FROM compliance_document_reviews WHERE id = :rid AND tenant_id = :tid")
|
||||||
|
row = db.execute(q, {"rid": review_id, "tid": tenant_id}).fetchone()
|
||||||
|
if not row:
|
||||||
|
raise HTTPException(404, "Review not found")
|
||||||
|
review = _row_to_dict(row)
|
||||||
|
if not review.get("reviewer_email"):
|
||||||
|
raise HTTPException(400, "No email for reviewer — assign a person to this role first")
|
||||||
|
|
||||||
|
try:
|
||||||
|
from compliance.services.smtp_sender import send_email
|
||||||
|
result = send_email(
|
||||||
|
recipient=review["reviewer_email"],
|
||||||
|
subject=f"[BreakPilot] Dokument zur Pruefung: {review['document_title']}",
|
||||||
|
body_html=f"""
|
||||||
|
<h2>Dokument zur Pruefung</h2>
|
||||||
|
<p>Sehr geehrte/r <strong>{review.get('reviewer_name') or 'Pruefer/in'}</strong>,</p>
|
||||||
|
<p>das folgende Dokument wurde Ihnen zur inhaltlichen Pruefung zugewiesen:</p>
|
||||||
|
<table style="border-collapse:collapse;margin:16px 0;">
|
||||||
|
<tr><td style="padding:4px 12px 4px 0;font-weight:bold;">Dokument:</td>
|
||||||
|
<td>{review['document_title']}</td></tr>
|
||||||
|
<tr><td style="padding:4px 12px 4px 0;font-weight:bold;">Typ:</td>
|
||||||
|
<td>{review['document_type']}</td></tr>
|
||||||
|
<tr><td style="padding:4px 12px 4px 0;font-weight:bold;">Eingereicht von:</td>
|
||||||
|
<td>{review.get('submitted_by') or 'System'}</td></tr>
|
||||||
|
</table>
|
||||||
|
<p>Bitte pruefen Sie das Dokument auf <strong>inhaltliche Richtigkeit</strong>,
|
||||||
|
<strong>Vollstaendigkeit</strong> und <strong>Umsetzbarkeit</strong>.</p>
|
||||||
|
{f'<p><a href="{review["review_link"]}" style="background:#7c3aed;color:white;padding:10px 20px;border-radius:6px;text-decoration:none;">Dokument oeffnen</a></p>' if review.get("review_link") else ''}
|
||||||
|
<p style="color:#888;font-size:12px;">BreakPilot Compliance SDK</p>
|
||||||
|
""",
|
||||||
|
)
|
||||||
|
# Update review status
|
||||||
|
db.execute(text("""
|
||||||
|
UPDATE compliance_document_reviews
|
||||||
|
SET status = 'in_review', email_sent = TRUE, email_sent_at = NOW(), updated_at = NOW()
|
||||||
|
WHERE id = :rid
|
||||||
|
"""), {"rid": review_id})
|
||||||
|
db.commit()
|
||||||
|
return {"sent": True, "email": review["reviewer_email"], "result": result}
|
||||||
|
except Exception as e:
|
||||||
|
logger.error("Failed to send review email: %s", e)
|
||||||
|
raise HTTPException(500, f"Email sending failed: {e}")
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/{review_id}/approve")
|
||||||
|
def approve_review(
|
||||||
|
review_id: str,
|
||||||
|
db: Session = Depends(get_db),
|
||||||
|
tenant_id: str = Depends(_get_tenant_id),
|
||||||
|
):
|
||||||
|
q = text("""
|
||||||
|
UPDATE compliance_document_reviews
|
||||||
|
SET status = 'approved', reviewed_at = NOW(), updated_at = NOW()
|
||||||
|
WHERE id = :rid AND tenant_id = :tid
|
||||||
|
RETURNING *
|
||||||
|
""")
|
||||||
|
row = db.execute(q, {"rid": review_id, "tid": tenant_id}).fetchone()
|
||||||
|
if not row:
|
||||||
|
raise HTTPException(404, "Review not found")
|
||||||
|
db.commit()
|
||||||
|
review = _row_to_dict(row)
|
||||||
|
|
||||||
|
# Notify all OTHER roles mapped to this document type about the approval
|
||||||
|
_notify_approval(db, tenant_id, review)
|
||||||
|
|
||||||
|
return review
|
||||||
|
|
||||||
|
|
||||||
|
def _notify_approval(db: Session, tenant_id: str, review: dict):
|
||||||
|
"""Send approval notification to all other roles mapped to this document type."""
|
||||||
|
try:
|
||||||
|
from compliance.services.smtp_sender import send_email
|
||||||
|
q = text("""
|
||||||
|
SELECT DISTINCT r.person_name, r.person_email, r.role_label
|
||||||
|
FROM compliance_document_role_mapping m
|
||||||
|
JOIN compliance_org_roles r
|
||||||
|
ON r.tenant_id = m.tenant_id AND r.role_key = m.role_key
|
||||||
|
AND (r.project_id = :pid OR r.project_id IS NULL)
|
||||||
|
WHERE m.tenant_id = :tid AND m.document_type = :dt
|
||||||
|
AND m.role_key != :reviewer_key AND r.person_email IS NOT NULL
|
||||||
|
""")
|
||||||
|
others = db.execute(q, {
|
||||||
|
"tid": tenant_id, "dt": review["document_type"],
|
||||||
|
"pid": review.get("project_id"), "reviewer_key": review["reviewer_role_key"],
|
||||||
|
}).fetchall()
|
||||||
|
for other in others:
|
||||||
|
o = _row_to_dict(other)
|
||||||
|
send_email(
|
||||||
|
recipient=o["person_email"],
|
||||||
|
subject=f"[BreakPilot] Freigabe: {review['document_title']}",
|
||||||
|
body_html=f"""
|
||||||
|
<h2>Dokument freigegeben</h2>
|
||||||
|
<p>Sehr geehrte/r <strong>{o.get('person_name') or o['role_label']}</strong>,</p>
|
||||||
|
<p>das Dokument <strong>{review['document_title']}</strong> wurde von
|
||||||
|
{review.get('reviewer_name') or review['reviewer_role_key']} freigegeben.</p>
|
||||||
|
<p>Bitte pruefen Sie, ob fuer Ihren Verantwortungsbereich Handlungsbedarf besteht
|
||||||
|
(z.B. Schulungsbedarf, Prozessanpassungen).</p>
|
||||||
|
<p style="color:#888;font-size:12px;">BreakPilot Compliance SDK</p>
|
||||||
|
""",
|
||||||
|
)
|
||||||
|
logger.info("Notified %d other roles about approval of %s", len(others), review["document_title"])
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning("Approval notification failed (non-blocking): %s", e)
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/{review_id}/reject")
|
||||||
|
def reject_review(
|
||||||
|
review_id: str,
|
||||||
|
body: ReviewReject,
|
||||||
|
db: Session = Depends(get_db),
|
||||||
|
tenant_id: str = Depends(_get_tenant_id),
|
||||||
|
):
|
||||||
|
q = text("""
|
||||||
|
UPDATE compliance_document_reviews
|
||||||
|
SET status = 'rejected', reviewed_at = NOW(), review_comment = :comment, updated_at = NOW()
|
||||||
|
WHERE id = :rid AND tenant_id = :tid
|
||||||
|
RETURNING *
|
||||||
|
""")
|
||||||
|
row = db.execute(q, {"rid": review_id, "tid": tenant_id, "comment": body.comment}).fetchone()
|
||||||
|
if not row:
|
||||||
|
raise HTTPException(404, "Review not found")
|
||||||
|
db.commit()
|
||||||
|
return _row_to_dict(row)
|
||||||
@@ -0,0 +1,255 @@
|
|||||||
|
"""
|
||||||
|
FastAPI routes for Organizational Compliance Roles.
|
||||||
|
|
||||||
|
Manages the 7 standard compliance roles (DSB, GF, IT-Leiter, etc.)
|
||||||
|
and the document-to-role mapping that determines who reviews which documents.
|
||||||
|
|
||||||
|
Endpoints:
|
||||||
|
GET /org-roles — list roles for tenant/project
|
||||||
|
POST /org-roles — create/upsert a role
|
||||||
|
PUT /org-roles/{id} — update role details
|
||||||
|
DELETE /org-roles/{id} — remove a role
|
||||||
|
GET /org-roles/defaults — 7 standard role definitions
|
||||||
|
POST /org-roles/seed — seed default roles for a project
|
||||||
|
POST /org-roles/{id}/send-test — send test email to role
|
||||||
|
GET /org-roles/mapping — document-to-role mapping
|
||||||
|
PUT /org-roles/mapping — update mapping
|
||||||
|
"""
|
||||||
|
|
||||||
|
import logging
|
||||||
|
from typing import Optional, List
|
||||||
|
|
||||||
|
from fastapi import APIRouter, Depends, HTTPException, Query
|
||||||
|
from pydantic import BaseModel
|
||||||
|
from sqlalchemy import text
|
||||||
|
from sqlalchemy.orm import Session
|
||||||
|
|
||||||
|
from classroom_engine.database import get_db
|
||||||
|
from .tenant_utils import get_tenant_id as _get_tenant_id
|
||||||
|
from .db_utils import row_to_dict as _row_to_dict
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
router = APIRouter(prefix="/org-roles", tags=["org-roles"])
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# Standard role definitions
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
DEFAULT_ROLES = [
|
||||||
|
{"role_key": "dsb", "role_label": "Datenschutzbeauftragter (DSB)"},
|
||||||
|
{"role_key": "gf", "role_label": "Geschaeftsfuehrung"},
|
||||||
|
{"role_key": "it_leiter", "role_label": "IT-Leiter / CISO"},
|
||||||
|
{"role_key": "hr_leitung", "role_label": "HR-Leitung"},
|
||||||
|
{"role_key": "marketing_leitung", "role_label": "Marketing-Leitung"},
|
||||||
|
{"role_key": "compliance_beauftragter", "role_label": "Compliance-Beauftragter"},
|
||||||
|
{"role_key": "einkauf", "role_label": "Einkauf / Vendor Management"},
|
||||||
|
]
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# Schemas
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
|
||||||
|
class OrgRoleCreate(BaseModel):
|
||||||
|
role_key: str
|
||||||
|
role_label: str
|
||||||
|
person_name: Optional[str] = None
|
||||||
|
person_email: Optional[str] = None
|
||||||
|
department: Optional[str] = None
|
||||||
|
project_id: Optional[str] = None
|
||||||
|
|
||||||
|
|
||||||
|
class OrgRoleUpdate(BaseModel):
|
||||||
|
role_label: Optional[str] = None
|
||||||
|
person_name: Optional[str] = None
|
||||||
|
person_email: Optional[str] = None
|
||||||
|
department: Optional[str] = None
|
||||||
|
is_active: Optional[bool] = None
|
||||||
|
|
||||||
|
|
||||||
|
class MappingEntry(BaseModel):
|
||||||
|
document_type: str
|
||||||
|
role_key: str
|
||||||
|
is_primary: bool = True
|
||||||
|
|
||||||
|
|
||||||
|
class MappingUpdate(BaseModel):
|
||||||
|
entries: List[MappingEntry]
|
||||||
|
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# Routes
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("")
|
||||||
|
def list_roles(
|
||||||
|
project_id: Optional[str] = Query(None),
|
||||||
|
db: Session = Depends(get_db),
|
||||||
|
tenant_id: str = Depends(_get_tenant_id),
|
||||||
|
):
|
||||||
|
q = text("""
|
||||||
|
SELECT * FROM compliance_org_roles
|
||||||
|
WHERE tenant_id = :tid AND (project_id = :pid OR (:pid IS NULL AND project_id IS NULL))
|
||||||
|
ORDER BY role_key
|
||||||
|
""")
|
||||||
|
rows = db.execute(q, {"tid": tenant_id, "pid": project_id}).fetchall()
|
||||||
|
return [_row_to_dict(r) for r in rows]
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/defaults")
|
||||||
|
def get_defaults():
|
||||||
|
return DEFAULT_ROLES
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("")
|
||||||
|
def create_role(
|
||||||
|
body: OrgRoleCreate,
|
||||||
|
db: Session = Depends(get_db),
|
||||||
|
tenant_id: str = Depends(_get_tenant_id),
|
||||||
|
):
|
||||||
|
q = text("""
|
||||||
|
INSERT INTO compliance_org_roles (tenant_id, project_id, role_key, role_label, person_name, person_email, department)
|
||||||
|
VALUES (:tid, :pid, :rk, :rl, :pn, :pe, :dept)
|
||||||
|
ON CONFLICT (tenant_id, project_id, role_key) DO UPDATE
|
||||||
|
SET role_label = EXCLUDED.role_label,
|
||||||
|
person_name = COALESCE(EXCLUDED.person_name, compliance_org_roles.person_name),
|
||||||
|
person_email = COALESCE(EXCLUDED.person_email, compliance_org_roles.person_email),
|
||||||
|
department = COALESCE(EXCLUDED.department, compliance_org_roles.department),
|
||||||
|
updated_at = NOW()
|
||||||
|
RETURNING *
|
||||||
|
""")
|
||||||
|
row = db.execute(q, {
|
||||||
|
"tid": tenant_id, "pid": body.project_id, "rk": body.role_key,
|
||||||
|
"rl": body.role_label, "pn": body.person_name, "pe": body.person_email,
|
||||||
|
"dept": body.department,
|
||||||
|
}).fetchone()
|
||||||
|
db.commit()
|
||||||
|
return _row_to_dict(row)
|
||||||
|
|
||||||
|
|
||||||
|
@router.put("/{role_id}")
|
||||||
|
def update_role(
|
||||||
|
role_id: str,
|
||||||
|
body: OrgRoleUpdate,
|
||||||
|
db: Session = Depends(get_db),
|
||||||
|
tenant_id: str = Depends(_get_tenant_id),
|
||||||
|
):
|
||||||
|
sets, params = [], {"rid": role_id, "tid": tenant_id}
|
||||||
|
for field in ["role_label", "person_name", "person_email", "department", "is_active"]:
|
||||||
|
val = getattr(body, field, None)
|
||||||
|
if val is not None:
|
||||||
|
sets.append(f"{field} = :{field}")
|
||||||
|
params[field] = val
|
||||||
|
if not sets:
|
||||||
|
raise HTTPException(400, "No fields to update")
|
||||||
|
sets.append("updated_at = NOW()")
|
||||||
|
q = text(f"UPDATE compliance_org_roles SET {', '.join(sets)} WHERE id = :rid AND tenant_id = :tid RETURNING *")
|
||||||
|
row = db.execute(q, params).fetchone()
|
||||||
|
if not row:
|
||||||
|
raise HTTPException(404, "Role not found")
|
||||||
|
db.commit()
|
||||||
|
return _row_to_dict(row)
|
||||||
|
|
||||||
|
|
||||||
|
@router.delete("/{role_id}")
|
||||||
|
def delete_role(
|
||||||
|
role_id: str,
|
||||||
|
db: Session = Depends(get_db),
|
||||||
|
tenant_id: str = Depends(_get_tenant_id),
|
||||||
|
):
|
||||||
|
q = text("DELETE FROM compliance_org_roles WHERE id = :rid AND tenant_id = :tid")
|
||||||
|
result = db.execute(q, {"rid": role_id, "tid": tenant_id})
|
||||||
|
db.commit()
|
||||||
|
if result.rowcount == 0:
|
||||||
|
raise HTTPException(404, "Role not found")
|
||||||
|
return {"deleted": True}
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/seed")
|
||||||
|
def seed_roles(
|
||||||
|
project_id: Optional[str] = Query(None),
|
||||||
|
db: Session = Depends(get_db),
|
||||||
|
tenant_id: str = Depends(_get_tenant_id),
|
||||||
|
):
|
||||||
|
created = 0
|
||||||
|
for role in DEFAULT_ROLES:
|
||||||
|
q = text("""
|
||||||
|
INSERT INTO compliance_org_roles (tenant_id, project_id, role_key, role_label)
|
||||||
|
VALUES (:tid, :pid, :rk, :rl)
|
||||||
|
ON CONFLICT (tenant_id, project_id, role_key) DO NOTHING
|
||||||
|
""")
|
||||||
|
result = db.execute(q, {"tid": tenant_id, "pid": project_id, "rk": role["role_key"], "rl": role["role_label"]})
|
||||||
|
created += result.rowcount
|
||||||
|
db.commit()
|
||||||
|
return {"seeded": created, "total": len(DEFAULT_ROLES)}
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/{role_id}/send-test")
|
||||||
|
def send_test_email(
|
||||||
|
role_id: str,
|
||||||
|
db: Session = Depends(get_db),
|
||||||
|
tenant_id: str = Depends(_get_tenant_id),
|
||||||
|
):
|
||||||
|
q = text("SELECT * FROM compliance_org_roles WHERE id = :rid AND tenant_id = :tid")
|
||||||
|
role = db.execute(q, {"rid": role_id, "tid": tenant_id}).fetchone()
|
||||||
|
if not role:
|
||||||
|
raise HTTPException(404, "Role not found")
|
||||||
|
role_dict = _row_to_dict(role)
|
||||||
|
if not role_dict.get("person_email"):
|
||||||
|
raise HTTPException(400, "No email configured for this role")
|
||||||
|
|
||||||
|
try:
|
||||||
|
from compliance.services.smtp_sender import send_email
|
||||||
|
result = send_email(
|
||||||
|
recipient=role_dict["person_email"],
|
||||||
|
subject=f"[BreakPilot] Test-E-Mail fuer {role_dict['role_label']}",
|
||||||
|
body_html=f"""
|
||||||
|
<h2>Test-E-Mail</h2>
|
||||||
|
<p>Diese E-Mail bestaetigt, dass die Zustellung an die Rolle
|
||||||
|
<strong>{role_dict['role_label']}</strong> funktioniert.</p>
|
||||||
|
<p>Empfaenger: {role_dict['person_name'] or 'N/A'} ({role_dict['person_email']})</p>
|
||||||
|
<p style="color:#888;font-size:12px;">Gesendet von BreakPilot Compliance SDK</p>
|
||||||
|
""",
|
||||||
|
)
|
||||||
|
return {"sent": True, "email": role_dict["person_email"], "result": result}
|
||||||
|
except Exception as e:
|
||||||
|
logger.error("Failed to send test email: %s", e)
|
||||||
|
raise HTTPException(500, f"Email sending failed: {e}")
|
||||||
|
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# Document-to-Role Mapping
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/mapping")
|
||||||
|
def get_mapping(
|
||||||
|
db: Session = Depends(get_db),
|
||||||
|
tenant_id: str = Depends(_get_tenant_id),
|
||||||
|
):
|
||||||
|
q = text("""
|
||||||
|
SELECT * FROM compliance_document_role_mapping
|
||||||
|
WHERE tenant_id = :tid
|
||||||
|
ORDER BY document_type, role_key
|
||||||
|
""")
|
||||||
|
rows = db.execute(q, {"tid": tenant_id}).fetchall()
|
||||||
|
return [_row_to_dict(r) for r in rows]
|
||||||
|
|
||||||
|
|
||||||
|
@router.put("/mapping")
|
||||||
|
def update_mapping(
|
||||||
|
body: MappingUpdate,
|
||||||
|
db: Session = Depends(get_db),
|
||||||
|
tenant_id: str = Depends(_get_tenant_id),
|
||||||
|
):
|
||||||
|
for entry in body.entries:
|
||||||
|
q = text("""
|
||||||
|
INSERT INTO compliance_document_role_mapping (tenant_id, document_type, role_key, is_primary)
|
||||||
|
VALUES (:tid, :dt, :rk, :ip)
|
||||||
|
ON CONFLICT (tenant_id, document_type, role_key) DO UPDATE
|
||||||
|
SET is_primary = EXCLUDED.is_primary
|
||||||
|
""")
|
||||||
|
db.execute(q, {"tid": tenant_id, "dt": entry.document_type, "rk": entry.role_key, "ip": entry.is_primary})
|
||||||
|
db.commit()
|
||||||
|
return {"updated": len(body.entries)}
|
||||||
@@ -0,0 +1,177 @@
|
|||||||
|
-- Migration 111: Organizational Compliance Roles + Document Review Workflow
|
||||||
|
-- Creates tables for:
|
||||||
|
-- 1. compliance_org_roles — role assignments per project (DSB, GF, IT-Leiter, etc.)
|
||||||
|
-- 2. compliance_document_reviews — review tracking for generated documents
|
||||||
|
-- 3. compliance_document_role_mapping — which roles review which document types
|
||||||
|
|
||||||
|
BEGIN;
|
||||||
|
|
||||||
|
-- =============================================================================
|
||||||
|
-- Table 1: Organizational Compliance Roles
|
||||||
|
-- =============================================================================
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS compliance_org_roles (
|
||||||
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
|
tenant_id UUID NOT NULL,
|
||||||
|
project_id UUID,
|
||||||
|
role_key VARCHAR(50) NOT NULL,
|
||||||
|
role_label VARCHAR(200) NOT NULL,
|
||||||
|
person_name VARCHAR(300),
|
||||||
|
person_email VARCHAR(300),
|
||||||
|
department VARCHAR(200),
|
||||||
|
is_active BOOLEAN NOT NULL DEFAULT TRUE,
|
||||||
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||||
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||||
|
UNIQUE(tenant_id, project_id, role_key)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_org_roles_tenant ON compliance_org_roles(tenant_id);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_org_roles_project ON compliance_org_roles(tenant_id, project_id);
|
||||||
|
|
||||||
|
-- =============================================================================
|
||||||
|
-- Table 2: Document Reviews
|
||||||
|
-- =============================================================================
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS compliance_document_reviews (
|
||||||
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
|
tenant_id UUID NOT NULL,
|
||||||
|
project_id UUID,
|
||||||
|
document_type VARCHAR(100) NOT NULL,
|
||||||
|
document_title VARCHAR(500) NOT NULL,
|
||||||
|
document_content_hash VARCHAR(64),
|
||||||
|
reviewer_role_key VARCHAR(50) NOT NULL,
|
||||||
|
reviewer_name VARCHAR(300),
|
||||||
|
reviewer_email VARCHAR(300),
|
||||||
|
status VARCHAR(20) NOT NULL DEFAULT 'pending'
|
||||||
|
CHECK (status IN ('pending', 'in_review', 'approved', 'rejected')),
|
||||||
|
submitted_at TIMESTAMPTZ,
|
||||||
|
submitted_by VARCHAR(200),
|
||||||
|
reviewed_at TIMESTAMPTZ,
|
||||||
|
review_comment TEXT,
|
||||||
|
review_link TEXT,
|
||||||
|
email_sent BOOLEAN NOT NULL DEFAULT FALSE,
|
||||||
|
email_sent_at TIMESTAMPTZ,
|
||||||
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||||
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_doc_reviews_tenant ON compliance_document_reviews(tenant_id);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_doc_reviews_status ON compliance_document_reviews(tenant_id, status);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_doc_reviews_doctype ON compliance_document_reviews(document_type);
|
||||||
|
|
||||||
|
-- =============================================================================
|
||||||
|
-- Table 3: Document-to-Role Mapping (seed defaults)
|
||||||
|
-- =============================================================================
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS compliance_document_role_mapping (
|
||||||
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
|
tenant_id UUID NOT NULL,
|
||||||
|
document_type VARCHAR(100) NOT NULL,
|
||||||
|
role_key VARCHAR(50) NOT NULL,
|
||||||
|
is_primary BOOLEAN NOT NULL DEFAULT TRUE,
|
||||||
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||||
|
UNIQUE(tenant_id, document_type, role_key)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_role_mapping_tenant ON compliance_document_role_mapping(tenant_id);
|
||||||
|
|
||||||
|
-- =============================================================================
|
||||||
|
-- Seed: Default document-to-role mapping (tenant_id = '__default__')
|
||||||
|
-- Every document type has at least one primary reviewer.
|
||||||
|
-- Tenants get a copy on first use; editable per tenant.
|
||||||
|
-- =============================================================================
|
||||||
|
|
||||||
|
-- DSB — Datenschutzbeauftragter
|
||||||
|
INSERT INTO compliance_document_role_mapping (tenant_id, document_type, role_key) VALUES
|
||||||
|
('__default__', 'privacy_policy', 'dsb'), ('__default__', 'vvt_register', 'dsb'),
|
||||||
|
('__default__', 'tom_documentation', 'dsb'), ('__default__', 'dsfa', 'dsb'),
|
||||||
|
('__default__', 'loeschkonzept', 'dsb'), ('__default__', 'dpa', 'dsb'),
|
||||||
|
('__default__', 'pflichtenregister', 'dsb'), ('__default__', 'data_protection_concept', 'dsb'),
|
||||||
|
('__default__', 'data_protection_policy', 'dsb'), ('__default__', 'data_retention_policy', 'dsb'),
|
||||||
|
('__default__', 'data_transfer_policy', 'dsb'), ('__default__', 'data_classification_policy', 'dsb'),
|
||||||
|
('__default__', 'privacy_incident_policy', 'dsb'), ('__default__', 'informationspflichten', 'dsb'),
|
||||||
|
('__default__', 'verpflichtungserklaerung', 'dsb'), ('__default__', 'consent_texts', 'dsb'),
|
||||||
|
('__default__', 'dsr_process_art15', 'dsb'), ('__default__', 'dsr_process_art16', 'dsb'),
|
||||||
|
('__default__', 'dsr_process_art17', 'dsb'), ('__default__', 'dsr_process_art18', 'dsb'),
|
||||||
|
('__default__', 'dsr_process_art19', 'dsb'), ('__default__', 'dsr_process_art20', 'dsb'),
|
||||||
|
('__default__', 'dsr_process_art21', 'dsb')
|
||||||
|
ON CONFLICT DO NOTHING;
|
||||||
|
|
||||||
|
-- GF — Geschaeftsfuehrung
|
||||||
|
INSERT INTO compliance_document_role_mapping (tenant_id, document_type, role_key) VALUES
|
||||||
|
('__default__', 'agb', 'gf'), ('__default__', 'terms_of_use', 'gf'),
|
||||||
|
('__default__', 'nda', 'gf'), ('__default__', 'sla', 'gf'),
|
||||||
|
('__default__', 'impressum', 'gf'), ('__default__', 'widerruf', 'gf'),
|
||||||
|
('__default__', 'whistleblower_policy', 'gf'),
|
||||||
|
('__default__', 'cloud_service_agreement', 'gf'),
|
||||||
|
('__default__', 'standard_operating_procedure', 'gf'),
|
||||||
|
('__default__', 'data_usage_clause', 'gf')
|
||||||
|
ON CONFLICT DO NOTHING;
|
||||||
|
|
||||||
|
-- IT-Leiter / CISO
|
||||||
|
INSERT INTO compliance_document_role_mapping (tenant_id, document_type, role_key) VALUES
|
||||||
|
('__default__', 'it_security_concept', 'it_leiter'),
|
||||||
|
('__default__', 'isms_manual', 'it_leiter'),
|
||||||
|
('__default__', 'backup_recovery_concept', 'it_leiter'),
|
||||||
|
('__default__', 'logging_concept', 'it_leiter'),
|
||||||
|
('__default__', 'incident_response_plan', 'it_leiter'),
|
||||||
|
('__default__', 'access_control_concept', 'it_leiter'),
|
||||||
|
('__default__', 'risk_management_concept', 'it_leiter'),
|
||||||
|
('__default__', 'information_security_policy', 'it_leiter'),
|
||||||
|
('__default__', 'access_control_policy', 'it_leiter'),
|
||||||
|
('__default__', 'password_policy', 'it_leiter'),
|
||||||
|
('__default__', 'encryption_policy', 'it_leiter'),
|
||||||
|
('__default__', 'logging_policy', 'it_leiter'),
|
||||||
|
('__default__', 'backup_policy', 'it_leiter'),
|
||||||
|
('__default__', 'incident_response_policy', 'it_leiter'),
|
||||||
|
('__default__', 'change_management_policy', 'it_leiter'),
|
||||||
|
('__default__', 'patch_management_policy', 'it_leiter'),
|
||||||
|
('__default__', 'asset_management_policy', 'it_leiter'),
|
||||||
|
('__default__', 'cloud_security_policy', 'it_leiter'),
|
||||||
|
('__default__', 'devsecops_policy', 'it_leiter'),
|
||||||
|
('__default__', 'secrets_management_policy', 'it_leiter'),
|
||||||
|
('__default__', 'vulnerability_management_policy', 'it_leiter'),
|
||||||
|
('__default__', 'cybersecurity_policy', 'it_leiter'),
|
||||||
|
('__default__', 'business_continuity_policy', 'it_leiter'),
|
||||||
|
('__default__', 'disaster_recovery_policy', 'it_leiter'),
|
||||||
|
('__default__', 'crisis_management_policy', 'it_leiter')
|
||||||
|
ON CONFLICT DO NOTHING;
|
||||||
|
|
||||||
|
-- HR-Leitung
|
||||||
|
INSERT INTO compliance_document_role_mapping (tenant_id, document_type, role_key) VALUES
|
||||||
|
('__default__', 'employee_dsi', 'hr_leitung'),
|
||||||
|
('__default__', 'applicant_dsi', 'hr_leitung'),
|
||||||
|
('__default__', 'employee_security_policy', 'hr_leitung'),
|
||||||
|
('__default__', 'security_awareness_policy', 'hr_leitung'),
|
||||||
|
('__default__', 'remote_work_policy', 'hr_leitung'),
|
||||||
|
('__default__', 'offboarding_policy', 'hr_leitung'),
|
||||||
|
('__default__', 'byod_policy', 'hr_leitung')
|
||||||
|
ON CONFLICT DO NOTHING;
|
||||||
|
|
||||||
|
-- Marketing-Leitung
|
||||||
|
INSERT INTO compliance_document_role_mapping (tenant_id, document_type, role_key) VALUES
|
||||||
|
('__default__', 'cookie_banner', 'marketing_leitung'),
|
||||||
|
('__default__', 'cookie_policy', 'marketing_leitung'),
|
||||||
|
('__default__', 'social_media_dsi', 'marketing_leitung'),
|
||||||
|
('__default__', 'community_guidelines', 'marketing_leitung'),
|
||||||
|
('__default__', 'acceptable_use', 'marketing_leitung'),
|
||||||
|
('__default__', 'media_content_policy', 'marketing_leitung'),
|
||||||
|
('__default__', 'copyright_policy', 'marketing_leitung'),
|
||||||
|
('__default__', 'video_conference_dsi', 'marketing_leitung')
|
||||||
|
ON CONFLICT DO NOTHING;
|
||||||
|
|
||||||
|
-- Compliance-Beauftragter
|
||||||
|
INSERT INTO compliance_document_role_mapping (tenant_id, document_type, role_key) VALUES
|
||||||
|
('__default__', 'ai_usage_policy', 'compliance_beauftragter')
|
||||||
|
ON CONFLICT DO NOTHING;
|
||||||
|
|
||||||
|
-- Einkauf / Vendor Management
|
||||||
|
INSERT INTO compliance_document_role_mapping (tenant_id, document_type, role_key) VALUES
|
||||||
|
('__default__', 'vendor_risk_management_policy', 'einkauf'),
|
||||||
|
('__default__', 'third_party_security_policy', 'einkauf'),
|
||||||
|
('__default__', 'supplier_security_policy', 'einkauf'),
|
||||||
|
('__default__', 'transfer_impact_assessment', 'einkauf'),
|
||||||
|
('__default__', 'scc_companion', 'einkauf')
|
||||||
|
ON CONFLICT DO NOTHING;
|
||||||
|
|
||||||
|
COMMIT;
|
||||||
@@ -0,0 +1,137 @@
|
|||||||
|
-- Migration 112: Standard Operating Procedure (SOP) Template
|
||||||
|
-- ISO 9001-konforme Struktur, eigene Formulierung (kein Normtext)
|
||||||
|
|
||||||
|
INSERT INTO compliance_legal_templates (
|
||||||
|
tenant_id, document_type, template_type, language, jurisdiction,
|
||||||
|
document_title, description, version, status,
|
||||||
|
text, placeholders,
|
||||||
|
license_id, license_name, source_name, attribution_required
|
||||||
|
) VALUES (
|
||||||
|
'__default__',
|
||||||
|
'standard_operating_procedure',
|
||||||
|
'standard_operating_procedure',
|
||||||
|
'de',
|
||||||
|
'DE',
|
||||||
|
'Standard Operating Procedure (SOP)',
|
||||||
|
'Vorlage fuer standardisierte Verfahrensanweisungen mit Rollen-Matrix, Ablaufbeschreibung und Freigabeprozess',
|
||||||
|
'1.0',
|
||||||
|
'published',
|
||||||
|
$template$# Standard Operating Procedure (SOP)
|
||||||
|
|
||||||
|
**{{COMPANY_NAME}}** | SOP-Nr.: {{SOP_NUMBER}} | Version {{DOCUMENT_VERSION}} | Stand: {{VERSION_DATE}}
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Zweck
|
||||||
|
|
||||||
|
{{SOP_PURPOSE}}
|
||||||
|
|
||||||
|
## 2. Geltungsbereich
|
||||||
|
|
||||||
|
{{SOP_SCOPE}}
|
||||||
|
|
||||||
|
## 3. Verantwortlichkeiten
|
||||||
|
|
||||||
|
| Rolle | Verantwortung | Name |
|
||||||
|
|-------|---------------|------|
|
||||||
|
| Ersteller | Erstellung und Pflege dieser SOP | {{SOP_AUTHOR_NAME}} |
|
||||||
|
| Pruefer/in | Fachliche Pruefung auf Richtigkeit | {{SOP_REVIEWER_NAME}} |
|
||||||
|
| Freigebende/r | Formale Freigabe und Inkraftsetzung | {{SOP_APPROVER_NAME}} |
|
||||||
|
| Durchfuehrende/r | Operative Umsetzung des Verfahrens | {{SOP_EXECUTOR_NAME}} |
|
||||||
|
|
||||||
|
### Zusaetzliche Rollen
|
||||||
|
|
||||||
|
{{#IF HAS_ADDITIONAL_ROLES}}
|
||||||
|
| Rolle | Verantwortung | Kontakt |
|
||||||
|
|-------|---------------|---------|
|
||||||
|
{{SOP_ADDITIONAL_ROLES}}
|
||||||
|
{{/IF}}
|
||||||
|
|
||||||
|
## 4. Begriffe und Abkuerzungen
|
||||||
|
|
||||||
|
{{SOP_DEFINITIONS}}
|
||||||
|
|
||||||
|
## 5. Ablaufbeschreibung
|
||||||
|
|
||||||
|
### 5.1 Voraussetzungen
|
||||||
|
|
||||||
|
{{SOP_PREREQUISITES}}
|
||||||
|
|
||||||
|
### 5.2 Durchfuehrung
|
||||||
|
|
||||||
|
{{SOP_PROCEDURE_STEPS}}
|
||||||
|
|
||||||
|
### 5.3 Nachbereitung und Qualitaetskontrolle
|
||||||
|
|
||||||
|
{{SOP_POST_STEPS}}
|
||||||
|
|
||||||
|
### 5.4 Eskalation
|
||||||
|
|
||||||
|
{{#IF HAS_ESCALATION}}
|
||||||
|
Bei Abweichungen vom Standardprozess gilt folgender Eskalationsweg:
|
||||||
|
|
||||||
|
{{SOP_ESCALATION_PATH}}
|
||||||
|
{{/IF}}
|
||||||
|
{{#IF_NOT HAS_ESCALATION}}
|
||||||
|
Abweichungen sind dem/der Freigebenden ({{SOP_APPROVER_NAME}}) unverzueglich zu melden.
|
||||||
|
{{/IF_NOT}}
|
||||||
|
|
||||||
|
## 6. Dokumentation und Nachweise
|
||||||
|
|
||||||
|
{{SOP_DOCUMENTATION_REQUIREMENTS}}
|
||||||
|
|
||||||
|
**Aufbewahrungsfrist:** {{SOP_RETENTION_PERIOD}}
|
||||||
|
|
||||||
|
**Speicherort:** {{SOP_STORAGE_LOCATION}}
|
||||||
|
|
||||||
|
## 7. Abweichungsbehandlung
|
||||||
|
|
||||||
|
{{SOP_DEVIATION_HANDLING}}
|
||||||
|
|
||||||
|
## 8. Referenzen
|
||||||
|
|
||||||
|
{{SOP_REFERENCES}}
|
||||||
|
|
||||||
|
## 9. Revisionshistorie
|
||||||
|
|
||||||
|
| Version | Datum | Autor | Aenderungen |
|
||||||
|
|---------|-------|-------|-------------|
|
||||||
|
| {{DOCUMENT_VERSION}} | {{VERSION_DATE}} | {{SOP_AUTHOR_NAME}} | Erstversion |
|
||||||
|
|
||||||
|
## 10. Freigabe
|
||||||
|
|
||||||
|
| Rolle | Name | Datum | Unterschrift |
|
||||||
|
|-------|------|-------|-------------|
|
||||||
|
| Erstellt von | {{SOP_AUTHOR_NAME}} | {{VERSION_DATE}} | _______________ |
|
||||||
|
| Geprueft von | {{SOP_REVIEWER_NAME}} | | _______________ |
|
||||||
|
| Freigegeben von | {{SOP_APPROVER_NAME}} | | _______________ |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
*Dieses Dokument wurde mit dem BreakPilot Compliance SDK erstellt.*
|
||||||
|
$template$,
|
||||||
|
'[
|
||||||
|
{"key": "{{COMPANY_NAME}}", "label": "Firmenname", "required": true, "section": "PROVIDER"},
|
||||||
|
{"key": "{{SOP_NUMBER}}", "label": "SOP-Nummer (z.B. SOP-DS-001)", "required": true, "section": "LEGAL"},
|
||||||
|
{"key": "{{DOCUMENT_VERSION}}", "label": "Version (z.B. 1.0)", "required": true, "section": "LEGAL"},
|
||||||
|
{"key": "{{VERSION_DATE}}", "label": "Datum (YYYY-MM-DD)", "required": true, "section": "LEGAL"},
|
||||||
|
{"key": "{{SOP_PURPOSE}}", "label": "Zweck der SOP", "required": true, "section": "LEGAL"},
|
||||||
|
{"key": "{{SOP_SCOPE}}", "label": "Geltungsbereich", "required": true, "section": "LEGAL"},
|
||||||
|
{"key": "{{SOP_AUTHOR_NAME}}", "label": "Ersteller (Name)", "required": true, "section": "PROVIDER"},
|
||||||
|
{"key": "{{SOP_REVIEWER_NAME}}", "label": "Pruefer/in (Name)", "required": true, "section": "PROVIDER"},
|
||||||
|
{"key": "{{SOP_APPROVER_NAME}}", "label": "Freigebende/r (Name)", "required": true, "section": "PROVIDER"},
|
||||||
|
{"key": "{{SOP_EXECUTOR_NAME}}", "label": "Durchfuehrende/r (Name)", "required": true, "section": "PROVIDER"},
|
||||||
|
{"key": "{{SOP_ADDITIONAL_ROLES}}", "label": "Weitere Rollen (Markdown-Tabelle)", "required": false, "section": "PROVIDER"},
|
||||||
|
{"key": "{{SOP_DEFINITIONS}}", "label": "Begriffe und Abkuerzungen", "required": false, "section": "LEGAL"},
|
||||||
|
{"key": "{{SOP_PREREQUISITES}}", "label": "Voraussetzungen", "required": true, "section": "LEGAL"},
|
||||||
|
{"key": "{{SOP_PROCEDURE_STEPS}}", "label": "Ablaufbeschreibung (Schritte)", "required": true, "section": "LEGAL"},
|
||||||
|
{"key": "{{SOP_POST_STEPS}}", "label": "Nachbereitung / QS", "required": false, "section": "LEGAL"},
|
||||||
|
{"key": "{{SOP_ESCALATION_PATH}}", "label": "Eskalationsweg", "required": false, "section": "LEGAL"},
|
||||||
|
{"key": "{{SOP_DOCUMENTATION_REQUIREMENTS}}", "label": "Dokumentationspflichten", "required": true, "section": "LEGAL"},
|
||||||
|
{"key": "{{SOP_RETENTION_PERIOD}}", "label": "Aufbewahrungsfrist", "required": false, "section": "LEGAL"},
|
||||||
|
{"key": "{{SOP_STORAGE_LOCATION}}", "label": "Speicherort", "required": false, "section": "LEGAL"},
|
||||||
|
{"key": "{{SOP_DEVIATION_HANDLING}}", "label": "Abweichungsbehandlung", "required": true, "section": "LEGAL"},
|
||||||
|
{"key": "{{SOP_REFERENCES}}", "label": "Referenzen / Normen", "required": false, "section": "LEGAL"}
|
||||||
|
]'::jsonb,
|
||||||
|
'mit', 'MIT License', 'BreakPilot Compliance', false
|
||||||
|
) ON CONFLICT DO NOTHING;
|
||||||
Reference in New Issue
Block a user