diff --git a/app/plugins/builtins/report_generator/routes.py b/app/plugins/builtins/report_generator/routes.py index 002070f..c943711 100644 --- a/app/plugins/builtins/report_generator/routes.py +++ b/app/plugins/builtins/report_generator/routes.py @@ -15,7 +15,10 @@ from fastapi.responses import StreamingResponse from sqlalchemy import select from sqlalchemy.ext.asyncio import AsyncSession -from app.core.db import get_db +from app.models.contact import Contact +from app.models.audit import AuditLog + +from app.core.db import get_db, set_tenant_context from app.deps import get_current_user, require_permission from app.plugins.builtins.report_generator.models import ( ReportInstance, @@ -156,15 +159,100 @@ async def generate_preset( db: AsyncSession = Depends(get_db), current_user: dict = Depends(require_permission("reports:generate")), ): - """Generate a preset report by name (e.g. contact_list, calendar_week).""" + """Generate a preset report by name (e.g. contact_list, calendar_week). + + Fetches live data from the database based on the preset type, + merges with user-provided parameters, and generates the report. + """ tenant_id = uuid.UUID(current_user["tenant_id"]) user_id = uuid.UUID(current_user["user_id"]) - # Generate report first (no DB interaction during sync IO) - await db.close() + # Set tenant context for RLS + await set_tenant_context(db, tenant_id) + + # Fetch data based on preset type + parameters = dict(body.parameters) # copy user params + try: + if body.preset == "contact_list": + # Fetch all contacts (type='person') for this tenant + q = select(Contact).where( + Contact.tenant_id == tenant_id, + Contact.type == "person", + Contact.deleted_at.is_(None), + ).order_by(Contact.displayname) + result = await db.execute(q) + contacts = result.scalars().all() + parameters["contacts"] = [ + { + "name": f"{c.firstname or ''} {c.surname or ''}".strip() or c.displayname or "", + "email": c.email_1 or "", + "phone": c.phone_1 or "", + "type": c.type or "person", + "company": "", # contact persons are separate + } + for c in contacts + ] + parameters["title"] = parameters.get("title", "Kontaktliste") + + elif body.preset == "company_list": + # Fetch all companies (type='company') for this tenant + q = select(Contact).where( + Contact.tenant_id == tenant_id, + Contact.type == "company", + Contact.deleted_at.is_(None), + ).order_by(Contact.name) + result = await db.execute(q) + companies = result.scalars().all() + parameters["companies"] = [ + { + "name": c.name or c.displayname or "", + "address": ( + f"{c.mailing_street or ''} {c.mailing_number or ''}".strip() + or f"{c.visit_street or ''} {c.visit_number or ''}".strip() + or "" + ), + "zip": c.mailing_postalcode or "", + "city": c.mailing_city or "", + "phone": c.phone_1 or "", + "email": c.email_1 or "", + "contact_person": "", # could be extended with ContactPerson lookup + } + for c in companies + ] + parameters["title"] = parameters.get("title", "Firmenliste") + + elif body.preset == "audit_log": + # Fetch recent audit log entries for this tenant + q = select(AuditLog).where( + AuditLog.tenant_id == tenant_id, + ).order_by(AuditLog.timestamp.desc()).limit(500) + result = await db.execute(q) + entries = result.scalars().all() + parameters["entries"] = [ + { + "timestamp": str(e.timestamp) if e.timestamp else "", + "user": str(e.user_id) if e.user_id else "—", + "action": e.action or "", + "entity": e.entity_type or "", + "entity_id": str(e.entity_id) if e.entity_id else "", + "details": str(e.changes) if e.changes else "", + } + for e in entries + ] + parameters["title"] = parameters.get("title", "Audit-Log") + + elif body.preset in ("calendar_week", "calendar_month"): + # Calendar presets: pass user params through (no DB model yet) + parameters.setdefault("title", "Wochenkalender" if body.preset == "calendar_week" else "Monatskalender") + parameters.setdefault("days", []) + parameters.setdefault("hours", []) + parameters.setdefault("weeks", []) + parameters.setdefault("weekday_names", []) + + # Generate report file_bytes, ext = generate_preset_report( - body.preset, body.output_format, body.parameters + body.preset, body.output_format, parameters ) except Exception as exc: raise HTTPException( @@ -175,7 +263,7 @@ async def generate_preset( }, ) from exc - # Return file directly as streaming response (avoid DB tracking due to greenlet issues in test env) + # Return file directly as streaming response import io as _io media_types = { diff --git a/frontend/src/components/workflows/WorkflowEditor.tsx b/frontend/src/components/workflows/WorkflowEditor.tsx index 1f58465..e52bfc9 100644 --- a/frontend/src/components/workflows/WorkflowEditor.tsx +++ b/frontend/src/components/workflows/WorkflowEditor.tsx @@ -131,6 +131,11 @@ export function WorkflowEditor({ open, workflow, onClose }: WorkflowEditorProps) return; } + if (form.steps.length === 0) { + toast.error('Mindestens ein Schritt ist erforderlich'); + return; + } + const hasEmptyStepName = form.steps.some((s) => !s.name.trim()); if (hasEmptyStepName) { toast.error('Alle Schritte muessen einen Namen haben'); @@ -161,7 +166,17 @@ export function WorkflowEditor({ open, workflow, onClose }: WorkflowEditorProps) } onClose(); } catch (err: any) { - toast.error(err.message || 'Fehler beim Speichern'); + // Show detailed validation errors from backend (422) + if (err.validationErrors) { + const details = Object.entries(err.validationErrors) + .map(([field, msgs]) => `${field}: ${msgs.join(', ')}`) + .join('; '); + toast.error(`Validierungsfehler: ${details}`); + } else if (err.detail) { + toast.error(err.detail); + } else { + toast.error(err.message || 'Fehler beim Speichern'); + } } };