491 lines
17 KiB
Python
Executable File
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)
|