bmc_hub/app/settings/backend/email_templates.py
Christian 56d6d45aa2 feat(sag): Add Varekøb & Salg module with database migration and frontend template
- Created a new SQL migration for the sag_salgsvarer table to manage sales and purchase items.
- Implemented a new HTML template for the Varekøb & Salg module, including summary cards and tables for sales and purchases.
- Added JavaScript functions for loading and rendering order data dynamically.
- Introduced a new backend search module for customers, contacts, hardware, and locations with autocomplete functionality.
- Developed an email templates API for managing system and customer-specific email templates.
- Created multiple migrations for Nextcloud instances, cache, audit logs, email templates, sag comments, hardware locations, and billing methods.
- Enhanced the sag module with solutions, order lines, work types, and 2FA support for user authentication.
2026-02-02 20:23:56 +01:00

226 lines
7.6 KiB
Python

"""
Email Templates API Router
Management of system and customer-specific email templates
"""
from typing import List, Optional, Dict, Any
from fastapi import APIRouter, HTTPException, Query
from pydantic import BaseModel, Json
from app.core.database import execute_query
import json
import logging
from datetime import datetime
logger = logging.getLogger(__name__)
router = APIRouter(prefix="/email-templates", tags=["Email Templates"])
# --- Models ---
class EmailTemplateBase(BaseModel):
name: str
slug: str
subject: str
body: str
category: str = "general"
description: Optional[str] = None
variables: Dict[str, str] = {}
customer_id: Optional[int] = None
class EmailTemplateCreate(EmailTemplateBase):
pass
class EmailTemplateUpdate(BaseModel):
name: Optional[str] = None
subject: Optional[str] = None
body: Optional[str] = None
category: Optional[str] = None
description: Optional[str] = None
variables: Optional[Dict[str, str]] = None
customer_id: Optional[int] = None
class EmailTemplate(BaseModel):
id: int
name: str
slug: str
subject: str
body: str
category: str
description: Optional[str] = None
variables: Optional[Dict[str, str]] = {}
is_system: bool
customer_id: Optional[int] = None
created_at: datetime
updated_at: datetime
# --- Endpoints ---
@router.get("/", response_model=List[EmailTemplate])
async def get_email_templates(
category: Optional[str] = None,
customer_id: Optional[int] = None
):
"""
Get all email templates.
Optionally filter by category or specific customer.
"""
sql = """
SELECT id, name, slug, subject, body, category, description, variables,
is_system, customer_id, created_at, updated_at
FROM email_templates
WHERE 1=1
"""
params = []
if category:
sql += " AND category = %s"
params.append(category)
if customer_id is not None:
# Fetch global templates (customer_id IS NULL) AND this specific customer's templates
sql += " AND (customer_id IS NULL OR customer_id = %s)"
params.append(customer_id)
sql += " ORDER BY category, name"
rows = execute_query(sql, tuple(params))
return rows
@router.get("/{template_id}", response_model=EmailTemplate)
async def get_email_template(template_id: int):
"""Get a single template by ID"""
sql = """
SELECT id, name, slug, subject, body, category, description, variables,
is_system, customer_id, created_at, updated_at
FROM email_templates
WHERE id = %s
"""
rows = execute_query(sql, (template_id,))
if not rows:
raise HTTPException(status_code=404, detail="Template not found")
return rows[0]
@router.post("/", response_model=EmailTemplate)
async def create_email_template(template: EmailTemplateCreate):
"""Create a new email template"""
# Check for slug uniqueness
check_sql = "SELECT id FROM email_templates WHERE slug = %s AND (customer_id IS NULL OR customer_id = %s)"
check_val = (template.customer_id,) if template.customer_id else (None,)
# If customer_id is None, we check where customer_id IS NULL.
# Actually, SQL `slug = %s AND customer_id = %s` works if we handle NULL correctly in python -> SQL
# Simpler uniqueness check in python logic tailored to the constraint
if template.customer_id:
existing = execute_query(
"SELECT id FROM email_templates WHERE slug = %s AND customer_id = %s",
(template.slug, template.customer_id)
)
else:
existing = execute_query(
"SELECT id FROM email_templates WHERE slug = %s AND customer_id IS NULL",
(template.slug,)
)
if existing:
raise HTTPException(status_code=400, detail=f"A template with slug '{template.slug}' already exists for this context.")
sql = """
INSERT INTO email_templates
(name, slug, subject, body, category, description, variables, customer_id, is_system)
VALUES (%s, %s, %s, %s, %s, %s, %s, %s, FALSE)
RETURNING id, name, slug, subject, body, category, description, variables,
is_system, customer_id, created_at, updated_at
"""
params = (
template.name,
template.slug,
template.subject,
template.body,
template.category,
template.description,
json.dumps(template.variables),
template.customer_id
)
try:
rows = execute_query(sql, params)
return rows[0]
except Exception as e:
logger.error(f"Error creating template: {e}")
raise HTTPException(status_code=500, detail=str(e))
@router.put("/{template_id}", response_model=EmailTemplate)
async def update_email_template(template_id: int, update: EmailTemplateUpdate):
"""Update an existing template"""
# Verify existence
current = execute_query("SELECT is_system FROM email_templates WHERE id = %s", (template_id,))
if not current:
raise HTTPException(status_code=404, detail="Template not found")
is_system = current[0]['is_system']
# Prevent changing critical fields on system templates?
# Usually we allow editing subject/body, but maybe not slug.
# For now, we allow everything except slug changes on system templates if defined so,
# but the logic below updates whatever is provided.
fields = []
params = []
if update.name is not None:
fields.append("name = %s")
params.append(update.name)
if update.subject is not None:
fields.append("subject = %s")
params.append(update.subject)
if update.body is not None:
fields.append("body = %s")
params.append(update.body)
if update.category is not None:
fields.append("category = %s")
params.append(update.category)
if update.description is not None:
fields.append("description = %s")
params.append(update.description)
if update.variables is not None:
fields.append("variables = %s")
params.append(json.dumps(update.variables))
if update.customer_id is not None:
fields.append("customer_id = %s")
params.append(update.customer_id)
# Don't change slug if it's a system template?
# Usually slugs are fixed for system templates so the code can find them.
# But for now we don't expose slug update in the provided model if we want to be strict.
# Wait, the frontend might need to update other things.
# Let's assume we don't update slug for now via this endpoint as per my minimalist implementation.
if not fields:
raise HTTPException(status_code=400, detail="No fields to update")
fields.append("updated_at = NOW()")
sql = f"UPDATE email_templates SET {', '.join(fields)} WHERE id = %s RETURNING *"
params.append(template_id)
rows = execute_query(sql, tuple(params))
return rows[0]
@router.delete("/{template_id}")
async def delete_email_template(template_id: int):
"""Delete a template (Non-system only)"""
current = execute_query("SELECT is_system FROM email_templates WHERE id = %s", (template_id,))
if not current:
raise HTTPException(status_code=404, detail="Template not found")
if current[0]['is_system']:
raise HTTPException(status_code=403, detail="Cannot delete system templates")
execute_query("DELETE FROM email_templates WHERE id = %s", (template_id,))
return {"message": "Template deleted successfully"}