mirror of
https://github.com/idrainformatica/PecFlow.git
synced 2026-06-16 12:45:42 +02:00
Conservazionee
This commit is contained in:
+14
-10
@@ -47,18 +47,22 @@ export default function App() {
|
||||
<Route path="/" element={<Navigate to="/inbox" replace />} />
|
||||
|
||||
{/* Vista globale: tutte le caselle insieme */}
|
||||
<Route path="/inbox" element={<InboxPage viewMode="inbox" />} />
|
||||
<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" />} />
|
||||
<Route path="/inbox" element={<InboxPage viewMode="inbox" />} />
|
||||
<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" />} />
|
||||
<Route path="/conservation-pending" element={<InboxPage viewMode="conservation_pending" />} />
|
||||
<Route path="/conservation-archived" element={<InboxPage viewMode="conservation_archived" />} />
|
||||
|
||||
{/* 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" />} />
|
||||
<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" />} />
|
||||
<Route path="/mailbox/:mailboxId/conservation-pending" element={<InboxPage viewMode="conservation_pending" />} />
|
||||
<Route path="/mailbox/:mailboxId/conservation-archived" element={<InboxPage viewMode="conservation_archived" />} />
|
||||
|
||||
{/* Vista per Virtual Box assegnata */}
|
||||
<Route path="/virtual-box/:vboxId/inbox" element={<InboxPage viewMode="inbox" />} />
|
||||
|
||||
@@ -18,6 +18,10 @@ export interface MessageFilters {
|
||||
is_starred?: boolean
|
||||
is_archived?: boolean
|
||||
is_trashed?: boolean
|
||||
/** Filtra per messaggi in attesa di conservazione (cartella Da Conservare) */
|
||||
is_pending_conservation?: boolean
|
||||
/** Filtra per messaggi gia' conservati (cartella Storico) */
|
||||
is_conserved?: boolean
|
||||
search?: string
|
||||
/** Data minima nel formato ISO 8601 (es. "2026-01-01T00:00:00Z") */
|
||||
date_from?: string
|
||||
@@ -31,6 +35,8 @@ export interface MessageBulkUpdatePayload {
|
||||
is_starred?: boolean
|
||||
is_archived?: boolean
|
||||
is_trashed?: boolean
|
||||
is_pending_conservation?: boolean
|
||||
is_conserved?: boolean
|
||||
}
|
||||
|
||||
export interface MessageBulkUpdateResponse {
|
||||
@@ -78,6 +84,18 @@ export const messagesApi = {
|
||||
.patch<MessageResponse>(`/messages/${id}`, { is_trashed: false })
|
||||
.then((r) => r.data),
|
||||
|
||||
/** Sposta un messaggio nella cartella Da Conservare */
|
||||
conserve: (id: string) =>
|
||||
apiClient
|
||||
.patch<MessageResponse>(`/messages/${id}`, { is_pending_conservation: true })
|
||||
.then((r) => r.data),
|
||||
|
||||
/** Rimuove un messaggio dalla cartella Da Conservare */
|
||||
unconserve: (id: string) =>
|
||||
apiClient
|
||||
.patch<MessageResponse>(`/messages/${id}`, { is_pending_conservation: 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
|
||||
|
||||
@@ -53,6 +53,7 @@ import {
|
||||
Search,
|
||||
BarChart2,
|
||||
ClipboardList,
|
||||
ShieldCheck,
|
||||
} from 'lucide-react'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { useAuth } from '@/hooks/useAuth'
|
||||
@@ -297,6 +298,53 @@ export function Sidebar() {
|
||||
<Trash2 className="h-4 w-4 flex-shrink-0" />
|
||||
{!collapsed && <span>Cestino</span>}
|
||||
</NavLink>
|
||||
|
||||
{/* ── Sezione Conservazione (admin e supervisor) ── */}
|
||||
{(isAdmin || isSupervisor) && (
|
||||
<>
|
||||
{!collapsed && (
|
||||
<div className="pt-1 pb-0.5 px-1">
|
||||
<p className="text-[10px] font-semibold text-teal-500/70 uppercase tracking-wider">
|
||||
Conservazione
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
<NavLink
|
||||
to="/conservation-pending"
|
||||
end
|
||||
className={({ isActive }) =>
|
||||
cn(
|
||||
'flex items-center gap-3 px-3 py-2 rounded-lg text-sm font-medium transition-colors',
|
||||
isActive
|
||||
? 'bg-teal-700 text-white'
|
||||
: 'text-teal-300 hover:bg-teal-900/40 hover:text-white',
|
||||
collapsed && 'justify-center px-2',
|
||||
)
|
||||
}
|
||||
title={collapsed ? 'Da Conservare' : undefined}
|
||||
>
|
||||
<ShieldCheck className="h-4 w-4 flex-shrink-0" />
|
||||
{!collapsed && <span>Da Conservare</span>}
|
||||
</NavLink>
|
||||
<NavLink
|
||||
to="/conservation-archived"
|
||||
end
|
||||
className={({ isActive }) =>
|
||||
cn(
|
||||
'flex items-center gap-3 px-3 py-2 rounded-lg text-sm font-medium transition-colors',
|
||||
isActive
|
||||
? 'bg-teal-700 text-white'
|
||||
: 'text-teal-300 hover:bg-teal-900/40 hover:text-white',
|
||||
collapsed && 'justify-center px-2',
|
||||
)
|
||||
}
|
||||
title={collapsed ? 'Storico Conservazione' : undefined}
|
||||
>
|
||||
<ShieldCheck className="h-4 w-4 flex-shrink-0 opacity-60" />
|
||||
{!collapsed && <span>Storico</span>}
|
||||
</NavLink>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -604,6 +652,8 @@ function MailboxNavItem({ mailbox, collapsed, isExpanded, onToggle, unreadCount
|
||||
const displayName = mailbox.display_name || mailbox.email_address
|
||||
const initial = displayName[0]?.toUpperCase() ?? '?'
|
||||
const dotClass = statusDot(mailbox.status)
|
||||
const { isAdmin, isSupervisor } = useAuth()
|
||||
const canSeeConservation = isAdmin || isSupervisor
|
||||
|
||||
/* ── Modalità compressa: solo avatar/iniziale → link diretto all'inbox ── */
|
||||
if (collapsed) {
|
||||
@@ -759,6 +809,40 @@ function MailboxNavItem({ mailbox, collapsed, isExpanded, onToggle, unreadCount
|
||||
<Trash2 className="h-3.5 w-3.5 flex-shrink-0" />
|
||||
<span>Cestino</span>
|
||||
</NavLink>
|
||||
|
||||
{/* Conservazione (solo admin/supervisor) */}
|
||||
{canSeeConservation && (
|
||||
<>
|
||||
<NavLink
|
||||
to={`/mailbox/${mailbox.id}/conservation-pending`}
|
||||
className={({ isActive }) =>
|
||||
cn(
|
||||
'flex items-center gap-2 px-2 py-1.5 rounded-md text-xs font-medium transition-colors',
|
||||
isActive
|
||||
? 'bg-teal-700 text-white'
|
||||
: 'text-teal-400 hover:bg-teal-900/40 hover:text-white',
|
||||
)
|
||||
}
|
||||
>
|
||||
<ShieldCheck className="h-3.5 w-3.5 flex-shrink-0" />
|
||||
<span>Da Conservare</span>
|
||||
</NavLink>
|
||||
<NavLink
|
||||
to={`/mailbox/${mailbox.id}/conservation-archived`}
|
||||
className={({ isActive }) =>
|
||||
cn(
|
||||
'flex items-center gap-2 px-2 py-1.5 rounded-md text-xs font-medium transition-colors',
|
||||
isActive
|
||||
? 'bg-teal-700 text-white'
|
||||
: 'text-teal-400 hover:bg-teal-900/40 hover:text-white',
|
||||
)
|
||||
}
|
||||
>
|
||||
<ShieldCheck className="h-3.5 w-3.5 flex-shrink-0 opacity-60" />
|
||||
<span>Storico</span>
|
||||
</NavLink>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -39,6 +39,8 @@ import {
|
||||
SlidersHorizontal,
|
||||
ChevronDown,
|
||||
ChevronUp,
|
||||
ShieldCheck,
|
||||
ShieldX,
|
||||
} from 'lucide-react'
|
||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
|
||||
import toast from 'react-hot-toast'
|
||||
@@ -47,6 +49,7 @@ import { Input } from '@/components/ui/Input'
|
||||
import { PecStateBadge } from '@/components/PecBadge/PecBadge'
|
||||
import { TagBadgeList } from '@/components/TagManager/TagBadge'
|
||||
import { TagSelector } from '@/components/TagManager/TagSelector'
|
||||
import { useAuth } from '@/hooks/useAuth'
|
||||
import { messagesApi } from '@/api/messages.api'
|
||||
import { labelsApi } from '@/api/labels.api'
|
||||
import { mailboxesApi } from '@/api/mailboxes.api'
|
||||
@@ -58,7 +61,7 @@ import { getErrorMessage } from '@/api/client'
|
||||
|
||||
// ─── Props ────────────────────────────────────────────────────────────────────
|
||||
|
||||
export type InboxViewMode = 'inbox' | 'sent' | 'starred' | 'archived' | 'trash'
|
||||
export type InboxViewMode = 'inbox' | 'sent' | 'starred' | 'archived' | 'trash' | 'conservation_pending' | 'conservation_archived'
|
||||
|
||||
interface InboxPageProps {
|
||||
viewMode: InboxViewMode
|
||||
@@ -75,6 +78,10 @@ export function InboxPage({ viewMode }: InboxPageProps) {
|
||||
// true = stiamo navigando in una casella reale (non virtual box)
|
||||
const isMailboxMode = !vboxId
|
||||
|
||||
// ── Ruolo utente per visibilita' pulsante Conservazione ─────────────────────
|
||||
const { isAdmin, isSupervisor } = useAuth()
|
||||
const canConserve = isAdmin || isSupervisor
|
||||
|
||||
// ── Stato filtri locale ──────────────────────────────────────────────────────
|
||||
const [searchInput, setSearchInput] = useState('')
|
||||
const [debouncedSearch, setDebouncedSearch] = useState('')
|
||||
@@ -196,6 +203,16 @@ export function InboxPage({ viewMode }: InboxPageProps) {
|
||||
...advancedBase,
|
||||
is_trashed: true,
|
||||
}
|
||||
case 'conservation_pending':
|
||||
return {
|
||||
...advancedBase,
|
||||
is_pending_conservation: true,
|
||||
}
|
||||
case 'conservation_archived':
|
||||
return {
|
||||
...advancedBase,
|
||||
is_conserved: true,
|
||||
}
|
||||
}
|
||||
})()
|
||||
|
||||
@@ -310,6 +327,24 @@ export function InboxPage({ viewMode }: InboxPageProps) {
|
||||
onError: (error) => toast.error(getErrorMessage(error)),
|
||||
})
|
||||
|
||||
// ── Conservazione/Rimozione singolo ───────────────────────────────────────────
|
||||
const conserveMutation = useMutation({
|
||||
mutationFn: ({ id, conserve }: { id: string; conserve: boolean }) =>
|
||||
conserve ? messagesApi.conserve(id) : messagesApi.unconserve(id),
|
||||
onSuccess: (updatedMsg, { conserve }) => {
|
||||
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(conserve ? 'Messaggio inviato in Da Conservare' : 'Messaggio rimosso da Da Conservare')
|
||||
invalidateMessages()
|
||||
},
|
||||
onError: (error) => toast.error(getErrorMessage(error)),
|
||||
})
|
||||
|
||||
// ── Azioni bulk ──────────────────────────────────────────────────────────────
|
||||
const bulkMutation = useMutation({
|
||||
mutationFn: messagesApi.bulkUpdate,
|
||||
@@ -324,6 +359,8 @@ export function InboxPage({ viewMode }: InboxPageProps) {
|
||||
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'}`)
|
||||
else if (payload.is_pending_conservation === true) toast.success(`${n} ${n === 1 ? 'messaggio inviato' : 'messaggi inviati'} in Da Conservare`)
|
||||
else if (payload.is_pending_conservation === false) toast.success(`${n} ${n === 1 ? 'messaggio rimosso' : 'messaggi rimossi'} da Da Conservare`)
|
||||
},
|
||||
onError: (error) => toast.error(getErrorMessage(error)),
|
||||
})
|
||||
@@ -367,6 +404,10 @@ export function InboxPage({ viewMode }: InboxPageProps) {
|
||||
bulkMutation.mutate({ ids: Array.from(selectedIds), is_trashed: true })
|
||||
const handleBulkUntrash = () =>
|
||||
bulkMutation.mutate({ ids: Array.from(selectedIds), is_trashed: false })
|
||||
const handleBulkConserve = () =>
|
||||
bulkMutation.mutate({ ids: Array.from(selectedIds), is_pending_conservation: true })
|
||||
const handleBulkUnconserve = () =>
|
||||
bulkMutation.mutate({ ids: Array.from(selectedIds), is_pending_conservation: false })
|
||||
|
||||
// ── Selezione ────────────────────────────────────────────────────────────────
|
||||
const handleToggleSelect = (id: string, e: React.MouseEvent) => {
|
||||
@@ -409,25 +450,20 @@ export function InboxPage({ viewMode }: InboxPageProps) {
|
||||
// ── Label e icone folder ────────────────────────────────────────────────────
|
||||
const isInbound = viewMode === 'inbox'
|
||||
const folderLabel =
|
||||
viewMode === 'inbox'
|
||||
? 'Posta in Arrivo'
|
||||
: viewMode === 'sent'
|
||||
? 'Posta Inviata'
|
||||
: viewMode === 'starred'
|
||||
? 'Preferiti'
|
||||
: viewMode === 'archived'
|
||||
? 'Archiviati'
|
||||
: 'Cestino'
|
||||
viewMode === 'inbox' ? 'Posta in Arrivo'
|
||||
: viewMode === 'sent' ? 'Posta Inviata'
|
||||
: viewMode === 'starred' ? 'Preferiti'
|
||||
: viewMode === 'archived' ? 'Archiviati'
|
||||
: viewMode === 'trash' ? 'Cestino'
|
||||
: viewMode === 'conservation_pending' ? 'Da Conservare'
|
||||
: 'Storico Conservazione'
|
||||
const FolderIcon =
|
||||
viewMode === 'inbox'
|
||||
? Inbox
|
||||
: viewMode === 'sent'
|
||||
? Send
|
||||
: viewMode === 'starred'
|
||||
? Star
|
||||
: viewMode === 'archived'
|
||||
? Archive
|
||||
: Trash2
|
||||
viewMode === 'inbox' ? Inbox
|
||||
: viewMode === 'sent' ? Send
|
||||
: viewMode === 'starred' ? Star
|
||||
: viewMode === 'archived' ? Archive
|
||||
: viewMode === 'trash' ? Trash2
|
||||
: ShieldCheck
|
||||
|
||||
const selectedCount = selectedIds.size
|
||||
const allSelected = messages.length > 0 && selectedCount === messages.length
|
||||
@@ -760,6 +796,34 @@ export function InboxPage({ viewMode }: InboxPageProps) {
|
||||
Tag
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{/* Invia a Conservazione (solo admin/supervisor, non nelle viste conservazione) */}
|
||||
{canConserve && viewMode !== 'conservation_pending' && viewMode !== 'conservation_archived' && viewMode !== 'trash' && (
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="h-8 text-xs border-teal-200 hover:bg-teal-50 dark:border-teal-800 text-teal-700"
|
||||
onClick={handleBulkConserve}
|
||||
isLoading={bulkMutation.isPending}
|
||||
>
|
||||
<ShieldCheck className="h-3.5 w-3.5 mr-1" />
|
||||
Invia a Conservazione
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{/* Rimuovi da Da Conservare */}
|
||||
{canConserve && viewMode === 'conservation_pending' && (
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="h-8 text-xs border-orange-200 hover:bg-orange-50 dark:border-orange-800 text-orange-700"
|
||||
onClick={handleBulkUnconserve}
|
||||
isLoading={bulkMutation.isPending}
|
||||
>
|
||||
<ShieldX className="h-3.5 w-3.5 mr-1" />
|
||||
Rimuovi da Da Conservare
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -818,6 +882,7 @@ export function InboxPage({ viewMode }: InboxPageProps) {
|
||||
message={message}
|
||||
viewMode={viewMode}
|
||||
isMailboxMode={isMailboxMode}
|
||||
canConserve={canConserve}
|
||||
isSelected={selectedIds.has(message.id)}
|
||||
onSelect={(e) => handleToggleSelect(message.id, e)}
|
||||
onClick={() => handleMessageClick(message)}
|
||||
@@ -837,6 +902,10 @@ export function InboxPage({ viewMode }: InboxPageProps) {
|
||||
e.stopPropagation()
|
||||
trashMutation.mutate({ id: message.id, trashed: !message.is_trashed })
|
||||
}}
|
||||
onToggleConserve={(e) => {
|
||||
e.stopPropagation()
|
||||
conserveMutation.mutate({ id: message.id, conserve: !message.is_pending_conservation })
|
||||
}}
|
||||
mailboxName={
|
||||
!mailboxId
|
||||
? mailboxesData?.items.find((m) => m.id === message.mailbox_id)?.email_address
|
||||
@@ -897,12 +966,14 @@ interface MessageRowProps {
|
||||
viewMode: InboxViewMode
|
||||
isMailboxMode: boolean
|
||||
isSelected: boolean
|
||||
canConserve: boolean
|
||||
onSelect: (e: React.MouseEvent) => void
|
||||
onClick: () => void
|
||||
onToggleStar: (e: React.MouseEvent) => void
|
||||
onToggleArchive: (e: React.MouseEvent) => void
|
||||
onMarkUnread: (e: React.MouseEvent) => void
|
||||
onToggleTrash: (e: React.MouseEvent) => void
|
||||
onToggleConserve: (e: React.MouseEvent) => void
|
||||
mailboxName?: string
|
||||
}
|
||||
|
||||
@@ -911,12 +982,14 @@ function MessageRow({
|
||||
viewMode,
|
||||
isMailboxMode,
|
||||
isSelected,
|
||||
canConserve,
|
||||
onSelect,
|
||||
onClick,
|
||||
onToggleStar,
|
||||
onToggleArchive,
|
||||
onMarkUnread,
|
||||
onToggleTrash,
|
||||
onToggleConserve,
|
||||
mailboxName,
|
||||
}: MessageRowProps) {
|
||||
const [hovered, setHovered] = useState(false)
|
||||
@@ -1093,6 +1166,33 @@ function MessageRow({
|
||||
</button>
|
||||
)}
|
||||
|
||||
{/* Invia a Conservazione / Rimuovi da Da Conservare */}
|
||||
{canConserve && viewMode !== 'trash' && viewMode !== 'conservation_archived' && (
|
||||
<button
|
||||
onClick={onToggleConserve}
|
||||
title={viewMode === 'conservation_pending' ? 'Rimuovi da Da Conservare' : 'Invia a Conservazione'}
|
||||
className={cn(
|
||||
'p-1 rounded hover:bg-muted transition-all',
|
||||
message.is_pending_conservation
|
||||
? 'opacity-100'
|
||||
: hovered
|
||||
? 'opacity-100'
|
||||
: 'opacity-0 pointer-events-none',
|
||||
)}
|
||||
>
|
||||
{viewMode === 'conservation_pending' ? (
|
||||
<ShieldX className="h-4 w-4 text-orange-500" />
|
||||
) : (
|
||||
<ShieldCheck
|
||||
className={cn(
|
||||
'h-4 w-4',
|
||||
message.is_pending_conservation ? 'text-teal-600' : 'text-muted-foreground',
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
</button>
|
||||
)}
|
||||
|
||||
{/* Indicatore allegati */}
|
||||
{message.has_attachments && (
|
||||
<span className="text-xs text-muted-foreground"></span>
|
||||
|
||||
@@ -262,6 +262,10 @@ export interface MessageResponse {
|
||||
archived_at: string | null
|
||||
is_trashed: boolean
|
||||
trashed_at: string | null
|
||||
is_pending_conservation: boolean
|
||||
pending_conservation_at: string | null
|
||||
is_conserved: boolean
|
||||
conserved_at: string | null
|
||||
raw_eml_path: string | null
|
||||
created_at: string
|
||||
updated_at: string
|
||||
@@ -342,6 +346,7 @@ export interface PermissionGrantRequest {
|
||||
can_read?: boolean
|
||||
can_send?: boolean
|
||||
can_manage?: boolean
|
||||
can_conserve?: boolean
|
||||
}
|
||||
|
||||
export interface MailboxUserPermissionResponse {
|
||||
@@ -352,6 +357,7 @@ export interface MailboxUserPermissionResponse {
|
||||
can_read: boolean
|
||||
can_send: boolean
|
||||
can_manage: boolean
|
||||
can_conserve: boolean
|
||||
granted_at: string
|
||||
}
|
||||
|
||||
@@ -362,6 +368,7 @@ export interface UserMailboxPermissionResponse {
|
||||
can_read: boolean
|
||||
can_send: boolean
|
||||
can_manage: boolean
|
||||
can_conserve: boolean
|
||||
}
|
||||
|
||||
// ─── Virtual Box ──────────────────────────────────────────────────────────────
|
||||
|
||||
Reference in New Issue
Block a user