mirror of
https://github.com/idrainformatica/PecFlow.git
synced 2026-06-16 20:55:41 +02:00
398 lines
15 KiB
TypeScript
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>
|
|
)
|
|
}
|