mirror of
https://github.com/idrainformatica/PecFlow.git
synced 2026-06-17 05:05:44 +02:00
fase 5
This commit is contained in:
@@ -0,0 +1,397 @@
|
||||
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>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user