kamau1's picture
feat: add custom reports with aggregations and export parity
367d57f
"""
Export API - Data Export Endpoints
Endpoints for exporting entity data to CSV/Excel.
Available entities: tickets, customers, subscriptions, field_agents, sales_orders
Authorization:
- platform_admin, client_admin, contractor_admin: Full access
- project_manager, sales_manager, dispatcher: Project-scoped access
- field_agent, sales_agent: No access
"""
from fastapi import APIRouter, Depends, HTTPException, status
from fastapi.responses import StreamingResponse
from sqlalchemy.orm import Session
from typing import List
import logging
from datetime import datetime
from app.api.deps import get_db, get_current_user
from app.models.user import User
from app.schemas.export import (
ExportRequest,
AvailableColumnsResponse,
ColumnMetadata
)
from app.services.export_service import ExportService
from app.config.export_columns import get_available_columns
router = APIRouter()
logger = logging.getLogger(__name__)
# ============================================
# GET AVAILABLE COLUMNS
# ============================================
@router.get("/{entity}/columns", response_model=AvailableColumnsResponse)
async def get_available_export_columns(
entity: str,
current_user: User = Depends(get_current_user)
):
"""
Get list of available columns for export.
Returns column metadata including:
- key: Column identifier for API requests
- label: Human-readable header
- requires_join: Whether column needs database join (affects performance)
Available entities:
- tickets: Work orders/tickets
- customers: Customer records
- subscriptions: Active service subscriptions
- field_agents: Field agent users
- sales_orders: Sales orders
Authorization: project_manager+, dispatcher+, admins
"""
# Check authorization
if not ExportService.can_user_export(current_user):
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="You do not have permission to export data"
)
# Get columns for entity
columns_data = get_available_columns(entity)
if not columns_data:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=f"Entity '{entity}' not found or has no exportable columns"
)
# Convert to schema
columns = [ColumnMetadata(**col_data) for col_data in columns_data]
return AvailableColumnsResponse(
entity=entity,
columns=columns
)
# ============================================
# EXPORT DATA
# ============================================
@router.post("/{entity}/export")
async def export_entity_data(
entity: str,
request: ExportRequest,
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user)
):
"""
Export entity data to CSV/Excel.
Request body:
- columns: List of column keys to export (get from /columns endpoint)
- filters: Optional filters to apply (project_id, status, etc.)
- format: Export format ('csv' or 'excel')
Response: File download
Limits:
- Max file size: 1MB
- If file exceeds limit, reduce columns or add filters
Example filters:
- tickets: project_id, status, priority, ticket_type, region_id
- customers: client_id, is_active, region_id
- subscriptions: project_id, status, service_type, region_id
- field_agents: contractor_id, is_active, status
- sales_orders: project_id, status, is_processed, region_id
Authorization: project_manager+, dispatcher+, admins
"""
try:
# Ensure entity in request matches path parameter
if request.entity != entity:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=f"Entity mismatch: path='{entity}', body='{request.entity}'"
)
# Call export service
file_buffer = ExportService.export_entity(
db=db,
entity=request.entity,
columns=request.columns,
filters=request.filters,
format=request.format,
current_user=current_user,
calculated_fields=request.calculated_fields,
aggregations=request.aggregations,
group_by=request.group_by
)
# Prepare filename
timestamp = datetime.utcnow().strftime('%Y%m%d_%H%M%S')
filename = f"{entity}_export_{timestamp}.{request.format}"
# Determine media type
if request.format == 'csv':
media_type = 'text/csv'
elif request.format == 'excel':
media_type = 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
else:
media_type = 'application/octet-stream'
# Return file as streaming response
return StreamingResponse(
file_buffer,
media_type=media_type,
headers={
'Content-Disposition': f'attachment; filename="{filename}"',
'Content-Length': str(file_buffer.getbuffer().nbytes)
}
)
except HTTPException:
raise
except Exception as e:
logger.error(f"Export failed: {str(e)}")
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Export failed: {str(e)}"
)
# ============================================
# EXPORT PRESETS (Future Feature)
# ============================================
@router.get("/{entity}/presets")
async def get_export_presets(
entity: str,
current_user: User = Depends(get_current_user)
):
"""
Get saved export presets for entity.
Presets are pre-configured column selections for common export scenarios.
Example presets for tickets:
- basic: id, ticket_name, status, priority, customer_name
- detailed: All fields
- analytics: status, priority, created_at, completed_at, sla_violated
Future feature: Currently returns empty list.
Authorization: project_manager+, dispatcher+, admins
"""
# Check authorization
if not ExportService.can_user_export(current_user):
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="You do not have permission to export data"
)
# TODO: Implement saved presets (future feature)
return {
"entity": entity,
"presets": []
}