fix frontend
This commit is contained in:
@@ -33,7 +33,7 @@ from app.services.audit_service import get_real_ip
|
|||||||
from app.services.search_service import SearchService
|
from app.services.search_service import SearchService
|
||||||
|
|
||||||
from app.config import get_settings
|
from app.config import get_settings
|
||||||
from app.core.exceptions import ForbiddenError, NotFoundError
|
from app.core.exceptions import ConflictError, ForbiddenError, NotFoundError
|
||||||
from app.database import get_db
|
from app.database import get_db
|
||||||
from app.dependencies import CurrentUser, DB
|
from app.dependencies import CurrentUser, DB
|
||||||
from app.models.label import Label
|
from app.models.label import Label
|
||||||
@@ -446,11 +446,15 @@ async def bulk_update_messages(
|
|||||||
elif not data.is_trashed:
|
elif not data.is_trashed:
|
||||||
message.trashed_at = None
|
message.trashed_at = None
|
||||||
if data.is_pending_conservation is not None:
|
if data.is_pending_conservation is not None:
|
||||||
message.is_pending_conservation = data.is_pending_conservation
|
# Guard bulk: non rimandare in conservazione messaggi gia' conservati
|
||||||
if data.is_pending_conservation and not message.pending_conservation_at:
|
if data.is_pending_conservation and message.is_conserved:
|
||||||
message.pending_conservation_at = now
|
pass # skip — gia' conservato, non viene ri-accodato
|
||||||
elif not data.is_pending_conservation:
|
else:
|
||||||
message.pending_conservation_at = None
|
message.is_pending_conservation = data.is_pending_conservation
|
||||||
|
if data.is_pending_conservation and not message.pending_conservation_at:
|
||||||
|
message.pending_conservation_at = now
|
||||||
|
elif not data.is_pending_conservation:
|
||||||
|
message.pending_conservation_at = None
|
||||||
if data.is_conserved is not None:
|
if data.is_conserved is not None:
|
||||||
message.is_conserved = data.is_conserved
|
message.is_conserved = data.is_conserved
|
||||||
if data.is_conserved and not message.conserved_at:
|
if data.is_conserved and not message.conserved_at:
|
||||||
@@ -551,6 +555,12 @@ async def update_message(
|
|||||||
perm_svc = PermissionService(db)
|
perm_svc = PermissionService(db)
|
||||||
await perm_svc.require_can_conserve(current_user, message.mailbox_id)
|
await perm_svc.require_can_conserve(current_user, message.mailbox_id)
|
||||||
|
|
||||||
|
# Guard: blocca re-accodamento di messaggi gia' conservati
|
||||||
|
if data.is_pending_conservation is True and message.is_conserved:
|
||||||
|
raise ConflictError(
|
||||||
|
"Questo messaggio e' gia' stato conservato e non puo' essere rimandato in conservazione."
|
||||||
|
)
|
||||||
|
|
||||||
now = datetime.now(timezone.utc)
|
now = datetime.now(timezone.utc)
|
||||||
ip = get_real_ip(request)
|
ip = get_real_ip(request)
|
||||||
ua = request.headers.get("user-agent")
|
ua = request.headers.get("user-agent")
|
||||||
|
|||||||
@@ -69,6 +69,8 @@ class MailboxService:
|
|||||||
email_address=str(data.email_address),
|
email_address=str(data.email_address),
|
||||||
display_name=data.display_name,
|
display_name=data.display_name,
|
||||||
provider=data.provider,
|
provider=data.provider,
|
||||||
|
protocol_type=data.protocol_type,
|
||||||
|
rem_provider=data.rem_provider,
|
||||||
# Cifra tutte le credenziali IMAP
|
# Cifra tutte le credenziali IMAP
|
||||||
imap_host_enc=encrypt_credential(data.imap_host),
|
imap_host_enc=encrypt_credential(data.imap_host),
|
||||||
imap_port_enc=encrypt_credential(str(data.imap_port)),
|
imap_port_enc=encrypt_credential(str(data.imap_port)),
|
||||||
@@ -149,6 +151,10 @@ class MailboxService:
|
|||||||
mailbox.provider = data.provider
|
mailbox.provider = data.provider
|
||||||
if data.status is not None:
|
if data.status is not None:
|
||||||
mailbox.status = data.status
|
mailbox.status = data.status
|
||||||
|
if data.protocol_type is not None:
|
||||||
|
mailbox.protocol_type = data.protocol_type
|
||||||
|
if data.rem_provider is not None:
|
||||||
|
mailbox.rem_provider = data.rem_provider
|
||||||
|
|
||||||
# IMAP
|
# IMAP
|
||||||
if data.imap_host is not None:
|
if data.imap_host is not None:
|
||||||
@@ -382,6 +388,8 @@ class MailboxService:
|
|||||||
"last_sync_uid": mailbox.last_sync_uid,
|
"last_sync_uid": mailbox.last_sync_uid,
|
||||||
"sync_error_msg": mailbox.sync_error_msg,
|
"sync_error_msg": mailbox.sync_error_msg,
|
||||||
"sync_error_count": mailbox.sync_error_count,
|
"sync_error_count": mailbox.sync_error_count,
|
||||||
|
"protocol_type": mailbox.protocol_type,
|
||||||
|
"rem_provider": mailbox.rem_provider,
|
||||||
"created_by": mailbox.created_by,
|
"created_by": mailbox.created_by,
|
||||||
"created_at": mailbox.created_at,
|
"created_at": mailbox.created_at,
|
||||||
"updated_at": mailbox.updated_at,
|
"updated_at": mailbox.updated_at,
|
||||||
|
|||||||
@@ -1067,6 +1067,15 @@ function MessageRow({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex items-center gap-2 flex-shrink-0">
|
<div className="flex items-center gap-2 flex-shrink-0">
|
||||||
|
{message.is_conserved && (
|
||||||
|
<span
|
||||||
|
className="inline-flex items-center gap-1 px-1.5 py-0.5 rounded text-xs font-medium bg-teal-100 text-teal-700 border border-teal-200 flex-shrink-0"
|
||||||
|
title={`Conservato${message.conserved_at ? ' il ' + new Date(message.conserved_at).toLocaleDateString('it-IT') : ''}`}
|
||||||
|
>
|
||||||
|
<ShieldCheck className="h-3 w-3" />
|
||||||
|
Conservato
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
<PecStateBadge state={message.state} />
|
<PecStateBadge state={message.state} />
|
||||||
<span className="text-xs text-muted-foreground">
|
<span className="text-xs text-muted-foreground">
|
||||||
{formatRelative(message.received_at || message.sent_at || message.created_at)}
|
{formatRelative(message.received_at || message.sent_at || message.created_at)}
|
||||||
@@ -1211,31 +1220,40 @@ function MessageRow({
|
|||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Invia a Conservazione / Rimuovi da Da Conservare */}
|
{/* Conservazione: badge fisso se gia' conservato, pulsante interattivo altrimenti */}
|
||||||
{canConserve && viewMode !== 'trash' && viewMode !== 'conservation_archived' && (
|
{canConserve && viewMode !== 'trash' && (
|
||||||
<button
|
message.is_conserved ? (
|
||||||
onClick={onToggleConserve}
|
<span
|
||||||
title={viewMode === 'conservation_pending' ? 'Rimuovi da Da Conservare' : 'Invia a Conservazione'}
|
title={`Gia' conservato${message.conserved_at ? ' il ' + new Date(message.conserved_at).toLocaleDateString('it-IT') : ''}`}
|
||||||
className={cn(
|
className="p-1 cursor-default"
|
||||||
'p-1 rounded hover:bg-muted transition-all',
|
>
|
||||||
message.is_pending_conservation
|
<ShieldCheck className="h-4 w-4 text-teal-500" />
|
||||||
? 'opacity-100'
|
</span>
|
||||||
: hovered
|
) : (
|
||||||
|
<button
|
||||||
|
onClick={onToggleConserve}
|
||||||
|
title={viewMode === 'conservation_pending' ? 'Rimuovi da Da Conservare' : 'Invia a Conservazione'}
|
||||||
|
className={cn(
|
||||||
|
'p-1 rounded hover:bg-muted transition-all',
|
||||||
|
message.is_pending_conservation
|
||||||
? 'opacity-100'
|
? 'opacity-100'
|
||||||
: 'opacity-0 pointer-events-none',
|
: hovered
|
||||||
)}
|
? 'opacity-100'
|
||||||
>
|
: 'opacity-0 pointer-events-none',
|
||||||
{viewMode === 'conservation_pending' ? (
|
)}
|
||||||
<ShieldX className="h-4 w-4 text-orange-500" />
|
>
|
||||||
) : (
|
{viewMode === 'conservation_pending' ? (
|
||||||
<ShieldCheck
|
<ShieldX className="h-4 w-4 text-orange-500" />
|
||||||
className={cn(
|
) : (
|
||||||
'h-4 w-4',
|
<ShieldCheck
|
||||||
message.is_pending_conservation ? 'text-teal-600' : 'text-muted-foreground',
|
className={cn(
|
||||||
)}
|
'h-4 w-4',
|
||||||
/>
|
message.is_pending_conservation ? 'text-teal-600' : 'text-muted-foreground',
|
||||||
)}
|
)}
|
||||||
</button>
|
/>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
)
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Indicatore allegati */}
|
{/* Indicatore allegati */}
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ import {
|
|||||||
ChevronDown,
|
ChevronDown,
|
||||||
FolderOpen,
|
FolderOpen,
|
||||||
Plus,
|
Plus,
|
||||||
|
ShieldCheck,
|
||||||
} from 'lucide-react'
|
} from 'lucide-react'
|
||||||
import toast from 'react-hot-toast'
|
import toast from 'react-hot-toast'
|
||||||
import { Button } from '@/components/ui/Button'
|
import { Button } from '@/components/ui/Button'
|
||||||
@@ -1162,6 +1163,25 @@ export function MessageDetailPage() {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* Banner "Conservato" */}
|
||||||
|
{message.is_conserved && (
|
||||||
|
<div className="bg-teal-50 border-b border-teal-200 px-6 py-2.5 flex items-center gap-3">
|
||||||
|
<ShieldCheck className="h-4 w-4 text-teal-600 flex-shrink-0" />
|
||||||
|
<span className="text-sm text-teal-700">
|
||||||
|
Questo messaggio e' stato conservato in modo sostitutivo
|
||||||
|
{message.conserved_at
|
||||||
|
? ` il ${new Date(message.conserved_at).toLocaleDateString('it-IT', {
|
||||||
|
day: '2-digit',
|
||||||
|
month: '2-digit',
|
||||||
|
year: 'numeric',
|
||||||
|
hour: '2-digit',
|
||||||
|
minute: '2-digit',
|
||||||
|
})}`
|
||||||
|
: ''}.
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Contenuto */}
|
{/* Contenuto */}
|
||||||
<div className="flex-1 overflow-y-auto">
|
<div className="flex-1 overflow-y-auto">
|
||||||
<div className="max-w-4xl mx-auto px-6 py-8 space-y-6">
|
<div className="max-w-4xl mx-auto px-6 py-8 space-y-6">
|
||||||
|
|||||||
+2
-1
@@ -135,7 +135,8 @@ class WorkerSettings:
|
|||||||
"""Configurazione del worker arq."""
|
"""Configurazione del worker arq."""
|
||||||
|
|
||||||
# Funzioni/job registrati (code-driven, on-demand)
|
# Funzioni/job registrati (code-driven, on-demand)
|
||||||
functions = [sync_mailbox, send_pec, watch_receipt, dispatch_notification, apply_routing_rules, health_check]
|
# run_conservation e' incluso anche qui per permettere l'avvio manuale tramite enqueue_job
|
||||||
|
functions = [sync_mailbox, send_pec, watch_receipt, dispatch_notification, apply_routing_rules, health_check, run_conservation]
|
||||||
|
|
||||||
# Job schedulati (cron)
|
# Job schedulati (cron)
|
||||||
# run_conservation: ogni giorno alle 16:00 UTC = 18:00 ora Italia (CEST, UTC+2)
|
# run_conservation: ogni giorno alle 16:00 UTC = 18:00 ora Italia (CEST, UTC+2)
|
||||||
|
|||||||
@@ -125,6 +125,11 @@ class Message(Base):
|
|||||||
is_archived: Mapped[bool] = mapped_column(Boolean, nullable=False, default=False)
|
is_archived: Mapped[bool] = mapped_column(Boolean, nullable=False, default=False)
|
||||||
archived_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True)
|
archived_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True)
|
||||||
|
|
||||||
|
# Conservazione sostitutiva
|
||||||
|
is_pending_conservation: Mapped[bool] = mapped_column(Boolean, nullable=False, default=False)
|
||||||
|
is_conserved: Mapped[bool] = mapped_column(Boolean, nullable=False, default=False)
|
||||||
|
conserved_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True)
|
||||||
|
|
||||||
# Protocollo e REM europea (Feature N8)
|
# Protocollo e REM europea (Feature N8)
|
||||||
protocol_type: Mapped[str] = mapped_column(String(10), nullable=False, default="pec_it")
|
protocol_type: Mapped[str] = mapped_column(String(10), nullable=False, default="pec_it")
|
||||||
rem_evidence_type: Mapped[str | None] = mapped_column(String(100), nullable=True)
|
rem_evidence_type: Mapped[str | None] = mapped_column(String(100), nullable=True)
|
||||||
|
|||||||
@@ -38,6 +38,9 @@ dependencies = [
|
|||||||
"python-jose[cryptography]>=3.3.0",
|
"python-jose[cryptography]>=3.3.0",
|
||||||
"bcrypt>=4.0.0",
|
"bcrypt>=4.0.0",
|
||||||
|
|
||||||
|
# HTTP client (conservatore AgID, upload SIP)
|
||||||
|
"httpx>=0.27.0",
|
||||||
|
|
||||||
# Utilities
|
# Utilities
|
||||||
"python-dotenv>=1.0.0",
|
"python-dotenv>=1.0.0",
|
||||||
"email-validator>=2.2.0",
|
"email-validator>=2.2.0",
|
||||||
|
|||||||
Reference in New Issue
Block a user