Files

491 lines
17 KiB
Python
Executable File

"""Portal Settings model.
Stores configuration for the customer portal, including which fields
are visible and editable for customers.
"""
import json
from datetime import UTC, datetime
from typing import Any
from sqlalchemy import Column, DateTime, Integer, String, Text
from customer_portal.models import Base
# Default field configuration
DEFAULT_FIELD_CONFIG = {
"profile_fields": {
"name": {"visible": True, "editable": False, "label": "Name"},
"email": {"visible": True, "editable": False, "label": "E-Mail"},
"phone": {"visible": True, "editable": True, "label": "Telefon"},
"address_street": {"visible": True, "editable": True, "label": "Strasse"},
"address_zip": {"visible": True, "editable": True, "label": "PLZ"},
"address_city": {"visible": True, "editable": True, "label": "Ort"},
},
"sections": {
"contact": {"visible": True, "label": "Kontaktdaten"},
"address": {"visible": True, "label": "Adresse"},
"email_settings": {"visible": True, "label": "E-Mail Einstellungen"},
"account_info": {"visible": True, "label": "Konto"},
},
"wp_fields": {}, # WordPress booking fields (dynamically loaded from WP API)
"custom_fields_visible": False,
"sync_button_visible": False,
}
# Default mail configuration
DEFAULT_MAIL_CONFIG = {
"mail_server": "",
"mail_port": 587,
"mail_use_tls": True,
"mail_use_ssl": False,
"mail_username": "",
"mail_password": "", # Stored encrypted or empty
"mail_default_sender": "",
"mail_default_sender_name": "Kundenportal",
}
# Default OTP configuration
DEFAULT_OTP_CONFIG = {
"otp_expiry_minutes": 10,
"otp_length": 6,
"otp_max_attempts": 3,
"prefill_token_expiry": 300, # 5 minutes
}
# Default WordPress configuration
DEFAULT_WORDPRESS_CONFIG = {
"wp_api_url": "",
"wp_api_secret": "",
"wp_booking_page_url": "",
}
# Default customer preferences for new customers
DEFAULT_CUSTOMER_DEFAULTS = {
"email_notifications": True, # Buchungsbestaetigungen
"email_reminders": True, # Kurserinnerungen
"email_invoices": True, # Rechnungen
"email_marketing": False, # Marketing/Newsletter (Opt-in)
}
# Default field mapping (Portal → WordPress)
# Format: portal_field: wp_field
# When syncing, data flows: WordPress booking → Portal field
DEFAULT_FIELD_MAPPING = {
"mappings": {
# Portal field → WordPress field name
"db.phone": "telefon",
"db.address_street": "adresse",
"db.address_zip": "", # Not mapped by default
"db.address_city": "", # Not mapped by default
"custom.vorname": "vorname",
"custom.nachname": "nachname",
"custom.geburtsdatum": "geburtsdatum",
"custom.pferdename": "name_des_pferdes",
"custom.geschlecht_pferd": "geschlecht_des_pferdes",
},
"auto_sync_on_booking": True, # Sync when new booking arrives
}
# Default admin customer view configuration
DEFAULT_ADMIN_CUSTOMER_VIEW = {
"field_labels": {
# Standard-Labels fuer bekannte Felder
"address_zip": "PLZ",
"address_city": "Ort",
"address_street": "Strasse",
"vorname": "Vorname",
"nachname": "Nachname",
"geburtsdatum": "Geburtsdatum",
"pferdename": "Pferdename",
"geschlecht_pferd": "Geschlecht Pferd",
},
"hidden_fields": [], # Diese Felder nie anzeigen
"field_order": [], # Reihenfolge (leer = alphabetisch)
"contact_fields": ["name", "email", "phone", "address"], # In Kontaktdaten anzeigen
"sections": {
"contact": {"visible": True, "label": "Kontaktdaten"},
"personal": {"visible": True, "label": "Personendaten"},
"email_settings": {"visible": True, "label": "E-Mail-Einstellungen"},
"account_info": {"visible": True, "label": "Konto-Informationen"},
},
}
# Default branding configuration
DEFAULT_BRANDING_CONFIG = {
"company_name": "Kundenportal",
"logo_url": "",
"favicon_url": "",
"colors": {
"primary": "#198754", # Buttons, Links (Bootstrap success green)
"primary_hover": "#157347", # Hover states
"background": "#1a1d21", # Page background
"header_bg": "#212529", # Topbar/Sidebar background
"sidebar_bg": "#1a1d21", # Sidebar background
"text": "#f8f9fa", # Main text color
"muted": "#6c757d", # Muted text
"border": "#2d3238", # Border color
},
}
# Default CSV export/import configuration
DEFAULT_CSV_CONFIG = {
"export_fields": [
{"key": "id", "label": "ID", "enabled": True},
{"key": "email", "label": "E-Mail", "enabled": True},
{"key": "name", "label": "Name", "enabled": True},
{"key": "phone", "label": "Telefon", "enabled": True},
{"key": "address_street", "label": "Strasse", "enabled": True},
{"key": "address_zip", "label": "PLZ", "enabled": True},
{"key": "address_city", "label": "Ort", "enabled": True},
{
"key": "email_notifications",
"label": "E-Mail-Benachrichtigungen",
"enabled": True,
},
{"key": "email_reminders", "label": "E-Mail-Erinnerungen", "enabled": True},
{"key": "email_invoices", "label": "E-Mail-Rechnungen", "enabled": True},
{"key": "email_marketing", "label": "E-Mail-Marketing", "enabled": True},
{"key": "created_at", "label": "Erstellt am", "enabled": False},
{"key": "updated_at", "label": "Aktualisiert am", "enabled": False},
],
"include_custom_fields": False, # Include custom fields (Personendaten) as JSON
"delimiter": ";", # CSV delimiter
}
class PortalSettings(Base):
"""Portal configuration settings.
Stores all portal settings as JSON for flexibility.
Uses a key-value approach for different setting groups.
"""
__tablename__ = "portal_settings"
id = Column(Integer, primary_key=True)
key = Column(String(100), unique=True, nullable=False, index=True)
value = Column(Text, nullable=False, default="{}")
updated_at = Column(
DateTime,
default=lambda: datetime.now(UTC),
onupdate=lambda: datetime.now(UTC),
)
def __repr__(self) -> str:
return f"<PortalSettings {self.key}>"
def get_value(self) -> Any:
"""Get setting value as Python object."""
try:
return json.loads(self.value)
except (json.JSONDecodeError, TypeError):
return {}
def set_value(self, data: Any) -> None:
"""Set setting value from Python object."""
self.value = json.dumps(data, ensure_ascii=False)
@classmethod
def get_setting(cls, db, key: str, default: Any = None) -> Any:
"""Get a setting by key.
Args:
db: Database session
key: Setting key
default: Default value if not found
Returns:
Setting value or default
"""
setting = db.query(cls).filter(cls.key == key).first()
if setting:
return setting.get_value()
return default
@classmethod
def set_setting(cls, db, key: str, value: Any) -> "PortalSettings":
"""Set a setting by key.
Args:
db: Database session
key: Setting key
value: Setting value
Returns:
PortalSettings instance
"""
setting = db.query(cls).filter(cls.key == key).first()
if not setting:
setting = cls(key=key)
db.add(setting)
setting.set_value(value)
db.commit()
return setting
@classmethod
def get_field_config(cls, db) -> dict:
"""Get the field configuration.
Returns merged default config with saved config.
Args:
db: Database session
Returns:
Field configuration dictionary
"""
saved = cls.get_setting(db, "field_config", {})
# Merge with defaults
config = DEFAULT_FIELD_CONFIG.copy()
if saved:
# Deep merge for profile_fields
if "profile_fields" in saved:
for field, settings in saved["profile_fields"].items():
if field in config["profile_fields"]:
config["profile_fields"][field].update(settings)
else:
config["profile_fields"][field] = settings
# Deep merge for sections
if "sections" in saved:
for section, settings in saved["sections"].items():
if section in config["sections"]:
config["sections"][section].update(settings)
else:
config["sections"][section] = settings
# WordPress fields (complete override, not merge)
if "wp_fields" in saved:
config["wp_fields"] = saved["wp_fields"]
# Simple overrides
if "custom_fields_visible" in saved:
config["custom_fields_visible"] = saved["custom_fields_visible"]
if "sync_button_visible" in saved:
config["sync_button_visible"] = saved["sync_button_visible"]
return config
@classmethod
def save_field_config(cls, db, config: dict) -> None:
"""Save the field configuration.
Args:
db: Database session
config: Field configuration dictionary
"""
cls.set_setting(db, "field_config", config)
# Mail configuration methods
@classmethod
def get_mail_config(cls, db) -> dict:
"""Get mail configuration with defaults."""
saved = cls.get_setting(db, "mail_config", {})
config = DEFAULT_MAIL_CONFIG.copy()
config.update(saved)
return config
@classmethod
def save_mail_config(cls, db, config: dict) -> None:
"""Save mail configuration."""
cls.set_setting(db, "mail_config", config)
# OTP configuration methods
@classmethod
def get_otp_config(cls, db) -> dict:
"""Get OTP configuration with defaults."""
saved = cls.get_setting(db, "otp_config", {})
config = DEFAULT_OTP_CONFIG.copy()
config.update(saved)
return config
@classmethod
def save_otp_config(cls, db, config: dict) -> None:
"""Save OTP configuration."""
cls.set_setting(db, "otp_config", config)
# WordPress configuration methods
@classmethod
def get_wordpress_config(cls, db) -> dict:
"""Get WordPress configuration with defaults."""
saved = cls.get_setting(db, "wordpress_config", {})
config = DEFAULT_WORDPRESS_CONFIG.copy()
config.update(saved)
return config
@classmethod
def save_wordpress_config(cls, db, config: dict) -> None:
"""Save WordPress configuration."""
cls.set_setting(db, "wordpress_config", config)
# CSV configuration methods
@classmethod
def get_csv_config(cls, db) -> dict:
"""Get CSV export/import configuration with defaults."""
saved = cls.get_setting(db, "csv_config", {})
config = {
"export_fields": DEFAULT_CSV_CONFIG["export_fields"].copy(),
"include_custom_fields": DEFAULT_CSV_CONFIG["include_custom_fields"],
"delimiter": DEFAULT_CSV_CONFIG["delimiter"],
}
if saved:
# Merge export_fields
if "export_fields" in saved:
saved_map = {f["key"]: f for f in saved["export_fields"]}
for field in config["export_fields"]:
if field["key"] in saved_map:
field.update(saved_map[field["key"]])
if "include_custom_fields" in saved:
config["include_custom_fields"] = saved["include_custom_fields"]
if "delimiter" in saved:
config["delimiter"] = saved["delimiter"]
return config
@classmethod
def save_csv_config(cls, db, config: dict) -> None:
"""Save CSV configuration."""
cls.set_setting(db, "csv_config", config)
# Customer defaults configuration methods
@classmethod
def get_customer_defaults(cls, db) -> dict:
"""Get default preferences for new customers.
These settings are applied when a new customer is created.
Args:
db: Database session
Returns:
Customer defaults dictionary
"""
saved = cls.get_setting(db, "customer_defaults", {})
config = DEFAULT_CUSTOMER_DEFAULTS.copy()
config.update(saved)
return config
@classmethod
def save_customer_defaults(cls, db, config: dict) -> None:
"""Save default preferences for new customers.
Args:
db: Database session
config: Customer defaults dictionary
"""
cls.set_setting(db, "customer_defaults", config)
# Field mapping configuration methods
@classmethod
def get_field_mapping(cls, db) -> dict:
"""Get WordPress to Portal field mapping configuration.
Args:
db: Database session
Returns:
Field mapping dictionary with 'mappings' and 'auto_sync_on_booking'
"""
saved = cls.get_setting(db, "field_mapping", {})
config = {
"mappings": DEFAULT_FIELD_MAPPING["mappings"].copy(),
"auto_sync_on_booking": DEFAULT_FIELD_MAPPING["auto_sync_on_booking"],
}
if saved:
if "mappings" in saved:
config["mappings"].update(saved["mappings"])
if "auto_sync_on_booking" in saved:
config["auto_sync_on_booking"] = saved["auto_sync_on_booking"]
return config
@classmethod
def save_field_mapping(cls, db, config: dict) -> None:
"""Save field mapping configuration.
Args:
db: Database session
config: Field mapping dictionary
"""
cls.set_setting(db, "field_mapping", config)
# Branding configuration methods
@classmethod
def get_branding(cls, db) -> dict:
"""Get branding configuration with defaults.
Args:
db: Database session
Returns:
Branding configuration dictionary
"""
saved = cls.get_setting(db, "branding", {})
config = {
"company_name": DEFAULT_BRANDING_CONFIG["company_name"],
"logo_url": DEFAULT_BRANDING_CONFIG["logo_url"],
"favicon_url": DEFAULT_BRANDING_CONFIG["favicon_url"],
"colors": DEFAULT_BRANDING_CONFIG["colors"].copy(),
}
if saved:
if "company_name" in saved:
config["company_name"] = saved["company_name"]
if "logo_url" in saved:
config["logo_url"] = saved["logo_url"]
if "favicon_url" in saved:
config["favicon_url"] = saved["favicon_url"]
if "colors" in saved:
config["colors"].update(saved["colors"])
return config
@classmethod
def save_branding(cls, db, config: dict) -> None:
"""Save branding configuration.
Args:
db: Database session
config: Branding configuration dictionary
"""
cls.set_setting(db, "branding", config)
# Admin Customer View configuration methods
@classmethod
def get_admin_customer_view(cls, db) -> dict:
"""Get admin customer view configuration with defaults.
Args:
db: Database session
Returns:
Admin customer view configuration dictionary
"""
saved = cls.get_setting(db, "admin_customer_view", {})
config = {
"field_labels": DEFAULT_ADMIN_CUSTOMER_VIEW["field_labels"].copy(),
"hidden_fields": DEFAULT_ADMIN_CUSTOMER_VIEW["hidden_fields"].copy(),
"field_order": DEFAULT_ADMIN_CUSTOMER_VIEW["field_order"].copy(),
"contact_fields": DEFAULT_ADMIN_CUSTOMER_VIEW["contact_fields"].copy(),
"sections": {
k: v.copy() for k, v in DEFAULT_ADMIN_CUSTOMER_VIEW["sections"].items()
},
}
if saved:
if "field_labels" in saved:
config["field_labels"].update(saved["field_labels"])
if "hidden_fields" in saved:
config["hidden_fields"] = saved["hidden_fields"]
if "field_order" in saved:
config["field_order"] = saved["field_order"]
if "contact_fields" in saved:
config["contact_fields"] = saved["contact_fields"]
if "sections" in saved:
for section_key, section_data in saved["sections"].items():
if section_key in config["sections"]:
config["sections"][section_key].update(section_data)
return config
@classmethod
def save_admin_customer_view(cls, db, config: dict) -> None:
"""Save admin customer view configuration.
Args:
db: Database session
config: Admin customer view configuration dictionary
"""
cls.set_setting(db, "admin_customer_view", config)