diff --git a/plugins/zammad/alembic.ini b/plugins/zammad/alembic.ini deleted file mode 100644 index ad77bf5..0000000 --- a/plugins/zammad/alembic.ini +++ /dev/null @@ -1,90 +0,0 @@ -# A generic, single database configuration. - -[alembic] -# template used to generate migration file names; The default value is %%(rev)s_%%(slug)s -# Uncomment the line below if you want the files to be prepended with date and time -# file_template = %%(year)d_%%(month).2d_%%(day).2d_%%(hour).2d%%(minute).2d-%%(rev)s_%%(slug)s - -# path to migration scripts -script_location = migrations - -# template used to generate migration file names; The default value is %%(rev)s_%%(slug)s -# Uncomment the line below if you want the files to be prepended with date and time -# file_template = %%(year)d_%%(month).2d_%%(day).2d_%%(hour).2d%%(minute).2d-%%(rev)s_%%(slug)s - -# sys.path path, will be prepended to sys.path if present. -# defaults to the current working directory. -prepend_sys_path = . - -# timezone to use when rendering the date within the migration file -# as well as the filename. -# If specified, requires the python-dateutil library that can be installed by running "pip install alembic[tz]" -# timezone = - -# max length of characters to apply to the -# "slug" field -# truncate_slug_length = 40 - -# set to 'true' to run the environment during -# the 'revision' command, regardless of autogenerate -# revision_environment = false - -# set to 'true' to allow .pyc and .pyo files without -# a source .py file to be detected as revisions in the -# versions/ directory -# sourceless = false - -# version path separator; As mentioned above, this is the character used to split -# version_locations. The default within new alembic.ini files is "os", which uses -# os.pathsep. If this key is omitted entirely, it falls back to the legacy -# behavior of splitting on spaces and/or commas. -# Valid values for version_path_separator are: -# -# version_path_separator = : -# version_path_separator = ; -# version_path_separator = space -version_path_separator = os - -# set to 'true' to search source files recursively -# in each "version_locations" directory -# new in Alembic version 1.10 -# recursive_version_locations = false - -# the output encoding used when revision files -# are written from script.py.mako -# output_encoding = utf-8 - -# Logging configuration -[loggers] -keys = root,sqlalchemy,alembic - -[handlers] -keys = console - -[formatters] -keys = generic - -[logger_root] -level = WARN -handlers = console -qualname = - -[logger_sqlalchemy] -level = WARN -handlers = -qualname = sqlalchemy.engine - -[logger_alembic] -level = INFO -handlers = -qualname = alembic - -[handler_console] -class = StreamHandler -args = (sys.stderr,) -level = NOTSET -formatter = generic - -[formatter_generic] -format = %(levelname)-5.5s [%(name)s] %(message)s -datefmt = %H:%M:%S \ No newline at end of file diff --git a/plugins/zammad/main.py b/plugins/zammad/main.py deleted file mode 100644 index e232e06..0000000 --- a/plugins/zammad/main.py +++ /dev/null @@ -1,719 +0,0 @@ -""" -Zammad Plugin Implementation -Provides integration between Enclava platform and Zammad helpdesk system -""" -from typing import Dict, Any, List, Optional -from fastapi import APIRouter, HTTPException, Depends, BackgroundTasks -from pydantic import BaseModel -import aiohttp -import asyncio -from datetime import datetime, timezone - -from app.services.base_plugin import BasePlugin, PluginContext -from app.services.plugin_database import PluginDatabaseSession, plugin_db_manager -from app.services.plugin_security import plugin_security_policy_manager -from sqlalchemy import Column, String, DateTime, Text, Boolean, Integer, ForeignKey -from sqlalchemy.ext.declarative import declarative_base -from sqlalchemy.orm import relationship -from sqlalchemy.dialects.postgresql import UUID -import uuid - - -class ZammadTicket(BaseModel): - """Zammad ticket model""" - id: str - title: str - body: str - status: str - priority: str - customer_id: str - group_id: str - created_at: datetime - updated_at: datetime - ai_summary: Optional[str] = None - - -class ZammadConfiguration(BaseModel): - """Zammad configuration model""" - name: str - zammad_url: str - api_token: str - chatbot_id: str - ai_summarization: Dict[str, Any] - sync_settings: Dict[str, Any] - webhook_settings: Dict[str, Any] - - -# Plugin database models -Base = declarative_base() - -class ZammadConfiguration(Base): - __tablename__ = "zammad_configurations" - - id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) - user_id = Column(String, nullable=False, index=True) - name = Column(String(100), nullable=False) - zammad_url = Column(String(500), nullable=False) - api_token_encrypted = Column(Text, nullable=False) - chatbot_id = Column(String(100), nullable=False) - is_active = Column(Boolean, default=True) - ai_summarization_enabled = Column(Boolean, default=True) - auto_summarize = Column(Boolean, default=True) - sync_enabled = Column(Boolean, default=True) - sync_interval_hours = Column(Integer, default=2) - created_at = Column(DateTime, default=datetime.now(timezone.utc)) - updated_at = Column(DateTime, default=datetime.now(timezone.utc), onupdate=datetime.now(timezone.utc)) - -class ZammadTicket(Base): - __tablename__ = "zammad_tickets" - - id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) - zammad_ticket_id = Column(String(50), nullable=False, index=True) - configuration_id = Column(UUID(as_uuid=True), ForeignKey("zammad_configurations.id")) - title = Column(String(500), nullable=False) - body = Column(Text) - status = Column(String(50)) - priority = Column(String(50)) - customer_id = Column(String(50)) - group_id = Column(String(50)) - ai_summary = Column(Text) - last_synced = Column(DateTime, default=datetime.now(timezone.utc)) - created_at = Column(DateTime, default=datetime.now(timezone.utc)) - updated_at = Column(DateTime, default=datetime.now(timezone.utc), onupdate=datetime.now(timezone.utc)) - - configuration = relationship("ZammadConfiguration", back_populates="tickets") - -ZammadConfiguration.tickets = relationship("ZammadTicket", back_populates="configuration") - -class ZammadPlugin(BasePlugin): - """Zammad helpdesk integration plugin with full framework integration""" - - def __init__(self, manifest, plugin_token: str): - super().__init__(manifest, plugin_token) - self.zammad_client = None - self.db_models = [ZammadConfiguration, ZammadTicket] - - async def initialize(self) -> bool: - """Initialize Zammad plugin with database setup""" - try: - self.logger.info("Initializing Zammad plugin") - - # Create database tables - await self._create_database_tables() - - # Test platform API connectivity - health = await self.api_client.get("/health") - self.logger.info(f"Platform API health: {health.get('status')}") - - # Validate security policy - policy = plugin_security_policy_manager.get_security_policy(self.plugin_id, None) - self.logger.info(f"Security policy loaded: {policy.get('max_api_calls_per_minute')} calls/min") - - self.logger.info("Zammad plugin initialized successfully") - return True - - except Exception as e: - self.logger.error(f"Failed to initialize Zammad plugin: {e}") - return False - - async def _create_database_tables(self): - """Create plugin database tables""" - try: - engine = await plugin_db_manager.get_plugin_engine(self.plugin_id) - if engine: - async with engine.begin() as conn: - await conn.run_sync(Base.metadata.create_all) - self.logger.info("Database tables created successfully") - except Exception as e: - self.logger.error(f"Failed to create database tables: {e}") - raise - - async def cleanup(self) -> bool: - """Cleanup plugin resources""" - try: - self.logger.info("Cleaning up Zammad plugin") - # Close any open connections - return True - except Exception as e: - self.logger.error(f"Error during cleanup: {e}") - return False - - def get_api_router(self) -> APIRouter: - """Return FastAPI router for Zammad endpoints""" - router = APIRouter() - - @router.get("/health") - async def health_check(): - """Plugin health check endpoint""" - return await self.health_check() - - @router.get("/tickets") - async def get_tickets(context: PluginContext = Depends(self.get_auth_context)): - """Get tickets from Zammad""" - try: - self._track_request() - - config = await self.get_active_config(context.user_id) - if not config: - raise HTTPException(status_code=404, detail="No Zammad configuration found") - - tickets = await self.fetch_tickets_from_zammad(config) - return {"tickets": tickets, "count": len(tickets)} - - except Exception as e: - self._track_request(success=False) - self.logger.error(f"Error fetching tickets: {e}") - raise HTTPException(status_code=500, detail=str(e)) - - @router.get("/tickets/{ticket_id}") - async def get_ticket(ticket_id: str, context: PluginContext = Depends(self.get_auth_context)): - """Get specific ticket from Zammad""" - try: - self._track_request() - - config = await self.get_active_config(context.user_id) - if not config: - raise HTTPException(status_code=404, detail="No Zammad configuration found") - - ticket = await self.fetch_ticket_from_zammad(config, ticket_id) - return {"ticket": ticket} - - except Exception as e: - self._track_request(success=False) - self.logger.error(f"Error fetching ticket {ticket_id}: {e}") - raise HTTPException(status_code=500, detail=str(e)) - - @router.post("/tickets/{ticket_id}/summarize") - async def summarize_ticket( - ticket_id: str, - background_tasks: BackgroundTasks, - context: PluginContext = Depends(self.get_auth_context) - ): - """Generate AI summary for ticket""" - try: - self._track_request() - - config = await self.get_active_config(context.user_id) - if not config: - raise HTTPException(status_code=404, detail="No Zammad configuration found") - - # Start summarization in background - background_tasks.add_task( - self.summarize_ticket_async, - config, - ticket_id, - context.user_id - ) - - return { - "status": "started", - "ticket_id": ticket_id, - "message": "AI summarization started in background" - } - - except Exception as e: - self._track_request(success=False) - self.logger.error(f"Error starting summarization for ticket {ticket_id}: {e}") - raise HTTPException(status_code=500, detail=str(e)) - - @router.post("/webhooks/ticket-created") - async def handle_ticket_webhook(webhook_data: Dict[str, Any]): - """Handle Zammad webhook for new tickets""" - try: - ticket_id = webhook_data.get("ticket", {}).get("id") - if not ticket_id: - raise HTTPException(status_code=400, detail="Invalid webhook data") - - self.logger.info(f"Received webhook for ticket: {ticket_id}") - - # Process webhook asynchronously - asyncio.create_task(self.process_ticket_webhook(webhook_data)) - - return {"status": "processed", "ticket_id": ticket_id} - - except Exception as e: - self.logger.error(f"Error processing webhook: {e}") - raise HTTPException(status_code=500, detail=str(e)) - - @router.get("/configurations") - async def get_configurations(context: PluginContext = Depends(self.get_auth_context)): - """Get user's Zammad configurations""" - try: - configs = await self.get_user_configurations(context.user_id) - return {"configurations": configs} - except Exception as e: - self.logger.error(f"Error fetching configurations: {e}") - raise HTTPException(status_code=500, detail=str(e)) - - @router.post("/configurations") - async def create_configuration( - config_data: Dict[str, Any], - context: PluginContext = Depends(self.get_auth_context) - ): - """Create new Zammad configuration""" - try: - # Validate configuration against schema - schema = await self.get_configuration_schema() - is_valid, errors = await self.config.validate_config(config_data, schema) - - if not is_valid: - raise HTTPException(status_code=400, detail=f"Invalid configuration: {errors}") - - # Test connection before saving - connection_test = await self.test_zammad_connection(config_data) - if not connection_test["success"]: - raise HTTPException( - status_code=400, - detail=f"Connection test failed: {connection_test['error']}" - ) - - # Save configuration to plugin database - success = await self._save_configuration_to_db(config_data, context.user_id) - if not success: - raise HTTPException(status_code=500, detail="Failed to save configuration") - - return {"status": "created", "config": {"name": config_data.get("name"), "zammad_url": config_data.get("zammad_url")}} - - except HTTPException: - raise - except Exception as e: - self.logger.error(f"Error creating configuration: {e}") - raise HTTPException(status_code=500, detail=str(e)) - - @router.get("/statistics") - async def get_statistics(context: PluginContext = Depends(self.get_auth_context)): - """Get plugin usage statistics""" - try: - stats = await self._get_plugin_statistics(context.user_id) - return stats - except Exception as e: - self.logger.error(f"Error getting statistics: {e}") - raise HTTPException(status_code=500, detail=str(e)) - - @router.get("/tickets/sync") - async def sync_tickets_manual(context: PluginContext = Depends(self.get_auth_context)): - """Manually trigger ticket sync""" - try: - result = await self._sync_user_tickets(context.user_id) - return {"status": "completed", "synced_count": result} - except Exception as e: - self.logger.error(f"Error syncing tickets: {e}") - raise HTTPException(status_code=500, detail=str(e)) - - return router - - # Plugin-specific methods - - async def get_active_config(self, user_id: str) -> Optional[Dict[str, Any]]: - """Get active Zammad configuration for user from database""" - try: - async with PluginDatabaseSession(self.plugin_id, plugin_db_manager) as db: - config = await db.query(ZammadConfiguration).filter( - ZammadConfiguration.user_id == user_id, - ZammadConfiguration.is_active == True - ).first() - - if config: - # Decrypt API token - from app.services.plugin_security import plugin_token_manager - api_token = plugin_token_manager.decrypt_plugin_secret(config.api_token_encrypted) - - return { - "id": str(config.id), - "name": config.name, - "zammad_url": config.zammad_url, - "api_token": api_token, - "chatbot_id": config.chatbot_id, - "ai_summarization": { - "enabled": config.ai_summarization_enabled, - "auto_summarize": config.auto_summarize - }, - "sync_settings": { - "enabled": config.sync_enabled, - "interval_hours": config.sync_interval_hours - } - } - return None - except Exception as e: - self.logger.error(f"Failed to get active config: {e}") - return None - - async def get_user_configurations(self, user_id: str) -> List[Dict[str, Any]]: - """Get all configurations for user from database""" - try: - async with PluginDatabaseSession(self.plugin_id, plugin_db_manager) as db: - configs = await db.query(ZammadConfiguration).filter( - ZammadConfiguration.user_id == user_id - ).all() - - result = [] - for config in configs: - result.append({ - "id": str(config.id), - "name": config.name, - "zammad_url": config.zammad_url, - "chatbot_id": config.chatbot_id, - "is_active": config.is_active, - "created_at": config.created_at.isoformat(), - "updated_at": config.updated_at.isoformat() - }) - - return result - except Exception as e: - self.logger.error(f"Failed to get user configurations: {e}") - return [] - - async def fetch_tickets_from_zammad(self, config: Dict[str, Any]) -> List[Dict[str, Any]]: - """Fetch tickets from Zammad API""" - async with aiohttp.ClientSession() as session: - headers = { - "Authorization": f"Token {config['api_token']}", - "Content-Type": "application/json" - } - - async with session.get( - f"{config['zammad_url']}/api/v1/tickets", - headers=headers, - timeout=30 - ) as response: - if response.status != 200: - raise HTTPException( - status_code=response.status, - detail=f"Zammad API error: {await response.text()}" - ) - - return await response.json() - - async def fetch_ticket_from_zammad(self, config: Dict[str, Any], ticket_id: str) -> Dict[str, Any]: - """Fetch specific ticket from Zammad""" - async with aiohttp.ClientSession() as session: - headers = { - "Authorization": f"Token {config['api_token']}", - "Content-Type": "application/json" - } - - async with session.get( - f"{config['zammad_url']}/api/v1/tickets/{ticket_id}", - headers=headers, - timeout=30 - ) as response: - if response.status != 200: - raise HTTPException( - status_code=response.status, - detail=f"Zammad API error: {await response.text()}" - ) - - return await response.json() - - async def summarize_ticket_async(self, config: Dict[str, Any], ticket_id: str, user_id: str): - """Asynchronously summarize a ticket using platform AI""" - try: - # Get ticket details - ticket = await self.fetch_ticket_from_zammad(config, ticket_id) - - # Use platform chatbot API for summarization - chatbot_response = await self.api_client.call_chatbot_api( - chatbot_id=config["chatbot_id"], - message=f"Summarize this support ticket:\n\nTitle: {ticket.get('title', '')}\n\nContent: {ticket.get('body', '')}" - ) - - summary = chatbot_response.get("response", "") - - # TODO: Store summary in database - self.logger.info(f"Generated summary for ticket {ticket_id}: {summary[:100]}...") - - # Update ticket in Zammad with summary - await self.update_ticket_summary(config, ticket_id, summary) - - except Exception as e: - self.logger.error(f"Error summarizing ticket {ticket_id}: {e}") - - async def update_ticket_summary(self, config: Dict[str, Any], ticket_id: str, summary: str): - """Update ticket with AI summary""" - async with aiohttp.ClientSession() as session: - headers = { - "Authorization": f"Token {config['api_token']}", - "Content-Type": "application/json" - } - - update_data = { - "note": f"AI Summary: {summary}" - } - - async with session.put( - f"{config['zammad_url']}/api/v1/tickets/{ticket_id}", - headers=headers, - json=update_data, - timeout=30 - ) as response: - if response.status not in [200, 201]: - self.logger.error(f"Failed to update ticket {ticket_id} with summary") - - async def test_zammad_connection(self, config: Dict[str, Any]) -> Dict[str, Any]: - """Test connection to Zammad instance""" - try: - async with aiohttp.ClientSession() as session: - headers = { - "Authorization": f"Token {config['api_token']}", - "Content-Type": "application/json" - } - - async with session.get( - f"{config['zammad_url']}/api/v1/users/me", - headers=headers, - timeout=10 - ) as response: - if response.status == 200: - user_data = await response.json() - return { - "success": True, - "user": user_data.get("login", "unknown"), - "zammad_version": response.headers.get("X-Zammad-Version", "unknown") - } - else: - return { - "success": False, - "error": f"HTTP {response.status}: {await response.text()}" - } - - except Exception as e: - return { - "success": False, - "error": str(e) - } - - async def process_ticket_webhook(self, webhook_data: Dict[str, Any]): - """Process ticket webhook asynchronously""" - try: - ticket_data = webhook_data.get("ticket", {}) - ticket_id = ticket_data.get("id") - - self.logger.info(f"Processing webhook for ticket {ticket_id}") - - # TODO: Get configuration and auto-summarize if enabled - # This would require looking up the configuration associated with the webhook - - except Exception as e: - self.logger.error(f"Error processing webhook: {e}") - - # Cron job functions - - async def sync_tickets_from_zammad(self) -> bool: - """Sync tickets from Zammad (cron job)""" - try: - self.logger.info("Starting ticket sync from Zammad") - - # TODO: Get all active configurations and sync tickets - # This would iterate through all user configurations - - self.logger.info("Ticket sync completed successfully") - return True - - except Exception as e: - self.logger.error(f"Ticket sync failed: {e}") - return False - - async def cleanup_old_summaries(self) -> bool: - """Clean up old AI summaries (cron job)""" - try: - self.logger.info("Starting cleanup of old summaries") - - # TODO: Clean up summaries older than retention period - - self.logger.info("Summary cleanup completed") - return True - - except Exception as e: - self.logger.error(f"Summary cleanup failed: {e}") - return False - - async def check_zammad_connection(self) -> bool: - """Check Zammad connectivity (cron job)""" - try: - # TODO: Test all configured Zammad instances - self.logger.info("Zammad connectivity check completed") - return True - - except Exception as e: - self.logger.error(f"Connectivity check failed: {e}") - return False - - async def generate_weekly_reports(self) -> bool: - """Generate weekly reports (cron job)""" - try: - self.logger.info("Generating weekly reports") - - # TODO: Generate and send weekly ticket reports - - self.logger.info("Weekly reports generated successfully") - return True - - except Exception as e: - self.logger.error(f"Report generation failed: {e}") - return False - - # Enhanced database integration methods - - async def _save_configuration_to_db(self, config_data: Dict[str, Any], user_id: str) -> bool: - """Save Zammad configuration to plugin database""" - try: - from app.services.plugin_security import plugin_token_manager - - # Encrypt API token - encrypted_token = plugin_token_manager.encrypt_plugin_secret(config_data["api_token"]) - - async with PluginDatabaseSession(self.plugin_id, plugin_db_manager) as db: - # Deactivate existing configurations if this is set as active - if config_data.get("is_active", True): - await db.query(ZammadConfiguration).filter( - ZammadConfiguration.user_id == user_id, - ZammadConfiguration.is_active == True - ).update({"is_active": False}) - - # Create new configuration - config = ZammadConfiguration( - user_id=user_id, - name=config_data["name"], - zammad_url=config_data["zammad_url"], - api_token_encrypted=encrypted_token, - chatbot_id=config_data["chatbot_id"], - is_active=config_data.get("is_active", True), - ai_summarization_enabled=config_data.get("ai_summarization", {}).get("enabled", True), - auto_summarize=config_data.get("ai_summarization", {}).get("auto_summarize", True), - sync_enabled=config_data.get("sync_settings", {}).get("enabled", True), - sync_interval_hours=config_data.get("sync_settings", {}).get("interval_hours", 2) - ) - - db.add(config) - await db.commit() - - self.logger.info(f"Saved Zammad configuration for user {user_id}") - return True - - except Exception as e: - self.logger.error(f"Failed to save configuration: {e}") - return False - - async def _get_plugin_statistics(self, user_id: str) -> Dict[str, Any]: - """Get plugin usage statistics""" - try: - async with PluginDatabaseSession(self.plugin_id, plugin_db_manager) as db: - # Get configuration count - config_count = await db.query(ZammadConfiguration).filter( - ZammadConfiguration.user_id == user_id - ).count() - - # Get ticket count - ticket_count = await db.query(ZammadTicket).join(ZammadConfiguration).filter( - ZammadConfiguration.user_id == user_id - ).count() - - # Get tickets with AI summaries - summarized_count = await db.query(ZammadTicket).join(ZammadConfiguration).filter( - ZammadConfiguration.user_id == user_id, - ZammadTicket.ai_summary.isnot(None) - ).count() - - # Get recent activity (last 7 days) - from datetime import timedelta - week_ago = datetime.now(timezone.utc) - timedelta(days=7) - recent_tickets = await db.query(ZammadTicket).join(ZammadConfiguration).filter( - ZammadConfiguration.user_id == user_id, - ZammadTicket.last_synced >= week_ago - ).count() - - return { - "configurations": config_count, - "total_tickets": ticket_count, - "tickets_with_summaries": summarized_count, - "recent_tickets": recent_tickets, - "summary_rate": round((summarized_count / max(ticket_count, 1)) * 100, 1), - "last_sync": datetime.now(timezone.utc).isoformat() - } - - except Exception as e: - self.logger.error(f"Failed to get statistics: {e}") - return { - "error": str(e), - "configurations": 0, - "total_tickets": 0, - "tickets_with_summaries": 0, - "recent_tickets": 0, - "summary_rate": 0.0 - } - - async def _sync_user_tickets(self, user_id: str) -> int: - """Sync tickets for a specific user""" - try: - config = await self.get_active_config(user_id) - if not config: - return 0 - - # Fetch tickets from Zammad - tickets = await self.fetch_tickets_from_zammad(config) - synced_count = 0 - - async with PluginDatabaseSession(self.plugin_id, plugin_db_manager) as db: - config_record = await db.query(ZammadConfiguration).filter( - ZammadConfiguration.id == config["id"] - ).first() - - if not config_record: - return 0 - - for ticket_data in tickets: - # Check if ticket already exists - existing_ticket = await db.query(ZammadTicket).filter( - ZammadTicket.zammad_ticket_id == str(ticket_data["id"]), - ZammadTicket.configuration_id == config_record.id - ).first() - - if existing_ticket: - # Update existing ticket - existing_ticket.title = ticket_data.get("title", "") - existing_ticket.body = ticket_data.get("body", "") - existing_ticket.status = ticket_data.get("state", "") - existing_ticket.priority = ticket_data.get("priority", "") - existing_ticket.last_synced = datetime.now(timezone.utc) - existing_ticket.updated_at = datetime.now(timezone.utc) - else: - # Create new ticket - new_ticket = ZammadTicket( - zammad_ticket_id=str(ticket_data["id"]), - configuration_id=config_record.id, - title=ticket_data.get("title", ""), - body=ticket_data.get("body", ""), - status=ticket_data.get("state", ""), - priority=ticket_data.get("priority", ""), - customer_id=str(ticket_data.get("customer_id", "")), - group_id=str(ticket_data.get("group_id", "")), - last_synced=datetime.now(timezone.utc) - ) - db.add(new_ticket) - synced_count += 1 - - await db.commit() - self.logger.info(f"Synced {synced_count} new tickets for user {user_id}") - return synced_count - - except Exception as e: - self.logger.error(f"Failed to sync tickets for user {user_id}: {e}") - return 0 - - async def _store_ticket_summary(self, ticket_id: str, summary: str, config_id: str): - """Store AI-generated summary in database""" - try: - async with PluginDatabaseSession(self.plugin_id, plugin_db_manager) as db: - ticket = await db.query(ZammadTicket).filter( - ZammadTicket.zammad_ticket_id == ticket_id, - ZammadTicket.configuration_id == config_id - ).first() - - if ticket: - ticket.ai_summary = summary - ticket.updated_at = datetime.now(timezone.utc) - await db.commit() - self.logger.info(f"Stored AI summary for ticket {ticket_id}") - - except Exception as e: - self.logger.error(f"Failed to store summary for ticket {ticket_id}: {e}") \ No newline at end of file diff --git a/plugins/zammad/manifest.yaml b/plugins/zammad/manifest.yaml deleted file mode 100644 index 9d419e4..0000000 --- a/plugins/zammad/manifest.yaml +++ /dev/null @@ -1,253 +0,0 @@ -apiVersion: "v1" -kind: "Plugin" -metadata: - name: "zammad" - version: "1.0.0" - description: "Zammad helpdesk integration with AI summarization and ticket management" - author: "Enclava Team" - license: "MIT" - homepage: "https://github.com/enclava/plugins/zammad" - repository: "https://github.com/enclava/plugins/zammad" - tags: - - "helpdesk" - - "ticket-management" - - "ai-summarization" - - "integration" - -spec: - runtime: - python_version: "3.11" - dependencies: - - "aiohttp>=3.8.0" - - "pydantic>=2.0.0" - - "httpx>=0.24.0" - - "python-dateutil>=2.8.0" - environment_variables: - ZAMMAD_TIMEOUT: "30" - ZAMMAD_MAX_RETRIES: "3" - - permissions: - platform_apis: - - "chatbot:invoke" - - "rag:query" - - "llm:completion" - - "llm:embeddings" - plugin_scopes: - - "tickets:read" - - "tickets:write" - - "tickets:summarize" - - "webhooks:receive" - - "config:manage" - - "sync:execute" - external_domains: - - "*.zammad.com" - - "*.zammad.org" - - "api.zammad.org" - - database: - schema: "plugin_zammad" - migrations_path: "./migrations" - auto_migrate: true - - api_endpoints: - - path: "/tickets" - methods: ["GET", "POST"] - description: "List and create Zammad tickets" - auth_required: true - - - path: "/tickets/{ticket_id}" - methods: ["GET", "PUT", "DELETE"] - description: "Get, update, or delete specific ticket" - auth_required: true - - - path: "/tickets/{ticket_id}/summarize" - methods: ["POST"] - description: "Generate AI summary for ticket" - auth_required: true - - - path: "/tickets/{ticket_id}/articles" - methods: ["GET", "POST"] - description: "Get ticket articles or add new article" - auth_required: true - - - path: "/webhooks/ticket-created" - methods: ["POST"] - description: "Handle Zammad webhook for new tickets" - auth_required: false - - - path: "/webhooks/ticket-updated" - methods: ["POST"] - description: "Handle Zammad webhook for updated tickets" - auth_required: false - - - path: "/configurations" - methods: ["GET", "POST", "PUT", "DELETE"] - description: "Manage Zammad configurations" - auth_required: true - - - path: "/configurations/{config_id}/test" - methods: ["POST"] - description: "Test Zammad configuration connection" - auth_required: true - - - path: "/statistics" - methods: ["GET"] - description: "Get plugin usage statistics" - auth_required: true - - - path: "/health" - methods: ["GET"] - description: "Plugin health check" - auth_required: false - - cron_jobs: - - name: "sync_tickets" - schedule: "0 */2 * * *" - function: "sync_tickets_from_zammad" - description: "Sync tickets from Zammad every 2 hours" - enabled: true - timeout_seconds: 600 - max_retries: 3 - - - name: "cleanup_summaries" - schedule: "0 3 * * 0" - function: "cleanup_old_summaries" - description: "Clean up old AI summaries weekly" - enabled: true - timeout_seconds: 300 - max_retries: 1 - - - name: "health_check" - schedule: "*/15 * * * *" - function: "check_zammad_connection" - description: "Check Zammad API connectivity every 15 minutes" - enabled: true - timeout_seconds: 60 - max_retries: 2 - - - name: "generate_reports" - schedule: "0 9 * * 1" - function: "generate_weekly_reports" - description: "Generate weekly ticket reports" - enabled: false - timeout_seconds: 900 - max_retries: 2 - - ui_config: - configuration_schema: "./config_schema.json" - ui_components: "./ui/components" - pages: - - name: "dashboard" - path: "/plugins/zammad" - component: "ZammadDashboard" - - - name: "settings" - path: "/plugins/zammad/settings" - component: "ZammadSettings" - - - name: "tickets" - path: "/plugins/zammad/tickets" - component: "ZammadTicketList" - - - name: "analytics" - path: "/plugins/zammad/analytics" - component: "ZammadAnalytics" - - external_services: - allowed_domains: - - "*.zammad.com" - - "*.zammad.org" - - "api.zammad.org" - - "help.zammad.com" - - webhooks: - - endpoint: "/webhooks/ticket-created" - security: "signature_validation" - - - endpoint: "/webhooks/ticket-updated" - security: "signature_validation" - - rate_limits: - "*.zammad.com": 100 - "*.zammad.org": 100 - "api.zammad.org": 200 - - config_schema: - type: "object" - required: - - "zammad_url" - - "api_token" - - "chatbot_id" - properties: - zammad_url: - type: "string" - format: "uri" - title: "Zammad URL" - description: "The base URL of your Zammad instance" - examples: - - "https://company.zammad.com" - - "https://support.example.com" - - api_token: - type: "string" - title: "API Token" - description: "Zammad API token with ticket read/write permissions" - minLength: 20 - format: "password" - - chatbot_id: - type: "string" - title: "Chatbot ID" - description: "Platform chatbot ID for AI summarization" - examples: - - "zammad-summarizer" - - "ticket-assistant" - - ai_summarization: - type: "object" - title: "AI Summarization Settings" - properties: - enabled: - type: "boolean" - title: "Enable AI Summarization" - description: "Automatically summarize tickets using AI" - default: true - - model: - type: "string" - title: "AI Model" - description: "LLM model to use for summarization" - default: "gpt-3.5-turbo" - - max_tokens: - type: "integer" - title: "Max Summary Tokens" - description: "Maximum tokens for AI summary" - minimum: 50 - maximum: 500 - default: 150 - - draft_settings: - type: "object" - title: "AI Draft Settings" - properties: - enabled: - type: "boolean" - title: "Enable AI Drafts" - description: "Generate AI draft responses for tickets" - default: false - - model: - type: "string" - title: "Draft Model" - description: "LLM model to use for draft generation" - default: "gpt-3.5-turbo" - - max_tokens: - type: "integer" - title: "Max Draft Tokens" - description: "Maximum tokens for AI draft responses" - minimum: 100 - maximum: 1000 - default: 300 - diff --git a/plugins/zammad/migrations/env.py b/plugins/zammad/migrations/env.py deleted file mode 100644 index 28b9fa3..0000000 --- a/plugins/zammad/migrations/env.py +++ /dev/null @@ -1,85 +0,0 @@ -"""Alembic environment for Zammad plugin""" -from logging.config import fileConfig -from sqlalchemy import engine_from_config -from sqlalchemy import pool -from alembic import context -import os -import sys - -# this is the Alembic Config object, which provides -# access to the values within the .ini file in use. -config = context.config - -# Interpret the config file for Python logging. -# This line sets up loggers basically. -if config.config_file_name is not None: - fileConfig(config.config_file_name) - -# add your model's MetaData object here -# for 'autogenerate' support -from main import Base -target_metadata = Base.metadata - -# other values from the config, defined by the needs of env.py, -# can be acquired: -# my_important_option = config.get_main_option("my_important_option") -# ... etc. - - -def run_migrations_offline() -> None: - """Run migrations in 'offline' mode. - - This configures the context with just a URL - and not an Engine, though an Engine is acceptable - here as well. By skipping the Engine creation - we don't even need a DBAPI to be available. - - Calls to context.execute() here emit the given string to the - script output. - - """ - # Get database URL from environment variable - url = os.getenv("DATABASE_URL") - context.configure( - url=url, - target_metadata=target_metadata, - literal_binds=True, - dialect_opts={"paramstyle": "named"}, - ) - - with context.begin_transaction(): - context.run_migrations() - - -def run_migrations_online() -> None: - """Run migrations in 'online' mode. - - In this scenario we need to create an Engine - and associate a connection with the context. - - """ - # Get database URL from environment variable - database_url = os.getenv("DATABASE_URL") - - configuration = config.get_section(config.config_ini_section) - configuration["sqlalchemy.url"] = database_url - - connectable = engine_from_config( - configuration, - prefix="sqlalchemy.", - poolclass=pool.NullPool, - ) - - with connectable.connect() as connection: - context.configure( - connection=connection, target_metadata=target_metadata - ) - - with context.begin_transaction(): - context.run_migrations() - - -if context.is_offline_mode(): - run_migrations_offline() -else: - run_migrations_online() \ No newline at end of file diff --git a/plugins/zammad/migrations/script.py.mako b/plugins/zammad/migrations/script.py.mako deleted file mode 100644 index 37d0cac..0000000 --- a/plugins/zammad/migrations/script.py.mako +++ /dev/null @@ -1,24 +0,0 @@ -"""${message} - -Revision ID: ${up_revision} -Revises: ${down_revision | comma,n} -Create Date: ${create_date} - -""" -from alembic import op -import sqlalchemy as sa -${imports if imports else ""} - -# revision identifiers, used by Alembic. -revision = ${repr(up_revision)} -down_revision = ${repr(down_revision)} -branch_labels = ${repr(branch_labels)} -depends_on = ${repr(depends_on)} - - -def upgrade() -> None: - ${upgrades if upgrades else "pass"} - - -def downgrade() -> None: - ${downgrades if downgrades else "pass"} \ No newline at end of file diff --git a/plugins/zammad/migrations/versions/001_initial_zammad_schema.py b/plugins/zammad/migrations/versions/001_initial_zammad_schema.py deleted file mode 100644 index 8b5ccff..0000000 --- a/plugins/zammad/migrations/versions/001_initial_zammad_schema.py +++ /dev/null @@ -1,112 +0,0 @@ -"""Initial Zammad plugin schema - -Revision ID: 001 -Revises: -Create Date: 2024-12-22 12:00:00.000000 - -""" -from alembic import op -import sqlalchemy as sa -from sqlalchemy.dialects.postgresql import UUID - -# revision identifiers, used by Alembic. -revision = '001' -down_revision = None -branch_labels = None -depends_on = None - - -def upgrade() -> None: - """Create initial Zammad plugin schema""" - - # Create zammad_configurations table - op.create_table( - 'zammad_configurations', - sa.Column('id', UUID(as_uuid=True), primary_key=True, server_default=sa.text('gen_random_uuid()')), - sa.Column('user_id', sa.String(255), nullable=False), - sa.Column('name', sa.String(100), nullable=False), - sa.Column('zammad_url', sa.String(500), nullable=False), - sa.Column('api_token_encrypted', sa.Text(), nullable=False), - sa.Column('chatbot_id', sa.String(100), nullable=False), - sa.Column('is_active', sa.Boolean(), nullable=False, server_default=sa.text('TRUE')), - sa.Column('ai_summarization_enabled', sa.Boolean(), nullable=False, server_default=sa.text('TRUE')), - sa.Column('auto_summarize', sa.Boolean(), nullable=False, server_default=sa.text('TRUE')), - sa.Column('sync_enabled', sa.Boolean(), nullable=False, server_default=sa.text('TRUE')), - sa.Column('sync_interval_hours', sa.Integer(), nullable=False, server_default=sa.text('2')), - sa.Column('created_at', sa.TIMESTAMP(timezone=True), nullable=False, server_default=sa.text('CURRENT_TIMESTAMP')), - sa.Column('updated_at', sa.TIMESTAMP(timezone=True), nullable=False, server_default=sa.text('CURRENT_TIMESTAMP')), - ) - - # Create zammad_tickets table - op.create_table( - 'zammad_tickets', - sa.Column('id', UUID(as_uuid=True), primary_key=True, server_default=sa.text('gen_random_uuid()')), - sa.Column('zammad_ticket_id', sa.String(50), nullable=False), - sa.Column('configuration_id', UUID(as_uuid=True), nullable=True), - sa.Column('title', sa.String(500), nullable=False), - sa.Column('body', sa.Text(), nullable=True), - sa.Column('status', sa.String(50), nullable=True), - sa.Column('priority', sa.String(50), nullable=True), - sa.Column('customer_id', sa.String(50), nullable=True), - sa.Column('group_id', sa.String(50), nullable=True), - sa.Column('ai_summary', sa.Text(), nullable=True), - sa.Column('last_synced', sa.TIMESTAMP(timezone=True), nullable=False, server_default=sa.text('CURRENT_TIMESTAMP')), - sa.Column('created_at', sa.TIMESTAMP(timezone=True), nullable=False, server_default=sa.text('CURRENT_TIMESTAMP')), - sa.Column('updated_at', sa.TIMESTAMP(timezone=True), nullable=False, server_default=sa.text('CURRENT_TIMESTAMP')), - sa.ForeignKeyConstraint(['configuration_id'], ['zammad_configurations.id'], ondelete='CASCADE'), - ) - - # Create indexes for performance - op.create_index('idx_zammad_configurations_user_id', 'zammad_configurations', ['user_id']) - op.create_index('idx_zammad_configurations_user_active', 'zammad_configurations', ['user_id', 'is_active']) - - op.create_index('idx_zammad_tickets_zammad_id', 'zammad_tickets', ['zammad_ticket_id']) - op.create_index('idx_zammad_tickets_config_id', 'zammad_tickets', ['configuration_id']) - op.create_index('idx_zammad_tickets_status', 'zammad_tickets', ['status']) - op.create_index('idx_zammad_tickets_last_synced', 'zammad_tickets', ['last_synced']) - - # Create updated_at trigger function if it doesn't exist - op.execute(""" - CREATE OR REPLACE FUNCTION update_updated_at_column() - RETURNS TRIGGER AS $$ - BEGIN - NEW.updated_at = CURRENT_TIMESTAMP; - RETURN NEW; - END; - $$ LANGUAGE 'plpgsql'; - """) - - # Create triggers to automatically update updated_at columns - op.execute(""" - CREATE TRIGGER update_zammad_configurations_updated_at - BEFORE UPDATE ON zammad_configurations - FOR EACH ROW EXECUTE FUNCTION update_updated_at_column(); - """) - - op.execute(""" - CREATE TRIGGER update_zammad_tickets_updated_at - BEFORE UPDATE ON zammad_tickets - FOR EACH ROW EXECUTE FUNCTION update_updated_at_column(); - """) - - -def downgrade() -> None: - """Drop Zammad plugin schema""" - - # Drop triggers first - op.execute("DROP TRIGGER IF EXISTS update_zammad_tickets_updated_at ON zammad_tickets;") - op.execute("DROP TRIGGER IF EXISTS update_zammad_configurations_updated_at ON zammad_configurations;") - - # Drop indexes - op.drop_index('idx_zammad_tickets_last_synced') - op.drop_index('idx_zammad_tickets_status') - op.drop_index('idx_zammad_tickets_config_id') - op.drop_index('idx_zammad_tickets_zammad_id') - op.drop_index('idx_zammad_configurations_user_active') - op.drop_index('idx_zammad_configurations_user_id') - - # Drop tables (tickets first due to foreign key) - op.drop_table('zammad_tickets') - op.drop_table('zammad_configurations') - - # Note: We don't drop the update_updated_at_column function as it might be used by other tables \ No newline at end of file diff --git a/plugins/zammad/requirements.txt b/plugins/zammad/requirements.txt deleted file mode 100644 index 47097f2..0000000 --- a/plugins/zammad/requirements.txt +++ /dev/null @@ -1,4 +0,0 @@ -aiohttp>=3.8.0 -pydantic>=2.0.0 -httpx>=0.24.0 -python-dateutil>=2.8.0 \ No newline at end of file diff --git a/plugins/zammad/ui/components/ZammadDashboard.tsx b/plugins/zammad/ui/components/ZammadDashboard.tsx deleted file mode 100644 index f5f6aec..0000000 --- a/plugins/zammad/ui/components/ZammadDashboard.tsx +++ /dev/null @@ -1,414 +0,0 @@ -/** - * Zammad Plugin Dashboard Component - * Main dashboard for Zammad plugin showing tickets, statistics, and quick actions - */ -import React, { useState, useEffect } from 'react'; -import { - Box, - Grid, - Card, - CardContent, - Typography, - Button, - Chip, - Alert, - Table, - TableBody, - TableCell, - TableHead, - TableRow, - IconButton, - Dialog, - DialogTitle, - DialogContent, - DialogActions, - LinearProgress, - Tooltip -} from '@mui/material'; -import { - Refresh as RefreshIcon, - Sync as SyncIcon, - Analytics as AnalyticsIcon, - Assignment as TicketIcon, - AutoAwesome as AIIcon, - Settings as SettingsIcon, - OpenInNew as OpenIcon -} from '@mui/icons-material'; - -interface ZammadTicket { - id: string; - title: string; - status: string; - priority: string; - customer_id: string; - created_at: string; - ai_summary?: string; -} - -interface ZammadStats { - configurations: number; - total_tickets: number; - tickets_with_summaries: number; - recent_tickets: number; - summary_rate: number; - last_sync: string; -} - -export const ZammadDashboard: React.FC = () => { - const [tickets, setTickets] = useState([]); - const [stats, setStats] = useState(null); - const [loading, setLoading] = useState(false); - const [error, setError] = useState(null); - const [selectedTicket, setSelectedTicket] = useState(null); - const [dialogOpen, setDialogOpen] = useState(false); - const [syncing, setSyncing] = useState(false); - - useEffect(() => { - loadDashboardData(); - }, []); - - const loadDashboardData = async () => { - setLoading(true); - setError(null); - - try { - // Load statistics - const statsResponse = await fetch('/api/v1/plugins/zammad/statistics'); - if (statsResponse.ok) { - const statsData = await statsResponse.json(); - setStats(statsData); - } - - // Load recent tickets - const ticketsResponse = await fetch('/api/v1/plugins/zammad/tickets?limit=10'); - if (ticketsResponse.ok) { - const ticketsData = await ticketsResponse.json(); - setTickets(ticketsData.tickets || []); - } - - } catch (err) { - setError('Failed to load dashboard data'); - console.error('Dashboard load error:', err); - } finally { - setLoading(false); - } - }; - - const handleSyncTickets = async () => { - setSyncing(true); - try { - const response = await fetch('/api/v1/plugins/zammad/tickets/sync', { - method: 'GET' - }); - - if (response.ok) { - const result = await response.json(); - // Reload dashboard data after sync - await loadDashboardData(); - // Show success message with sync count - console.log(`Synced ${result.synced_count} tickets`); - } else { - throw new Error('Sync failed'); - } - } catch (err) { - setError('Failed to sync tickets'); - } finally { - setSyncing(false); - } - }; - - const handleTicketClick = (ticket: ZammadTicket) => { - setSelectedTicket(ticket); - setDialogOpen(true); - }; - - const handleSummarizeTicket = async (ticketId: string) => { - try { - const response = await fetch(`/api/v1/plugins/zammad/tickets/${ticketId}/summarize`, { - method: 'POST' - }); - - if (response.ok) { - // Show success message - console.log('Summarization started'); - } - } catch (err) { - console.error('Summarization failed:', err); - } - }; - - const getStatusColor = (status: string) => { - switch (status.toLowerCase()) { - case 'open': return 'error'; - case 'pending': return 'warning'; - case 'closed': return 'success'; - default: return 'default'; - } - }; - - const getPriorityColor = (priority: string) => { - switch (priority) { - case '3 high': return 'error'; - case '2 normal': return 'warning'; - case '1 low': return 'success'; - default: return 'default'; - } - }; - - return ( - - {/* Header */} - - - Zammad Dashboard - - - - - - - - - - {error && ( - - {error} - - )} - - {loading && } - - {/* Statistics Cards */} - {stats && ( - - - - - - - - {stats.total_tickets} - - Total Tickets - - - - - - - - - - - - - - {stats.tickets_with_summaries} - - AI Summaries - - - - - - - - - - - - - - {stats.summary_rate}% - - Summary Rate - - - - - - - - - - - - - - {stats.recent_tickets} - - Recent (7 days) - - - - - - - - )} - - {/* Recent Tickets Table */} - - - - Recent Tickets - - - - {tickets.length === 0 ? ( - - No tickets found. Try syncing with Zammad. - - ) : ( - - - - Title - Status - Priority - AI Summary - Actions - - - - {tickets.map((ticket) => ( - handleTicketClick(ticket)}> - - - {ticket.title} - - - - - - - - - - {ticket.ai_summary ? ( - - ) : ( - - )} - - - - { - e.stopPropagation(); - handleSummarizeTicket(ticket.id); - }} - disabled={!!ticket.ai_summary} - > - - - - - - ))} - -
- )} -
-
- - {/* Ticket Detail Dialog */} - setDialogOpen(false)} - maxWidth="md" - fullWidth - > - - Ticket Details - - - - {selectedTicket && ( - - - {selectedTicket.title} - - - - - - - - - Customer: {selectedTicket.customer_id} - - - - Created: {new Date(selectedTicket.created_at).toLocaleString()} - - - {selectedTicket.ai_summary && ( - - - AI Summary - - - {selectedTicket.ai_summary} - - - )} - - )} - - - - - {selectedTicket && !selectedTicket.ai_summary && ( - - )} - - -
- ); -}; \ No newline at end of file diff --git a/plugins/zammad/ui/components/ZammadSettings.tsx b/plugins/zammad/ui/components/ZammadSettings.tsx deleted file mode 100644 index c729d1d..0000000 --- a/plugins/zammad/ui/components/ZammadSettings.tsx +++ /dev/null @@ -1,512 +0,0 @@ -/** - * Zammad Plugin Settings Component - * Configuration interface for Zammad plugin - */ -import React, { useState, useEffect } from 'react'; -import { - Box, - Card, - CardContent, - Typography, - TextField, - Button, - Switch, - FormControlLabel, - FormGroup, - Select, - MenuItem, - FormControl, - InputLabel, - Alert, - Divider, - Accordion, - AccordionSummary, - AccordionDetails, - Chip, - LinearProgress -} from '@mui/material'; -import { - ExpandMore as ExpandMoreIcon, - Save as SaveIcon, - TestTube as TestIcon, - Security as SecurityIcon, - Sync as SyncIcon, - Smart as AIIcon -} from '@mui/icons-material'; - -interface ZammadConfig { - name: string; - zammad_url: string; - api_token: string; - chatbot_id: string; - ai_summarization: { - enabled: boolean; - model: string; - max_tokens: number; - auto_summarize: boolean; - }; - sync_settings: { - enabled: boolean; - interval_hours: number; - sync_articles: boolean; - max_tickets_per_sync: number; - }; - webhook_settings: { - secret: string; - enabled_events: string[]; - }; - notification_settings: { - email_notifications: boolean; - slack_webhook_url: string; - notification_events: string[]; - }; -} - -const defaultConfig: ZammadConfig = { - name: '', - zammad_url: '', - api_token: '', - chatbot_id: '', - ai_summarization: { - enabled: true, - model: 'gpt-3.5-turbo', - max_tokens: 150, - auto_summarize: true - }, - sync_settings: { - enabled: true, - interval_hours: 2, - sync_articles: true, - max_tickets_per_sync: 100 - }, - webhook_settings: { - secret: '', - enabled_events: ['ticket.create', 'ticket.update'] - }, - notification_settings: { - email_notifications: false, - slack_webhook_url: '', - notification_events: ['sync_error', 'api_error'] - } -}; - -export const ZammadSettings: React.FC = () => { - const [config, setConfig] = useState(defaultConfig); - const [loading, setLoading] = useState(false); - const [saving, setSaving] = useState(false); - const [testing, setTesting] = useState(false); - const [error, setError] = useState(null); - const [success, setSuccess] = useState(null); - const [testResult, setTestResult] = useState(null); - - useEffect(() => { - loadConfiguration(); - }, []); - - const loadConfiguration = async () => { - setLoading(true); - try { - const response = await fetch('/api/v1/plugins/zammad/configurations'); - if (response.ok) { - const data = await response.json(); - if (data.configurations.length > 0) { - // Load the first (active) configuration - const loadedConfig = data.configurations[0]; - setConfig({ - ...defaultConfig, - ...loadedConfig - }); - } - } - } catch (err) { - setError('Failed to load configuration'); - } finally { - setLoading(false); - } - }; - - const handleConfigChange = (path: string, value: any) => { - setConfig(prev => { - const newConfig = { ...prev }; - const keys = path.split('.'); - let current: any = newConfig; - - for (let i = 0; i < keys.length - 1; i++) { - current = current[keys[i]]; - } - - current[keys[keys.length - 1]] = value; - return newConfig; - }); - }; - - const handleTestConnection = async () => { - setTesting(true); - setTestResult(null); - setError(null); - - try { - const response = await fetch('/api/v1/plugins/zammad/configurations/test', { - method: 'POST', - headers: { - 'Content-Type': 'application/json' - }, - body: JSON.stringify({ - zammad_url: config.zammad_url, - api_token: config.api_token - }) - }); - - const result = await response.json(); - setTestResult(result); - - if (!result.success) { - setError(`Connection test failed: ${result.error}`); - } - } catch (err) { - setError('Connection test failed'); - } finally { - setTesting(false); - } - }; - - const handleSaveConfiguration = async () => { - setSaving(true); - setError(null); - setSuccess(null); - - try { - const response = await fetch('/api/v1/plugins/zammad/configurations', { - method: 'POST', - headers: { - 'Content-Type': 'application/json' - }, - body: JSON.stringify(config) - }); - - if (response.ok) { - setSuccess('Configuration saved successfully'); - } else { - const errorData = await response.json(); - setError(errorData.detail || 'Failed to save configuration'); - } - } catch (err) { - setError('Failed to save configuration'); - } finally { - setSaving(false); - } - }; - - const handleArrayToggle = (path: string, value: string) => { - const currentArray = path.split('.').reduce((obj, key) => obj[key], config) as string[]; - const newArray = currentArray.includes(value) - ? currentArray.filter(item => item !== value) - : [...currentArray, value]; - handleConfigChange(path, newArray); - }; - - if (loading) { - return ( - - Zammad Settings - - - ); - } - - return ( - - - - Zammad Settings - - - - - - - - - - {error && ( - - {error} - - )} - - {success && ( - - {success} - - )} - - {testResult && ( - - {testResult.success - ? `Connection successful! User: ${testResult.user}, Version: ${testResult.zammad_version}` - : `Connection failed: ${testResult.error}` - } - - )} - - {/* Basic Configuration */} - - - - Basic Configuration - - - - handleConfigChange('name', e.target.value)} - fullWidth - required - /> - - handleConfigChange('zammad_url', e.target.value)} - fullWidth - required - placeholder="https://company.zammad.com" - /> - - handleConfigChange('api_token', e.target.value)} - fullWidth - required - helperText="Zammad API token with ticket read/write permissions" - /> - - handleConfigChange('chatbot_id', e.target.value)} - fullWidth - required - helperText="Platform chatbot ID for AI summarization" - /> - - - - - {/* AI Summarization Settings */} - - }> - - - AI Summarization - - - - - - handleConfigChange('ai_summarization.enabled', e.target.checked)} - /> - } - label="Enable AI Summarization" - /> - - - AI Model - - - - handleConfigChange('ai_summarization.max_tokens', parseInt(e.target.value))} - inputProps={{ min: 50, max: 500 }} - /> - - handleConfigChange('ai_summarization.auto_summarize', e.target.checked)} - /> - } - label="Auto-summarize New Tickets" - /> - - - - - {/* Sync Settings */} - - }> - - - Sync Settings - - - - - - handleConfigChange('sync_settings.enabled', e.target.checked)} - /> - } - label="Enable Automatic Sync" - /> - - handleConfigChange('sync_settings.interval_hours', parseInt(e.target.value))} - inputProps={{ min: 1, max: 24 }} - /> - - handleConfigChange('sync_settings.sync_articles', e.target.checked)} - /> - } - label="Sync Ticket Articles" - /> - - handleConfigChange('sync_settings.max_tickets_per_sync', parseInt(e.target.value))} - inputProps={{ min: 10, max: 1000 }} - /> - - - - - {/* Webhook Settings */} - - }> - - - Webhook Settings - - - - - handleConfigChange('webhook_settings.secret', e.target.value)} - fullWidth - helperText="Secret for webhook signature validation" - /> - - Enabled Webhook Events - - {['ticket.create', 'ticket.update', 'ticket.close', 'article.create'].map((event) => ( - handleArrayToggle('webhook_settings.enabled_events', event)} - /> - } - label={event} - /> - ))} - - - - - - {/* Notification Settings */} - - }> - Notification Settings - - - - handleConfigChange('notification_settings.email_notifications', e.target.checked)} - /> - } - label="Email Notifications" - /> - - handleConfigChange('notification_settings.slack_webhook_url', e.target.value)} - fullWidth - placeholder="https://hooks.slack.com/services/..." - /> - - Notification Events - - {['sync_error', 'api_error', 'new_tickets', 'summarization_complete'].map((event) => ( - handleArrayToggle('notification_settings.notification_events', event)} - /> - } - label={event.replace('_', ' ').replace(/\b\w/g, l => l.toUpperCase())} - /> - ))} - - - - - - ); -}; \ No newline at end of file