""" Admin Quote API router for ClaudeTools. This module defines all admin REST API endpoints for managing quotes, requiring JWT authentication for access. """ from typing import Optional from uuid import UUID from fastapi import APIRouter, Depends, Query, status from sqlalchemy.orm import Session from api.database import get_db from api.middleware.auth import get_current_user from api.schemas.quote import ( QuoteAdminResponse, QuoteAdminUpdate, QuoteActivityResponse, QuoteItemResponse, QuoteListItem, QuoteListResponse, QuoteNotificationResponse, QuoteStatsResponse, QuoteStatus, ) from api.services import quote_service # Create router with authentication required router = APIRouter() @router.get( "", response_model=QuoteListResponse, summary="List all quotes", description="Retrieve a paginated list of all quotes with optional filtering", status_code=status.HTTP_200_OK, ) def list_quotes( skip: int = Query( default=0, ge=0, description="Number of records to skip for pagination" ), limit: int = Query( default=100, ge=1, le=1000, description="Maximum number of records to return (max 1000)" ), status_filter: Optional[str] = Query( default=None, alias="status", description="Filter by status (draft, submitted, viewed, followed_up, converted, expired)" ), search: Optional[str] = Query( default=None, description="Search in company_name, contact_name, contact_email" ), db: Session = Depends(get_db), current_user: dict = Depends(get_current_user), ): """ List all quotes with pagination and filtering. - **skip**: Number of quotes to skip (default: 0) - **limit**: Maximum number of quotes to return (default: 100, max: 1000) - **status**: Filter by quote status - **search**: Search in company name, contact name, or email Returns a list of quotes with pagination metadata. **Example Request:** ``` GET /api/admin/quotes?skip=0&limit=50&status=submitted Authorization: Bearer ``` **Example Response:** ```json { "total": 25, "skip": 0, "limit": 50, "quotes": [ { "id": "123e4567-e89b-12d3-a456-426614174000", "access_token": "xYz123...", "status": "submitted", "company_name": "Acme Corporation", "contact_name": "John Doe", "contact_email": "john@acme.com", "employee_count": 25, "monthly_total": "450.00", "setup_total": "500.00", "item_count": 3, "submitted_at": "2024-01-15T14:30:00Z", "created_at": "2024-01-15T10:30:00Z" } ] } ``` """ quotes, total = quote_service.list_quotes( db=db, skip=skip, limit=limit, status_filter=status_filter, search=search ) # Build list items with item counts quote_items = [] for quote in quotes: quote_items.append(QuoteListItem( id=quote.id, access_token=quote.access_token, status=quote.status, company_name=quote.company_name, contact_name=quote.contact_name, contact_email=quote.contact_email, employee_count=quote.employee_count, monthly_total=quote.monthly_total, setup_total=quote.setup_total, item_count=len(quote.items), submitted_at=quote.submitted_at, created_at=quote.created_at )) return QuoteListResponse( total=total, skip=skip, limit=limit, quotes=quote_items ) @router.get( "/stats", response_model=QuoteStatsResponse, summary="Get quote statistics", description="Get dashboard statistics for quotes", status_code=status.HTTP_200_OK, ) def get_stats( db: Session = Depends(get_db), current_user: dict = Depends(get_current_user), ): """ Get quote statistics for the admin dashboard. Returns aggregate statistics including totals, counts by status, and conversion rates. **Example Request:** ``` GET /api/admin/quotes/stats Authorization: Bearer ``` **Example Response:** ```json { "total_quotes": 150, "quotes_by_status": { "draft": 45, "submitted": 60, "viewed": 15, "followed_up": 10, "converted": 25, "expired": 2 }, "total_monthly_value": "12500.00", "total_setup_value": "8500.00", "quotes_this_month": 28, "quotes_submitted_this_month": 18, "average_monthly_value": "125.00", "conversion_rate": "66.67" } ``` """ return quote_service.get_quote_stats(db) @router.get( "/{quote_id}", response_model=QuoteAdminResponse, summary="Get quote by ID", description="Retrieve a single quote by its ID with full details", status_code=status.HTTP_200_OK, responses={ 200: { "description": "Quote found and returned", "model": QuoteAdminResponse, }, 404: { "description": "Quote not found", "content": { "application/json": { "example": {"detail": "Quote with ID 123e4567-e89b-12d3-a456-426614174000 not found"} } }, }, }, ) def get_quote( quote_id: UUID, db: Session = Depends(get_db), current_user: dict = Depends(get_current_user), ): """ Get a specific quote by ID with full admin details. Returns the quote with all items, activities, and notifications. **Example Request:** ``` GET /api/admin/quotes/123e4567-e89b-12d3-a456-426614174000 Authorization: Bearer ``` **Example Response:** ```json { "id": "123e4567-e89b-12d3-a456-426614174000", "access_token": "xYz123...", "status": "submitted", "company_name": "Acme Corporation", "contact_name": "John Doe", "contact_email": "john@acme.com", "ip_address": "192.168.1.100", "user_agent": "Mozilla/5.0...", "items": [...], "activities": [ { "id": "789...", "action": "submitted", "description": "Quote submitted by John Doe (john@acme.com)", "actor": "john@acme.com", "created_at": "2024-01-15T14:30:00Z" } ], "notifications": [...] } ``` """ quote = quote_service.get_quote_by_id(db, quote_id) # Build response with all related data items_response = [] for item in quote.items: items_response.append(QuoteItemResponse( id=item.id, quote_id=item.quote_id, category=item.category, product_code=item.product_code, product_name=item.product_name, description=item.description, quantity=item.quantity, unit_price=item.unit_price, setup_price=item.setup_price, billing_frequency=item.billing_frequency, tier=item.tier, is_recommended=item.is_recommended, line_total=item.line_total, monthly_amount=item.monthly_amount, created_at=item.created_at, )) activities_response = [] for activity in quote.activities: activities_response.append(QuoteActivityResponse( id=activity.id, quote_id=activity.quote_id, action=activity.action, step_name=activity.step_name, details=activity.details, ip_address=activity.ip_address, created_at=activity.created_at )) notifications_response = [] for notification in quote.notifications: notifications_response.append(QuoteNotificationResponse( id=notification.id, quote_id=notification.quote_id, notification_type=notification.notification_type, recipient=notification.recipient, subject=notification.subject, status=notification.status, attempts=notification.attempts, last_attempt_at=notification.last_attempt_at, sent_at=notification.sent_at, error_message=notification.error_message, created_at=notification.created_at )) return QuoteAdminResponse( id=quote.id, access_token=quote.access_token, status=quote.status, company_name=quote.company_name, contact_name=quote.contact_name, contact_email=quote.contact_email, contact_phone=quote.contact_phone, employee_count=quote.employee_count, monthly_total=quote.monthly_total, setup_total=quote.setup_total, expires_at=quote.expires_at, submitted_at=quote.submitted_at, ip_address=quote.ip_address, user_agent=quote.user_agent, created_at=quote.created_at, updated_at=quote.updated_at, items=items_response, activities=activities_response, notifications=notifications_response ) @router.put( "/{quote_id}", response_model=QuoteAdminResponse, summary="Update quote status/notes", description="Update a quote's status or admin notes", status_code=status.HTTP_200_OK, responses={ 200: { "description": "Quote updated successfully", "model": QuoteAdminResponse, }, 404: { "description": "Quote not found", }, }, ) def update_quote( quote_id: UUID, update_data: QuoteAdminUpdate, db: Session = Depends(get_db), current_user: dict = Depends(get_current_user), ): """ Update a quote's status or admin notes. Admins can change the quote status (e.g., from submitted to viewed or converted) and update expiration. **Example Request:** ```json PUT /api/admin/quotes/123e4567-e89b-12d3-a456-426614174000 Authorization: Bearer Content-Type: application/json { "status": "viewed" } ``` **Example Response:** ```json { "id": "123e4567-e89b-12d3-a456-426614174000", "status": "viewed", ... } ``` """ # Get admin username from token admin_user = current_user.get("sub", "admin") quote_service.update_quote_status( db=db, quote_id=quote_id, update_data=update_data, admin_user=admin_user ) return get_quote(quote_id, db, current_user) @router.post( "/{quote_id}/sync-syncro", summary="Sync quote to SyncroRMM", description="Create or update a lead in SyncroRMM from a submitted quote", status_code=status.HTTP_200_OK, responses={ 200: { "description": "Sync result", "content": { "application/json": { "example": { "synced": True, "is_existing_customer": False, "syncro_lead_id": "12345", "error": None, } } }, }, 404: {"description": "Quote not found"}, }, ) async def sync_quote_to_syncro( quote_id: UUID, db: Session = Depends(get_db), current_user: dict = Depends(get_current_user), ): """ Manually trigger a SyncroRMM sync for a quote. Checks for an existing customer in Syncro and creates a lead with the quote details. The quote must have a contact email to sync. **Example Request:** ``` POST /api/admin/quotes/123e4567-e89b-12d3-a456-426614174000/sync-syncro Authorization: Bearer ``` """ quote = quote_service.get_quote_by_id(db, quote_id) result = await quote_service.sync_quote_to_syncro(db, quote) return result