Files
PecHub/frontend/src/pages/Contacts/ContactsPage.tsx
T
2026-03-27 20:59:06 +01:00

283 lines
11 KiB
TypeScript

import { useState, useRef } from 'react'
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
import { Plus, Search, Star, Trash2, Pencil, Upload, BookUser, Building2 } from 'lucide-react'
import toast from 'react-hot-toast'
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 { contactsApi, type PecContactResponse, type PecContactCreate, type PecContactUpdate } from '@/api/contacts.api'
import { getErrorMessage } from '@/api/client'
import { formatDate } from '@/lib/utils'
export function ContactsPage() {
const queryClient = useQueryClient()
const [q, setQ] = useState('')
const [page] = useState(1)
const [showForm, setShowForm] = useState(false)
const [editing, setEditing] = useState<PecContactResponse | null>(null)
const fileInputRef = useRef<HTMLInputElement>(null)
// Form state
const [formEmail, setFormEmail] = useState('')
const [formName, setFormName] = useState('')
const [formOrg, setFormOrg] = useState('')
const [formNotes, setFormNotes] = useState('')
const [formFavorite, setFormFavorite] = useState(false)
const { data, isLoading } = useQuery({
queryKey: ['contacts', q, page],
queryFn: () => contactsApi.list({ q: q || undefined, page, page_size: 50 }),
})
const createMutation = useMutation({
mutationFn: (d: PecContactCreate) => contactsApi.create(d),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['contacts'] })
toast.success('Contatto aggiunto')
closeForm()
},
onError: (e) => toast.error(getErrorMessage(e)),
})
const updateMutation = useMutation({
mutationFn: ({ id, data }: { id: string; data: PecContactUpdate }) =>
contactsApi.update(id, data),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['contacts'] })
toast.success('Contatto aggiornato')
closeForm()
},
onError: (e) => toast.error(getErrorMessage(e)),
})
const deleteMutation = useMutation({
mutationFn: (id: string) => contactsApi.delete(id),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['contacts'] })
toast.success('Contatto eliminato')
},
onError: (e) => toast.error(getErrorMessage(e)),
})
const importMutation = useMutation({
mutationFn: (file: File) => contactsApi.importCsv(file),
onSuccess: (res) => {
queryClient.invalidateQueries({ queryKey: ['contacts'] })
toast.success(`Importati: ${res.created} nuovi, ${res.updated} aggiornati, ${res.skipped} saltati`)
},
onError: (e) => toast.error(getErrorMessage(e)),
})
const toggleFavMutation = useMutation({
mutationFn: ({ id, fav }: { id: string; fav: boolean }) =>
contactsApi.update(id, { is_favorite: fav }),
onSuccess: () => queryClient.invalidateQueries({ queryKey: ['contacts'] }),
onError: (e) => toast.error(getErrorMessage(e)),
})
const openCreate = () => {
setEditing(null)
setFormEmail('')
setFormName('')
setFormOrg('')
setFormNotes('')
setFormFavorite(false)
setShowForm(true)
}
const openEdit = (c: PecContactResponse) => {
setEditing(c)
setFormEmail(c.email)
setFormName(c.name ?? '')
setFormOrg(c.organization ?? '')
setFormNotes(c.notes ?? '')
setFormFavorite(c.is_favorite)
setShowForm(true)
}
const closeForm = () => {
setShowForm(false)
setEditing(null)
}
const handleSubmit = () => {
if (!formEmail.trim()) return toast.error('L\'email e\' obbligatoria')
const payload = {
email: formEmail.trim(),
name: formName.trim() || null,
organization: formOrg.trim() || null,
notes: formNotes.trim() || null,
is_favorite: formFavorite,
}
if (editing) {
updateMutation.mutate({ id: editing.id, data: payload })
} else {
createMutation.mutate(payload)
}
}
const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0]
if (file) {
importMutation.mutate(file)
e.target.value = ''
}
}
const items = data?.items ?? []
const total = data?.total ?? 0
return (
<div className="flex flex-col h-full">
<div className="border-b bg-background px-6 py-4 flex items-center justify-between">
<div>
<h1 className="text-xl font-semibold">Rubrica PEC</h1>
<p className="text-sm text-muted-foreground">
{total} contatti nella rubrica
</p>
</div>
<div className="flex gap-2">
<Button variant="outline" onClick={() => fileInputRef.current?.click()} isLoading={importMutation.isPending}>
<Upload className="h-4 w-4 mr-2" />
Importa CSV
</Button>
<input ref={fileInputRef} type="file" accept=".csv" className="hidden" onChange={handleFileChange} />
<Button onClick={openCreate}>
<Plus className="h-4 w-4 mr-2" />
Nuovo contatto
</Button>
</div>
</div>
<div className="p-6 space-y-4 flex-1 overflow-y-auto">
<div className="relative w-full max-w-sm">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
<Input
placeholder="Cerca per email, nome o organizzazione..."
value={q}
onChange={(e) => setQ(e.target.value)}
className="pl-9"
/>
</div>
{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>
) : items.length === 0 ? (
<div className="text-center py-12 text-muted-foreground">
<BookUser className="h-12 w-12 mx-auto mb-4 opacity-30" />
<p className="text-lg font-medium">Nessun contatto trovato</p>
<p className="text-sm mt-1">
Aggiungi contatti manualmente o importa un CSV con colonne: email, name, organization
</p>
</div>
) : (
<div className="rounded-lg border overflow-hidden">
<table className="w-full text-sm">
<thead className="bg-muted/50">
<tr>
<th className="text-left px-4 py-3 font-medium text-muted-foreground">Email PEC</th>
<th className="text-left px-4 py-3 font-medium text-muted-foreground">Nome</th>
<th className="text-left px-4 py-3 font-medium text-muted-foreground">Organizzazione</th>
<th className="text-left px-4 py-3 font-medium text-muted-foreground">Tipo</th>
<th className="text-left px-4 py-3 font-medium text-muted-foreground">Aggiornato</th>
<th className="px-4 py-3" />
</tr>
</thead>
<tbody className="divide-y">
{items.map((c) => (
<tr key={c.id} className="hover:bg-muted/30 transition-colors">
<td className="px-4 py-3 font-mono text-xs">{c.email}</td>
<td className="px-4 py-3">{c.name ?? '-'}</td>
<td className="px-4 py-3 text-muted-foreground">
{c.organization ? (
<span className="flex items-center gap-1">
<Building2 className="h-3.5 w-3.5" />
{c.organization}
</span>
) : '-'}
</td>
<td className="px-4 py-3">
<span className={`text-xs px-2 py-0.5 rounded-full ${c.auto_saved ? 'bg-blue-100 text-blue-700' : 'bg-green-100 text-green-700'}`}>
{c.auto_saved ? 'Automatico' : 'Manuale'}
</span>
</td>
<td className="px-4 py-3 text-muted-foreground">{formatDate(c.updated_at)}</td>
<td className="px-4 py-3">
<div className="flex items-center gap-1 justify-end">
<button
onClick={() => toggleFavMutation.mutate({ id: c.id, fav: !c.is_favorite })}
title={c.is_favorite ? 'Rimuovi dai preferiti' : 'Aggiungi ai preferiti'}
className="p-1 rounded hover:bg-muted transition-colors"
>
<Star className={`h-4 w-4 ${c.is_favorite ? 'fill-yellow-400 text-yellow-400' : 'text-muted-foreground'}`} />
</button>
<button
onClick={() => openEdit(c)}
className="p-1 rounded hover:bg-muted transition-colors"
title="Modifica"
>
<Pencil className="h-4 w-4 text-muted-foreground" />
</button>
<button
onClick={() => {
if (confirm(`Eliminare il contatto ${c.email}?`)) deleteMutation.mutate(c.id)
}}
className="p-1 rounded hover:bg-muted transition-colors"
title="Elimina"
>
<Trash2 className="h-4 w-4 text-destructive" />
</button>
</div>
</td>
</tr>
))}
</tbody>
</table>
</div>
)}
</div>
<Dialog open={showForm} onOpenChange={(o) => !o && closeForm()}>
<DialogContent className="max-w-lg">
<DialogHeader>
<DialogTitle>{editing ? 'Modifica contatto' : 'Nuovo contatto'}</DialogTitle>
</DialogHeader>
<div className="space-y-4 mt-4">
<div className="space-y-2">
<Label>Indirizzo PEC *</Label>
<Input value={formEmail} onChange={(e) => setFormEmail(e.target.value)} placeholder="indirizzo@pec.it" disabled={!!editing} type="email" />
</div>
<div className="space-y-2">
<Label>Nome</Label>
<Input value={formName} onChange={(e) => setFormName(e.target.value)} placeholder="Mario Rossi" />
</div>
<div className="space-y-2">
<Label>Organizzazione</Label>
<Input value={formOrg} onChange={(e) => setFormOrg(e.target.value)} placeholder="Comune di Roma" />
</div>
<div className="space-y-2">
<Label>Note</Label>
<Input value={formNotes} onChange={(e) => setFormNotes(e.target.value)} placeholder="Note aggiuntive..." />
</div>
<label className="flex items-center gap-2 cursor-pointer">
<input type="checkbox" checked={formFavorite} onChange={(e) => setFormFavorite(e.target.checked)} className="rounded" />
<span className="text-sm">Aggiungi ai preferiti</span>
</label>
</div>
<DialogFooter className="mt-4">
<Button variant="outline" onClick={closeForm}>Annulla</Button>
<Button onClick={handleSubmit} isLoading={createMutation.isPending || updateMutation.isPending}>
{editing ? 'Salva' : 'Aggiungi'}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</div>
)
}