"""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"" 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)