mirror of
https://github.com/idrainformatica/PecFlow.git
synced 2026-06-16 12:45:42 +02:00
Fix vari frontend
This commit is contained in:
@@ -21,6 +21,8 @@ from app.schemas.mailbox import (
|
|||||||
MailboxCreateRequest,
|
MailboxCreateRequest,
|
||||||
MailboxListResponse,
|
MailboxListResponse,
|
||||||
MailboxResponse,
|
MailboxResponse,
|
||||||
|
MailboxSyncResponse,
|
||||||
|
MailboxUnreadCountsResponse,
|
||||||
MailboxUpdateRequest,
|
MailboxUpdateRequest,
|
||||||
)
|
)
|
||||||
from app.services.mailbox_service import MailboxService
|
from app.services.mailbox_service import MailboxService
|
||||||
@@ -123,6 +125,52 @@ async def list_mailboxes(
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/unread-counts", response_model=MailboxUnreadCountsResponse)
|
||||||
|
async def get_unread_counts(
|
||||||
|
current_user: CurrentUser,
|
||||||
|
db: DB,
|
||||||
|
) -> MailboxUnreadCountsResponse:
|
||||||
|
"""
|
||||||
|
Restituisce il numero di messaggi non letti per ciascuna casella accessibile.
|
||||||
|
Usato dalla sidebar per mostrare i badge per casella.
|
||||||
|
- Admin: conta su tutte le caselle del tenant.
|
||||||
|
- Operatori: solo le caselle con permesso can_read.
|
||||||
|
"""
|
||||||
|
from sqlalchemy import func, select
|
||||||
|
from app.models.message import Message
|
||||||
|
|
||||||
|
# Determina le caselle visibili
|
||||||
|
if current_user.is_admin:
|
||||||
|
visible_ids = None # nessun filtro
|
||||||
|
else:
|
||||||
|
from app.services.permission_service import PermissionService
|
||||||
|
perm_svc = PermissionService(db)
|
||||||
|
visible_ids = await perm_svc.get_visible_mailboxes(current_user)
|
||||||
|
if not visible_ids:
|
||||||
|
return MailboxUnreadCountsResponse(counts={})
|
||||||
|
|
||||||
|
q = (
|
||||||
|
select(Message.mailbox_id, func.count().label("cnt"))
|
||||||
|
.where(
|
||||||
|
Message.tenant_id == current_user.tenant_id,
|
||||||
|
Message.is_read == False, # noqa: E712
|
||||||
|
Message.direction == "inbound",
|
||||||
|
Message.is_trashed == False, # noqa: E712
|
||||||
|
Message.is_archived == False, # noqa: E712
|
||||||
|
Message.parent_message_id.is_(None),
|
||||||
|
)
|
||||||
|
.group_by(Message.mailbox_id)
|
||||||
|
)
|
||||||
|
|
||||||
|
if visible_ids is not None:
|
||||||
|
from app.models.mailbox import Mailbox
|
||||||
|
q = q.where(Message.mailbox_id.in_(visible_ids))
|
||||||
|
|
||||||
|
rows = (await db.execute(q)).all()
|
||||||
|
counts = {str(row.mailbox_id): row.cnt for row in rows}
|
||||||
|
return MailboxUnreadCountsResponse(counts=counts)
|
||||||
|
|
||||||
|
|
||||||
@router.get("/{mailbox_id}", response_model=MailboxResponse)
|
@router.get("/{mailbox_id}", response_model=MailboxResponse)
|
||||||
async def get_mailbox(
|
async def get_mailbox(
|
||||||
mailbox_id: uuid.UUID,
|
mailbox_id: uuid.UUID,
|
||||||
@@ -200,3 +248,52 @@ async def test_mailbox_connection(
|
|||||||
tenant_id=current_user.tenant_id,
|
tenant_id=current_user.tenant_id,
|
||||||
data=data,
|
data=data,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@router.post(
|
||||||
|
"/{mailbox_id}/sync",
|
||||||
|
response_model=MailboxSyncResponse,
|
||||||
|
)
|
||||||
|
async def force_sync_mailbox(
|
||||||
|
mailbox_id: uuid.UUID,
|
||||||
|
current_user: AdminUser,
|
||||||
|
db: DB,
|
||||||
|
) -> MailboxSyncResponse:
|
||||||
|
"""
|
||||||
|
Forza una sincronizzazione IMAP immediata della casella.
|
||||||
|
Accoda il job sync_mailbox nel worker tramite arq/Redis.
|
||||||
|
Utile dopo un errore di connessione o per forzare un aggiornamento.
|
||||||
|
Richiede ruolo **admin**.
|
||||||
|
"""
|
||||||
|
from app.core.exceptions import NotFoundError
|
||||||
|
|
||||||
|
svc = _svc(db)
|
||||||
|
mailbox = await svc.get_mailbox(mailbox_id, current_user.tenant_id)
|
||||||
|
|
||||||
|
if mailbox.status == "deleted":
|
||||||
|
raise NotFoundError("Casella non trovata o eliminata")
|
||||||
|
|
||||||
|
try:
|
||||||
|
from arq.connections import RedisSettings, create_pool as arq_create_pool
|
||||||
|
from app.config import get_settings
|
||||||
|
|
||||||
|
cfg = get_settings()
|
||||||
|
arq_settings = RedisSettings.from_dsn(cfg.redis_url)
|
||||||
|
arq_redis = await arq_create_pool(arq_settings)
|
||||||
|
await arq_redis.enqueue_job("sync_mailbox", str(mailbox_id))
|
||||||
|
await arq_redis.aclose()
|
||||||
|
except Exception as exc:
|
||||||
|
from app.core.logging import get_logger
|
||||||
|
logger = get_logger(__name__)
|
||||||
|
logger.error(f"[force_sync] Impossibile accodare job per {mailbox_id}: {exc}")
|
||||||
|
return MailboxSyncResponse(
|
||||||
|
status="error",
|
||||||
|
mailbox_id=mailbox_id,
|
||||||
|
message=f"Impossibile accodare il job: {exc}",
|
||||||
|
)
|
||||||
|
|
||||||
|
return MailboxSyncResponse(
|
||||||
|
status="enqueued",
|
||||||
|
mailbox_id=mailbox_id,
|
||||||
|
message=f"Sincronizzazione avviata per {mailbox.email_address}",
|
||||||
|
)
|
||||||
|
|||||||
@@ -116,3 +116,15 @@ class ConnectionTestResult(BaseModel):
|
|||||||
message: str
|
message: str
|
||||||
latency_ms: float | None = None
|
latency_ms: float | None = None
|
||||||
capabilities: list[str] | None = None # Solo per IMAP
|
capabilities: list[str] | None = None # Solo per IMAP
|
||||||
|
|
||||||
|
|
||||||
|
class MailboxSyncResponse(BaseModel):
|
||||||
|
"""Risposta all'accodamento di un job di sincronizzazione manuale."""
|
||||||
|
status: str
|
||||||
|
mailbox_id: uuid.UUID
|
||||||
|
message: str
|
||||||
|
|
||||||
|
|
||||||
|
class MailboxUnreadCountsResponse(BaseModel):
|
||||||
|
"""Conteggio messaggi non letti per casella."""
|
||||||
|
counts: dict[str, int]
|
||||||
|
|||||||
@@ -28,4 +28,16 @@ export const mailboxesApi = {
|
|||||||
apiClient
|
apiClient
|
||||||
.post<ConnectionTestResult>(`/mailboxes/${id}/test-connection`, { protocol })
|
.post<ConnectionTestResult>(`/mailboxes/${id}/test-connection`, { protocol })
|
||||||
.then((r) => r.data),
|
.then((r) => r.data),
|
||||||
|
|
||||||
|
/** Forza una sincronizzazione IMAP manuale (accoda job arq). */
|
||||||
|
forceSync: (id: string) =>
|
||||||
|
apiClient
|
||||||
|
.post<{ status: string; mailbox_id: string; message: string }>(`/mailboxes/${id}/sync`)
|
||||||
|
.then((r) => r.data),
|
||||||
|
|
||||||
|
/** Restituisce il conteggio messaggi non letti per ciascuna casella. */
|
||||||
|
getUnreadCounts: () =>
|
||||||
|
apiClient
|
||||||
|
.get<{ counts: Record<string, number> }>('/mailboxes/unread-counts')
|
||||||
|
.then((r) => r.data.counts),
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -86,6 +86,14 @@ export function Sidebar() {
|
|||||||
})
|
})
|
||||||
const mailboxes = mailboxesData?.items ?? []
|
const mailboxes = mailboxesData?.items ?? []
|
||||||
|
|
||||||
|
// Conteggio messaggi non letti per singola casella
|
||||||
|
const { data: unreadCounts = {} } = useQuery({
|
||||||
|
queryKey: ['mailboxes', 'unread-counts'],
|
||||||
|
queryFn: () => mailboxesApi.getUnreadCounts(),
|
||||||
|
staleTime: 60 * 1000,
|
||||||
|
refetchInterval: 120 * 1000,
|
||||||
|
})
|
||||||
|
|
||||||
// Virtual Box assegnate all'utente corrente
|
// Virtual Box assegnate all'utente corrente
|
||||||
const { data: myVboxes = [] } = useQuery({
|
const { data: myVboxes = [] } = useQuery({
|
||||||
queryKey: ['virtual-boxes', 'my'],
|
queryKey: ['virtual-boxes', 'my'],
|
||||||
@@ -312,6 +320,7 @@ export function Sidebar() {
|
|||||||
collapsed={collapsed}
|
collapsed={collapsed}
|
||||||
isExpanded={isMailboxExpanded(mailbox.id)}
|
isExpanded={isMailboxExpanded(mailbox.id)}
|
||||||
onToggle={() => toggleMailbox(mailbox.id)}
|
onToggle={() => toggleMailbox(mailbox.id)}
|
||||||
|
unreadCount={unreadCounts[mailbox.id] ?? 0}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
@@ -536,6 +545,7 @@ interface MailboxNavItemProps {
|
|||||||
collapsed: boolean
|
collapsed: boolean
|
||||||
isExpanded: boolean
|
isExpanded: boolean
|
||||||
onToggle: () => void
|
onToggle: () => void
|
||||||
|
unreadCount?: number
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Colore del pallino di stato casella */
|
/** Colore del pallino di stato casella */
|
||||||
@@ -554,7 +564,7 @@ function statusDot(status: MailboxResponse['status']): string {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function MailboxNavItem({ mailbox, collapsed, isExpanded, onToggle }: MailboxNavItemProps) {
|
function MailboxNavItem({ mailbox, collapsed, isExpanded, onToggle, unreadCount = 0 }: MailboxNavItemProps) {
|
||||||
const displayName = mailbox.display_name || mailbox.email_address
|
const displayName = mailbox.display_name || mailbox.email_address
|
||||||
const initial = displayName[0]?.toUpperCase() ?? '?'
|
const initial = displayName[0]?.toUpperCase() ?? '?'
|
||||||
const dotClass = statusDot(mailbox.status)
|
const dotClass = statusDot(mailbox.status)
|
||||||
@@ -572,7 +582,7 @@ function MailboxNavItem({ mailbox, collapsed, isExpanded, onToggle }: MailboxNav
|
|||||||
: 'text-gray-300 hover:bg-gray-700 hover:text-white',
|
: 'text-gray-300 hover:bg-gray-700 hover:text-white',
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
title={displayName}
|
title={`${displayName}${unreadCount > 0 ? ` (${unreadCount} non letti)` : ''}`}
|
||||||
>
|
>
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<div className="h-6 w-6 rounded-full bg-gray-600 flex items-center justify-center text-xs font-semibold">
|
<div className="h-6 w-6 rounded-full bg-gray-600 flex items-center justify-center text-xs font-semibold">
|
||||||
@@ -584,6 +594,11 @@ function MailboxNavItem({ mailbox, collapsed, isExpanded, onToggle }: MailboxNav
|
|||||||
dotClass,
|
dotClass,
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
{unreadCount > 0 && (
|
||||||
|
<span className="absolute -top-1.5 -right-1.5 h-3.5 min-w-[14px] px-0.5 rounded-full bg-blue-500 text-white text-[9px] font-bold flex items-center justify-center leading-none">
|
||||||
|
{unreadCount > 9 ? '9+' : unreadCount}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</NavLink>
|
</NavLink>
|
||||||
)
|
)
|
||||||
@@ -615,6 +630,13 @@ function MailboxNavItem({ mailbox, collapsed, isExpanded, onToggle }: MailboxNav
|
|||||||
{displayName}
|
{displayName}
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
|
{/* Badge non letti per casella */}
|
||||||
|
{unreadCount > 0 && (
|
||||||
|
<span className="inline-flex items-center justify-center h-4 min-w-[16px] px-1 rounded-full bg-blue-500 text-white text-[10px] font-bold flex-shrink-0">
|
||||||
|
{unreadCount > 99 ? '99+' : unreadCount}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Chevron espandi/comprimi */}
|
{/* Chevron espandi/comprimi */}
|
||||||
<ChevronDown
|
<ChevronDown
|
||||||
className={cn(
|
className={cn(
|
||||||
|
|||||||
@@ -50,24 +50,46 @@ export function ComposePage() {
|
|||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
const location = useLocation()
|
const location = useLocation()
|
||||||
const replyTo = location.state?.replyTo as MessageResponse | undefined
|
const replyTo = location.state?.replyTo as MessageResponse | undefined
|
||||||
|
const forwardOf = location.state?.forwardOf as MessageResponse | undefined
|
||||||
const [showCc, setShowCc] = useState(false)
|
const [showCc, setShowCc] = useState(false)
|
||||||
const fileInputRef = useRef<HTMLInputElement>(null)
|
const fileInputRef = useRef<HTMLInputElement>(null)
|
||||||
|
|
||||||
// Corpo HTML (gestito da TipTap)
|
// Corpo HTML (gestito da TipTap)
|
||||||
const [bodyHtml, setBodyHtml] = useState<string>(() => {
|
const [bodyHtml, setBodyHtml] = useState<string>(() => {
|
||||||
if (!replyTo) return ''
|
if (replyTo) {
|
||||||
const date = new Date(
|
const date = new Date(
|
||||||
replyTo.received_at || replyTo.created_at
|
replyTo.received_at || replyTo.created_at
|
||||||
).toLocaleDateString('it-IT')
|
).toLocaleDateString('it-IT')
|
||||||
return [
|
return [
|
||||||
'<p></p>',
|
'<p></p>',
|
||||||
'<p></p>',
|
'<p></p>',
|
||||||
'<hr>',
|
'<hr>',
|
||||||
`<p><strong>In risposta al messaggio del ${date}</strong></p>`,
|
`<p><strong>In risposta al messaggio del ${date}</strong></p>`,
|
||||||
`<p>Da: ${replyTo.from_address || ''}</p>`,
|
`<p>Da: ${replyTo.from_address || ''}</p>`,
|
||||||
`<p>A: ${replyTo.to_addresses?.join(', ') || ''}</p>`,
|
`<p>A: ${replyTo.to_addresses?.join(', ') || ''}</p>`,
|
||||||
`<p>Oggetto: ${replyTo.subject || ''}</p>`,
|
`<p>Oggetto: ${replyTo.subject || ''}</p>`,
|
||||||
].join('')
|
].join('')
|
||||||
|
}
|
||||||
|
if (forwardOf) {
|
||||||
|
const date = new Date(
|
||||||
|
forwardOf.received_at || forwardOf.sent_at || forwardOf.created_at
|
||||||
|
).toLocaleDateString('it-IT')
|
||||||
|
const bodyContent = forwardOf.body_text
|
||||||
|
? `<pre style="white-space:pre-wrap;font-family:inherit">${forwardOf.body_text}</pre>`
|
||||||
|
: ''
|
||||||
|
return [
|
||||||
|
'<p></p>',
|
||||||
|
'<p></p>',
|
||||||
|
'<hr>',
|
||||||
|
`<p><strong>Messaggio inoltrato</strong></p>`,
|
||||||
|
`<p>Da: ${forwardOf.from_address || ''}</p>`,
|
||||||
|
`<p>A: ${forwardOf.to_addresses?.join(', ') || ''}</p>`,
|
||||||
|
`<p>Data: ${date}</p>`,
|
||||||
|
`<p>Oggetto: ${forwardOf.subject || ''}</p>`,
|
||||||
|
bodyContent,
|
||||||
|
].join('')
|
||||||
|
}
|
||||||
|
return ''
|
||||||
})
|
})
|
||||||
|
|
||||||
// Allegati
|
// Allegati
|
||||||
@@ -80,12 +102,16 @@ export function ComposePage() {
|
|||||||
formState: { errors },
|
formState: { errors },
|
||||||
} = useForm<ComposeFormValues>({
|
} = useForm<ComposeFormValues>({
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
mailbox_id: replyTo?.mailbox_id || '',
|
mailbox_id: replyTo?.mailbox_id || forwardOf?.mailbox_id || '',
|
||||||
to_addresses: replyTo
|
to_addresses: replyTo
|
||||||
? [{ value: replyTo.from_address || '' }]
|
? [{ value: replyTo.from_address || '' }]
|
||||||
: [{ value: '' }],
|
: [{ value: '' }],
|
||||||
cc_addresses: [],
|
cc_addresses: [],
|
||||||
subject: replyTo ? `Re: ${replyTo.subject || ''}` : '',
|
subject: replyTo
|
||||||
|
? `Re: ${replyTo.subject || ''}`
|
||||||
|
: forwardOf
|
||||||
|
? `Fwd: ${forwardOf.subject || ''}`
|
||||||
|
: '',
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -214,13 +240,18 @@ export function ComposePage() {
|
|||||||
</Button>
|
</Button>
|
||||||
<div>
|
<div>
|
||||||
<h1 className="text-lg font-semibold">
|
<h1 className="text-lg font-semibold">
|
||||||
{replyTo ? 'Rispondi a PEC' : 'Nuova PEC'}
|
{replyTo ? 'Rispondi a PEC' : forwardOf ? 'Inoltra PEC' : 'Nuova PEC'}
|
||||||
</h1>
|
</h1>
|
||||||
{replyTo && (
|
{replyTo && (
|
||||||
<p className="text-xs text-muted-foreground">
|
<p className="text-xs text-muted-foreground">
|
||||||
In risposta a: {replyTo.subject}
|
In risposta a: {replyTo.subject}
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
|
{forwardOf && (
|
||||||
|
<p className="text-xs text-muted-foreground">
|
||||||
|
Inoltro di: {forwardOf.subject}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -239,6 +270,17 @@ export function ComposePage() {
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Avviso allegati messaggio inoltrato */}
|
||||||
|
{forwardOf && forwardOf.has_attachments && (
|
||||||
|
<div className="rounded-lg border border-amber-200 bg-amber-50 p-3 flex items-start gap-2">
|
||||||
|
<Paperclip className="h-4 w-4 text-amber-600 mt-0.5 flex-shrink-0" />
|
||||||
|
<p className="text-sm text-amber-700">
|
||||||
|
Il messaggio originale contiene allegati. Per includerli nell'inoltro,
|
||||||
|
scaricali dal messaggio originale e aggiungili qui manualmente.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Casella mittente */}
|
{/* Casella mittente */}
|
||||||
<div className="space-y-1.5">
|
<div className="space-y-1.5">
|
||||||
<Label htmlFor="mailbox_id">Casella mittente *</Label>
|
<Label htmlFor="mailbox_id">Casella mittente *</Label>
|
||||||
|
|||||||
@@ -88,8 +88,9 @@ export function InboxPage({ viewMode }: InboxPageProps) {
|
|||||||
const [dateFrom, setDateFrom] = useState('')
|
const [dateFrom, setDateFrom] = useState('')
|
||||||
const [dateTo, setDateTo] = useState('')
|
const [dateTo, setDateTo] = useState('')
|
||||||
const [pecTypeFilter, setPecTypeFilter] = useState('')
|
const [pecTypeFilter, setPecTypeFilter] = useState('')
|
||||||
|
const [pecStateFilter, setPecStateFilter] = useState('')
|
||||||
|
|
||||||
const activeAdvancedFiltersCount = [dateFrom, dateTo, pecTypeFilter].filter(Boolean).length
|
const activeAdvancedFiltersCount = [dateFrom, dateTo, pecTypeFilter, pecStateFilter].filter(Boolean).length
|
||||||
|
|
||||||
// ── Stato selezione ─────────────────────────────────────────────────────────
|
// ── Stato selezione ─────────────────────────────────────────────────────────
|
||||||
const [selectedIds, setSelectedIds] = useState<Set<string>>(new Set())
|
const [selectedIds, setSelectedIds] = useState<Set<string>>(new Set())
|
||||||
@@ -105,6 +106,7 @@ export function InboxPage({ viewMode }: InboxPageProps) {
|
|||||||
setDateFrom('')
|
setDateFrom('')
|
||||||
setDateTo('')
|
setDateTo('')
|
||||||
setPecTypeFilter('')
|
setPecTypeFilter('')
|
||||||
|
setPecStateFilter('')
|
||||||
setShowAdvancedFilters(false)
|
setShowAdvancedFilters(false)
|
||||||
setPage(1)
|
setPage(1)
|
||||||
setSelectedIds(new Set())
|
setSelectedIds(new Set())
|
||||||
@@ -152,6 +154,7 @@ export function InboxPage({ viewMode }: InboxPageProps) {
|
|||||||
mailbox_id: mailboxId,
|
mailbox_id: mailboxId,
|
||||||
search: debouncedSearch || undefined,
|
search: debouncedSearch || undefined,
|
||||||
pec_type: pecTypeFilter || undefined,
|
pec_type: pecTypeFilter || undefined,
|
||||||
|
state: pecStateFilter || undefined,
|
||||||
date_from: dateFrom ? new Date(dateFrom).toISOString() : undefined,
|
date_from: dateFrom ? new Date(dateFrom).toISOString() : undefined,
|
||||||
date_to: dateTo ? new Date(dateTo + 'T23:59:59').toISOString() : undefined,
|
date_to: dateTo ? new Date(dateTo + 'T23:59:59').toISOString() : undefined,
|
||||||
page,
|
page,
|
||||||
@@ -584,17 +587,35 @@ export function InboxPage({ viewMode }: InboxPageProps) {
|
|||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Stato PEC */}
|
||||||
|
<div>
|
||||||
|
<label className="text-xs font-medium text-muted-foreground mb-1 block">Stato PEC</label>
|
||||||
|
<select
|
||||||
|
className="w-full h-8 px-2 rounded-md border bg-background text-xs focus:outline-none focus:ring-2 focus:ring-ring"
|
||||||
|
value={pecStateFilter}
|
||||||
|
onChange={(e) => setPecStateFilter(e.target.value)}
|
||||||
|
>
|
||||||
|
<option value="">Tutti gli stati</option>
|
||||||
|
<option value="received">Ricevuto</option>
|
||||||
|
<option value="sent">Inviato</option>
|
||||||
|
<option value="accepted">Accettato</option>
|
||||||
|
<option value="delivered">Consegnato</option>
|
||||||
|
<option value="anomaly">Anomalia</option>
|
||||||
|
<option value="failed">Fallito</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Reset filtri */}
|
{/* Reset filtri */}
|
||||||
{activeAdvancedFiltersCount > 0 && (
|
{activeAdvancedFiltersCount > 0 && (
|
||||||
<div className="flex items-end">
|
<div className="flex items-end col-span-2 md:col-span-1">
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="sm"
|
size="sm"
|
||||||
className="h-8 text-xs text-muted-foreground w-full"
|
className="h-8 text-xs text-muted-foreground w-full"
|
||||||
onClick={() => { setDateFrom(''); setDateTo(''); setPecTypeFilter('') }}
|
onClick={() => { setDateFrom(''); setDateTo(''); setPecTypeFilter(''); setPecStateFilter('') }}
|
||||||
>
|
>
|
||||||
<X className="h-3.5 w-3.5 mr-1" />
|
<X className="h-3.5 w-3.5 mr-1" />
|
||||||
Azzera
|
Azzera filtri
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -49,6 +49,7 @@ export function MailboxesPage() {
|
|||||||
const [editingMailbox, setEditingMailbox] = useState<MailboxResponse | null>(null)
|
const [editingMailbox, setEditingMailbox] = useState<MailboxResponse | null>(null)
|
||||||
const [testingId, setTestingId] = useState<string | null>(null)
|
const [testingId, setTestingId] = useState<string | null>(null)
|
||||||
const [testResult, setTestResult] = useState<{ success: boolean; message: string } | null>(null)
|
const [testResult, setTestResult] = useState<{ success: boolean; message: string } | null>(null)
|
||||||
|
const [syncingId, setSyncingId] = useState<string | null>(null)
|
||||||
|
|
||||||
const { data: mailboxesData, isLoading } = useQuery({
|
const { data: mailboxesData, isLoading } = useQuery({
|
||||||
queryKey: ['mailboxes'],
|
queryKey: ['mailboxes'],
|
||||||
@@ -78,6 +79,23 @@ export function MailboxesPage() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const handleForceSync = async (mailbox: MailboxResponse) => {
|
||||||
|
setSyncingId(mailbox.id)
|
||||||
|
try {
|
||||||
|
const result = await mailboxesApi.forceSync(mailbox.id)
|
||||||
|
if (result.status === 'enqueued') {
|
||||||
|
toast.success(result.message)
|
||||||
|
queryClient.invalidateQueries({ queryKey: ['mailboxes'] })
|
||||||
|
} else {
|
||||||
|
toast.error(result.message)
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
toast.error(getErrorMessage(e))
|
||||||
|
} finally {
|
||||||
|
setSyncingId(null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const handleDelete = async (mailbox: MailboxResponse) => {
|
const handleDelete = async (mailbox: MailboxResponse) => {
|
||||||
if (!confirm(`Eliminare la casella ${mailbox.email_address}? L'operazione è irreversibile.`)) return
|
if (!confirm(`Eliminare la casella ${mailbox.email_address}? L'operazione è irreversibile.`)) return
|
||||||
deleteMutation.mutate(mailbox.id)
|
deleteMutation.mutate(mailbox.id)
|
||||||
@@ -181,6 +199,19 @@ export function MailboxesPage() {
|
|||||||
Test
|
Test
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
|
{/* Forza sincronizzazione */}
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => handleForceSync(mailbox)}
|
||||||
|
isLoading={syncingId === mailbox.id}
|
||||||
|
title="Forza sincronizzazione IMAP immediata"
|
||||||
|
disabled={mailbox.status === 'deleted'}
|
||||||
|
>
|
||||||
|
<RefreshCw className="h-4 w-4 mr-1" />
|
||||||
|
Sync
|
||||||
|
</Button>
|
||||||
|
|
||||||
{/* Modifica */}
|
{/* Modifica */}
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import {
|
|||||||
ArchiveX,
|
ArchiveX,
|
||||||
Download,
|
Download,
|
||||||
Reply,
|
Reply,
|
||||||
|
Forward,
|
||||||
Paperclip,
|
Paperclip,
|
||||||
Mail,
|
Mail,
|
||||||
Send,
|
Send,
|
||||||
@@ -330,6 +331,22 @@ export function MessageDetailPage() {
|
|||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* Inoltra (disponibile per qualsiasi messaggio PEC certificata, non nel cestino) */}
|
||||||
|
{message.pec_type === 'posta_certificata' && !message.is_trashed && (
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={() =>
|
||||||
|
navigate('/compose', {
|
||||||
|
state: { forwardOf: message },
|
||||||
|
})
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Forward className="h-4 w-4 mr-1" />
|
||||||
|
Inoltra
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Scarica pacchetto completo ZIP (sempre visibile) */}
|
{/* Scarica pacchetto completo ZIP (sempre visibile) */}
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
|
|||||||
Reference in New Issue
Block a user