kitchen/backend/api/support.py
jtricerolph 8d688b459d Initial kitchen scaffold — Phase 1 kitchen port (build-verified 2026-07-11)
FastAPI backend (Python 3.11, MSSQL ODBC for SambaPOS, Azure DI OCR),
kitchen_db on central PG. React/TS/Vite frontend with navy sidebar layout.

Backend: auth.py (APP_SLUG=kitchen, SimpleNamespace — archive routes use
.kitchen_id/.is_admin without modification), main.py (51 migrations, scheduler,
internal router for KDS bookings feed), api/internal.py, full archive API
(31 routers: invoices, recipes, menus, sambapos, resos, newbook, disputes,
purchase_orders, etc.), models, migrations, OCR pipeline.
kitchen_id pinned to 1 (B1 — single hotel).

Frontend: AuthGate (app=kitchen, token shim for archive compat — B5b pending),
Layout (navy sidebar, 6 sections, Lucide icons, teal --app-primary),
App.tsx (Outlet pattern, UploadApp outside Layout), index.css (full :root block).
strict: false — archive components have type issues; build clean.

Note: 45 archive components call fetch('/api/...') without /kitchen/ prefix
(B5b). Runtime 404s; deferred until after initial testing.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-12 12:15:39 +00:00

204 lines
6.7 KiB
Python

"""
Support Request API
Handles user support requests with page screenshots.
"""
import base64
import logging
from datetime import datetime
from fastapi import APIRouter, Depends, HTTPException
from pydantic import BaseModel
from sqlalchemy.ext.asyncio import AsyncSession
from database import get_db
from auth import get_current_user, require_cap
from models.user import User, Kitchen
from models.settings import KitchenSettings
from services.email_service import EmailService
from sqlalchemy import select
router = APIRouter()
logger = logging.getLogger(__name__)
class SupportRequest(BaseModel):
"""Support request payload"""
description: str
screenshot: str # Base64 encoded PNG
page_url: str
browser_info: str | None = None
class SupportResponse(BaseModel):
"""Support request response"""
success: bool
message: str
def generate_support_email_html(
user_name: str,
user_email: str,
kitchen_name: str,
description: str,
page_url: str,
browser_info: str | None,
timestamp: datetime
) -> str:
"""Generate HTML email body for support request"""
return f"""
<html>
<head>
<style>
body {{ font-family: Arial, sans-serif; color: #333; line-height: 1.6; }}
.header {{ background-color: #e94560; color: white; padding: 20px; }}
.content {{ padding: 20px; }}
.meta {{ background: #f5f5f5; padding: 15px; border-radius: 6px; margin-bottom: 20px; }}
.meta p {{ margin: 5px 0; }}
.label {{ font-weight: bold; color: #666; }}
.description {{ background: #fffbcc; padding: 15px; border-left: 4px solid #ffc107; margin: 20px 0; }}
.screenshot-note {{ color: #666; font-style: italic; margin-top: 20px; }}
</style>
</head>
<body>
<div class="header">
<h2>Support Request</h2>
</div>
<div class="content">
<div class="meta">
<p><span class="label">From:</span> {user_name} ({user_email})</p>
<p><span class="label">Kitchen:</span> {kitchen_name}</p>
<p><span class="label">Page URL:</span> {page_url}</p>
<p><span class="label">Timestamp:</span> {timestamp.strftime('%Y-%m-%d %H:%M:%S UTC')}</p>
{f'<p><span class="label">Browser:</span> {browser_info}</p>' if browser_info else ''}
</div>
<h3>Issue Description</h3>
<div class="description">
<p>{description.replace(chr(10), '<br>')}</p>
</div>
<p class="screenshot-note">A screenshot of the page is attached to this email.</p>
</div>
</body>
</html>
"""
@router.post("/support/request", response_model=SupportResponse)
async def submit_support_request(
request: SupportRequest,
current_user: User = Depends(get_current_user),
db: AsyncSession = Depends(get_db)
):
"""
Submit a support request with page screenshot.
The screenshot is sent as an email attachment to the configured support email.
"""
# Get kitchen settings
result = await db.execute(
select(KitchenSettings).where(KitchenSettings.kitchen_id == current_user.kitchen_id)
)
settings = result.scalar_one_or_none()
if not settings:
raise HTTPException(status_code=404, detail="Kitchen settings not found")
# Check if support email is configured
if not settings.support_email:
raise HTTPException(
status_code=400,
detail="Support email not configured. Please contact your administrator."
)
# Check if SMTP is configured
if not settings.smtp_host or not settings.smtp_from_email:
raise HTTPException(
status_code=400,
detail="Email settings not configured. Please contact your administrator."
)
# Fetch kitchen name explicitly to avoid lazy loading issues
kitchen_result = await db.execute(
select(Kitchen).where(Kitchen.id == current_user.kitchen_id)
)
kitchen = kitchen_result.scalar_one_or_none()
kitchen_name = kitchen.name if kitchen else "Unknown Kitchen"
try:
# Decode screenshot from base64
# Remove data URL prefix if present
screenshot_data = request.screenshot
if screenshot_data.startswith('data:'):
screenshot_data = screenshot_data.split(',', 1)[1]
screenshot_bytes = base64.b64decode(screenshot_data)
# Generate email
timestamp = datetime.utcnow()
html_body = generate_support_email_html(
user_name=current_user.name,
user_email=current_user.email,
kitchen_name=kitchen_name,
description=request.description,
page_url=request.page_url,
browser_info=request.browser_info,
timestamp=timestamp
)
# Create email subject
subject = f"Support Request from {current_user.name} - {kitchen_name}"
# Send email with screenshot attachment
email_service = EmailService(settings)
filename = f"screenshot_{timestamp.strftime('%Y%m%d_%H%M%S')}.png"
success = email_service.send_email(
to_email=settings.support_email,
subject=subject,
html_body=html_body,
attachments=[(filename, screenshot_bytes)]
)
if success:
logger.info(f"Support request sent from {current_user.name} to {settings.support_email}")
return SupportResponse(
success=True,
message="Support request sent successfully. We'll get back to you soon."
)
else:
logger.error(f"Failed to send support request email")
raise HTTPException(
status_code=500,
detail="Failed to send support request. Please try again later."
)
except base64.binascii.Error:
raise HTTPException(status_code=400, detail="Invalid screenshot data")
except Exception as e:
logger.error(f"Support request error: {e}")
raise HTTPException(status_code=500, detail=str(e))
@router.get("/support/enabled")
async def check_support_enabled(
current_user: User = Depends(get_current_user),
db: AsyncSession = Depends(get_db)
):
"""
Check if support requests are enabled (support email configured).
Returns whether the support button should be shown.
"""
result = await db.execute(
select(KitchenSettings).where(KitchenSettings.kitchen_id == current_user.kitchen_id)
)
settings = result.scalar_one_or_none()
enabled = bool(
settings and
settings.support_email and
settings.smtp_host and
settings.smtp_from_email
)
return {"enabled": enabled}