mirror of
https://github.com/idrainformatica/PecFlow.git
synced 2026-06-16 12:45:42 +02:00
Implementazioni varie
This commit is contained in:
@@ -0,0 +1,132 @@
|
||||
"""
|
||||
Router rubrica indirizzi PEC (Feature 6).
|
||||
|
||||
Endpoint:
|
||||
GET /contacts – lista contatti (con ricerca)
|
||||
POST /contacts – crea contatto manuale
|
||||
GET /contacts/autocomplete – autocomplete per compose
|
||||
GET /contacts/{id} – dettaglio contatto
|
||||
PUT /contacts/{id} – aggiorna contatto
|
||||
DELETE /contacts/{id} – elimina contatto
|
||||
POST /contacts/import – importa da CSV
|
||||
"""
|
||||
|
||||
import uuid
|
||||
|
||||
from fastapi import APIRouter, Query, UploadFile, File, status
|
||||
|
||||
from app.dependencies import CurrentUser, DB
|
||||
from app.schemas.pec_contact import (
|
||||
PecContactCreate,
|
||||
PecContactImportResult,
|
||||
PecContactListResponse,
|
||||
PecContactResponse,
|
||||
PecContactUpdate,
|
||||
)
|
||||
from app.services.pec_contact_service import PecContactService
|
||||
|
||||
router = APIRouter(tags=["Contacts"])
|
||||
|
||||
|
||||
@router.get("/contacts", response_model=PecContactListResponse)
|
||||
async def list_contacts(
|
||||
current_user: CurrentUser,
|
||||
db: DB,
|
||||
q: str | None = Query(None, description="Ricerca per email, nome o organizzazione"),
|
||||
page: int = Query(1, ge=1),
|
||||
page_size: int = Query(50, ge=1, le=200),
|
||||
) -> PecContactListResponse:
|
||||
"""Elenca i contatti della rubrica PEC del tenant."""
|
||||
svc = PecContactService(db)
|
||||
items, total = await svc.list_contacts(
|
||||
current_user.tenant_id, q=q, page=page, page_size=page_size
|
||||
)
|
||||
return PecContactListResponse(
|
||||
items=[PecContactResponse.model_validate(c) for c in items],
|
||||
total=total,
|
||||
)
|
||||
|
||||
|
||||
@router.get("/contacts/autocomplete", response_model=list[PecContactResponse])
|
||||
async def autocomplete_contacts(
|
||||
q: str,
|
||||
current_user: CurrentUser,
|
||||
db: DB,
|
||||
limit: int = Query(10, ge=1, le=20),
|
||||
) -> list[PecContactResponse]:
|
||||
"""Ricerca rapida contatti per autocomplete nel compose (minimo 2 caratteri)."""
|
||||
svc = PecContactService(db)
|
||||
items = await svc.search_for_autocomplete(current_user.tenant_id, q=q, limit=limit)
|
||||
return [PecContactResponse.model_validate(c) for c in items]
|
||||
|
||||
|
||||
@router.post("/contacts", response_model=PecContactResponse, status_code=status.HTTP_201_CREATED)
|
||||
async def create_contact(
|
||||
data: PecContactCreate,
|
||||
current_user: CurrentUser,
|
||||
db: DB,
|
||||
) -> PecContactResponse:
|
||||
"""Aggiunge un contatto manualmente alla rubrica."""
|
||||
svc = PecContactService(db)
|
||||
contact = await svc.create_contact(
|
||||
current_user.tenant_id, data, created_by=current_user.id
|
||||
)
|
||||
return PecContactResponse.model_validate(contact)
|
||||
|
||||
|
||||
@router.get("/contacts/{contact_id}", response_model=PecContactResponse)
|
||||
async def get_contact(
|
||||
contact_id: uuid.UUID,
|
||||
current_user: CurrentUser,
|
||||
db: DB,
|
||||
) -> PecContactResponse:
|
||||
"""Restituisce il dettaglio di un contatto."""
|
||||
svc = PecContactService(db)
|
||||
contact = await svc.get_contact(current_user.tenant_id, contact_id)
|
||||
return PecContactResponse.model_validate(contact)
|
||||
|
||||
|
||||
@router.put("/contacts/{contact_id}", response_model=PecContactResponse)
|
||||
async def update_contact(
|
||||
contact_id: uuid.UUID,
|
||||
data: PecContactUpdate,
|
||||
current_user: CurrentUser,
|
||||
db: DB,
|
||||
) -> PecContactResponse:
|
||||
"""Aggiorna un contatto della rubrica."""
|
||||
svc = PecContactService(db)
|
||||
contact = await svc.update_contact(current_user.tenant_id, contact_id, data)
|
||||
return PecContactResponse.model_validate(contact)
|
||||
|
||||
|
||||
@router.delete("/contacts/{contact_id}", status_code=status.HTTP_204_NO_CONTENT)
|
||||
async def delete_contact(
|
||||
contact_id: uuid.UUID,
|
||||
current_user: CurrentUser,
|
||||
db: DB,
|
||||
) -> None:
|
||||
"""Elimina un contatto dalla rubrica."""
|
||||
svc = PecContactService(db)
|
||||
await svc.delete_contact(current_user.tenant_id, contact_id)
|
||||
|
||||
|
||||
@router.post("/contacts/import", response_model=PecContactImportResult)
|
||||
async def import_contacts_csv(
|
||||
file: UploadFile = File(..., description="File CSV con colonne: email, name, organization"),
|
||||
current_user: CurrentUser = ..., # type: ignore
|
||||
db: DB = ..., # type: ignore
|
||||
) -> PecContactImportResult:
|
||||
"""
|
||||
Importa contatti dalla rubrica da file CSV.
|
||||
|
||||
Il CSV deve avere le intestazioni: email, name, organization
|
||||
(solo email e' obbligatoria).
|
||||
"""
|
||||
content = await file.read()
|
||||
try:
|
||||
csv_text = content.decode("utf-8")
|
||||
except UnicodeDecodeError:
|
||||
csv_text = content.decode("latin-1")
|
||||
|
||||
svc = PecContactService(db)
|
||||
return await svc.import_csv(current_user.tenant_id, csv_text, created_by=current_user.id)
|
||||
@@ -0,0 +1,152 @@
|
||||
"""
|
||||
Router scadenzario e tracking deadlines (Feature 4).
|
||||
|
||||
Endpoint:
|
||||
GET /deadlines – messaggi con scadenze imminenti
|
||||
POST /messages/{id}/deadline – imposta/modifica/rimuove scadenza
|
||||
"""
|
||||
|
||||
import uuid
|
||||
from datetime import datetime, timedelta, timezone
|
||||
|
||||
from fastapi import APIRouter, Query
|
||||
from pydantic import BaseModel
|
||||
from sqlalchemy import and_, select
|
||||
|
||||
from app.dependencies import CurrentUser, DB
|
||||
from app.models.message import Message
|
||||
from app.schemas.message import MessageResponse
|
||||
from app.core.exceptions import NotFoundError
|
||||
|
||||
router = APIRouter(tags=["Deadlines"])
|
||||
|
||||
|
||||
class DeadlineSetRequest(BaseModel):
|
||||
deadline_at: datetime | None = None
|
||||
"""Imposta a null per rimuovere la scadenza."""
|
||||
deadline_note: str | None = None
|
||||
|
||||
|
||||
class DeadlineMessageResponse(BaseModel):
|
||||
model_config = {"from_attributes": True}
|
||||
|
||||
id: uuid.UUID
|
||||
subject: str | None
|
||||
from_address: str | None
|
||||
to_addresses: list[str] | None = None
|
||||
direction: str
|
||||
pec_type: str
|
||||
state: str
|
||||
mailbox_id: uuid.UUID
|
||||
deadline_at: datetime | None = None
|
||||
deadline_note: str | None = None
|
||||
is_overdue: bool = False
|
||||
received_at: datetime | None = None
|
||||
sent_at: datetime | None = None
|
||||
created_at: datetime
|
||||
|
||||
|
||||
@router.get("/deadlines", response_model=list[DeadlineMessageResponse])
|
||||
async def list_deadlines(
|
||||
current_user: CurrentUser,
|
||||
db: DB,
|
||||
days_ahead: int = Query(30, ge=1, le=365, description="Giorni da considerare in avanti"),
|
||||
include_overdue: bool = Query(True, description="Includi scadenze gia' passate"),
|
||||
) -> list[DeadlineMessageResponse]:
|
||||
"""
|
||||
Restituisce i messaggi con scadenze nel range specificato.
|
||||
|
||||
Ordinati per: scaduti prima, poi per deadline_at ASC.
|
||||
"""
|
||||
now = datetime.now(timezone.utc)
|
||||
future_limit = now + timedelta(days=days_ahead)
|
||||
|
||||
conditions = [
|
||||
Message.tenant_id == current_user.tenant_id,
|
||||
Message.deadline_at.is_not(None),
|
||||
Message.is_trashed == False, # noqa: E712
|
||||
]
|
||||
|
||||
if include_overdue:
|
||||
# Include scaduti e futuri fino al limite
|
||||
conditions.append(Message.deadline_at <= future_limit)
|
||||
else:
|
||||
# Solo scadenze future
|
||||
conditions.append(and_(Message.deadline_at > now, Message.deadline_at <= future_limit))
|
||||
|
||||
result = await db.execute(
|
||||
select(Message)
|
||||
.where(and_(*conditions))
|
||||
.order_by(Message.deadline_at)
|
||||
.limit(200)
|
||||
)
|
||||
messages = list(result.scalars().all())
|
||||
|
||||
items = []
|
||||
for msg in messages:
|
||||
is_overdue = msg.deadline_at < now if msg.deadline_at else False
|
||||
items.append(DeadlineMessageResponse(
|
||||
id=msg.id,
|
||||
subject=msg.subject,
|
||||
from_address=msg.from_address,
|
||||
to_addresses=msg.to_addresses,
|
||||
direction=msg.direction,
|
||||
pec_type=msg.pec_type,
|
||||
state=msg.state,
|
||||
mailbox_id=msg.mailbox_id,
|
||||
deadline_at=msg.deadline_at,
|
||||
deadline_note=msg.deadline_note,
|
||||
is_overdue=is_overdue,
|
||||
received_at=msg.received_at,
|
||||
sent_at=msg.sent_at,
|
||||
created_at=msg.created_at,
|
||||
))
|
||||
return items
|
||||
|
||||
|
||||
@router.post("/messages/{message_id}/deadline", response_model=DeadlineMessageResponse)
|
||||
async def set_deadline(
|
||||
message_id: uuid.UUID,
|
||||
data: DeadlineSetRequest,
|
||||
current_user: CurrentUser,
|
||||
db: DB,
|
||||
) -> DeadlineMessageResponse:
|
||||
"""
|
||||
Imposta, modifica o rimuove la scadenza di un messaggio.
|
||||
|
||||
Passa deadline_at=null per rimuovere la scadenza.
|
||||
"""
|
||||
result = await db.execute(
|
||||
select(Message).where(
|
||||
Message.id == message_id,
|
||||
Message.tenant_id == current_user.tenant_id,
|
||||
)
|
||||
)
|
||||
msg = result.scalar_one_or_none()
|
||||
if not msg:
|
||||
raise NotFoundError(f"Messaggio {message_id} non trovato")
|
||||
|
||||
msg.deadline_at = data.deadline_at
|
||||
msg.deadline_note = data.deadline_note
|
||||
await db.commit()
|
||||
await db.refresh(msg)
|
||||
|
||||
now = datetime.now(timezone.utc)
|
||||
is_overdue = msg.deadline_at < now if msg.deadline_at else False
|
||||
|
||||
return DeadlineMessageResponse(
|
||||
id=msg.id,
|
||||
subject=msg.subject,
|
||||
from_address=msg.from_address,
|
||||
to_addresses=msg.to_addresses,
|
||||
direction=msg.direction,
|
||||
pec_type=msg.pec_type,
|
||||
state=msg.state,
|
||||
mailbox_id=msg.mailbox_id,
|
||||
deadline_at=msg.deadline_at,
|
||||
deadline_note=msg.deadline_note,
|
||||
is_overdue=is_overdue,
|
||||
received_at=msg.received_at,
|
||||
sent_at=msg.sent_at,
|
||||
created_at=msg.created_at,
|
||||
)
|
||||
@@ -709,3 +709,284 @@ async def list_receipts(
|
||||
)
|
||||
receipts = list(result.scalars().all())
|
||||
return [MessageResponse.model_validate(r) for r in receipts]
|
||||
|
||||
|
||||
# ─── Feature 3: Thread/conversazioni ─────────────────────────────────────────
|
||||
|
||||
@router.get("/{message_id}/thread", response_model=list[MessageResponse])
|
||||
async def get_thread(
|
||||
message_id: uuid.UUID,
|
||||
current_user: CurrentUser,
|
||||
db: DB,
|
||||
) -> list[MessageResponse]:
|
||||
"""
|
||||
Restituisce l'intera conversazione (thread) di cui fa parte il messaggio.
|
||||
|
||||
Risale alla radice della conversazione (risalendo i parent_message_id),
|
||||
poi carica tutti i messaggi del thread ordinati cronologicamente.
|
||||
Esclude le ricevute PEC (pec_type != posta_certificata).
|
||||
"""
|
||||
message = await _resolve_message(message_id, current_user, db)
|
||||
|
||||
# Risale alla radice del thread
|
||||
root_id = message.id
|
||||
visited: set[uuid.UUID] = {message.id}
|
||||
current = message
|
||||
while current.parent_message_id and current.parent_message_id not in visited:
|
||||
visited.add(current.parent_message_id)
|
||||
parent_result = await db.execute(
|
||||
select(Message).where(
|
||||
Message.id == current.parent_message_id,
|
||||
Message.tenant_id == current_user.tenant_id,
|
||||
)
|
||||
)
|
||||
parent = parent_result.scalar_one_or_none()
|
||||
if not parent:
|
||||
break
|
||||
current = parent
|
||||
root_id = current.id
|
||||
|
||||
# Carica ricorsivamente tutti i messaggi del thread dalla radice
|
||||
# Limita a posta_certificata (esclude accettazioni, consegne, ecc.)
|
||||
thread_messages: list[Message] = []
|
||||
|
||||
async def _collect(msg_id: uuid.UUID) -> None:
|
||||
result = await db.execute(
|
||||
select(Message)
|
||||
.where(
|
||||
Message.id == msg_id,
|
||||
Message.tenant_id == current_user.tenant_id,
|
||||
Message.pec_type == "posta_certificata",
|
||||
)
|
||||
.options(selectinload(Message.labels))
|
||||
)
|
||||
msg = result.scalar_one_or_none()
|
||||
if msg:
|
||||
thread_messages.append(msg)
|
||||
|
||||
children_result = await db.execute(
|
||||
select(Message)
|
||||
.where(
|
||||
Message.parent_message_id == msg_id,
|
||||
Message.tenant_id == current_user.tenant_id,
|
||||
Message.pec_type == "posta_certificata",
|
||||
)
|
||||
.options(selectinload(Message.labels))
|
||||
.order_by(Message.received_at.asc().nullslast(), Message.created_at.asc())
|
||||
)
|
||||
children = list(children_result.scalars().all())
|
||||
for child in children:
|
||||
await _collect(child.id)
|
||||
|
||||
await _collect(root_id)
|
||||
|
||||
# Ordina cronologicamente
|
||||
thread_messages.sort(
|
||||
key=lambda m: m.received_at or m.sent_at or m.created_at
|
||||
)
|
||||
|
||||
return [MessageResponse.model_validate(m) for m in thread_messages]
|
||||
|
||||
|
||||
# ─── Feature 7: Preview allegati (presigned URL) ──────────────────────────────
|
||||
|
||||
@router.get("/{message_id}/attachments/{attachment_id}/preview-url")
|
||||
async def get_attachment_preview_url(
|
||||
message_id: uuid.UUID,
|
||||
attachment_id: uuid.UUID,
|
||||
current_user: CurrentUser,
|
||||
db: DB,
|
||||
) -> dict:
|
||||
"""
|
||||
Restituisce una presigned URL MinIO per la preview inline dell'allegato.
|
||||
|
||||
La URL e' valida per 5 minuti. Supporta PDF e immagini.
|
||||
Per altri tipi di file reindirizza al download normale.
|
||||
"""
|
||||
await _resolve_message(message_id, current_user, db)
|
||||
|
||||
result = await db.execute(
|
||||
select(Attachment).where(
|
||||
Attachment.id == attachment_id,
|
||||
Attachment.message_id == message_id,
|
||||
)
|
||||
)
|
||||
attachment = result.scalar_one_or_none()
|
||||
if not attachment:
|
||||
raise NotFoundError(f"Allegato {attachment_id} non trovato")
|
||||
|
||||
content_type = attachment.content_type or "application/octet-stream"
|
||||
previewable = (
|
||||
content_type.startswith("image/") or
|
||||
content_type == "application/pdf"
|
||||
)
|
||||
|
||||
if not previewable:
|
||||
return {
|
||||
"previewable": False,
|
||||
"content_type": content_type,
|
||||
"filename": attachment.filename,
|
||||
}
|
||||
|
||||
try:
|
||||
from datetime import timedelta as _timedelta
|
||||
from miniopy_async import Minio
|
||||
|
||||
client = Minio(
|
||||
endpoint=settings.minio_endpoint,
|
||||
access_key=settings.minio_access_key,
|
||||
secret_key=settings.minio_secret_key,
|
||||
secure=settings.minio_use_ssl,
|
||||
)
|
||||
presigned_url = await client.presigned_get_object(
|
||||
settings.minio_bucket,
|
||||
attachment.storage_path,
|
||||
expires=_timedelta(minutes=5),
|
||||
)
|
||||
return {
|
||||
"previewable": True,
|
||||
"content_type": content_type,
|
||||
"filename": attachment.filename,
|
||||
"url": presigned_url,
|
||||
}
|
||||
except Exception as e:
|
||||
from app.core.logging import get_logger
|
||||
logger = get_logger(__name__)
|
||||
logger.error(f"Errore generazione presigned URL allegato {attachment_id}: {e}")
|
||||
return {
|
||||
"previewable": False,
|
||||
"content_type": content_type,
|
||||
"filename": attachment.filename,
|
||||
}
|
||||
|
||||
|
||||
# ─── Feature 8: Stampa/export HTML ────────────────────────────────────────────
|
||||
|
||||
@router.get("/{message_id}/print")
|
||||
async def print_message(
|
||||
message_id: uuid.UUID,
|
||||
current_user: CurrentUser,
|
||||
db: DB,
|
||||
) -> "HTMLResponse":
|
||||
"""
|
||||
Restituisce una rappresentazione HTML ottimizzata per la stampa del messaggio.
|
||||
|
||||
Include: intestazione, corpo, lista allegati, albero ricevute.
|
||||
Pronto per window.print() o salvataggio come PDF tramite browser.
|
||||
"""
|
||||
from fastapi.responses import HTMLResponse
|
||||
|
||||
message = await _resolve_message(message_id, current_user, db)
|
||||
|
||||
att_result = await db.execute(
|
||||
select(Attachment).where(Attachment.message_id == message.id).order_by(Attachment.created_at)
|
||||
)
|
||||
attachments = list(att_result.scalars().all())
|
||||
|
||||
receipts_html = ""
|
||||
if message.direction == "outbound":
|
||||
rec_result = await db.execute(
|
||||
select(Message)
|
||||
.where(Message.parent_message_id == message.id)
|
||||
.order_by(Message.received_at.asc().nullslast(), Message.created_at.asc())
|
||||
)
|
||||
receipts = list(rec_result.scalars().all())
|
||||
|
||||
PEC_TYPE_LABELS = {
|
||||
"accettazione": "Accettazione",
|
||||
"avvenuta_consegna": "Avvenuta consegna",
|
||||
"non_accettazione": "Non accettazione",
|
||||
"mancata_consegna": "Mancata consegna",
|
||||
"errore_consegna": "Errore consegna",
|
||||
"presa_in_carico": "Presa in carico",
|
||||
"preavviso_mancata_consegna": "Preavviso mancata consegna",
|
||||
"rilevazione_virus": "Rilevazione virus",
|
||||
}
|
||||
|
||||
receipt_rows = ""
|
||||
for r in receipts:
|
||||
label = PEC_TYPE_LABELS.get(r.pec_type, r.pec_type)
|
||||
date_str = ""
|
||||
if r.received_at:
|
||||
date_str = r.received_at.strftime("%d/%m/%Y %H:%M:%S")
|
||||
receipt_rows += f"<tr><td>{label}</td><td>{date_str}</td></tr>"
|
||||
|
||||
if receipt_rows:
|
||||
receipts_html = f"""
|
||||
<section>
|
||||
<h3>Tracciamento invio</h3>
|
||||
<table>
|
||||
<thead><tr><th>Tipo ricevuta</th><th>Data</th></tr></thead>
|
||||
<tbody>{receipt_rows}</tbody>
|
||||
</table>
|
||||
</section>"""
|
||||
|
||||
att_rows = ""
|
||||
for att in attachments:
|
||||
size_str = f"{att.size_bytes:,} byte" if att.size_bytes else ""
|
||||
att_rows += f"<li>{att.filename} ({att.content_type or ''}) {size_str}</li>"
|
||||
|
||||
att_html = f"<section><h3>Allegati ({len(attachments)})</h3><ul>{att_rows}</ul></section>" if attachments else ""
|
||||
|
||||
from_label = "Da" if message.direction == "inbound" else "A"
|
||||
from_val = message.from_address if message.direction == "inbound" else ", ".join(message.to_addresses or [])
|
||||
date_val = ""
|
||||
date_field = message.received_at or message.sent_at or message.created_at
|
||||
if date_field:
|
||||
date_val = date_field.strftime("%d/%m/%Y %H:%M:%S")
|
||||
|
||||
body_html = ""
|
||||
if message.body_html:
|
||||
body_html = f"<div class='body'>{message.body_html}</div>"
|
||||
elif message.body_text:
|
||||
body_html = f"<pre class='body'>{message.body_text}</pre>"
|
||||
|
||||
deadline_html = ""
|
||||
if message.deadline_at:
|
||||
dl_str = message.deadline_at.strftime("%d/%m/%Y %H:%M")
|
||||
deadline_html = f"<p class='deadline'><strong>Scadenza:</strong> {dl_str}</p>"
|
||||
if message.deadline_note:
|
||||
deadline_html += f"<p><em>Nota scadenza: {message.deadline_note}</em></p>"
|
||||
|
||||
html = f"""<!DOCTYPE html>
|
||||
<html lang="it">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>PEC - {message.subject or '(nessun oggetto)'}</title>
|
||||
<style>
|
||||
body {{ font-family: Arial, sans-serif; font-size: 12pt; margin: 2cm; color: #000; }}
|
||||
h1 {{ font-size: 16pt; border-bottom: 2px solid #333; padding-bottom: 8px; }}
|
||||
h3 {{ font-size: 13pt; margin-top: 20px; border-bottom: 1px solid #ccc; }}
|
||||
.meta {{ background: #f5f5f5; border: 1px solid #ddd; padding: 12px; margin-bottom: 16px; }}
|
||||
.meta p {{ margin: 4px 0; }}
|
||||
.body {{ border: 1px solid #ddd; padding: 12px; white-space: pre-wrap; word-wrap: break-word; }}
|
||||
table {{ border-collapse: collapse; width: 100%; }}
|
||||
th, td {{ border: 1px solid #ccc; padding: 6px 10px; text-align: left; }}
|
||||
th {{ background: #eee; }}
|
||||
ul {{ padding-left: 20px; }}
|
||||
.deadline {{ color: #c00; font-weight: bold; }}
|
||||
@media print {{ body {{ margin: 1cm; }} }}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>{message.subject or '(nessun oggetto)'}</h1>
|
||||
<div class="meta">
|
||||
<p><strong>{from_label}:</strong> {from_val}</p>
|
||||
{'<p><strong>Da:</strong> ' + message.from_address + '</p>' if message.direction == "outbound" and message.from_address else ''}
|
||||
{'<p><strong>A:</strong> ' + ', '.join(message.to_addresses or []) + '</p>' if message.direction == "inbound" and message.to_addresses else ''}
|
||||
{'<p><strong>Cc:</strong> ' + ', '.join(message.cc_addresses or []) + '</p>' if message.cc_addresses else ''}
|
||||
<p><strong>Data:</strong> {date_val}</p>
|
||||
<p><strong>Stato:</strong> {message.state}</p>
|
||||
<p><strong>Tipo:</strong> {message.pec_type}</p>
|
||||
</div>
|
||||
{deadline_html}
|
||||
{body_html}
|
||||
{att_html}
|
||||
{receipts_html}
|
||||
<p style="margin-top: 30px; font-size: 9pt; color: #888;">
|
||||
Documento generato da PEChub il {date_val} – ID messaggio: {message.id}
|
||||
</p>
|
||||
</body>
|
||||
</html>"""
|
||||
|
||||
return HTMLResponse(content=html, media_type="text/html; charset=utf-8")
|
||||
|
||||
@@ -0,0 +1,106 @@
|
||||
"""
|
||||
Router regole di smistamento automatico (Feature 2).
|
||||
|
||||
Endpoint:
|
||||
GET /routing-rules – lista regole del tenant
|
||||
POST /routing-rules – crea regola (admin)
|
||||
GET /routing-rules/{id} – dettaglio regola
|
||||
PUT /routing-rules/{id} – aggiorna regola (admin)
|
||||
DELETE /routing-rules/{id} – elimina regola (admin)
|
||||
POST /routing-rules/{id}/toggle – abilita/disabilita regola (admin)
|
||||
"""
|
||||
|
||||
import uuid
|
||||
|
||||
from fastapi import APIRouter, status
|
||||
|
||||
from app.dependencies import AdminUser, CurrentUser, DB
|
||||
from app.schemas.routing_rule import (
|
||||
RoutingRuleCreate,
|
||||
RoutingRuleListResponse,
|
||||
RoutingRuleResponse,
|
||||
RoutingRuleUpdate,
|
||||
)
|
||||
from app.services.routing_rule_service import RoutingRuleService
|
||||
|
||||
router = APIRouter(tags=["Routing Rules"])
|
||||
|
||||
|
||||
@router.get("/routing-rules", response_model=RoutingRuleListResponse)
|
||||
async def list_rules(
|
||||
current_user: CurrentUser,
|
||||
db: DB,
|
||||
) -> RoutingRuleListResponse:
|
||||
"""Elenca le regole di smistamento del tenant."""
|
||||
svc = RoutingRuleService(db)
|
||||
items, total = await svc.list_rules(current_user.tenant_id)
|
||||
return RoutingRuleListResponse(
|
||||
items=[RoutingRuleResponse.model_validate(r) for r in items],
|
||||
total=total,
|
||||
)
|
||||
|
||||
|
||||
@router.post("/routing-rules", response_model=RoutingRuleResponse, status_code=status.HTTP_201_CREATED)
|
||||
async def create_rule(
|
||||
data: RoutingRuleCreate,
|
||||
current_user: AdminUser,
|
||||
db: DB,
|
||||
) -> RoutingRuleResponse:
|
||||
"""Crea una nuova regola di smistamento (solo admin)."""
|
||||
svc = RoutingRuleService(db)
|
||||
rule = await svc.create_rule(current_user.tenant_id, data, created_by=current_user.id)
|
||||
return RoutingRuleResponse.model_validate(rule)
|
||||
|
||||
|
||||
@router.get("/routing-rules/{rule_id}", response_model=RoutingRuleResponse)
|
||||
async def get_rule(
|
||||
rule_id: uuid.UUID,
|
||||
current_user: CurrentUser,
|
||||
db: DB,
|
||||
) -> RoutingRuleResponse:
|
||||
"""Restituisce il dettaglio di una regola."""
|
||||
svc = RoutingRuleService(db)
|
||||
rule = await svc.get_rule(current_user.tenant_id, rule_id)
|
||||
return RoutingRuleResponse.model_validate(rule)
|
||||
|
||||
|
||||
@router.put("/routing-rules/{rule_id}", response_model=RoutingRuleResponse)
|
||||
async def update_rule(
|
||||
rule_id: uuid.UUID,
|
||||
data: RoutingRuleUpdate,
|
||||
current_user: AdminUser,
|
||||
db: DB,
|
||||
) -> RoutingRuleResponse:
|
||||
"""Aggiorna una regola di smistamento (solo admin)."""
|
||||
svc = RoutingRuleService(db)
|
||||
rule = await svc.update_rule(current_user.tenant_id, rule_id, data)
|
||||
return RoutingRuleResponse.model_validate(rule)
|
||||
|
||||
|
||||
@router.delete("/routing-rules/{rule_id}", status_code=status.HTTP_204_NO_CONTENT)
|
||||
async def delete_rule(
|
||||
rule_id: uuid.UUID,
|
||||
current_user: AdminUser,
|
||||
db: DB,
|
||||
) -> None:
|
||||
"""Elimina una regola di smistamento (solo admin)."""
|
||||
svc = RoutingRuleService(db)
|
||||
await svc.delete_rule(current_user.tenant_id, rule_id)
|
||||
|
||||
|
||||
@router.post("/routing-rules/{rule_id}/toggle", response_model=RoutingRuleResponse)
|
||||
async def toggle_rule(
|
||||
rule_id: uuid.UUID,
|
||||
current_user: AdminUser,
|
||||
db: DB,
|
||||
) -> RoutingRuleResponse:
|
||||
"""Abilita o disabilita una regola di smistamento (admin)."""
|
||||
svc = RoutingRuleService(db)
|
||||
rule = await svc.get_rule(current_user.tenant_id, rule_id)
|
||||
from app.schemas.routing_rule import RoutingRuleUpdate
|
||||
updated = await svc.update_rule(
|
||||
current_user.tenant_id,
|
||||
rule_id,
|
||||
RoutingRuleUpdate(is_active=not rule.is_active),
|
||||
)
|
||||
return RoutingRuleResponse.model_validate(updated)
|
||||
@@ -0,0 +1,83 @@
|
||||
"""
|
||||
Router template messaggi (Feature 1).
|
||||
|
||||
Endpoint:
|
||||
GET /templates – lista template del tenant
|
||||
POST /templates – crea template (admin)
|
||||
GET /templates/{id} – dettaglio template
|
||||
PUT /templates/{id} – aggiorna template (admin)
|
||||
DELETE /templates/{id} – elimina template (admin)
|
||||
"""
|
||||
|
||||
import uuid
|
||||
|
||||
from fastapi import APIRouter, Query, status
|
||||
|
||||
from app.dependencies import AdminUser, CurrentUser, DB
|
||||
from app.schemas.template import TemplateCreate, TemplateListResponse, TemplateResponse, TemplateUpdate
|
||||
from app.services.template_service import TemplateService
|
||||
|
||||
router = APIRouter(tags=["Templates"])
|
||||
|
||||
|
||||
@router.get("/templates", response_model=TemplateListResponse)
|
||||
async def list_templates(
|
||||
current_user: CurrentUser,
|
||||
db: DB,
|
||||
q: str | None = Query(None, description="Filtro per nome"),
|
||||
) -> TemplateListResponse:
|
||||
"""Elenca i template del tenant corrente."""
|
||||
svc = TemplateService(db)
|
||||
items, total = await svc.list_templates(current_user.tenant_id, q=q)
|
||||
return TemplateListResponse(
|
||||
items=[TemplateResponse.model_validate(t) for t in items],
|
||||
total=total,
|
||||
)
|
||||
|
||||
|
||||
@router.post("/templates", response_model=TemplateResponse, status_code=status.HTTP_201_CREATED)
|
||||
async def create_template(
|
||||
data: TemplateCreate,
|
||||
current_user: AdminUser,
|
||||
db: DB,
|
||||
) -> TemplateResponse:
|
||||
"""Crea un nuovo template (solo admin)."""
|
||||
svc = TemplateService(db)
|
||||
template = await svc.create_template(current_user.tenant_id, data, created_by=current_user.id)
|
||||
return TemplateResponse.model_validate(template)
|
||||
|
||||
|
||||
@router.get("/templates/{template_id}", response_model=TemplateResponse)
|
||||
async def get_template(
|
||||
template_id: uuid.UUID,
|
||||
current_user: CurrentUser,
|
||||
db: DB,
|
||||
) -> TemplateResponse:
|
||||
"""Restituisce il dettaglio di un template."""
|
||||
svc = TemplateService(db)
|
||||
template = await svc.get_template(current_user.tenant_id, template_id)
|
||||
return TemplateResponse.model_validate(template)
|
||||
|
||||
|
||||
@router.put("/templates/{template_id}", response_model=TemplateResponse)
|
||||
async def update_template(
|
||||
template_id: uuid.UUID,
|
||||
data: TemplateUpdate,
|
||||
current_user: AdminUser,
|
||||
db: DB,
|
||||
) -> TemplateResponse:
|
||||
"""Aggiorna un template esistente (solo admin)."""
|
||||
svc = TemplateService(db)
|
||||
template = await svc.update_template(current_user.tenant_id, template_id, data)
|
||||
return TemplateResponse.model_validate(template)
|
||||
|
||||
|
||||
@router.delete("/templates/{template_id}", status_code=status.HTTP_204_NO_CONTENT)
|
||||
async def delete_template(
|
||||
template_id: uuid.UUID,
|
||||
current_user: AdminUser,
|
||||
db: DB,
|
||||
) -> None:
|
||||
"""Elimina un template (solo admin)."""
|
||||
svc = TemplateService(db)
|
||||
await svc.delete_template(current_user.tenant_id, template_id)
|
||||
Reference in New Issue
Block a user