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,228 @@
|
||||
import { useState } from 'react'
|
||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
|
||||
import { Plus, Pencil, Trash2, FileText, Search } 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 { RichTextEditor } from '@/components/RichTextEditor/RichTextEditor'
|
||||
import { templatesApi, type TemplateResponse, type TemplateCreate } from '@/api/templates.api'
|
||||
import { getErrorMessage } from '@/api/client'
|
||||
import { formatDate } from '@/lib/utils'
|
||||
import { useAuth } from '@/hooks/useAuth'
|
||||
|
||||
export function TemplatesPage() {
|
||||
const queryClient = useQueryClient()
|
||||
const { isAdmin } = useAuth()
|
||||
const [q, setQ] = useState('')
|
||||
const [showForm, setShowForm] = useState(false)
|
||||
const [editing, setEditing] = useState<TemplateResponse | null>(null)
|
||||
|
||||
// Form state
|
||||
const [formName, setFormName] = useState('')
|
||||
const [formDescription, setFormDescription] = useState('')
|
||||
const [formSubject, setFormSubject] = useState('')
|
||||
const [formBody, setFormBody] = useState('')
|
||||
|
||||
const { data, isLoading } = useQuery({
|
||||
queryKey: ['templates', q],
|
||||
queryFn: () => templatesApi.list(q || undefined),
|
||||
})
|
||||
|
||||
const createMutation = useMutation({
|
||||
mutationFn: (data: TemplateCreate) => templatesApi.create(data),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['templates'] })
|
||||
toast.success('Template creato')
|
||||
closeForm()
|
||||
},
|
||||
onError: (e) => toast.error(getErrorMessage(e)),
|
||||
})
|
||||
|
||||
const updateMutation = useMutation({
|
||||
mutationFn: ({ id, data }: { id: string; data: TemplateCreate }) =>
|
||||
templatesApi.update(id, data),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['templates'] })
|
||||
toast.success('Template aggiornato')
|
||||
closeForm()
|
||||
},
|
||||
onError: (e) => toast.error(getErrorMessage(e)),
|
||||
})
|
||||
|
||||
const deleteMutation = useMutation({
|
||||
mutationFn: (id: string) => templatesApi.delete(id),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['templates'] })
|
||||
toast.success('Template eliminato')
|
||||
},
|
||||
onError: (e) => toast.error(getErrorMessage(e)),
|
||||
})
|
||||
|
||||
const openCreate = () => {
|
||||
setEditing(null)
|
||||
setFormName('')
|
||||
setFormDescription('')
|
||||
setFormSubject('')
|
||||
setFormBody('')
|
||||
setShowForm(true)
|
||||
}
|
||||
|
||||
const openEdit = (t: TemplateResponse) => {
|
||||
setEditing(t)
|
||||
setFormName(t.name)
|
||||
setFormDescription(t.description ?? '')
|
||||
setFormSubject(t.subject)
|
||||
setFormBody(t.body_html ?? t.body_text ?? '')
|
||||
setShowForm(true)
|
||||
}
|
||||
|
||||
const closeForm = () => {
|
||||
setShowForm(false)
|
||||
setEditing(null)
|
||||
}
|
||||
|
||||
const handleSubmit = () => {
|
||||
if (!formName.trim()) return toast.error('Il nome e\' obbligatorio')
|
||||
const payload: TemplateCreate = {
|
||||
name: formName.trim(),
|
||||
description: formDescription.trim() || null,
|
||||
subject: formSubject.trim(),
|
||||
body_html: formBody || null,
|
||||
body_text: null,
|
||||
}
|
||||
if (editing) {
|
||||
updateMutation.mutate({ id: editing.id, data: payload })
|
||||
} else {
|
||||
createMutation.mutate(payload)
|
||||
}
|
||||
}
|
||||
|
||||
const items = data?.items ?? []
|
||||
|
||||
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">Template messaggi</h1>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Template riutilizzabili per la composizione PEC
|
||||
</p>
|
||||
</div>
|
||||
{isAdmin && (
|
||||
<Button onClick={openCreate}>
|
||||
<Plus className="h-4 w-4 mr-2" />
|
||||
Nuovo template
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="p-6 space-y-4 flex-1 overflow-y-auto">
|
||||
{/* Ricerca */}
|
||||
<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 nome..."
|
||||
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">
|
||||
<FileText className="h-12 w-12 mx-auto mb-4 opacity-30" />
|
||||
<p className="text-lg font-medium">Nessun template trovato</p>
|
||||
<p className="text-sm mt-1">
|
||||
{isAdmin ? 'Crea il tuo primo template con il pulsante in alto.' : 'Nessun template disponibile.'}
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
|
||||
{items.map((t) => (
|
||||
<div key={t.id} className="rounded-lg border bg-card p-4 space-y-2 hover:shadow-sm transition-shadow">
|
||||
<div className="flex items-start justify-between gap-2">
|
||||
<div className="min-w-0">
|
||||
<h3 className="font-semibold truncate">{t.name}</h3>
|
||||
{t.description && (
|
||||
<p className="text-xs text-muted-foreground line-clamp-2">{t.description}</p>
|
||||
)}
|
||||
</div>
|
||||
{isAdmin && (
|
||||
<div className="flex gap-1 flex-shrink-0">
|
||||
<Button variant="ghost" size="icon" onClick={() => openEdit(t)} className="h-8 w-8">
|
||||
<Pencil className="h-3.5 w-3.5" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-8 w-8 text-destructive hover:text-destructive"
|
||||
onClick={() => {
|
||||
if (confirm(`Eliminare il template "${t.name}"?`)) {
|
||||
deleteMutation.mutate(t.id)
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Trash2 className="h-3.5 w-3.5" />
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{t.subject && (
|
||||
<p className="text-sm font-medium text-foreground/80 truncate">
|
||||
Oggetto: {t.subject}
|
||||
</p>
|
||||
)}
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Aggiornato: {formatDate(t.updated_at)}
|
||||
</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Dialog form */}
|
||||
<Dialog open={showForm} onOpenChange={(o) => !o && closeForm()}>
|
||||
<DialogContent className="max-w-2xl max-h-[90vh] overflow-y-auto">
|
||||
<DialogHeader>
|
||||
<DialogTitle>{editing ? 'Modifica template' : 'Nuovo template'}</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="space-y-4 mt-4">
|
||||
<div className="space-y-2">
|
||||
<Label>Nome *</Label>
|
||||
<Input value={formName} onChange={(e) => setFormName(e.target.value)} placeholder="Es. Risposta a ricorso" />
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label>Descrizione (opzionale)</Label>
|
||||
<Input value={formDescription} onChange={(e) => setFormDescription(e.target.value)} placeholder="Breve descrizione del template" />
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label>Oggetto predefinito</Label>
|
||||
<Input value={formSubject} onChange={(e) => setFormSubject(e.target.value)} placeholder="Oggetto del messaggio PEC" />
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label>Corpo del messaggio</Label>
|
||||
<div className="min-h-[200px] border rounded-md overflow-hidden">
|
||||
<RichTextEditor value={formBody} onChange={setFormBody} placeholder="Testo del template..." />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<DialogFooter className="mt-4">
|
||||
<Button variant="outline" onClick={closeForm}>Annulla</Button>
|
||||
<Button onClick={handleSubmit} isLoading={createMutation.isPending || updateMutation.isPending}>
|
||||
{editing ? 'Salva modifiche' : 'Crea template'}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user