- {/* Contatore + deseleziona */}
- {/* Azioni: variano in base alla vista */}
- {viewMode !== 'starred' && (
+ {/* Segna come da leggere (solo in modalita' casella, non vbox) */}
+ {isMailboxMode && viewMode !== 'trash' && (
+
+ )}
+
+ {/* Stella */}
+ {viewMode !== 'starred' && viewMode !== 'trash' && (
)}
@@ -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'}
) : (
- {/* Riga "seleziona tutto" */}
{messages.length > 0 && (
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({
- {/* Tag badges */}
{message.labels && message.labels.length > 0 && (
e.stopPropagation()}>
@@ -784,9 +899,9 @@ function MessageRow({
)}
- {/* ── Azioni rapide (visibili su hover) + indicatori fissi ── */}
+ {/* ── Azioni rapide (visibili su hover) ── */}
- {/* Pulsante stella (rapido, su hover o se stellata) */}
+ {/* Stella */}
- {/* Pulsante archivia/ripristina (rapido, su hover) */}
-
- {viewMode === 'archived' ? (
-
- ) : (
-
- )}
-
+ {/* Segna come da leggere (solo in modalita' casella, solo messaggi gia' letti) */}
+ {isMailboxMode && message.is_read && viewMode !== 'trash' && (
+
+
+
+ )}
+
+ {/* Archivia/Ripristina (non nel cestino) */}
+ {viewMode !== 'trash' && (
+
+ {viewMode === 'archived' ? (
+
+ ) : (
+
+ )}
+
+ )}
+
+ {/* Cestino / Ripristina dal cestino (solo in modalita' casella) */}
+ {isMailboxMode && (
+
+ {viewMode === 'trash' ? (
+
+ ) : (
+
+ )}
+
+ )}
{/* Indicatore allegati */}
{message.has_attachments && (
diff --git a/frontend/src/pages/MessageDetail/MessageDetailPage.tsx b/frontend/src/pages/MessageDetail/MessageDetailPage.tsx
index c927521..c4dfb7a 100644
--- a/frontend/src/pages/MessageDetail/MessageDetailPage.tsx
+++ b/frontend/src/pages/MessageDetail/MessageDetailPage.tsx
@@ -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() {
/>
- {/* Archivia (se non ancora archiviato) */}
- {!message.is_archived && (
+ {/* Segna come da leggere (solo se gia' letto) */}
+ {message.is_read && !message.is_trashed && (
+
markUnreadMutation.mutate()}
+ title="Segna come da leggere"
+ isLoading={markUnreadMutation.isPending}
+ >
+
+
+ )}
+
+ {/* Archivia (se non ancora archiviato e non nel cestino) */}
+ {!message.is_archived && !message.is_trashed && (
)}
- {/* 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 && (
+ trashMutation.mutate()}
+ title="Sposta nel cestino"
+ isLoading={trashMutation.isPending}
+ >
+
+
+ )}
+
+ {/* Ripristina dal cestino (se nel cestino) */}
+ {message.is_trashed && (
+ untrashMutation.mutate()}
+ title="Ripristina dal cestino"
+ isLoading={untrashMutation.isPending}
+ >
+
+ Ripristina
+
+ )}
+
+ {/* Rispondi (solo per messaggi inbound PEC certificata, non nel cestino) */}
+ {message.direction === 'inbound' && message.pec_type === 'posta_certificata' && !message.is_trashed && (
+ {/* Banner "Nel Cestino" */}
+ {message.is_trashed && (
+
+
+
+ Questo messaggio si trova nel cestino.
+
+
untrashMutation.mutate()}
+ isLoading={untrashMutation.isPending}
+ >
+
+ Ripristina
+
+
+ )}
+
{/* Banner "Archiviato" */}
- {message.is_archived && (
+ {message.is_archived && !message.is_trashed && (
@@ -421,7 +515,7 @@ export function MessageDetailPage() {
- Questo è un messaggio automatico di tipo{' '}
+ Questo e un messaggio automatico di tipo{' '}
diff --git a/frontend/src/types/api.types.ts b/frontend/src/types/api.types.ts
index 8ba1a63..69e8f72 100644
--- a/frontend/src/types/api.types.ts
+++ b/frontend/src/types/api.types.ts
@@ -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