mirror of
https://github.com/idrainformatica/PecFlow.git
synced 2026-06-16 12:45:42 +02:00
Trash!
This commit is contained in:
@@ -161,3 +161,4 @@ cython_debug/
|
||||
# and can be added to the global gitignore or merged into this file. For a more nuclear
|
||||
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
|
||||
#.idea/
|
||||
KnowledgeBaseCline.md
|
||||
|
||||
@@ -30,7 +30,7 @@ Porta: 465
|
||||
SSL: Sì
|
||||
|
||||
|
||||
Se devi, effettua i test di invio solo al destinatario matteo1801@spidmail.it
|
||||
Quando necessario, effettua i test di invio solo al destinatario matteo1801@spidmail.it
|
||||
|
||||
Tutto il frontend deve essere in italiano
|
||||
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
"""add is_trashed and trashed_at to messages
|
||||
|
||||
Revision ID: 0007
|
||||
Revises: 0006
|
||||
Create Date: 2026-03-25
|
||||
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = '0007'
|
||||
down_revision = '0006'
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
op.add_column(
|
||||
'messages',
|
||||
sa.Column('is_trashed', sa.Boolean(), nullable=False, server_default=sa.text('false')),
|
||||
)
|
||||
op.add_column(
|
||||
'messages',
|
||||
sa.Column('trashed_at', sa.DateTime(timezone=True), nullable=True),
|
||||
)
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
op.drop_column('messages', 'trashed_at')
|
||||
op.drop_column('messages', 'is_trashed')
|
||||
@@ -4,7 +4,8 @@ Router messaggi PEC.
|
||||
Fornisce:
|
||||
- GET /messages – lista messaggi con filtri (inbox/sent/search/...)
|
||||
- GET /messages/{id} – singolo messaggio
|
||||
- PATCH /messages/{id} – aggiorna flags (is_read, is_starred, is_archived)
|
||||
- PATCH /messages/{id} – aggiorna flags (is_read, is_starred, is_archived, is_trashed)
|
||||
- PATCH /messages/bulk – aggiorna in blocco piu messaggi
|
||||
- GET /messages/{id}/attachments – lista allegati
|
||||
- GET /messages/{id}/attachments/{att_id}/download – scarica allegato da MinIO
|
||||
- GET /messages/{id}/receipts – ricevute (messaggi figlio)
|
||||
@@ -58,7 +59,7 @@ def _apply_vbox_rule(q, field: str, operator: str, value: str):
|
||||
elif field == "from_address":
|
||||
col = Message.from_address
|
||||
elif field == "to_address":
|
||||
# to_addresses è ARRAY(Text) – converte in stringa per il confronto
|
||||
# to_addresses e ARRAY(Text) – converte in stringa per il confronto
|
||||
arr_text = func.array_to_string(Message.to_addresses, ",")
|
||||
if operator == "contains":
|
||||
return q.where(arr_text.ilike(f"%{value}%"))
|
||||
@@ -94,7 +95,7 @@ async def _get_visible_mailbox_ids(
|
||||
) -> Optional[list[uuid.UUID]]:
|
||||
"""
|
||||
Per utenti non-admin restituisce la lista di mailbox_id accessibili.
|
||||
Restituisce None se l'utente è admin (accesso illimitato al tenant).
|
||||
Restituisce None se l'utente e admin (accesso illimitato al tenant).
|
||||
"""
|
||||
if user.is_admin:
|
||||
return None # nessun filtro per admin
|
||||
@@ -111,10 +112,10 @@ async def _resolve_message(
|
||||
) -> Message:
|
||||
"""Carica il messaggio e verifica i permessi di accesso.
|
||||
|
||||
L'accesso è consentito se:
|
||||
1. L'utente è admin del tenant, oppure
|
||||
L'accesso e consentito se:
|
||||
1. L'utente e admin del tenant, oppure
|
||||
2. L'utente ha un permesso diretto can_read sulla casella, oppure
|
||||
3. L'utente è assegnato a una Virtual Box attiva che include la casella.
|
||||
3. L'utente e assegnato a una Virtual Box attiva che include la casella.
|
||||
"""
|
||||
result = await db.execute(
|
||||
select(Message)
|
||||
@@ -182,6 +183,7 @@ async def list_messages(
|
||||
is_read: Optional[bool] = Query(None),
|
||||
is_starred: Optional[bool] = Query(None),
|
||||
is_archived: Optional[bool] = Query(False),
|
||||
is_trashed: Optional[bool] = Query(False),
|
||||
search: Optional[str] = Query(None, max_length=200),
|
||||
pec_type: Optional[str] = Query(None),
|
||||
# Paginazione
|
||||
@@ -192,6 +194,7 @@ async def list_messages(
|
||||
Elenca i messaggi PEC con filtri opzionali.
|
||||
|
||||
- `is_archived=False` (default) esclude i messaggi archiviati.
|
||||
- `is_trashed=False` (default) esclude i messaggi nel cestino.
|
||||
- `search` cerca su subject, from_address, to_addresses.
|
||||
- `vbox_id` filtra per Virtual Box assegnata all'utente corrente.
|
||||
"""
|
||||
@@ -278,6 +281,9 @@ async def list_messages(
|
||||
if is_archived is not None:
|
||||
q = q.where(Message.is_archived == is_archived)
|
||||
|
||||
if is_trashed is not None:
|
||||
q = q.where(Message.is_trashed == is_trashed)
|
||||
|
||||
if search:
|
||||
term = f"%{search}%"
|
||||
q = q.where(
|
||||
@@ -325,7 +331,7 @@ async def bulk_update_messages(
|
||||
db: DB,
|
||||
) -> MessageBulkUpdateResponse:
|
||||
"""
|
||||
Aggiorna in blocco i flag operativi (is_starred, is_archived) di più messaggi.
|
||||
Aggiorna in blocco i flag operativi (is_starred, is_archived, is_trashed) di piu messaggi.
|
||||
|
||||
Restituisce il numero di messaggi aggiornati e la lista aggiornata.
|
||||
I messaggi non trovati o non accessibili vengono silenziosamente ignorati.
|
||||
@@ -352,6 +358,8 @@ async def bulk_update_messages(
|
||||
|
||||
now = datetime.now(timezone.utc)
|
||||
for message in messages:
|
||||
if data.is_read is not None:
|
||||
message.is_read = data.is_read
|
||||
if data.is_starred is not None:
|
||||
message.is_starred = data.is_starred
|
||||
if data.is_archived is not None:
|
||||
@@ -360,6 +368,12 @@ async def bulk_update_messages(
|
||||
message.archived_at = now
|
||||
elif not data.is_archived:
|
||||
message.archived_at = None
|
||||
if data.is_trashed is not None:
|
||||
message.is_trashed = data.is_trashed
|
||||
if data.is_trashed and not message.trashed_at:
|
||||
message.trashed_at = now
|
||||
elif not data.is_trashed:
|
||||
message.trashed_at = None
|
||||
|
||||
await db.commit()
|
||||
|
||||
@@ -399,7 +413,7 @@ async def update_message(
|
||||
) -> MessageResponse:
|
||||
"""
|
||||
Aggiorna i flag operativi di un messaggio:
|
||||
is_read, is_starred, is_archived.
|
||||
is_read, is_starred, is_archived, is_trashed.
|
||||
"""
|
||||
message = await _resolve_message(message_id, current_user, db)
|
||||
|
||||
@@ -413,6 +427,12 @@ async def update_message(
|
||||
message.archived_at = datetime.now(timezone.utc)
|
||||
elif not data.is_archived:
|
||||
message.archived_at = None
|
||||
if data.is_trashed is not None:
|
||||
message.is_trashed = data.is_trashed
|
||||
if data.is_trashed and not message.trashed_at:
|
||||
message.trashed_at = datetime.now(timezone.utc)
|
||||
elif not data.is_trashed:
|
||||
message.trashed_at = None
|
||||
|
||||
await db.commit()
|
||||
# Re-query con selectinload per evitare MissingGreenlet sui labels
|
||||
@@ -422,7 +442,13 @@ async def update_message(
|
||||
.options(selectinload(Message.labels))
|
||||
)
|
||||
message = refreshed.scalar_one()
|
||||
return MessageResponse.model_validate(messa
|
||||
return MessageResponse.model_validate(message)
|
||||
|
||||
|
||||
@router.get("/{message_id}/attachments", response_model=list[AttachmentResponse])
|
||||
async def list_attachments(
|
||||
message_id: uuid.UUID,
|
||||
current_user: CurrentUser,
|
||||
db: DB,
|
||||
) -> list[AttachmentResponse]:
|
||||
"""Elenca gli allegati di un messaggio."""
|
||||
@@ -473,7 +499,7 @@ async def download_attachment(
|
||||
secure=settings.minio_use_ssl,
|
||||
)
|
||||
|
||||
# storage_path è del tipo "tenant_id/attachments/filename"
|
||||
# storage_path e del tipo "tenant_id/attachments/filename"
|
||||
storage_path = attachment.storage_path
|
||||
response = await client.get_object(settings.minio_bucket, storage_path)
|
||||
|
||||
|
||||
@@ -91,6 +91,8 @@ class Message(Base):
|
||||
is_starred: 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)
|
||||
is_trashed: Mapped[bool] = mapped_column(Boolean, nullable=False, default=False)
|
||||
trashed_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True)
|
||||
|
||||
raw_eml_path: Mapped[str | None] = mapped_column(Text, nullable=True)
|
||||
|
||||
|
||||
@@ -49,6 +49,8 @@ class MessageResponse(BaseModel):
|
||||
is_starred: bool = False
|
||||
is_archived: bool = False
|
||||
archived_at: Optional[datetime] = None
|
||||
is_trashed: bool = False
|
||||
trashed_at: Optional[datetime] = None
|
||||
raw_eml_path: Optional[str] = None
|
||||
created_at: datetime
|
||||
updated_at: datetime
|
||||
@@ -84,12 +86,15 @@ class MessageUpdateRequest(BaseModel):
|
||||
is_read: Optional[bool] = None
|
||||
is_starred: Optional[bool] = None
|
||||
is_archived: Optional[bool] = None
|
||||
is_trashed: Optional[bool] = None
|
||||
|
||||
|
||||
class MessageBulkUpdateRequest(BaseModel):
|
||||
ids: list[uuid.UUID]
|
||||
is_read: Optional[bool] = None
|
||||
is_starred: Optional[bool] = None
|
||||
is_archived: Optional[bool] = None
|
||||
is_trashed: Optional[bool] = None
|
||||
|
||||
|
||||
class MessageBulkUpdateResponse(BaseModel):
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
is"""
|
||||
"""
|
||||
Servizio Notifiche Multi-canale – CRUD canali, regole, log.
|
||||
|
||||
Nota: la cifratura AES-256-GCM di config_enc avviene qui usando
|
||||
|
||||
@@ -48,12 +48,14 @@ export default function App() {
|
||||
<Route path="/sent" element={<InboxPage viewMode="sent" />} />
|
||||
<Route path="/starred" element={<InboxPage viewMode="starred" />} />
|
||||
<Route path="/archived" element={<InboxPage viewMode="archived" />} />
|
||||
<Route path="/trash" element={<InboxPage viewMode="trash" />} />
|
||||
|
||||
{/* Vista per singola casella PEC */}
|
||||
<Route path="/mailbox/:mailboxId/inbox" element={<InboxPage viewMode="inbox" />} />
|
||||
<Route path="/mailbox/:mailboxId/sent" element={<InboxPage viewMode="sent" />} />
|
||||
<Route path="/mailbox/:mailboxId/starred" element={<InboxPage viewMode="starred" />} />
|
||||
<Route path="/mailbox/:mailboxId/archived" element={<InboxPage viewMode="archived" />} />
|
||||
<Route path="/mailbox/:mailboxId/trash" element={<InboxPage viewMode="trash" />} />
|
||||
|
||||
{/* Vista per Virtual Box assegnata */}
|
||||
<Route path="/virtual-box/:vboxId/inbox" element={<InboxPage viewMode="inbox" />} />
|
||||
|
||||
@@ -16,13 +16,16 @@ export interface MessageFilters {
|
||||
is_read?: boolean
|
||||
is_starred?: boolean
|
||||
is_archived?: boolean
|
||||
is_trashed?: boolean
|
||||
search?: string
|
||||
}
|
||||
|
||||
export interface MessageBulkUpdatePayload {
|
||||
ids: string[]
|
||||
is_read?: boolean
|
||||
is_starred?: boolean
|
||||
is_archived?: boolean
|
||||
is_trashed?: boolean
|
||||
}
|
||||
|
||||
export interface MessageBulkUpdateResponse {
|
||||
@@ -60,7 +63,17 @@ export const messagesApi = {
|
||||
.patch<MessageResponse>(`/messages/${id}`, { is_archived: false })
|
||||
.then((r) => r.data),
|
||||
|
||||
/** Aggiorna in blocco is_starred e/o is_archived su più messaggi */
|
||||
trash: (id: string) =>
|
||||
apiClient
|
||||
.patch<MessageResponse>(`/messages/${id}`, { is_trashed: true })
|
||||
.then((r) => r.data),
|
||||
|
||||
untrash: (id: string) =>
|
||||
apiClient
|
||||
.patch<MessageResponse>(`/messages/${id}`, { is_trashed: false })
|
||||
.then((r) => r.data),
|
||||
|
||||
/** Aggiorna in blocco is_starred e/o is_archived e/o is_trashed su più messaggi */
|
||||
bulkUpdate: (payload: MessageBulkUpdatePayload) =>
|
||||
apiClient
|
||||
.patch<MessageBulkUpdateResponse>('/messages/bulk', payload)
|
||||
|
||||
@@ -49,6 +49,7 @@ import {
|
||||
Star,
|
||||
Archive,
|
||||
Building2,
|
||||
Trash2,
|
||||
} from 'lucide-react'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { useAuth } from '@/hooks/useAuth'
|
||||
@@ -266,6 +267,25 @@ export function Sidebar() {
|
||||
<Archive className="h-4 w-4 flex-shrink-0" />
|
||||
{!collapsed && <span>Archiviati</span>}
|
||||
</NavLink>
|
||||
|
||||
{/* Cestino globale */}
|
||||
<NavLink
|
||||
to="/trash"
|
||||
end
|
||||
className={({ isActive }) =>
|
||||
cn(
|
||||
'flex items-center gap-3 px-3 py-2 rounded-lg text-sm font-medium transition-colors',
|
||||
isActive
|
||||
? 'bg-blue-600 text-white'
|
||||
: 'text-gray-300 hover:bg-gray-700 hover:text-white',
|
||||
collapsed && 'justify-center px-2',
|
||||
)
|
||||
}
|
||||
title={collapsed ? 'Cestino (tutte le caselle)' : undefined}
|
||||
>
|
||||
<Trash2 className="h-4 w-4 flex-shrink-0" />
|
||||
{!collapsed && <span>Cestino</span>}
|
||||
</NavLink>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -625,6 +645,21 @@ function MailboxNavItem({ mailbox, collapsed, isExpanded, onToggle }: MailboxNav
|
||||
<Archive className="h-3.5 w-3.5 flex-shrink-0" />
|
||||
<span>Archiviati</span>
|
||||
</NavLink>
|
||||
|
||||
<NavLink
|
||||
to={`/mailbox/${mailbox.id}/trash`}
|
||||
className={({ isActive }) =>
|
||||
cn(
|
||||
'flex items-center gap-2 px-2 py-1.5 rounded-md text-xs font-medium transition-colors',
|
||||
isActive
|
||||
? 'bg-blue-600 text-white'
|
||||
: 'text-gray-400 hover:bg-gray-700 hover:text-white',
|
||||
)
|
||||
}
|
||||
>
|
||||
<Trash2 className="h-3.5 w-3.5 flex-shrink-0" />
|
||||
<span>Cestino</span>
|
||||
</NavLink>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -1,17 +1,18 @@
|
||||
/**
|
||||
* InboxPage – visualizza la posta in arrivo, inviata, preferiti o archiviata.
|
||||
* InboxPage – visualizza la posta in arrivo, inviata, preferiti, archiviata o cestino.
|
||||
*
|
||||
* Può operare in quattro modalità (viewMode):
|
||||
* - 'inbox' → Posta in Arrivo (solo inbound, is_archived=false)
|
||||
* - 'sent' → Posta Inviata (solo outbound, is_archived=false)
|
||||
* - 'starred' → Preferiti (tutte le direzioni, is_starred=true)
|
||||
* - 'archived' → Archiviati (tutte le direzioni, is_archived=true)
|
||||
* Modalita' (viewMode):
|
||||
* - 'inbox' → Posta in Arrivo (solo inbound, is_archived=false, is_trashed=false)
|
||||
* - 'sent' → Posta Inviata (solo outbound, is_archived=false, is_trashed=false)
|
||||
* - 'starred' → Preferiti (is_starred=true, is_trashed=false)
|
||||
* - 'archived' → Archiviati (is_archived=true, is_trashed=false)
|
||||
* - 'trash' → Cestino (is_trashed=true)
|
||||
*
|
||||
* Funzionalità:
|
||||
* Funzionalita':
|
||||
* - Selezione singola e multipla tramite checkbox
|
||||
* - Barra azioni bulk (stella, rimuovi stella, archivia, rimuovi archivio, assegna tag)
|
||||
* - Pulsanti azione rapida su hover di ogni riga (stella, archivia)
|
||||
* - Badge tag colorati per ogni messaggio
|
||||
* - Barra azioni bulk (stella, archivia, cestino, segna da leggere, tag)
|
||||
* - Pulsanti azione rapida su hover di ogni riga
|
||||
* - "Segna come da leggere" e "Sposta nel cestino" solo fuori dalle Virtual Box
|
||||
*/
|
||||
import { useEffect, useState, useCallback } from 'react'
|
||||
import { useNavigate, useParams } from 'react-router-dom'
|
||||
@@ -32,6 +33,9 @@ import {
|
||||
Square,
|
||||
X,
|
||||
Tag,
|
||||
Trash2,
|
||||
RotateCcw,
|
||||
MailX,
|
||||
} from 'lucide-react'
|
||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
|
||||
import toast from 'react-hot-toast'
|
||||
@@ -51,10 +55,9 @@ import { getErrorMessage } from '@/api/client'
|
||||
|
||||
// ─── Props ────────────────────────────────────────────────────────────────────
|
||||
|
||||
export type InboxViewMode = 'inbox' | 'sent' | 'starred' | 'archived'
|
||||
export type InboxViewMode = 'inbox' | 'sent' | 'starred' | 'archived' | 'trash'
|
||||
|
||||
interface InboxPageProps {
|
||||
/** Modalità vista */
|
||||
viewMode: InboxViewMode
|
||||
}
|
||||
|
||||
@@ -64,10 +67,11 @@ export function InboxPage({ viewMode }: InboxPageProps) {
|
||||
const navigate = useNavigate()
|
||||
const queryClient = useQueryClient()
|
||||
|
||||
// mailboxId è presente solo nei percorsi /mailbox/:mailboxId/...
|
||||
// vboxId è presente solo nei percorsi /virtual-box/:vboxId/...
|
||||
const { mailboxId, vboxId } = useParams<{ mailboxId?: string; vboxId?: string }>()
|
||||
|
||||
// true = stiamo navigando in una casella reale (non virtual box)
|
||||
const isMailboxMode = !vboxId
|
||||
|
||||
// ── Stato filtri locale ──────────────────────────────────────────────────────
|
||||
const [searchInput, setSearchInput] = useState('')
|
||||
const [debouncedSearch, setDebouncedSearch] = useState('')
|
||||
@@ -82,7 +86,6 @@ export function InboxPage({ viewMode }: InboxPageProps) {
|
||||
// ── Stato dialog tag bulk ────────────────────────────────────────────────────
|
||||
const [showTagSelector, setShowTagSelector] = useState(false)
|
||||
|
||||
// Resetta lo stato UI ogni volta che si cambia casella, virtual box o cartella
|
||||
useEffect(() => {
|
||||
setSearchInput('')
|
||||
setDebouncedSearch('')
|
||||
@@ -92,7 +95,6 @@ export function InboxPage({ viewMode }: InboxPageProps) {
|
||||
setSelectedIds(new Set())
|
||||
}, [mailboxId, vboxId, viewMode])
|
||||
|
||||
// Debounce della ricerca
|
||||
useEffect(() => {
|
||||
const timer = setTimeout(() => {
|
||||
setDebouncedSearch(searchInput)
|
||||
@@ -112,7 +114,6 @@ export function InboxPage({ viewMode }: InboxPageProps) {
|
||||
? mailboxesData?.items.find((m) => m.id === mailboxId)
|
||||
: undefined
|
||||
|
||||
// ── Virtual Box corrente (per breadcrumb) ────────────────────────────────────
|
||||
const { data: myVboxes = [] } = useQuery({
|
||||
queryKey: ['virtual-boxes', 'my'],
|
||||
queryFn: () => virtualBoxesApi.myVirtualBoxes(),
|
||||
@@ -141,6 +142,7 @@ export function InboxPage({ viewMode }: InboxPageProps) {
|
||||
is_read: isReadFilter,
|
||||
is_starred: isStarredFilter,
|
||||
is_archived: false,
|
||||
is_trashed: false,
|
||||
}
|
||||
case 'sent':
|
||||
return {
|
||||
@@ -148,17 +150,25 @@ export function InboxPage({ viewMode }: InboxPageProps) {
|
||||
direction: 'outbound' as const,
|
||||
is_starred: isStarredFilter,
|
||||
is_archived: false,
|
||||
is_trashed: false,
|
||||
}
|
||||
case 'starred':
|
||||
return {
|
||||
...base,
|
||||
is_starred: true,
|
||||
is_archived: false,
|
||||
is_trashed: false,
|
||||
}
|
||||
case 'archived':
|
||||
return {
|
||||
...base,
|
||||
is_archived: true,
|
||||
is_trashed: false,
|
||||
}
|
||||
case 'trash':
|
||||
return {
|
||||
...base,
|
||||
is_trashed: true,
|
||||
}
|
||||
}
|
||||
})()
|
||||
@@ -178,7 +188,6 @@ export function InboxPage({ viewMode }: InboxPageProps) {
|
||||
const total = messagesData?.total || 0
|
||||
const totalPages = Math.ceil(total / PAGE_SIZE)
|
||||
|
||||
// ── Invalida query messaggi dopo operazioni ──────────────────────────────────
|
||||
const invalidateMessages = useCallback(() => {
|
||||
queryClient.invalidateQueries({ queryKey: ['messages'] })
|
||||
}, [queryClient])
|
||||
@@ -200,6 +209,25 @@ export function InboxPage({ viewMode }: InboxPageProps) {
|
||||
},
|
||||
})
|
||||
|
||||
// ── Segna come DA leggere (unread) ───────────────────────────────────────────
|
||||
const markUnreadMutation = useMutation({
|
||||
mutationFn: (id: string) => messagesApi.markUnread(id),
|
||||
onSuccess: (updatedMsg) => {
|
||||
queryClient.setQueryData(
|
||||
['messages', queryFilters],
|
||||
(old: { items: MessageResponse[]; total: number } | undefined) => {
|
||||
if (!old) return old
|
||||
return {
|
||||
...old,
|
||||
items: old.items.map((m) => (m.id === updatedMsg.id ? updatedMsg : m)),
|
||||
}
|
||||
},
|
||||
)
|
||||
toast.success('Messaggio segnato come da leggere')
|
||||
},
|
||||
onError: (error) => toast.error(getErrorMessage(error)),
|
||||
})
|
||||
|
||||
// ── Toggle stella singolo ────────────────────────────────────────────────────
|
||||
const toggleStarMutation = useMutation({
|
||||
mutationFn: ({ id, starred }: { id: string; starred: boolean }) =>
|
||||
@@ -209,7 +237,6 @@ export function InboxPage({ viewMode }: InboxPageProps) {
|
||||
['messages', queryFilters],
|
||||
(old: { items: MessageResponse[]; total: number } | undefined) => {
|
||||
if (!old) return old
|
||||
// In vista "starred" rimuoviamo il messaggio se è stato rimosso dai preferiti
|
||||
if (viewMode === 'starred' && !updatedMsg.is_starred) {
|
||||
return { ...old, items: old.items.filter((m) => m.id !== updatedMsg.id), total: old.total - 1 }
|
||||
}
|
||||
@@ -226,7 +253,6 @@ export function InboxPage({ viewMode }: InboxPageProps) {
|
||||
mutationFn: ({ id, archived }: { id: string; archived: boolean }) =>
|
||||
archived ? messagesApi.archive(id) : messagesApi.unarchive(id),
|
||||
onSuccess: (updatedMsg, { archived }) => {
|
||||
// Rimuove il messaggio dalla lista corrente (ha cambiato "stanza")
|
||||
queryClient.setQueryData(
|
||||
['messages', queryFilters],
|
||||
(old: { items: MessageResponse[]; total: number } | undefined) => {
|
||||
@@ -240,6 +266,24 @@ export function InboxPage({ viewMode }: InboxPageProps) {
|
||||
onError: (error) => toast.error(getErrorMessage(error)),
|
||||
})
|
||||
|
||||
// ── Cestino/Ripristina singolo ────────────────────────────────────────────────
|
||||
const trashMutation = useMutation({
|
||||
mutationFn: ({ id, trashed }: { id: string; trashed: boolean }) =>
|
||||
trashed ? messagesApi.trash(id) : messagesApi.untrash(id),
|
||||
onSuccess: (updatedMsg, { trashed }) => {
|
||||
queryClient.setQueryData(
|
||||
['messages', queryFilters],
|
||||
(old: { items: MessageResponse[]; total: number } | undefined) => {
|
||||
if (!old) return old
|
||||
return { ...old, items: old.items.filter((m) => m.id !== updatedMsg.id), total: old.total - 1 }
|
||||
},
|
||||
)
|
||||
toast.success(trashed ? 'Messaggio spostato nel cestino' : 'Messaggio ripristinato dal cestino')
|
||||
invalidateMessages()
|
||||
},
|
||||
onError: (error) => toast.error(getErrorMessage(error)),
|
||||
})
|
||||
|
||||
// ── Azioni bulk ──────────────────────────────────────────────────────────────
|
||||
const bulkMutation = useMutation({
|
||||
mutationFn: messagesApi.bulkUpdate,
|
||||
@@ -247,10 +291,13 @@ export function InboxPage({ viewMode }: InboxPageProps) {
|
||||
invalidateMessages()
|
||||
setSelectedIds(new Set())
|
||||
const n = result.updated
|
||||
if (payload.is_starred === true) toast.success(`${n} ${n === 1 ? 'messaggio aggiunto' : 'messaggi aggiunti'} ai preferiti`)
|
||||
if (payload.is_read === false) toast.success(`${n} ${n === 1 ? 'messaggio segnato' : 'messaggi segnati'} come da leggere`)
|
||||
else if (payload.is_starred === true) toast.success(`${n} ${n === 1 ? 'messaggio aggiunto' : 'messaggi aggiunti'} ai preferiti`)
|
||||
else if (payload.is_starred === false) toast.success(`${n} ${n === 1 ? 'messaggio rimosso' : 'messaggi rimossi'} dai preferiti`)
|
||||
else if (payload.is_archived === true) toast.success(`${n} ${n === 1 ? 'messaggio archiviato' : 'messaggi archiviati'}`)
|
||||
else if (payload.is_archived === false) toast.success(`${n} ${n === 1 ? 'messaggio ripristinato' : 'messaggi ripristinati'}`)
|
||||
else if (payload.is_trashed === true) toast.success(`${n} ${n === 1 ? 'messaggio spostato nel cestino' : 'messaggi spostati nel cestino'}`)
|
||||
else if (payload.is_trashed === false) toast.success(`${n} ${n === 1 ? 'messaggio ripristinato dal cestino' : 'messaggi ripristinati dal cestino'}`)
|
||||
},
|
||||
onError: (error) => toast.error(getErrorMessage(error)),
|
||||
})
|
||||
@@ -280,6 +327,8 @@ export function InboxPage({ viewMode }: InboxPageProps) {
|
||||
})
|
||||
}
|
||||
|
||||
const handleBulkMarkUnread = () =>
|
||||
bulkMutation.mutate({ ids: Array.from(selectedIds), is_read: false })
|
||||
const handleBulkStar = () =>
|
||||
bulkMutation.mutate({ ids: Array.from(selectedIds), is_starred: true })
|
||||
const handleBulkUnstar = () =>
|
||||
@@ -288,6 +337,10 @@ export function InboxPage({ viewMode }: InboxPageProps) {
|
||||
bulkMutation.mutate({ ids: Array.from(selectedIds), is_archived: true })
|
||||
const handleBulkUnarchive = () =>
|
||||
bulkMutation.mutate({ ids: Array.from(selectedIds), is_archived: false })
|
||||
const handleBulkTrash = () =>
|
||||
bulkMutation.mutate({ ids: Array.from(selectedIds), is_trashed: true })
|
||||
const handleBulkUntrash = () =>
|
||||
bulkMutation.mutate({ ids: Array.from(selectedIds), is_trashed: false })
|
||||
|
||||
// ── Selezione ────────────────────────────────────────────────────────────────
|
||||
const handleToggleSelect = (id: string, e: React.MouseEvent) => {
|
||||
@@ -336,7 +389,9 @@ export function InboxPage({ viewMode }: InboxPageProps) {
|
||||
? 'Posta Inviata'
|
||||
: viewMode === 'starred'
|
||||
? 'Preferiti'
|
||||
: 'Archiviati'
|
||||
: viewMode === 'archived'
|
||||
? 'Archiviati'
|
||||
: 'Cestino'
|
||||
const FolderIcon =
|
||||
viewMode === 'inbox'
|
||||
? Inbox
|
||||
@@ -344,7 +399,9 @@ export function InboxPage({ viewMode }: InboxPageProps) {
|
||||
? Send
|
||||
: viewMode === 'starred'
|
||||
? Star
|
||||
: Archive
|
||||
: viewMode === 'archived'
|
||||
? Archive
|
||||
: Trash2
|
||||
|
||||
const selectedCount = selectedIds.size
|
||||
const allSelected = messages.length > 0 && selectedCount === messages.length
|
||||
@@ -396,10 +453,12 @@ export function InboxPage({ viewMode }: InboxPageProps) {
|
||||
<RefreshCw className="h-4 w-4 mr-1" />
|
||||
Aggiorna
|
||||
</Button>
|
||||
{viewMode !== 'trash' && (
|
||||
<Button size="sm" onClick={() => navigate('/compose')}>
|
||||
<Send className="h-4 w-4 mr-1" />
|
||||
Nuova PEC
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -443,10 +502,9 @@ export function InboxPage({ viewMode }: InboxPageProps) {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* ── Barra azioni bulk (appare quando ci sono selezioni) ── */}
|
||||
{/* ── Barra azioni bulk ── */}
|
||||
{someSelected && (
|
||||
<div className="border-b bg-blue-50 dark:bg-blue-950/30 px-6 py-2.5 flex items-center gap-3 flex-wrap">
|
||||
{/* Contatore + deseleziona */}
|
||||
<div className="flex items-center gap-2">
|
||||
<button
|
||||
onClick={handleClearSelection}
|
||||
@@ -462,8 +520,22 @@ export function InboxPage({ viewMode }: InboxPageProps) {
|
||||
|
||||
<div className="h-4 w-px bg-blue-200 dark:bg-blue-700" />
|
||||
|
||||
{/* Azioni: variano in base alla vista */}
|
||||
{viewMode !== 'starred' && (
|
||||
{/* Segna come da leggere (solo in modalita' casella, non vbox) */}
|
||||
{isMailboxMode && viewMode !== 'trash' && (
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="h-8 text-xs border-blue-200 hover:bg-blue-100 dark:border-blue-700"
|
||||
onClick={handleBulkMarkUnread}
|
||||
isLoading={bulkMutation.isPending}
|
||||
>
|
||||
<MailX className="h-3.5 w-3.5 mr-1" />
|
||||
Segna da leggere
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{/* Stella */}
|
||||
{viewMode !== 'starred' && viewMode !== 'trash' && (
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
@@ -502,7 +574,7 @@ export function InboxPage({ viewMode }: InboxPageProps) {
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{viewMode !== 'archived' && (
|
||||
{viewMode !== 'archived' && viewMode !== 'trash' && (
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
@@ -528,7 +600,35 @@ export function InboxPage({ viewMode }: InboxPageProps) {
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{/* Assegna tag bulk */}
|
||||
{/* Cestino / Ripristina cestino (solo modalita' casella) */}
|
||||
{isMailboxMode && viewMode !== 'trash' && (
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="h-8 text-xs border-red-200 hover:bg-red-50 dark:border-red-800 text-red-600"
|
||||
onClick={handleBulkTrash}
|
||||
isLoading={bulkMutation.isPending}
|
||||
>
|
||||
<Trash2 className="h-3.5 w-3.5 mr-1" />
|
||||
Cestino
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{isMailboxMode && viewMode === 'trash' && (
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="h-8 text-xs border-blue-200 hover:bg-blue-100 dark:border-blue-700"
|
||||
onClick={handleBulkUntrash}
|
||||
isLoading={bulkMutation.isPending}
|
||||
>
|
||||
<RotateCcw className="h-3.5 w-3.5 mr-1" />
|
||||
Ripristina dal cestino
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{/* Tag */}
|
||||
{viewMode !== 'trash' && (
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
@@ -538,6 +638,7 @@ export function InboxPage({ viewMode }: InboxPageProps) {
|
||||
<Tag className="h-3.5 w-3.5 mr-1 text-primary" />
|
||||
Tag
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -558,17 +659,18 @@ export function InboxPage({ viewMode }: InboxPageProps) {
|
||||
{debouncedSearch || isReadFilter !== undefined || isStarredFilter !== undefined
|
||||
? 'Prova a modificare i filtri di ricerca'
|
||||
: viewMode === 'inbox'
|
||||
? 'La posta in arrivo è vuota'
|
||||
? 'La posta in arrivo e vuota'
|
||||
: viewMode === 'sent'
|
||||
? 'Nessun messaggio inviato'
|
||||
: viewMode === 'starred'
|
||||
? 'Nessun messaggio nei preferiti'
|
||||
: 'Nessun messaggio archiviato'}
|
||||
: viewMode === 'archived'
|
||||
? 'Nessun messaggio archiviato'
|
||||
: 'Il cestino e vuoto'}
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="divide-y">
|
||||
{/* Riga "seleziona tutto" */}
|
||||
{messages.length > 0 && (
|
||||
<div className="flex items-center gap-3 px-6 py-2 bg-muted/20 border-b">
|
||||
<button
|
||||
@@ -594,6 +696,7 @@ export function InboxPage({ viewMode }: InboxPageProps) {
|
||||
key={message.id}
|
||||
message={message}
|
||||
viewMode={viewMode}
|
||||
isMailboxMode={isMailboxMode}
|
||||
isSelected={selectedIds.has(message.id)}
|
||||
onSelect={(e) => handleToggleSelect(message.id, e)}
|
||||
onClick={() => handleMessageClick(message)}
|
||||
@@ -605,6 +708,14 @@ export function InboxPage({ viewMode }: InboxPageProps) {
|
||||
e.stopPropagation()
|
||||
archiveMutation.mutate({ id: message.id, archived: !message.is_archived })
|
||||
}}
|
||||
onMarkUnread={(e) => {
|
||||
e.stopPropagation()
|
||||
markUnreadMutation.mutate(message.id)
|
||||
}}
|
||||
onToggleTrash={(e) => {
|
||||
e.stopPropagation()
|
||||
trashMutation.mutate({ id: message.id, trashed: !message.is_trashed })
|
||||
}}
|
||||
mailboxName={
|
||||
!mailboxId
|
||||
? mailboxesData?.items.find((m) => m.id === message.mailbox_id)?.email_address
|
||||
@@ -663,23 +774,28 @@ export function InboxPage({ viewMode }: InboxPageProps) {
|
||||
interface MessageRowProps {
|
||||
message: MessageResponse
|
||||
viewMode: InboxViewMode
|
||||
isMailboxMode: boolean
|
||||
isSelected: boolean
|
||||
onSelect: (e: React.MouseEvent) => void
|
||||
onClick: () => void
|
||||
onToggleStar: (e: React.MouseEvent) => void
|
||||
onToggleArchive: (e: React.MouseEvent) => void
|
||||
/** Presente solo nella vista globale – mostra la casella di appartenenza */
|
||||
onMarkUnread: (e: React.MouseEvent) => void
|
||||
onToggleTrash: (e: React.MouseEvent) => void
|
||||
mailboxName?: string
|
||||
}
|
||||
|
||||
function MessageRow({
|
||||
message,
|
||||
viewMode,
|
||||
isMailboxMode,
|
||||
isSelected,
|
||||
onSelect,
|
||||
onClick,
|
||||
onToggleStar,
|
||||
onToggleArchive,
|
||||
onMarkUnread,
|
||||
onToggleTrash,
|
||||
mailboxName,
|
||||
}: MessageRowProps) {
|
||||
const [hovered, setHovered] = useState(false)
|
||||
@@ -770,7 +886,6 @@ function MessageRow({
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Tag badges */}
|
||||
{message.labels && message.labels.length > 0 && (
|
||||
<div className="mt-1" onClick={(e) => e.stopPropagation()}>
|
||||
<TagBadgeList labels={message.labels} maxVisible={4} size="sm" />
|
||||
@@ -784,9 +899,9 @@ function MessageRow({
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* ── Azioni rapide (visibili su hover) + indicatori fissi ── */}
|
||||
{/* ── Azioni rapide (visibili su hover) ── */}
|
||||
<div className="flex items-center gap-1 flex-shrink-0">
|
||||
{/* Pulsante stella (rapido, su hover o se stellata) */}
|
||||
{/* Stella */}
|
||||
<button
|
||||
onClick={onToggleStar}
|
||||
title={message.is_starred ? 'Rimuovi dai preferiti' : 'Aggiungi ai preferiti'}
|
||||
@@ -807,7 +922,22 @@ function MessageRow({
|
||||
/>
|
||||
</button>
|
||||
|
||||
{/* Pulsante archivia/ripristina (rapido, su hover) */}
|
||||
{/* Segna come da leggere (solo in modalita' casella, solo messaggi gia' letti) */}
|
||||
{isMailboxMode && message.is_read && viewMode !== 'trash' && (
|
||||
<button
|
||||
onClick={onMarkUnread}
|
||||
title="Segna come da leggere"
|
||||
className={cn(
|
||||
'p-1 rounded hover:bg-muted transition-all',
|
||||
hovered ? 'opacity-100' : 'opacity-0 pointer-events-none',
|
||||
)}
|
||||
>
|
||||
<MailX className="h-4 w-4 text-muted-foreground" />
|
||||
</button>
|
||||
)}
|
||||
|
||||
{/* Archivia/Ripristina (non nel cestino) */}
|
||||
{viewMode !== 'trash' && (
|
||||
<button
|
||||
onClick={onToggleArchive}
|
||||
title={viewMode === 'archived' ? 'Ripristina dalla posta' : 'Archivia'}
|
||||
@@ -822,6 +952,25 @@ function MessageRow({
|
||||
<Archive className="h-4 w-4 text-muted-foreground" />
|
||||
)}
|
||||
</button>
|
||||
)}
|
||||
|
||||
{/* Cestino / Ripristina dal cestino (solo in modalita' casella) */}
|
||||
{isMailboxMode && (
|
||||
<button
|
||||
onClick={onToggleTrash}
|
||||
title={viewMode === 'trash' ? 'Ripristina dal cestino' : 'Sposta nel cestino'}
|
||||
className={cn(
|
||||
'p-1 rounded hover:bg-muted transition-all',
|
||||
hovered ? 'opacity-100' : 'opacity-0 pointer-events-none',
|
||||
)}
|
||||
>
|
||||
{viewMode === 'trash' ? (
|
||||
<RotateCcw className="h-4 w-4 text-muted-foreground" />
|
||||
) : (
|
||||
<Trash2 className="h-4 w-4 text-muted-foreground" />
|
||||
)}
|
||||
</button>
|
||||
)}
|
||||
|
||||
{/* Indicatore allegati */}
|
||||
{message.has_attachments && (
|
||||
|
||||
@@ -12,6 +12,9 @@ import {
|
||||
Mail,
|
||||
Send,
|
||||
Tag,
|
||||
Trash2,
|
||||
RotateCcw,
|
||||
MailX,
|
||||
} from 'lucide-react'
|
||||
import toast from 'react-hot-toast'
|
||||
import { Button } from '@/components/ui/Button'
|
||||
@@ -29,7 +32,6 @@ export function MessageDetailPage() {
|
||||
const navigate = useNavigate()
|
||||
const queryClient = useQueryClient()
|
||||
|
||||
// Dialog tag
|
||||
const [showTagSelector, setShowTagSelector] = useState(false)
|
||||
|
||||
// Carica messaggio
|
||||
@@ -62,7 +64,6 @@ export function MessageDetailPage() {
|
||||
mutationFn: (starred: boolean) => messagesApi.toggleStar(id!, starred),
|
||||
onSuccess: (updated) => {
|
||||
queryClient.setQueryData(['message', id], updated)
|
||||
// Invalida le query messaggi per aggiornare le viste Preferiti
|
||||
queryClient.invalidateQueries({ queryKey: ['messages'] })
|
||||
toast.success(updated.is_starred ? 'Aggiunto ai preferiti' : 'Rimosso dai preferiti')
|
||||
},
|
||||
@@ -80,15 +81,6 @@ export function MessageDetailPage() {
|
||||
onError: (error) => toast.error(getErrorMessage(error)),
|
||||
})
|
||||
|
||||
// Download allegato autenticato
|
||||
const handleDownloadAttachment = async (att: { id: string; filename: string }) => {
|
||||
try {
|
||||
await messagesApi.downloadAttachment(message?.id ?? id!, att.id, att.filename)
|
||||
} catch (error) {
|
||||
toast.error(getErrorMessage(error))
|
||||
}
|
||||
}
|
||||
|
||||
// Ripristina dall'archivio
|
||||
const unarchiveMutation = useMutation({
|
||||
mutationFn: () => messagesApi.unarchive(id!),
|
||||
@@ -100,17 +92,59 @@ export function MessageDetailPage() {
|
||||
onError: (error) => toast.error(getErrorMessage(error)),
|
||||
})
|
||||
|
||||
// Segna come da leggere
|
||||
const markUnreadMutation = useMutation({
|
||||
mutationFn: () => messagesApi.markUnread(id!),
|
||||
onSuccess: (updated) => {
|
||||
queryClient.setQueryData(['message', id], updated)
|
||||
queryClient.invalidateQueries({ queryKey: ['messages'] })
|
||||
toast.success('Messaggio segnato come da leggere')
|
||||
navigate(-1)
|
||||
},
|
||||
onError: (error) => toast.error(getErrorMessage(error)),
|
||||
})
|
||||
|
||||
// Sposta nel cestino
|
||||
const trashMutation = useMutation({
|
||||
mutationFn: () => messagesApi.trash(id!),
|
||||
onSuccess: (updated) => {
|
||||
queryClient.setQueryData(['message', id], updated)
|
||||
queryClient.invalidateQueries({ queryKey: ['messages'] })
|
||||
toast.success('Messaggio spostato nel cestino')
|
||||
navigate(-1)
|
||||
},
|
||||
onError: (error) => toast.error(getErrorMessage(error)),
|
||||
})
|
||||
|
||||
// Ripristina dal cestino
|
||||
const untrashMutation = useMutation({
|
||||
mutationFn: () => messagesApi.untrash(id!),
|
||||
onSuccess: (updated) => {
|
||||
queryClient.setQueryData(['message', id], updated)
|
||||
queryClient.invalidateQueries({ queryKey: ['messages'] })
|
||||
toast.success('Messaggio ripristinato dal cestino')
|
||||
},
|
||||
onError: (error) => toast.error(getErrorMessage(error)),
|
||||
})
|
||||
|
||||
// Download allegato autenticato
|
||||
const handleDownloadAttachment = async (att: { id: string; filename: string }) => {
|
||||
try {
|
||||
await messagesApi.downloadAttachment(message?.id ?? id!, att.id, att.filename)
|
||||
} catch (error) {
|
||||
toast.error(getErrorMessage(error))
|
||||
}
|
||||
}
|
||||
|
||||
// Imposta tag del messaggio
|
||||
const setLabelsMutation = useMutation({
|
||||
mutationFn: (labelIds: string[]) =>
|
||||
labelsApi.setMessageLabels(id!, { label_ids: labelIds }),
|
||||
onSuccess: (updatedLabels) => {
|
||||
// Aggiorna la cache del messaggio con i nuovi label
|
||||
queryClient.setQueryData(['message', id], (old: typeof message) => {
|
||||
if (!old) return old
|
||||
return { ...old, labels: updatedLabels }
|
||||
})
|
||||
// Invalida la lista messaggi per aggiornare i badge nella inbox
|
||||
queryClient.invalidateQueries({ queryKey: ['messages'] })
|
||||
setShowTagSelector(false)
|
||||
toast.success('Tag aggiornati')
|
||||
@@ -118,7 +152,7 @@ export function MessageDetailPage() {
|
||||
onError: (error) => toast.error(getErrorMessage(error)),
|
||||
})
|
||||
|
||||
// Rimuove singolo tag (click su × nel badge)
|
||||
// Rimuove singolo tag
|
||||
const removeLabelMutation = useMutation({
|
||||
mutationFn: (labelId: string) =>
|
||||
labelsApi.removeMessageLabels(id!, { label_ids: [labelId] }),
|
||||
@@ -197,8 +231,21 @@ export function MessageDetailPage() {
|
||||
/>
|
||||
</Button>
|
||||
|
||||
{/* Archivia (se non ancora archiviato) */}
|
||||
{!message.is_archived && (
|
||||
{/* Segna come da leggere (solo se gia' letto) */}
|
||||
{message.is_read && !message.is_trashed && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={() => markUnreadMutation.mutate()}
|
||||
title="Segna come da leggere"
|
||||
isLoading={markUnreadMutation.isPending}
|
||||
>
|
||||
<MailX className="h-5 w-5 text-muted-foreground" />
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{/* Archivia (se non ancora archiviato e non nel cestino) */}
|
||||
{!message.is_archived && !message.is_trashed && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
@@ -211,7 +258,7 @@ export function MessageDetailPage() {
|
||||
)}
|
||||
|
||||
{/* Ripristina dall'archivio (se archiviato) */}
|
||||
{message.is_archived && (
|
||||
{message.is_archived && !message.is_trashed && (
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
@@ -224,8 +271,35 @@ export function MessageDetailPage() {
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{/* Rispondi (solo per messaggi inbound PEC certificata) */}
|
||||
{message.direction === 'inbound' && message.pec_type === 'posta_certificata' && (
|
||||
{/* Sposta nel cestino (se non gia' nel cestino) */}
|
||||
{!message.is_trashed && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={() => trashMutation.mutate()}
|
||||
title="Sposta nel cestino"
|
||||
isLoading={trashMutation.isPending}
|
||||
>
|
||||
<Trash2 className="h-5 w-5 text-muted-foreground" />
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{/* Ripristina dal cestino (se nel cestino) */}
|
||||
{message.is_trashed && (
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => untrashMutation.mutate()}
|
||||
title="Ripristina dal cestino"
|
||||
isLoading={untrashMutation.isPending}
|
||||
>
|
||||
<RotateCcw className="h-4 w-4 mr-1" />
|
||||
Ripristina
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{/* Rispondi (solo per messaggi inbound PEC certificata, non nel cestino) */}
|
||||
{message.direction === 'inbound' && message.pec_type === 'posta_certificata' && !message.is_trashed && (
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
@@ -242,8 +316,28 @@ export function MessageDetailPage() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Banner "Nel Cestino" */}
|
||||
{message.is_trashed && (
|
||||
<div className="bg-red-50 border-b border-red-200 px-6 py-2.5 flex items-center justify-between">
|
||||
<div className="flex items-center gap-2 text-sm text-red-700">
|
||||
<Trash2 className="h-4 w-4" />
|
||||
<span>Questo messaggio si trova nel cestino.</span>
|
||||
</div>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="h-7 text-xs border-red-300 hover:bg-red-100 text-red-700"
|
||||
onClick={() => untrashMutation.mutate()}
|
||||
isLoading={untrashMutation.isPending}
|
||||
>
|
||||
<RotateCcw className="h-3.5 w-3.5 mr-1" />
|
||||
Ripristina
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Banner "Archiviato" */}
|
||||
{message.is_archived && (
|
||||
{message.is_archived && !message.is_trashed && (
|
||||
<div className="bg-amber-50 border-b border-amber-200 px-6 py-2.5 flex items-center justify-between">
|
||||
<div className="flex items-center gap-2 text-sm text-amber-700">
|
||||
<Archive className="h-4 w-4" />
|
||||
@@ -421,7 +515,7 @@ export function MessageDetailPage() {
|
||||
<div className="flex items-center gap-2 text-sm text-blue-700">
|
||||
<Mail className="h-4 w-4" />
|
||||
<span>
|
||||
Questo è un messaggio automatico di tipo{' '}
|
||||
Questo e un messaggio automatico di tipo{' '}
|
||||
<strong>
|
||||
<PecTypeBadge type={message.pec_type} />
|
||||
</strong>
|
||||
|
||||
@@ -260,6 +260,8 @@ export interface MessageResponse {
|
||||
is_starred: boolean
|
||||
is_archived: boolean
|
||||
archived_at: string | null
|
||||
is_trashed: boolean
|
||||
trashed_at: string | null
|
||||
raw_eml_path: string | null
|
||||
created_at: string
|
||||
updated_at: string
|
||||
|
||||
Reference in New Issue
Block a user