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
|
# 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.
|
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
|
||||||
#.idea/
|
#.idea/
|
||||||
|
KnowledgeBaseCline.md
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ Porta: 465
|
|||||||
SSL: Sì
|
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
|
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:
|
Fornisce:
|
||||||
- GET /messages – lista messaggi con filtri (inbox/sent/search/...)
|
- GET /messages – lista messaggi con filtri (inbox/sent/search/...)
|
||||||
- GET /messages/{id} – singolo messaggio
|
- 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 – lista allegati
|
||||||
- GET /messages/{id}/attachments/{att_id}/download – scarica allegato da MinIO
|
- GET /messages/{id}/attachments/{att_id}/download – scarica allegato da MinIO
|
||||||
- GET /messages/{id}/receipts – ricevute (messaggi figlio)
|
- 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":
|
elif field == "from_address":
|
||||||
col = Message.from_address
|
col = Message.from_address
|
||||||
elif field == "to_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, ",")
|
arr_text = func.array_to_string(Message.to_addresses, ",")
|
||||||
if operator == "contains":
|
if operator == "contains":
|
||||||
return q.where(arr_text.ilike(f"%{value}%"))
|
return q.where(arr_text.ilike(f"%{value}%"))
|
||||||
@@ -94,7 +95,7 @@ async def _get_visible_mailbox_ids(
|
|||||||
) -> Optional[list[uuid.UUID]]:
|
) -> Optional[list[uuid.UUID]]:
|
||||||
"""
|
"""
|
||||||
Per utenti non-admin restituisce la lista di mailbox_id accessibili.
|
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:
|
if user.is_admin:
|
||||||
return None # nessun filtro per admin
|
return None # nessun filtro per admin
|
||||||
@@ -111,10 +112,10 @@ async def _resolve_message(
|
|||||||
) -> Message:
|
) -> Message:
|
||||||
"""Carica il messaggio e verifica i permessi di accesso.
|
"""Carica il messaggio e verifica i permessi di accesso.
|
||||||
|
|
||||||
L'accesso è consentito se:
|
L'accesso e consentito se:
|
||||||
1. L'utente è admin del tenant, oppure
|
1. L'utente e admin del tenant, oppure
|
||||||
2. L'utente ha un permesso diretto can_read sulla casella, 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(
|
result = await db.execute(
|
||||||
select(Message)
|
select(Message)
|
||||||
@@ -182,6 +183,7 @@ async def list_messages(
|
|||||||
is_read: Optional[bool] = Query(None),
|
is_read: Optional[bool] = Query(None),
|
||||||
is_starred: Optional[bool] = Query(None),
|
is_starred: Optional[bool] = Query(None),
|
||||||
is_archived: Optional[bool] = Query(False),
|
is_archived: Optional[bool] = Query(False),
|
||||||
|
is_trashed: Optional[bool] = Query(False),
|
||||||
search: Optional[str] = Query(None, max_length=200),
|
search: Optional[str] = Query(None, max_length=200),
|
||||||
pec_type: Optional[str] = Query(None),
|
pec_type: Optional[str] = Query(None),
|
||||||
# Paginazione
|
# Paginazione
|
||||||
@@ -192,6 +194,7 @@ async def list_messages(
|
|||||||
Elenca i messaggi PEC con filtri opzionali.
|
Elenca i messaggi PEC con filtri opzionali.
|
||||||
|
|
||||||
- `is_archived=False` (default) esclude i messaggi archiviati.
|
- `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.
|
- `search` cerca su subject, from_address, to_addresses.
|
||||||
- `vbox_id` filtra per Virtual Box assegnata all'utente corrente.
|
- `vbox_id` filtra per Virtual Box assegnata all'utente corrente.
|
||||||
"""
|
"""
|
||||||
@@ -278,6 +281,9 @@ async def list_messages(
|
|||||||
if is_archived is not None:
|
if is_archived is not None:
|
||||||
q = q.where(Message.is_archived == is_archived)
|
q = q.where(Message.is_archived == is_archived)
|
||||||
|
|
||||||
|
if is_trashed is not None:
|
||||||
|
q = q.where(Message.is_trashed == is_trashed)
|
||||||
|
|
||||||
if search:
|
if search:
|
||||||
term = f"%{search}%"
|
term = f"%{search}%"
|
||||||
q = q.where(
|
q = q.where(
|
||||||
@@ -325,7 +331,7 @@ async def bulk_update_messages(
|
|||||||
db: DB,
|
db: DB,
|
||||||
) -> MessageBulkUpdateResponse:
|
) -> 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.
|
Restituisce il numero di messaggi aggiornati e la lista aggiornata.
|
||||||
I messaggi non trovati o non accessibili vengono silenziosamente ignorati.
|
I messaggi non trovati o non accessibili vengono silenziosamente ignorati.
|
||||||
@@ -352,6 +358,8 @@ async def bulk_update_messages(
|
|||||||
|
|
||||||
now = datetime.now(timezone.utc)
|
now = datetime.now(timezone.utc)
|
||||||
for message in messages:
|
for message in messages:
|
||||||
|
if data.is_read is not None:
|
||||||
|
message.is_read = data.is_read
|
||||||
if data.is_starred is not None:
|
if data.is_starred is not None:
|
||||||
message.is_starred = data.is_starred
|
message.is_starred = data.is_starred
|
||||||
if data.is_archived is not None:
|
if data.is_archived is not None:
|
||||||
@@ -360,6 +368,12 @@ async def bulk_update_messages(
|
|||||||
message.archived_at = now
|
message.archived_at = now
|
||||||
elif not data.is_archived:
|
elif not data.is_archived:
|
||||||
message.archived_at = None
|
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()
|
await db.commit()
|
||||||
|
|
||||||
@@ -399,7 +413,7 @@ async def update_message(
|
|||||||
) -> MessageResponse:
|
) -> MessageResponse:
|
||||||
"""
|
"""
|
||||||
Aggiorna i flag operativi di un messaggio:
|
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)
|
message = await _resolve_message(message_id, current_user, db)
|
||||||
|
|
||||||
@@ -413,6 +427,12 @@ async def update_message(
|
|||||||
message.archived_at = datetime.now(timezone.utc)
|
message.archived_at = datetime.now(timezone.utc)
|
||||||
elif not data.is_archived:
|
elif not data.is_archived:
|
||||||
message.archived_at = None
|
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()
|
await db.commit()
|
||||||
# Re-query con selectinload per evitare MissingGreenlet sui labels
|
# Re-query con selectinload per evitare MissingGreenlet sui labels
|
||||||
@@ -422,7 +442,13 @@ async def update_message(
|
|||||||
.options(selectinload(Message.labels))
|
.options(selectinload(Message.labels))
|
||||||
)
|
)
|
||||||
message = refreshed.scalar_one()
|
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,
|
db: DB,
|
||||||
) -> list[AttachmentResponse]:
|
) -> list[AttachmentResponse]:
|
||||||
"""Elenca gli allegati di un messaggio."""
|
"""Elenca gli allegati di un messaggio."""
|
||||||
@@ -473,7 +499,7 @@ async def download_attachment(
|
|||||||
secure=settings.minio_use_ssl,
|
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
|
storage_path = attachment.storage_path
|
||||||
response = await client.get_object(settings.minio_bucket, 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_starred: Mapped[bool] = mapped_column(Boolean, nullable=False, default=False)
|
||||||
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)
|
||||||
|
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)
|
raw_eml_path: Mapped[str | None] = mapped_column(Text, nullable=True)
|
||||||
|
|
||||||
|
|||||||
@@ -49,6 +49,8 @@ class MessageResponse(BaseModel):
|
|||||||
is_starred: bool = False
|
is_starred: bool = False
|
||||||
is_archived: bool = False
|
is_archived: bool = False
|
||||||
archived_at: Optional[datetime] = None
|
archived_at: Optional[datetime] = None
|
||||||
|
is_trashed: bool = False
|
||||||
|
trashed_at: Optional[datetime] = None
|
||||||
raw_eml_path: Optional[str] = None
|
raw_eml_path: Optional[str] = None
|
||||||
created_at: datetime
|
created_at: datetime
|
||||||
updated_at: datetime
|
updated_at: datetime
|
||||||
@@ -84,12 +86,15 @@ class MessageUpdateRequest(BaseModel):
|
|||||||
is_read: Optional[bool] = None
|
is_read: Optional[bool] = None
|
||||||
is_starred: Optional[bool] = None
|
is_starred: Optional[bool] = None
|
||||||
is_archived: Optional[bool] = None
|
is_archived: Optional[bool] = None
|
||||||
|
is_trashed: Optional[bool] = None
|
||||||
|
|
||||||
|
|
||||||
class MessageBulkUpdateRequest(BaseModel):
|
class MessageBulkUpdateRequest(BaseModel):
|
||||||
ids: list[uuid.UUID]
|
ids: list[uuid.UUID]
|
||||||
|
is_read: Optional[bool] = None
|
||||||
is_starred: Optional[bool] = None
|
is_starred: Optional[bool] = None
|
||||||
is_archived: Optional[bool] = None
|
is_archived: Optional[bool] = None
|
||||||
|
is_trashed: Optional[bool] = None
|
||||||
|
|
||||||
|
|
||||||
class MessageBulkUpdateResponse(BaseModel):
|
class MessageBulkUpdateResponse(BaseModel):
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
is"""
|
"""
|
||||||
Servizio Notifiche Multi-canale – CRUD canali, regole, log.
|
Servizio Notifiche Multi-canale – CRUD canali, regole, log.
|
||||||
|
|
||||||
Nota: la cifratura AES-256-GCM di config_enc avviene qui usando
|
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="/sent" element={<InboxPage viewMode="sent" />} />
|
||||||
<Route path="/starred" element={<InboxPage viewMode="starred" />} />
|
<Route path="/starred" element={<InboxPage viewMode="starred" />} />
|
||||||
<Route path="/archived" element={<InboxPage viewMode="archived" />} />
|
<Route path="/archived" element={<InboxPage viewMode="archived" />} />
|
||||||
|
<Route path="/trash" element={<InboxPage viewMode="trash" />} />
|
||||||
|
|
||||||
{/* Vista per singola casella PEC */}
|
{/* Vista per singola casella PEC */}
|
||||||
<Route path="/mailbox/:mailboxId/inbox" element={<InboxPage viewMode="inbox" />} />
|
<Route path="/mailbox/:mailboxId/inbox" element={<InboxPage viewMode="inbox" />} />
|
||||||
<Route path="/mailbox/:mailboxId/sent" element={<InboxPage viewMode="sent" />} />
|
<Route path="/mailbox/:mailboxId/sent" element={<InboxPage viewMode="sent" />} />
|
||||||
<Route path="/mailbox/:mailboxId/starred" element={<InboxPage viewMode="starred" />} />
|
<Route path="/mailbox/:mailboxId/starred" element={<InboxPage viewMode="starred" />} />
|
||||||
<Route path="/mailbox/:mailboxId/archived" element={<InboxPage viewMode="archived" />} />
|
<Route path="/mailbox/:mailboxId/archived" element={<InboxPage viewMode="archived" />} />
|
||||||
|
<Route path="/mailbox/:mailboxId/trash" element={<InboxPage viewMode="trash" />} />
|
||||||
|
|
||||||
{/* Vista per Virtual Box assegnata */}
|
{/* Vista per Virtual Box assegnata */}
|
||||||
<Route path="/virtual-box/:vboxId/inbox" element={<InboxPage viewMode="inbox" />} />
|
<Route path="/virtual-box/:vboxId/inbox" element={<InboxPage viewMode="inbox" />} />
|
||||||
|
|||||||
@@ -16,13 +16,16 @@ export interface MessageFilters {
|
|||||||
is_read?: boolean
|
is_read?: boolean
|
||||||
is_starred?: boolean
|
is_starred?: boolean
|
||||||
is_archived?: boolean
|
is_archived?: boolean
|
||||||
|
is_trashed?: boolean
|
||||||
search?: string
|
search?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface MessageBulkUpdatePayload {
|
export interface MessageBulkUpdatePayload {
|
||||||
ids: string[]
|
ids: string[]
|
||||||
|
is_read?: boolean
|
||||||
is_starred?: boolean
|
is_starred?: boolean
|
||||||
is_archived?: boolean
|
is_archived?: boolean
|
||||||
|
is_trashed?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface MessageBulkUpdateResponse {
|
export interface MessageBulkUpdateResponse {
|
||||||
@@ -60,7 +63,17 @@ export const messagesApi = {
|
|||||||
.patch<MessageResponse>(`/messages/${id}`, { is_archived: false })
|
.patch<MessageResponse>(`/messages/${id}`, { is_archived: false })
|
||||||
.then((r) => r.data),
|
.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) =>
|
bulkUpdate: (payload: MessageBulkUpdatePayload) =>
|
||||||
apiClient
|
apiClient
|
||||||
.patch<MessageBulkUpdateResponse>('/messages/bulk', payload)
|
.patch<MessageBulkUpdateResponse>('/messages/bulk', payload)
|
||||||
|
|||||||
@@ -49,6 +49,7 @@ import {
|
|||||||
Star,
|
Star,
|
||||||
Archive,
|
Archive,
|
||||||
Building2,
|
Building2,
|
||||||
|
Trash2,
|
||||||
} from 'lucide-react'
|
} from 'lucide-react'
|
||||||
import { cn } from '@/lib/utils'
|
import { cn } from '@/lib/utils'
|
||||||
import { useAuth } from '@/hooks/useAuth'
|
import { useAuth } from '@/hooks/useAuth'
|
||||||
@@ -266,6 +267,25 @@ export function Sidebar() {
|
|||||||
<Archive className="h-4 w-4 flex-shrink-0" />
|
<Archive className="h-4 w-4 flex-shrink-0" />
|
||||||
{!collapsed && <span>Archiviati</span>}
|
{!collapsed && <span>Archiviati</span>}
|
||||||
</NavLink>
|
</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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -625,6 +645,21 @@ function MailboxNavItem({ mailbox, collapsed, isExpanded, onToggle }: MailboxNav
|
|||||||
<Archive className="h-3.5 w-3.5 flex-shrink-0" />
|
<Archive className="h-3.5 w-3.5 flex-shrink-0" />
|
||||||
<span>Archiviati</span>
|
<span>Archiviati</span>
|
||||||
</NavLink>
|
</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>
|
||||||
)}
|
)}
|
||||||
</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):
|
* Modalita' (viewMode):
|
||||||
* - 'inbox' → Posta in Arrivo (solo inbound, is_archived=false)
|
* - 'inbox' → Posta in Arrivo (solo inbound, is_archived=false, is_trashed=false)
|
||||||
* - 'sent' → Posta Inviata (solo outbound, is_archived=false)
|
* - 'sent' → Posta Inviata (solo outbound, is_archived=false, is_trashed=false)
|
||||||
* - 'starred' → Preferiti (tutte le direzioni, is_starred=true)
|
* - 'starred' → Preferiti (is_starred=true, is_trashed=false)
|
||||||
* - 'archived' → Archiviati (tutte le direzioni, is_archived=true)
|
* - 'archived' → Archiviati (is_archived=true, is_trashed=false)
|
||||||
|
* - 'trash' → Cestino (is_trashed=true)
|
||||||
*
|
*
|
||||||
* Funzionalità:
|
* Funzionalita':
|
||||||
* - Selezione singola e multipla tramite checkbox
|
* - Selezione singola e multipla tramite checkbox
|
||||||
* - Barra azioni bulk (stella, rimuovi stella, archivia, rimuovi archivio, assegna tag)
|
* - Barra azioni bulk (stella, archivia, cestino, segna da leggere, tag)
|
||||||
* - Pulsanti azione rapida su hover di ogni riga (stella, archivia)
|
* - Pulsanti azione rapida su hover di ogni riga
|
||||||
* - Badge tag colorati per ogni messaggio
|
* - "Segna come da leggere" e "Sposta nel cestino" solo fuori dalle Virtual Box
|
||||||
*/
|
*/
|
||||||
import { useEffect, useState, useCallback } from 'react'
|
import { useEffect, useState, useCallback } from 'react'
|
||||||
import { useNavigate, useParams } from 'react-router-dom'
|
import { useNavigate, useParams } from 'react-router-dom'
|
||||||
@@ -32,6 +33,9 @@ import {
|
|||||||
Square,
|
Square,
|
||||||
X,
|
X,
|
||||||
Tag,
|
Tag,
|
||||||
|
Trash2,
|
||||||
|
RotateCcw,
|
||||||
|
MailX,
|
||||||
} from 'lucide-react'
|
} from 'lucide-react'
|
||||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
|
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
|
||||||
import toast from 'react-hot-toast'
|
import toast from 'react-hot-toast'
|
||||||
@@ -51,10 +55,9 @@ import { getErrorMessage } from '@/api/client'
|
|||||||
|
|
||||||
// ─── Props ────────────────────────────────────────────────────────────────────
|
// ─── Props ────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
export type InboxViewMode = 'inbox' | 'sent' | 'starred' | 'archived'
|
export type InboxViewMode = 'inbox' | 'sent' | 'starred' | 'archived' | 'trash'
|
||||||
|
|
||||||
interface InboxPageProps {
|
interface InboxPageProps {
|
||||||
/** Modalità vista */
|
|
||||||
viewMode: InboxViewMode
|
viewMode: InboxViewMode
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -64,10 +67,11 @@ export function InboxPage({ viewMode }: InboxPageProps) {
|
|||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
const queryClient = useQueryClient()
|
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 }>()
|
const { mailboxId, vboxId } = useParams<{ mailboxId?: string; vboxId?: string }>()
|
||||||
|
|
||||||
|
// true = stiamo navigando in una casella reale (non virtual box)
|
||||||
|
const isMailboxMode = !vboxId
|
||||||
|
|
||||||
// ── Stato filtri locale ──────────────────────────────────────────────────────
|
// ── Stato filtri locale ──────────────────────────────────────────────────────
|
||||||
const [searchInput, setSearchInput] = useState('')
|
const [searchInput, setSearchInput] = useState('')
|
||||||
const [debouncedSearch, setDebouncedSearch] = useState('')
|
const [debouncedSearch, setDebouncedSearch] = useState('')
|
||||||
@@ -82,7 +86,6 @@ export function InboxPage({ viewMode }: InboxPageProps) {
|
|||||||
// ── Stato dialog tag bulk ────────────────────────────────────────────────────
|
// ── Stato dialog tag bulk ────────────────────────────────────────────────────
|
||||||
const [showTagSelector, setShowTagSelector] = useState(false)
|
const [showTagSelector, setShowTagSelector] = useState(false)
|
||||||
|
|
||||||
// Resetta lo stato UI ogni volta che si cambia casella, virtual box o cartella
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setSearchInput('')
|
setSearchInput('')
|
||||||
setDebouncedSearch('')
|
setDebouncedSearch('')
|
||||||
@@ -92,7 +95,6 @@ export function InboxPage({ viewMode }: InboxPageProps) {
|
|||||||
setSelectedIds(new Set())
|
setSelectedIds(new Set())
|
||||||
}, [mailboxId, vboxId, viewMode])
|
}, [mailboxId, vboxId, viewMode])
|
||||||
|
|
||||||
// Debounce della ricerca
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const timer = setTimeout(() => {
|
const timer = setTimeout(() => {
|
||||||
setDebouncedSearch(searchInput)
|
setDebouncedSearch(searchInput)
|
||||||
@@ -112,7 +114,6 @@ export function InboxPage({ viewMode }: InboxPageProps) {
|
|||||||
? mailboxesData?.items.find((m) => m.id === mailboxId)
|
? mailboxesData?.items.find((m) => m.id === mailboxId)
|
||||||
: undefined
|
: undefined
|
||||||
|
|
||||||
// ── Virtual Box corrente (per breadcrumb) ────────────────────────────────────
|
|
||||||
const { data: myVboxes = [] } = useQuery({
|
const { data: myVboxes = [] } = useQuery({
|
||||||
queryKey: ['virtual-boxes', 'my'],
|
queryKey: ['virtual-boxes', 'my'],
|
||||||
queryFn: () => virtualBoxesApi.myVirtualBoxes(),
|
queryFn: () => virtualBoxesApi.myVirtualBoxes(),
|
||||||
@@ -141,6 +142,7 @@ export function InboxPage({ viewMode }: InboxPageProps) {
|
|||||||
is_read: isReadFilter,
|
is_read: isReadFilter,
|
||||||
is_starred: isStarredFilter,
|
is_starred: isStarredFilter,
|
||||||
is_archived: false,
|
is_archived: false,
|
||||||
|
is_trashed: false,
|
||||||
}
|
}
|
||||||
case 'sent':
|
case 'sent':
|
||||||
return {
|
return {
|
||||||
@@ -148,17 +150,25 @@ export function InboxPage({ viewMode }: InboxPageProps) {
|
|||||||
direction: 'outbound' as const,
|
direction: 'outbound' as const,
|
||||||
is_starred: isStarredFilter,
|
is_starred: isStarredFilter,
|
||||||
is_archived: false,
|
is_archived: false,
|
||||||
|
is_trashed: false,
|
||||||
}
|
}
|
||||||
case 'starred':
|
case 'starred':
|
||||||
return {
|
return {
|
||||||
...base,
|
...base,
|
||||||
is_starred: true,
|
is_starred: true,
|
||||||
is_archived: false,
|
is_archived: false,
|
||||||
|
is_trashed: false,
|
||||||
}
|
}
|
||||||
case 'archived':
|
case 'archived':
|
||||||
return {
|
return {
|
||||||
...base,
|
...base,
|
||||||
is_archived: true,
|
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 total = messagesData?.total || 0
|
||||||
const totalPages = Math.ceil(total / PAGE_SIZE)
|
const totalPages = Math.ceil(total / PAGE_SIZE)
|
||||||
|
|
||||||
// ── Invalida query messaggi dopo operazioni ──────────────────────────────────
|
|
||||||
const invalidateMessages = useCallback(() => {
|
const invalidateMessages = useCallback(() => {
|
||||||
queryClient.invalidateQueries({ queryKey: ['messages'] })
|
queryClient.invalidateQueries({ queryKey: ['messages'] })
|
||||||
}, [queryClient])
|
}, [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 ────────────────────────────────────────────────────
|
// ── Toggle stella singolo ────────────────────────────────────────────────────
|
||||||
const toggleStarMutation = useMutation({
|
const toggleStarMutation = useMutation({
|
||||||
mutationFn: ({ id, starred }: { id: string; starred: boolean }) =>
|
mutationFn: ({ id, starred }: { id: string; starred: boolean }) =>
|
||||||
@@ -209,7 +237,6 @@ export function InboxPage({ viewMode }: InboxPageProps) {
|
|||||||
['messages', queryFilters],
|
['messages', queryFilters],
|
||||||
(old: { items: MessageResponse[]; total: number } | undefined) => {
|
(old: { items: MessageResponse[]; total: number } | undefined) => {
|
||||||
if (!old) return old
|
if (!old) return old
|
||||||
// In vista "starred" rimuoviamo il messaggio se è stato rimosso dai preferiti
|
|
||||||
if (viewMode === 'starred' && !updatedMsg.is_starred) {
|
if (viewMode === 'starred' && !updatedMsg.is_starred) {
|
||||||
return { ...old, items: old.items.filter((m) => m.id !== updatedMsg.id), total: old.total - 1 }
|
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 }) =>
|
mutationFn: ({ id, archived }: { id: string; archived: boolean }) =>
|
||||||
archived ? messagesApi.archive(id) : messagesApi.unarchive(id),
|
archived ? messagesApi.archive(id) : messagesApi.unarchive(id),
|
||||||
onSuccess: (updatedMsg, { archived }) => {
|
onSuccess: (updatedMsg, { archived }) => {
|
||||||
// Rimuove il messaggio dalla lista corrente (ha cambiato "stanza")
|
|
||||||
queryClient.setQueryData(
|
queryClient.setQueryData(
|
||||||
['messages', queryFilters],
|
['messages', queryFilters],
|
||||||
(old: { items: MessageResponse[]; total: number } | undefined) => {
|
(old: { items: MessageResponse[]; total: number } | undefined) => {
|
||||||
@@ -240,6 +266,24 @@ export function InboxPage({ viewMode }: InboxPageProps) {
|
|||||||
onError: (error) => toast.error(getErrorMessage(error)),
|
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 ──────────────────────────────────────────────────────────────
|
// ── Azioni bulk ──────────────────────────────────────────────────────────────
|
||||||
const bulkMutation = useMutation({
|
const bulkMutation = useMutation({
|
||||||
mutationFn: messagesApi.bulkUpdate,
|
mutationFn: messagesApi.bulkUpdate,
|
||||||
@@ -247,10 +291,13 @@ export function InboxPage({ viewMode }: InboxPageProps) {
|
|||||||
invalidateMessages()
|
invalidateMessages()
|
||||||
setSelectedIds(new Set())
|
setSelectedIds(new Set())
|
||||||
const n = result.updated
|
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_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 === 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_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)),
|
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 = () =>
|
const handleBulkStar = () =>
|
||||||
bulkMutation.mutate({ ids: Array.from(selectedIds), is_starred: true })
|
bulkMutation.mutate({ ids: Array.from(selectedIds), is_starred: true })
|
||||||
const handleBulkUnstar = () =>
|
const handleBulkUnstar = () =>
|
||||||
@@ -288,6 +337,10 @@ export function InboxPage({ viewMode }: InboxPageProps) {
|
|||||||
bulkMutation.mutate({ ids: Array.from(selectedIds), is_archived: true })
|
bulkMutation.mutate({ ids: Array.from(selectedIds), is_archived: true })
|
||||||
const handleBulkUnarchive = () =>
|
const handleBulkUnarchive = () =>
|
||||||
bulkMutation.mutate({ ids: Array.from(selectedIds), is_archived: false })
|
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 ────────────────────────────────────────────────────────────────
|
// ── Selezione ────────────────────────────────────────────────────────────────
|
||||||
const handleToggleSelect = (id: string, e: React.MouseEvent) => {
|
const handleToggleSelect = (id: string, e: React.MouseEvent) => {
|
||||||
@@ -336,7 +389,9 @@ export function InboxPage({ viewMode }: InboxPageProps) {
|
|||||||
? 'Posta Inviata'
|
? 'Posta Inviata'
|
||||||
: viewMode === 'starred'
|
: viewMode === 'starred'
|
||||||
? 'Preferiti'
|
? 'Preferiti'
|
||||||
: 'Archiviati'
|
: viewMode === 'archived'
|
||||||
|
? 'Archiviati'
|
||||||
|
: 'Cestino'
|
||||||
const FolderIcon =
|
const FolderIcon =
|
||||||
viewMode === 'inbox'
|
viewMode === 'inbox'
|
||||||
? Inbox
|
? Inbox
|
||||||
@@ -344,7 +399,9 @@ export function InboxPage({ viewMode }: InboxPageProps) {
|
|||||||
? Send
|
? Send
|
||||||
: viewMode === 'starred'
|
: viewMode === 'starred'
|
||||||
? Star
|
? Star
|
||||||
: Archive
|
: viewMode === 'archived'
|
||||||
|
? Archive
|
||||||
|
: Trash2
|
||||||
|
|
||||||
const selectedCount = selectedIds.size
|
const selectedCount = selectedIds.size
|
||||||
const allSelected = messages.length > 0 && selectedCount === messages.length
|
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" />
|
<RefreshCw className="h-4 w-4 mr-1" />
|
||||||
Aggiorna
|
Aggiorna
|
||||||
</Button>
|
</Button>
|
||||||
|
{viewMode !== 'trash' && (
|
||||||
<Button size="sm" onClick={() => navigate('/compose')}>
|
<Button size="sm" onClick={() => navigate('/compose')}>
|
||||||
<Send className="h-4 w-4 mr-1" />
|
<Send className="h-4 w-4 mr-1" />
|
||||||
Nuova PEC
|
Nuova PEC
|
||||||
</Button>
|
</Button>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -443,10 +502,9 @@ export function InboxPage({ viewMode }: InboxPageProps) {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* ── Barra azioni bulk (appare quando ci sono selezioni) ── */}
|
{/* ── Barra azioni bulk ── */}
|
||||||
{someSelected && (
|
{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">
|
<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">
|
<div className="flex items-center gap-2">
|
||||||
<button
|
<button
|
||||||
onClick={handleClearSelection}
|
onClick={handleClearSelection}
|
||||||
@@ -462,8 +520,22 @@ export function InboxPage({ viewMode }: InboxPageProps) {
|
|||||||
|
|
||||||
<div className="h-4 w-px bg-blue-200 dark:bg-blue-700" />
|
<div className="h-4 w-px bg-blue-200 dark:bg-blue-700" />
|
||||||
|
|
||||||
{/* Azioni: variano in base alla vista */}
|
{/* Segna come da leggere (solo in modalita' casella, non vbox) */}
|
||||||
{viewMode !== 'starred' && (
|
{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
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
size="sm"
|
size="sm"
|
||||||
@@ -502,7 +574,7 @@ export function InboxPage({ viewMode }: InboxPageProps) {
|
|||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{viewMode !== 'archived' && (
|
{viewMode !== 'archived' && viewMode !== 'trash' && (
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
size="sm"
|
size="sm"
|
||||||
@@ -528,7 +600,35 @@ export function InboxPage({ viewMode }: InboxPageProps) {
|
|||||||
</Button>
|
</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
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
size="sm"
|
size="sm"
|
||||||
@@ -538,6 +638,7 @@ export function InboxPage({ viewMode }: InboxPageProps) {
|
|||||||
<Tag className="h-3.5 w-3.5 mr-1 text-primary" />
|
<Tag className="h-3.5 w-3.5 mr-1 text-primary" />
|
||||||
Tag
|
Tag
|
||||||
</Button>
|
</Button>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@@ -558,17 +659,18 @@ export function InboxPage({ viewMode }: InboxPageProps) {
|
|||||||
{debouncedSearch || isReadFilter !== undefined || isStarredFilter !== undefined
|
{debouncedSearch || isReadFilter !== undefined || isStarredFilter !== undefined
|
||||||
? 'Prova a modificare i filtri di ricerca'
|
? 'Prova a modificare i filtri di ricerca'
|
||||||
: viewMode === 'inbox'
|
: viewMode === 'inbox'
|
||||||
? 'La posta in arrivo è vuota'
|
? 'La posta in arrivo e vuota'
|
||||||
: viewMode === 'sent'
|
: viewMode === 'sent'
|
||||||
? 'Nessun messaggio inviato'
|
? 'Nessun messaggio inviato'
|
||||||
: viewMode === 'starred'
|
: viewMode === 'starred'
|
||||||
? 'Nessun messaggio nei preferiti'
|
? 'Nessun messaggio nei preferiti'
|
||||||
: 'Nessun messaggio archiviato'}
|
: viewMode === 'archived'
|
||||||
|
? 'Nessun messaggio archiviato'
|
||||||
|
: 'Il cestino e vuoto'}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="divide-y">
|
<div className="divide-y">
|
||||||
{/* Riga "seleziona tutto" */}
|
|
||||||
{messages.length > 0 && (
|
{messages.length > 0 && (
|
||||||
<div className="flex items-center gap-3 px-6 py-2 bg-muted/20 border-b">
|
<div className="flex items-center gap-3 px-6 py-2 bg-muted/20 border-b">
|
||||||
<button
|
<button
|
||||||
@@ -594,6 +696,7 @@ export function InboxPage({ viewMode }: InboxPageProps) {
|
|||||||
key={message.id}
|
key={message.id}
|
||||||
message={message}
|
message={message}
|
||||||
viewMode={viewMode}
|
viewMode={viewMode}
|
||||||
|
isMailboxMode={isMailboxMode}
|
||||||
isSelected={selectedIds.has(message.id)}
|
isSelected={selectedIds.has(message.id)}
|
||||||
onSelect={(e) => handleToggleSelect(message.id, e)}
|
onSelect={(e) => handleToggleSelect(message.id, e)}
|
||||||
onClick={() => handleMessageClick(message)}
|
onClick={() => handleMessageClick(message)}
|
||||||
@@ -605,6 +708,14 @@ export function InboxPage({ viewMode }: InboxPageProps) {
|
|||||||
e.stopPropagation()
|
e.stopPropagation()
|
||||||
archiveMutation.mutate({ id: message.id, archived: !message.is_archived })
|
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={
|
mailboxName={
|
||||||
!mailboxId
|
!mailboxId
|
||||||
? mailboxesData?.items.find((m) => m.id === message.mailbox_id)?.email_address
|
? mailboxesData?.items.find((m) => m.id === message.mailbox_id)?.email_address
|
||||||
@@ -663,23 +774,28 @@ export function InboxPage({ viewMode }: InboxPageProps) {
|
|||||||
interface MessageRowProps {
|
interface MessageRowProps {
|
||||||
message: MessageResponse
|
message: MessageResponse
|
||||||
viewMode: InboxViewMode
|
viewMode: InboxViewMode
|
||||||
|
isMailboxMode: boolean
|
||||||
isSelected: boolean
|
isSelected: boolean
|
||||||
onSelect: (e: React.MouseEvent) => void
|
onSelect: (e: React.MouseEvent) => void
|
||||||
onClick: () => void
|
onClick: () => void
|
||||||
onToggleStar: (e: React.MouseEvent) => void
|
onToggleStar: (e: React.MouseEvent) => void
|
||||||
onToggleArchive: (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
|
mailboxName?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
function MessageRow({
|
function MessageRow({
|
||||||
message,
|
message,
|
||||||
viewMode,
|
viewMode,
|
||||||
|
isMailboxMode,
|
||||||
isSelected,
|
isSelected,
|
||||||
onSelect,
|
onSelect,
|
||||||
onClick,
|
onClick,
|
||||||
onToggleStar,
|
onToggleStar,
|
||||||
onToggleArchive,
|
onToggleArchive,
|
||||||
|
onMarkUnread,
|
||||||
|
onToggleTrash,
|
||||||
mailboxName,
|
mailboxName,
|
||||||
}: MessageRowProps) {
|
}: MessageRowProps) {
|
||||||
const [hovered, setHovered] = useState(false)
|
const [hovered, setHovered] = useState(false)
|
||||||
@@ -770,7 +886,6 @@ function MessageRow({
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Tag badges */}
|
|
||||||
{message.labels && message.labels.length > 0 && (
|
{message.labels && message.labels.length > 0 && (
|
||||||
<div className="mt-1" onClick={(e) => e.stopPropagation()}>
|
<div className="mt-1" onClick={(e) => e.stopPropagation()}>
|
||||||
<TagBadgeList labels={message.labels} maxVisible={4} size="sm" />
|
<TagBadgeList labels={message.labels} maxVisible={4} size="sm" />
|
||||||
@@ -784,9 +899,9 @@ function MessageRow({
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* ── Azioni rapide (visibili su hover) + indicatori fissi ── */}
|
{/* ── Azioni rapide (visibili su hover) ── */}
|
||||||
<div className="flex items-center gap-1 flex-shrink-0">
|
<div className="flex items-center gap-1 flex-shrink-0">
|
||||||
{/* Pulsante stella (rapido, su hover o se stellata) */}
|
{/* Stella */}
|
||||||
<button
|
<button
|
||||||
onClick={onToggleStar}
|
onClick={onToggleStar}
|
||||||
title={message.is_starred ? 'Rimuovi dai preferiti' : 'Aggiungi ai preferiti'}
|
title={message.is_starred ? 'Rimuovi dai preferiti' : 'Aggiungi ai preferiti'}
|
||||||
@@ -807,7 +922,22 @@ function MessageRow({
|
|||||||
/>
|
/>
|
||||||
</button>
|
</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
|
<button
|
||||||
onClick={onToggleArchive}
|
onClick={onToggleArchive}
|
||||||
title={viewMode === 'archived' ? 'Ripristina dalla posta' : 'Archivia'}
|
title={viewMode === 'archived' ? 'Ripristina dalla posta' : 'Archivia'}
|
||||||
@@ -822,6 +952,25 @@ function MessageRow({
|
|||||||
<Archive className="h-4 w-4 text-muted-foreground" />
|
<Archive className="h-4 w-4 text-muted-foreground" />
|
||||||
)}
|
)}
|
||||||
</button>
|
</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 */}
|
{/* Indicatore allegati */}
|
||||||
{message.has_attachments && (
|
{message.has_attachments && (
|
||||||
|
|||||||
@@ -12,6 +12,9 @@ import {
|
|||||||
Mail,
|
Mail,
|
||||||
Send,
|
Send,
|
||||||
Tag,
|
Tag,
|
||||||
|
Trash2,
|
||||||
|
RotateCcw,
|
||||||
|
MailX,
|
||||||
} 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'
|
||||||
@@ -29,7 +32,6 @@ export function MessageDetailPage() {
|
|||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
const queryClient = useQueryClient()
|
const queryClient = useQueryClient()
|
||||||
|
|
||||||
// Dialog tag
|
|
||||||
const [showTagSelector, setShowTagSelector] = useState(false)
|
const [showTagSelector, setShowTagSelector] = useState(false)
|
||||||
|
|
||||||
// Carica messaggio
|
// Carica messaggio
|
||||||
@@ -62,7 +64,6 @@ export function MessageDetailPage() {
|
|||||||
mutationFn: (starred: boolean) => messagesApi.toggleStar(id!, starred),
|
mutationFn: (starred: boolean) => messagesApi.toggleStar(id!, starred),
|
||||||
onSuccess: (updated) => {
|
onSuccess: (updated) => {
|
||||||
queryClient.setQueryData(['message', id], updated)
|
queryClient.setQueryData(['message', id], updated)
|
||||||
// Invalida le query messaggi per aggiornare le viste Preferiti
|
|
||||||
queryClient.invalidateQueries({ queryKey: ['messages'] })
|
queryClient.invalidateQueries({ queryKey: ['messages'] })
|
||||||
toast.success(updated.is_starred ? 'Aggiunto ai preferiti' : 'Rimosso dai preferiti')
|
toast.success(updated.is_starred ? 'Aggiunto ai preferiti' : 'Rimosso dai preferiti')
|
||||||
},
|
},
|
||||||
@@ -80,15 +81,6 @@ export function MessageDetailPage() {
|
|||||||
onError: (error) => toast.error(getErrorMessage(error)),
|
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
|
// Ripristina dall'archivio
|
||||||
const unarchiveMutation = useMutation({
|
const unarchiveMutation = useMutation({
|
||||||
mutationFn: () => messagesApi.unarchive(id!),
|
mutationFn: () => messagesApi.unarchive(id!),
|
||||||
@@ -100,17 +92,59 @@ export function MessageDetailPage() {
|
|||||||
onError: (error) => toast.error(getErrorMessage(error)),
|
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
|
// Imposta tag del messaggio
|
||||||
const setLabelsMutation = useMutation({
|
const setLabelsMutation = useMutation({
|
||||||
mutationFn: (labelIds: string[]) =>
|
mutationFn: (labelIds: string[]) =>
|
||||||
labelsApi.setMessageLabels(id!, { label_ids: labelIds }),
|
labelsApi.setMessageLabels(id!, { label_ids: labelIds }),
|
||||||
onSuccess: (updatedLabels) => {
|
onSuccess: (updatedLabels) => {
|
||||||
// Aggiorna la cache del messaggio con i nuovi label
|
|
||||||
queryClient.setQueryData(['message', id], (old: typeof message) => {
|
queryClient.setQueryData(['message', id], (old: typeof message) => {
|
||||||
if (!old) return old
|
if (!old) return old
|
||||||
return { ...old, labels: updatedLabels }
|
return { ...old, labels: updatedLabels }
|
||||||
})
|
})
|
||||||
// Invalida la lista messaggi per aggiornare i badge nella inbox
|
|
||||||
queryClient.invalidateQueries({ queryKey: ['messages'] })
|
queryClient.invalidateQueries({ queryKey: ['messages'] })
|
||||||
setShowTagSelector(false)
|
setShowTagSelector(false)
|
||||||
toast.success('Tag aggiornati')
|
toast.success('Tag aggiornati')
|
||||||
@@ -118,7 +152,7 @@ export function MessageDetailPage() {
|
|||||||
onError: (error) => toast.error(getErrorMessage(error)),
|
onError: (error) => toast.error(getErrorMessage(error)),
|
||||||
})
|
})
|
||||||
|
|
||||||
// Rimuove singolo tag (click su × nel badge)
|
// Rimuove singolo tag
|
||||||
const removeLabelMutation = useMutation({
|
const removeLabelMutation = useMutation({
|
||||||
mutationFn: (labelId: string) =>
|
mutationFn: (labelId: string) =>
|
||||||
labelsApi.removeMessageLabels(id!, { label_ids: [labelId] }),
|
labelsApi.removeMessageLabels(id!, { label_ids: [labelId] }),
|
||||||
@@ -197,8 +231,21 @@ export function MessageDetailPage() {
|
|||||||
/>
|
/>
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
{/* Archivia (se non ancora archiviato) */}
|
{/* Segna come da leggere (solo se gia' letto) */}
|
||||||
{!message.is_archived && (
|
{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
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="icon"
|
size="icon"
|
||||||
@@ -211,7 +258,7 @@ export function MessageDetailPage() {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Ripristina dall'archivio (se archiviato) */}
|
{/* Ripristina dall'archivio (se archiviato) */}
|
||||||
{message.is_archived && (
|
{message.is_archived && !message.is_trashed && (
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
size="sm"
|
size="sm"
|
||||||
@@ -224,8 +271,35 @@ export function MessageDetailPage() {
|
|||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Rispondi (solo per messaggi inbound PEC certificata) */}
|
{/* Sposta nel cestino (se non gia' nel cestino) */}
|
||||||
{message.direction === 'inbound' && message.pec_type === 'posta_certificata' && (
|
{!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
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
size="sm"
|
size="sm"
|
||||||
@@ -242,8 +316,28 @@ export function MessageDetailPage() {
|
|||||||
</div>
|
</div>
|
||||||
</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" */}
|
{/* 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="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">
|
<div className="flex items-center gap-2 text-sm text-amber-700">
|
||||||
<Archive className="h-4 w-4" />
|
<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">
|
<div className="flex items-center gap-2 text-sm text-blue-700">
|
||||||
<Mail className="h-4 w-4" />
|
<Mail className="h-4 w-4" />
|
||||||
<span>
|
<span>
|
||||||
Questo è un messaggio automatico di tipo{' '}
|
Questo e un messaggio automatico di tipo{' '}
|
||||||
<strong>
|
<strong>
|
||||||
<PecTypeBadge type={message.pec_type} />
|
<PecTypeBadge type={message.pec_type} />
|
||||||
</strong>
|
</strong>
|
||||||
|
|||||||
@@ -260,6 +260,8 @@ export interface MessageResponse {
|
|||||||
is_starred: boolean
|
is_starred: boolean
|
||||||
is_archived: boolean
|
is_archived: boolean
|
||||||
archived_at: string | null
|
archived_at: string | null
|
||||||
|
is_trashed: boolean
|
||||||
|
trashed_at: string | null
|
||||||
raw_eml_path: string | null
|
raw_eml_path: string | null
|
||||||
created_at: string
|
created_at: string
|
||||||
updated_at: string
|
updated_at: string
|
||||||
|
|||||||
Reference in New Issue
Block a user