""" Mailing Labels & Envelopes API Endpoints: - POST /api/labels/rolodex/labels-5160 - POST /api/labels/files/labels-5160 - POST /api/labels/rolodex/envelopes - POST /api/labels/files/envelopes """ from __future__ import annotations from typing import List, Optional, Sequence from datetime import datetime, timezone from fastapi import APIRouter, Depends, HTTPException, status, Query from pydantic import BaseModel, Field from sqlalchemy.orm import Session import io import csv from app.auth.security import get_current_user from app.database.base import get_db from app.models.user import User from app.models.rolodex import Rolodex from app.services.customers_search import apply_customer_filters from app.services.mailing import ( Address, build_addresses_from_files, build_addresses_from_rolodex, build_address_from_rolodex, render_labels_html, render_envelopes_html, save_html_bytes, ) router = APIRouter() class Labels5160Request(BaseModel): ids: List[str] = Field(default_factory=list, description="Rolodex IDs or File numbers depending on route") start_position: int = Field(default=1, ge=1, le=30, description="Starting label position on sheet (1-30)") include_name: bool = Field(default=True, description="Include name/company as first line") class GenerateResult(BaseModel): url: Optional[str] = None storage_path: Optional[str] = None mime_type: str size: int created_at: str @router.post("/rolodex/labels-5160", response_model=GenerateResult) async def generate_rolodex_labels( payload: Labels5160Request, db: Session = Depends(get_db), current_user: User = Depends(get_current_user), ): if not payload.ids: raise HTTPException(status_code=400, detail="No rolodex IDs provided") addresses = build_addresses_from_rolodex(db, payload.ids) if not addresses: raise HTTPException(status_code=404, detail="No matching rolodex entries found") html_bytes = render_labels_html(addresses, start_position=payload.start_position, include_name=payload.include_name) result = save_html_bytes(html_bytes, filename_hint=f"labels_5160_{datetime.now(timezone.utc).strftime('%Y%m%d_%H%M%S')}", subdir="mailing/labels") return GenerateResult(**result) @router.post("/files/labels-5160", response_model=GenerateResult) async def generate_file_labels( payload: Labels5160Request, db: Session = Depends(get_db), current_user: User = Depends(get_current_user), ): if not payload.ids: raise HTTPException(status_code=400, detail="No file numbers provided") addresses = build_addresses_from_files(db, payload.ids) if not addresses: raise HTTPException(status_code=404, detail="No matching file owners found") html_bytes = render_labels_html(addresses, start_position=payload.start_position, include_name=payload.include_name) result = save_html_bytes(html_bytes, filename_hint=f"labels_5160_{datetime.now(timezone.utc).strftime('%Y%m%d_%H%M%S')}", subdir="mailing/labels") return GenerateResult(**result) class EnvelopesRequest(BaseModel): ids: List[str] = Field(default_factory=list, description="Rolodex IDs or File numbers depending on route") include_name: bool = Field(default=True) return_address_lines: Optional[List[str]] = Field(default=None, description="Lines for return address (top-left)") @router.post("/rolodex/envelopes", response_model=GenerateResult) async def generate_rolodex_envelopes( payload: EnvelopesRequest, db: Session = Depends(get_db), current_user: User = Depends(get_current_user), ): if not payload.ids: raise HTTPException(status_code=400, detail="No rolodex IDs provided") addresses = build_addresses_from_rolodex(db, payload.ids) if not addresses: raise HTTPException(status_code=404, detail="No matching rolodex entries found") html_bytes = render_envelopes_html(addresses, return_address_lines=payload.return_address_lines, include_name=payload.include_name) result = save_html_bytes(html_bytes, filename_hint=f"envelopes_{datetime.now(timezone.utc).strftime('%Y%m%d_%H%M%S')}", subdir="mailing/envelopes") return GenerateResult(**result) @router.post("/files/envelopes", response_model=GenerateResult) async def generate_file_envelopes( payload: EnvelopesRequest, db: Session = Depends(get_db), current_user: User = Depends(get_current_user), ): if not payload.ids: raise HTTPException(status_code=400, detail="No file numbers provided") addresses = build_addresses_from_files(db, payload.ids) if not addresses: raise HTTPException(status_code=404, detail="No matching file owners found") html_bytes = render_envelopes_html(addresses, return_address_lines=payload.return_address_lines, include_name=payload.include_name) result = save_html_bytes(html_bytes, filename_hint=f"envelopes_{datetime.now(timezone.utc).strftime('%Y%m%d_%H%M%S')}", subdir="mailing/envelopes") return GenerateResult(**result) @router.get("/rolodex/labels-5160/export") async def export_rolodex_labels_5160( start_position: int = Query(1, ge=1, le=30, description="Starting label position on sheet (1-30)"), include_name: bool = Query(True, description="Include name/company as first line"), group: Optional[str] = Query(None, description="Filter by customer group (exact match)"), groups: Optional[List[str]] = Query(None, description="Filter by multiple groups (repeat param)"), name_prefix: Optional[str] = Query(None, description="Prefix search across first/last name"), format: str = Query("html", description="Output format: html | csv"), db: Session = Depends(get_db), current_user: User = Depends(get_current_user), ): """Generate Avery 5160 labels for Rolodex entries selected by filters and stream as HTML or CSV.""" fmt = (format or "").strip().lower() if fmt not in {"html", "csv"}: raise HTTPException(status_code=400, detail="Invalid format. Use 'html' or 'csv'.") q = db.query(Rolodex) q = apply_customer_filters( q, search=None, group=group, state=None, groups=groups, states=None, name_prefix=name_prefix, ) entries = q.all() if not entries: raise HTTPException(status_code=404, detail="No matching rolodex entries found") if fmt == "html": addresses = [build_address_from_rolodex(r) for r in entries] html_bytes = render_labels_html(addresses, start_position=start_position, include_name=include_name) from fastapi.responses import StreamingResponse ts = datetime.now(timezone.utc).strftime('%Y%m%d_%H%M%S') filename = f"labels_5160_{ts}.html" return StreamingResponse( iter([html_bytes]), media_type="text/html", headers={"Content-Disposition": f"attachment; filename=\"{filename}\""}, ) else: # CSV of address fields output = io.StringIO() writer = csv.writer(output) writer.writerow(["Name", "Address 1", "Address 2", "Address 3", "City", "State", "ZIP"]) for r in entries: addr = build_address_from_rolodex(r) writer.writerow([ addr.display_name, r.a1 or "", r.a2 or "", r.a3 or "", r.city or "", r.abrev or "", r.zip or "", ]) output.seek(0) from fastapi.responses import StreamingResponse ts = datetime.now(timezone.utc).strftime('%Y%m%d_%H%M%S') filename = f"labels_5160_{ts}.csv" return StreamingResponse( iter([output.getvalue()]), media_type="text/csv", headers={"Content-Disposition": f"attachment; filename=\"{filename}\""}, ) @router.get("/rolodex/envelopes/export") async def export_rolodex_envelopes( include_name: bool = Query(True, description="Include name/company"), return_address_lines: Optional[List[str]] = Query(None, description="Optional return address lines"), group: Optional[str] = Query(None, description="Filter by customer group (exact match)"), groups: Optional[List[str]] = Query(None, description="Filter by multiple groups (repeat param)"), name_prefix: Optional[str] = Query(None, description="Prefix search across first/last name"), format: str = Query("html", description="Output format: html | csv"), db: Session = Depends(get_db), current_user: User = Depends(get_current_user), ): """Generate envelopes for Rolodex entries selected by filters and stream as HTML or CSV of addresses.""" fmt = (format or "").strip().lower() if fmt not in {"html", "csv"}: raise HTTPException(status_code=400, detail="Invalid format. Use 'html' or 'csv'.") q = db.query(Rolodex) q = apply_customer_filters( q, search=None, group=group, state=None, groups=groups, states=None, name_prefix=name_prefix, ) entries = q.all() if not entries: raise HTTPException(status_code=404, detail="No matching rolodex entries found") if fmt == "html": addresses = [build_address_from_rolodex(r) for r in entries] html_bytes = render_envelopes_html(addresses, return_address_lines=return_address_lines, include_name=include_name) from fastapi.responses import StreamingResponse ts = datetime.now(timezone.utc).strftime('%Y%m%d_%H%M%S') filename = f"envelopes_{ts}.html" return StreamingResponse( iter([html_bytes]), media_type="text/html", headers={"Content-Disposition": f"attachment; filename=\"{filename}\""}, ) else: # CSV of address fields output = io.StringIO() writer = csv.writer(output) writer.writerow(["Name", "Address 1", "Address 2", "Address 3", "City", "State", "ZIP"]) for r in entries: addr = build_address_from_rolodex(r) writer.writerow([ addr.display_name, r.a1 or "", r.a2 or "", r.a3 or "", r.city or "", r.abrev or "", r.zip or "", ]) output.seek(0) from fastapi.responses import StreamingResponse ts = datetime.now(timezone.utc).strftime('%Y%m%d_%H%M%S') filename = f"envelopes_{ts}.csv" return StreamingResponse( iter([output.getvalue()]), media_type="text/csv", headers={"Content-Disposition": f"attachment; filename=\"{filename}\""}, )