This commit is contained in:
HotSwapp
2025-08-18 20:20:04 -05:00
parent 89b2bc0aa2
commit bac8cc4bd5
114 changed files with 30258 additions and 1341 deletions

258
app/api/labels.py Normal file
View File

@@ -0,0 +1,258 @@
"""
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}\""},
)