mirror of
https://github.com/idrainformatica/PecFlow.git
synced 2026-06-16 12:45:42 +02:00
Implementazioni varie
This commit is contained in:
@@ -0,0 +1,282 @@
|
||||
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>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user