Files
PecHub/frontend/src/pages/Mailboxes/MailboxesPage.tsx
T
2026-03-18 20:54:43 +01:00

398 lines
15 KiB
TypeScript

import { useState } from 'react'
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
import {
Plus,
MailCheck,
Trash2,
Edit,
TestTube,
AlertCircle,
CheckCircle,
Clock,
RefreshCw,
} from 'lucide-react'
import toast from 'react-hot-toast'
import { useForm } from 'react-hook-form'
import { Button } from '@/components/ui/Button'
import { Input } from '@/components/ui/Input'
import { Label } from '@/components/ui/Label'
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
DialogFooter,
} from '@/components/ui/Dialog'
import { mailboxesApi } from '@/api/mailboxes.api'
import { getErrorMessage } from '@/api/client'
import { formatDate, MAILBOX_STATUS_LABELS } from '@/lib/utils'
import { cn } from '@/lib/utils'
import type { MailboxCreateRequest, MailboxResponse } from '@/types/api.types'
const STATUS_COLORS = {
active: 'text-green-700 bg-green-100',
paused: 'text-yellow-700 bg-yellow-100',
error: 'text-red-700 bg-red-100',
deleted: 'text-gray-700 bg-gray-100',
}
const STATUS_ICONS = {
active: CheckCircle,
paused: Clock,
error: AlertCircle,
deleted: Trash2,
}
export function MailboxesPage() {
const queryClient = useQueryClient()
const [showCreateDialog, setShowCreateDialog] = useState(false)
const [editingMailbox, setEditingMailbox] = useState<MailboxResponse | null>(null)
const [testingId, setTestingId] = useState<string | null>(null)
const [testResult, setTestResult] = useState<{ success: boolean; message: string } | null>(null)
const { data: mailboxesData, isLoading } = useQuery({
queryKey: ['mailboxes'],
queryFn: () => mailboxesApi.list(1, 200),
})
const deleteMutation = useMutation({
mutationFn: mailboxesApi.delete,
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['mailboxes'] })
toast.success('Casella eliminata')
},
onError: (e) => toast.error(getErrorMessage(e)),
})
const handleTest = async (mailbox: MailboxResponse) => {
setTestingId(mailbox.id)
setTestResult(null)
try {
const result = await mailboxesApi.testConnection(mailbox.id, 'imap')
setTestResult(result)
toast[result.success ? 'success' : 'error'](result.message)
} catch (e) {
toast.error(getErrorMessage(e))
} finally {
setTestingId(null)
}
}
const handleDelete = async (mailbox: MailboxResponse) => {
if (!confirm(`Eliminare la casella ${mailbox.email_address}? L'operazione è irreversibile.`)) return
deleteMutation.mutate(mailbox.id)
}
const mailboxes = mailboxesData?.items || []
return (
<div className="flex flex-col h-full">
{/* Header */}
<div className="border-b bg-background px-6 py-4 flex items-center justify-between">
<div className="flex items-center gap-2">
<MailCheck className="h-5 w-5 text-primary" />
<h1 className="text-xl font-semibold">Caselle PEC</h1>
<span className="text-sm text-muted-foreground">({mailboxes.length})</span>
</div>
<Button onClick={() => setShowCreateDialog(true)}>
<Plus className="h-4 w-4 mr-2" />
Aggiungi casella
</Button>
</div>
{/* Contenuto */}
<div className="flex-1 overflow-y-auto p-6">
{isLoading ? (
<div className="flex justify-center py-12">
<div className="h-8 w-8 animate-spin rounded-full border-4 border-primary border-t-transparent" />
</div>
) : mailboxes.length === 0 ? (
<div className="flex flex-col items-center justify-center py-12 text-center">
<MailCheck className="h-12 w-12 text-muted-foreground/30 mb-3" />
<p className="text-muted-foreground font-medium">Nessuna casella PEC configurata</p>
<p className="text-sm text-muted-foreground/70 mt-1">
Aggiungi una casella per iniziare a gestire le PEC
</p>
<Button className="mt-4" onClick={() => setShowCreateDialog(true)}>
<Plus className="h-4 w-4 mr-2" />
Aggiungi casella
</Button>
</div>
) : (
<div className="grid gap-4">
{mailboxes.map((mailbox) => {
const StatusIcon = STATUS_ICONS[mailbox.status] || AlertCircle
return (
<div
key={mailbox.id}
className="rounded-lg border bg-card p-5 flex items-start justify-between gap-4"
>
<div className="flex items-start gap-4 min-w-0">
<div className="h-10 w-10 rounded-lg bg-primary/10 flex items-center justify-center flex-shrink-0">
<MailCheck className="h-5 w-5 text-primary" />
</div>
<div className="min-w-0">
<div className="flex items-center gap-2 flex-wrap">
<h3 className="font-semibold text-foreground">
{mailbox.display_name || mailbox.email_address}
</h3>
<span
className={cn(
'inline-flex items-center gap-1 rounded-full px-2 py-0.5 text-xs font-medium',
STATUS_COLORS[mailbox.status],
)}
>
<StatusIcon className="h-3 w-3" />
{MAILBOX_STATUS_LABELS[mailbox.status]}
</span>
{mailbox.provider && (
<span className="text-xs text-muted-foreground bg-muted px-2 py-0.5 rounded">
{mailbox.provider}
</span>
)}
</div>
<p className="text-sm text-muted-foreground mt-0.5">{mailbox.email_address}</p>
<div className="flex gap-4 mt-2 text-xs text-muted-foreground">
<span>IMAP: {mailbox.imap_host}:{mailbox.imap_port}</span>
<span>SMTP: {mailbox.smtp_host}:{mailbox.smtp_port}</span>
{mailbox.last_sync_at && (
<span>Ultima sync: {formatDate(mailbox.last_sync_at)}</span>
)}
</div>
{mailbox.status === 'error' && mailbox.sync_error_msg && (
<p className="text-xs text-red-600 mt-1 flex items-center gap-1">
<AlertCircle className="h-3 w-3" />
{mailbox.sync_error_msg}
</p>
)}
</div>
</div>
<div className="flex items-center gap-2 flex-shrink-0">
{/* Test connessione */}
<Button
variant="outline"
size="sm"
onClick={() => handleTest(mailbox)}
isLoading={testingId === mailbox.id}
title="Testa connessione IMAP"
>
<TestTube className="h-4 w-4 mr-1" />
Test
</Button>
{/* Modifica */}
<Button
variant="outline"
size="sm"
onClick={() => setEditingMailbox(mailbox)}
>
<Edit className="h-4 w-4" />
</Button>
{/* Elimina */}
<Button
variant="outline"
size="sm"
onClick={() => handleDelete(mailbox)}
className="text-destructive hover:bg-destructive hover:text-destructive-foreground"
>
<Trash2 className="h-4 w-4" />
</Button>
</div>
</div>
)
})}
</div>
)}
</div>
{/* Dialog crea/modifica casella */}
<MailboxFormDialog
open={showCreateDialog || editingMailbox !== null}
onClose={() => {
setShowCreateDialog(false)
setEditingMailbox(null)
}}
editingMailbox={editingMailbox}
onSaved={() => {
queryClient.invalidateQueries({ queryKey: ['mailboxes'] })
setShowCreateDialog(false)
setEditingMailbox(null)
}}
/>
</div>
)
}
// ─── Dialog creazione/modifica casella ───────────────────────────────────────
interface MailboxFormDialogProps {
open: boolean
onClose: () => void
editingMailbox: MailboxResponse | null
onSaved: () => void
}
function MailboxFormDialog({ open, onClose, editingMailbox, onSaved }: MailboxFormDialogProps) {
const {
register,
handleSubmit,
reset,
formState: { errors },
} = useForm<MailboxCreateRequest>({
defaultValues: editingMailbox
? {
email_address: editingMailbox.email_address,
display_name: editingMailbox.display_name || '',
provider: editingMailbox.provider || '',
imap_host: editingMailbox.imap_host,
imap_port: editingMailbox.imap_port,
imap_user: editingMailbox.email_address,
imap_use_ssl: editingMailbox.imap_use_ssl,
smtp_host: editingMailbox.smtp_host,
smtp_port: editingMailbox.smtp_port,
smtp_user: editingMailbox.email_address,
smtp_use_tls: editingMailbox.smtp_use_tls,
}
: {
imap_port: 993,
smtp_port: 465,
imap_use_ssl: true,
smtp_use_tls: true,
},
})
const createMutation = useMutation({
mutationFn: mailboxesApi.create,
onSuccess: () => {
toast.success('Casella creata con successo')
onSaved()
reset()
},
onError: (e) => toast.error(getErrorMessage(e)),
})
const updateMutation = useMutation({
mutationFn: ({ id, data }: { id: string; data: MailboxCreateRequest }) =>
mailboxesApi.update(id, data),
onSuccess: () => {
toast.success('Casella aggiornata')
onSaved()
},
onError: (e) => toast.error(getErrorMessage(e)),
})
const onSubmit = (data: MailboxCreateRequest) => {
if (editingMailbox) {
updateMutation.mutate({ id: editingMailbox.id, data })
} else {
createMutation.mutate(data)
}
}
const isLoading = createMutation.isPending || updateMutation.isPending
return (
<Dialog open={open} onOpenChange={(o) => !o && onClose()}>
<DialogContent className="max-w-2xl max-h-[90vh] overflow-y-auto">
<DialogHeader>
<DialogTitle>
{editingMailbox ? 'Modifica casella PEC' : 'Aggiungi casella PEC'}
</DialogTitle>
</DialogHeader>
<form onSubmit={handleSubmit(onSubmit)} className="space-y-4 mt-4">
{/* Info generali */}
<div className="grid grid-cols-2 gap-4">
<div className="col-span-2 space-y-2">
<Label>Indirizzo PEC *</Label>
<Input
type="email"
placeholder="casella@pec.it"
disabled={!!editingMailbox}
{...register('email_address', { required: 'Obbligatorio' })}
/>
</div>
<div className="space-y-2">
<Label>Nome visualizzato</Label>
<Input placeholder="Es: Casella principale" {...register('display_name')} />
</div>
<div className="space-y-2">
<Label>Provider</Label>
<Input placeholder="aruba, namirial..." {...register('provider')} />
</div>
</div>
{/* Separatore IMAP */}
<div className="space-y-3 rounded-lg border p-4">
<h4 className="text-sm font-semibold">Configurazione IMAP (ricezione)</h4>
<div className="grid grid-cols-2 gap-3">
<div className="space-y-1.5">
<Label className="text-xs">Server IMAP *</Label>
<Input placeholder="imap.pec.it" {...register('imap_host', { required: true })} />
</div>
<div className="space-y-1.5">
<Label className="text-xs">Porta</Label>
<Input type="number" {...register('imap_port', { valueAsNumber: true })} />
</div>
<div className="space-y-1.5">
<Label className="text-xs">Utente IMAP *</Label>
<Input placeholder="casella@pec.it" {...register('imap_user', { required: true })} />
</div>
<div className="space-y-1.5">
<Label className="text-xs">Password IMAP *</Label>
<Input
type="password"
placeholder={editingMailbox ? '(invariata)' : ''}
{...register('imap_pass', {
required: !editingMailbox ? 'Obbligatoria' : false,
})}
/>
</div>
</div>
</div>
{/* Separatore SMTP */}
<div className="space-y-3 rounded-lg border p-4">
<h4 className="text-sm font-semibold">Configurazione SMTP (invio)</h4>
<div className="grid grid-cols-2 gap-3">
<div className="space-y-1.5">
<Label className="text-xs">Server SMTP *</Label>
<Input placeholder="smtp.pec.it" {...register('smtp_host', { required: true })} />
</div>
<div className="space-y-1.5">
<Label className="text-xs">Porta</Label>
<Input type="number" {...register('smtp_port', { valueAsNumber: true })} />
</div>
<div className="space-y-1.5">
<Label className="text-xs">Utente SMTP *</Label>
<Input placeholder="casella@pec.it" {...register('smtp_user', { required: true })} />
</div>
<div className="space-y-1.5">
<Label className="text-xs">Password SMTP *</Label>
<Input
type="password"
placeholder={editingMailbox ? '(invariata)' : ''}
{...register('smtp_pass', {
required: !editingMailbox ? 'Obbligatoria' : false,
})}
/>
</div>
</div>
</div>
<DialogFooter>
<Button type="button" variant="outline" onClick={onClose}>
Annulla
</Button>
<Button type="submit" isLoading={isLoading}>
{editingMailbox ? 'Salva modifiche' : 'Crea casella'}
</Button>
</DialogFooter>
</form>
</DialogContent>
</Dialog>
)
}