Scadenzario aggiunta pratiche

This commit is contained in:
2026-06-17 22:09:18 +02:00
parent 3fd3c72f06
commit 64442af182
7 changed files with 722 additions and 157 deletions
+22
View File
@@ -61,6 +61,22 @@ export interface MessageFascicoloSummary {
categoria: string | null
}
export interface FascicoloDeadlineResponse {
id: string
tenant_id: string
titolo: string
numero_pratica: string | null
stato: 'aperto' | 'chiuso' | 'archiviato'
categoria: string | null
responsabile_id: string | null
scadenza: string | null
note: string | null
created_at: string
updated_at: string
message_count: number
is_overdue: boolean
}
// ─── Client API ───────────────────────────────────────────────────────────────
export const fascicoliApi = {
@@ -117,4 +133,10 @@ export const fascicoliApi = {
apiClient
.get<MessageFascicoloSummary[]>(`/messages/${messageId}/fascicoli`)
.then((r) => r.data),
/** Scadenzario pratiche: fascicoli con scadenza imminente o scaduta */
scadenzario: (params?: { days_ahead?: number; include_overdue?: boolean }) =>
apiClient
.get<FascicoloDeadlineResponse[]>('/fascicoli/scadenzario', { params })
.then((r) => r.data),
}
+373 -83
View File
@@ -1,28 +1,41 @@
import { useState } from 'react'
import { useQuery } from '@tanstack/react-query'
import { useNavigate } from 'react-router-dom'
import { Calendar, AlertTriangle, Clock, CheckCircle2, ExternalLink } from 'lucide-react'
import { Button } from '@/components/ui/Button'
import {
Calendar,
AlertTriangle,
Clock,
CheckCircle2,
ExternalLink,
FolderOpen,
FolderCheck,
FolderArchive,
MessageSquare,
} from 'lucide-react'
import { deadlinesApi, type DeadlineMessageResponse } from '@/api/deadlines.api'
import { fascicoliApi, type FascicoloDeadlineResponse } from '@/api/fascicoli.api'
import { formatDate } from '@/lib/utils'
function groupDeadlines(items: DeadlineMessageResponse[]) {
// ─── Utilita' ─────────────────────────────────────────────────────────────────
function groupByScadenza<T extends { scadenza?: string | null; is_overdue?: boolean }>(
items: T[]
) {
const now = new Date()
const todayEnd = new Date(now)
todayEnd.setHours(23, 59, 59, 999)
const weekEnd = new Date(now)
weekEnd.setDate(weekEnd.getDate() + 7)
const monthEnd = new Date(now)
monthEnd.setDate(monthEnd.getDate() + 30)
const overdue: DeadlineMessageResponse[] = []
const today: DeadlineMessageResponse[] = []
const thisWeek: DeadlineMessageResponse[] = []
const later: DeadlineMessageResponse[] = []
const overdue: T[] = []
const today: T[] = []
const thisWeek: T[] = []
const later: T[] = []
for (const item of items) {
if (!item.deadline_at) continue
const d = new Date(item.deadline_at)
const raw = (item as any).scadenza ?? (item as any).deadline_at
if (!raw) continue
const d = new Date(raw)
if (d < now) {
overdue.push(item)
} else if (d <= todayEnd) {
@@ -36,13 +49,67 @@ function groupDeadlines(items: DeadlineMessageResponse[]) {
return { overdue, today, thisWeek, later }
}
function DeadlineItem({ item }: { item: DeadlineMessageResponse }) {
// ─── Gruppo scadenze ─────────────────────────────────────────────────────────
function DeadlineGroup<T>({
title,
items,
icon: Icon,
color,
renderItem,
}: {
title: string
items: T[]
icon: React.ComponentType<{ className?: string }>
color: string
renderItem: (item: T) => React.ReactNode
}) {
if (items.length === 0) return null
return (
<div className="space-y-2">
<div className={`flex items-center gap-2 ${color}`}>
<Icon className="h-4 w-4" />
<h3 className="font-semibold text-sm">
{title} ({items.length})
</h3>
</div>
<div className="space-y-2">{items.map((item, i) => <div key={i}>{renderItem(item)}</div>)}</div>
</div>
)
}
// ─── Tab: Messaggi ────────────────────────────────────────────────────────────
function groupMessages(items: DeadlineMessageResponse[]) {
const now = new Date()
const todayEnd = new Date(now); todayEnd.setHours(23, 59, 59, 999)
const weekEnd = new Date(now); weekEnd.setDate(weekEnd.getDate() + 7)
const overdue: DeadlineMessageResponse[] = []
const today: DeadlineMessageResponse[] = []
const thisWeek: DeadlineMessageResponse[] = []
const later: DeadlineMessageResponse[] = []
for (const item of items) {
if (!item.deadline_at) continue
const d = new Date(item.deadline_at)
if (d < now) overdue.push(item)
else if (d <= todayEnd) today.push(item)
else if (d <= weekEnd) thisWeek.push(item)
else later.push(item)
}
return { overdue, today, thisWeek, later }
}
function MessageDeadlineItem({ item }: { item: DeadlineMessageResponse }) {
const navigate = useNavigate()
const deadlineDate = item.deadline_at ? new Date(item.deadline_at) : null
return (
<div
className={`flex items-center gap-4 p-4 rounded-lg border bg-card hover:shadow-sm transition-shadow cursor-pointer ${item.is_overdue ? 'border-destructive/30 bg-destructive/5' : ''}`}
className={`flex items-center gap-4 p-4 rounded-lg border bg-card hover:shadow-sm transition-shadow cursor-pointer ${
item.is_overdue ? 'border-destructive/30 bg-destructive/5' : ''
}`}
onClick={() => navigate(`/messages/${item.id}`)}
>
<div className="flex-shrink-0">
@@ -55,7 +122,9 @@ function DeadlineItem({ item }: { item: DeadlineMessageResponse }) {
<div className="flex-1 min-w-0">
<p className="font-medium truncate">{item.subject || '(nessun oggetto)'}</p>
<p className="text-xs text-muted-foreground">
{item.direction === 'inbound' ? `Da: ${item.from_address}` : `A: ${(item.to_addresses ?? []).join(', ')}`}
{item.direction === 'inbound'
? `Da: ${item.from_address}`
: `A: ${(item.to_addresses ?? []).join(', ')}`}
</p>
{item.deadline_note && (
<p className="text-xs text-muted-foreground italic mt-0.5">{item.deadline_note}</p>
@@ -65,51 +134,254 @@ function DeadlineItem({ item }: { item: DeadlineMessageResponse }) {
<p className={`text-sm font-semibold ${item.is_overdue ? 'text-destructive' : 'text-amber-600'}`}>
{deadlineDate ? formatDate(deadlineDate.toISOString()) : '-'}
</p>
{item.is_overdue && (
<p className="text-xs text-destructive">Scaduto</p>
)}
{item.is_overdue && <p className="text-xs text-destructive">Scaduto</p>}
</div>
<ExternalLink className="h-4 w-4 text-muted-foreground flex-shrink-0" />
</div>
)
}
function DeadlineGroup({ title, items, icon: Icon, color }: {
title: string
items: DeadlineMessageResponse[]
icon: React.ComponentType<{ className?: string }>
color: string
function MessaggiTab({
daysAhead,
includeOverdue,
}: {
daysAhead: number
includeOverdue: boolean
}) {
if (items.length === 0) return null
return (
<div className="space-y-2">
<div className={`flex items-center gap-2 ${color}`}>
<Icon className="h-4 w-4" />
<h3 className="font-semibold text-sm">{title} ({items.length})</h3>
</div>
<div className="space-y-2">
{items.map((item) => (
<DeadlineItem key={item.id} item={item} />
))}
</div>
</div>
)
}
export function DeadlinesPage() {
const [daysAhead, setDaysAhead] = useState(30)
const [includeOverdue, setIncludeOverdue] = useState(true)
const { data = [], isLoading } = useQuery({
queryKey: ['deadlines', daysAhead, includeOverdue],
queryFn: () => deadlinesApi.list({ days_ahead: daysAhead, include_overdue: includeOverdue }),
})
const groups = groupDeadlines(data)
const total = data.length
const groups = groupMessages(data)
if (isLoading) {
return (
<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>
)
}
if (data.length === 0) {
return (
<div className="text-center py-12 text-muted-foreground">
<CheckCircle2 className="h-12 w-12 mx-auto mb-4 opacity-30 text-green-500" />
<p className="text-lg font-medium">Nessuna scadenza sui messaggi</p>
<p className="text-sm mt-1">
Le scadenze si impostano dal dettaglio di ogni messaggio.
</p>
</div>
)
}
return (
<div className="max-w-3xl mx-auto space-y-8">
<DeadlineGroup
title="Scaduti"
items={groups.overdue}
icon={AlertTriangle}
color="text-destructive"
renderItem={(item) => <MessageDeadlineItem item={item} />}
/>
<DeadlineGroup
title="Oggi"
items={groups.today}
icon={Clock}
color="text-amber-600"
renderItem={(item) => <MessageDeadlineItem item={item} />}
/>
<DeadlineGroup
title="Questa settimana"
items={groups.thisWeek}
icon={Calendar}
color="text-blue-600"
renderItem={(item) => <MessageDeadlineItem item={item} />}
/>
<DeadlineGroup
title="Successivamente"
items={groups.later}
icon={Calendar}
color="text-muted-foreground"
renderItem={(item) => <MessageDeadlineItem item={item} />}
/>
</div>
)
}
// ─── Tab: Fascicoli ───────────────────────────────────────────────────────────
function FascicoloDeadlineItem({ item }: { item: FascicoloDeadlineResponse }) {
const navigate = useNavigate()
const scadenzaDate = item.scadenza ? new Date(item.scadenza) : null
const FolderIcon =
item.stato === 'aperto'
? FolderOpen
: item.stato === 'chiuso'
? FolderCheck
: FolderArchive
return (
<div
className={`flex items-center gap-4 p-4 rounded-lg border bg-card hover:shadow-sm transition-shadow cursor-pointer ${
item.is_overdue ? 'border-destructive/30 bg-destructive/5' : ''
}`}
onClick={() => navigate(`/fascicoli/${item.id}`)}
>
<div className="flex-shrink-0">
{item.is_overdue ? (
<AlertTriangle className="h-5 w-5 text-destructive" />
) : (
<FolderIcon className="h-5 w-5 text-amber-500" />
)}
</div>
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2 flex-wrap">
<p className="font-medium truncate">{item.titolo}</p>
{item.numero_pratica && (
<span className="text-xs text-muted-foreground font-mono bg-muted px-1.5 py-0.5 rounded">
#{item.numero_pratica}
</span>
)}
{item.categoria && (
<span className="text-xs text-muted-foreground bg-blue-50 border border-blue-100 px-1.5 py-0.5 rounded">
{item.categoria}
</span>
)}
</div>
<p className="text-xs text-muted-foreground mt-0.5 flex items-center gap-1">
<MessageSquare className="h-3 w-3" />
{item.message_count} {item.message_count === 1 ? 'messaggio' : 'messaggi'}
{item.note && <span className="italic ml-2 truncate">{item.note}</span>}
</p>
</div>
<div className="flex-shrink-0 text-right">
<p className={`text-sm font-semibold ${item.is_overdue ? 'text-destructive' : 'text-amber-600'}`}>
{scadenzaDate ? formatDate(scadenzaDate.toISOString()) : '-'}
</p>
{item.is_overdue && <p className="text-xs text-destructive">Scaduto</p>}
</div>
<ExternalLink className="h-4 w-4 text-muted-foreground flex-shrink-0" />
</div>
)
}
function groupFascicoli(items: FascicoloDeadlineResponse[]) {
const now = new Date()
const todayEnd = new Date(now); todayEnd.setHours(23, 59, 59, 999)
const weekEnd = new Date(now); weekEnd.setDate(weekEnd.getDate() + 7)
const overdue: FascicoloDeadlineResponse[] = []
const today: FascicoloDeadlineResponse[] = []
const thisWeek: FascicoloDeadlineResponse[] = []
const later: FascicoloDeadlineResponse[] = []
for (const item of items) {
if (!item.scadenza) continue
const d = new Date(item.scadenza)
if (d < now) overdue.push(item)
else if (d <= todayEnd) today.push(item)
else if (d <= weekEnd) thisWeek.push(item)
else later.push(item)
}
return { overdue, today, thisWeek, later }
}
function FascicoliTab({
daysAhead,
includeOverdue,
}: {
daysAhead: number
includeOverdue: boolean
}) {
const { data = [], isLoading } = useQuery({
queryKey: ['fascicoli-scadenzario', daysAhead, includeOverdue],
queryFn: () => fascicoliApi.scadenzario({ days_ahead: daysAhead, include_overdue: includeOverdue }),
})
const groups = groupFascicoli(data)
if (isLoading) {
return (
<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>
)
}
if (data.length === 0) {
return (
<div className="text-center py-12 text-muted-foreground">
<CheckCircle2 className="h-12 w-12 mx-auto mb-4 opacity-30 text-green-500" />
<p className="text-lg font-medium">Nessuna scadenza sui fascicoli</p>
<p className="text-sm mt-1">
Le scadenze si impostano creando o modificando un fascicolo.
</p>
</div>
)
}
return (
<div className="max-w-3xl mx-auto space-y-8">
<DeadlineGroup
title="Scaduti"
items={groups.overdue}
icon={AlertTriangle}
color="text-destructive"
renderItem={(item) => <FascicoloDeadlineItem item={item} />}
/>
<DeadlineGroup
title="Oggi"
items={groups.today}
icon={Clock}
color="text-amber-600"
renderItem={(item) => <FascicoloDeadlineItem item={item} />}
/>
<DeadlineGroup
title="Questa settimana"
items={groups.thisWeek}
icon={Calendar}
color="text-blue-600"
renderItem={(item) => <FascicoloDeadlineItem item={item} />}
/>
<DeadlineGroup
title="Successivamente"
items={groups.later}
icon={Calendar}
color="text-muted-foreground"
renderItem={(item) => <FascicoloDeadlineItem item={item} />}
/>
</div>
)
}
// ─── Pagina principale ────────────────────────────────────────────────────────
type Tab = 'messaggi' | 'fascicoli'
export function DeadlinesPage() {
const [activeTab, setActiveTab] = useState<Tab>('messaggi')
const [daysAhead, setDaysAhead] = useState(30)
const [includeOverdue, setIncludeOverdue] = useState(true)
// Conteggi per i badge sulle tab
const { data: msgData = [] } = useQuery({
queryKey: ['deadlines', daysAhead, includeOverdue],
queryFn: () => deadlinesApi.list({ days_ahead: daysAhead, include_overdue: includeOverdue }),
})
const { data: fascData = [] } = useQuery({
queryKey: ['fascicoli-scadenzario', daysAhead, includeOverdue],
queryFn: () => fascicoliApi.scadenzario({ days_ahead: daysAhead, include_overdue: includeOverdue }),
})
const msgOverdue = msgData.filter((m: { is_overdue: boolean }) => m.is_overdue).length
const fascOverdue = fascData.filter((f: { is_overdue: boolean }) => f.is_overdue).length
const totalOverdue = msgOverdue + fascOverdue
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>
<h1 className="text-xl font-semibold flex items-center gap-2">
@@ -117,7 +389,13 @@ export function DeadlinesPage() {
Scadenzario
</h1>
<p className="text-sm text-muted-foreground">
{total} messaggi con scadenze
{totalOverdue > 0 ? (
<span className="text-destructive font-medium">
{totalOverdue} {totalOverdue === 1 ? 'elemento scaduto' : 'elementi scaduti'}
</span>
) : (
`${msgData.length + fascData.length} scadenze totali`
)}
</p>
</div>
<div className="flex items-center gap-4">
@@ -143,46 +421,58 @@ export function DeadlinesPage() {
</div>
</div>
{/* Tab bar */}
<div className="border-b bg-background px-6">
<div className="flex gap-0">
<button
onClick={() => setActiveTab('messaggi')}
className={`flex items-center gap-2 px-4 py-3 text-sm font-medium border-b-2 transition-colors ${
activeTab === 'messaggi'
? 'border-primary text-primary'
: 'border-transparent text-muted-foreground hover:text-foreground'
}`}
>
<Clock className="h-4 w-4" />
Messaggi
{msgData.length > 0 && (
<span className={`text-xs px-1.5 py-0.5 rounded-full font-medium ${
msgOverdue > 0
? 'bg-destructive/10 text-destructive'
: 'bg-muted text-muted-foreground'
}`}>
{msgData.length}
</span>
)}
</button>
<button
onClick={() => setActiveTab('fascicoli')}
className={`flex items-center gap-2 px-4 py-3 text-sm font-medium border-b-2 transition-colors ${
activeTab === 'fascicoli'
? 'border-primary text-primary'
: 'border-transparent text-muted-foreground hover:text-foreground'
}`}
>
<FolderOpen className="h-4 w-4" />
Pratiche
{fascData.length > 0 && (
<span className={`text-xs px-1.5 py-0.5 rounded-full font-medium ${
fascOverdue > 0
? 'bg-destructive/10 text-destructive'
: 'bg-muted text-muted-foreground'
}`}>
{fascData.length}
</span>
)}
</button>
</div>
</div>
{/* Contenuto tab */}
<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>
) : total === 0 ? (
<div className="text-center py-12 text-muted-foreground">
<CheckCircle2 className="h-12 w-12 mx-auto mb-4 opacity-30 text-green-500" />
<p className="text-lg font-medium">Nessuna scadenza trovata</p>
<p className="text-sm mt-1">
Le scadenze si impostano dal dettaglio di ogni messaggio.
</p>
</div>
{activeTab === 'messaggi' ? (
<MessaggiTab daysAhead={daysAhead} includeOverdue={includeOverdue} />
) : (
<div className="max-w-3xl mx-auto space-y-8">
<DeadlineGroup
title="Scaduti"
items={groups.overdue}
icon={AlertTriangle}
color="text-destructive"
/>
<DeadlineGroup
title="Oggi"
items={groups.today}
icon={Clock}
color="text-amber-600"
/>
<DeadlineGroup
title="Questa settimana"
items={groups.thisWeek}
icon={Calendar}
color="text-blue-600"
/>
<DeadlineGroup
title="Successivamente"
items={groups.later}
icon={Calendar}
color="text-muted-foreground"
/>
</div>
<FascicoliTab daysAhead={daysAhead} includeOverdue={includeOverdue} />
)}
</div>
</div>
+178 -73
View File
@@ -1,4 +1,4 @@
import { useState } from 'react'
import { useState, useMemo } from 'react'
import { useNavigate } from 'react-router-dom'
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
import {
@@ -13,6 +13,8 @@ import {
ChevronRight,
MessageSquare,
Calendar,
AlertTriangle,
Clock,
} from 'lucide-react'
import toast from 'react-hot-toast'
import { Button } from '@/components/ui/Button'
@@ -23,6 +25,31 @@ import { formatDate } from '@/lib/utils'
import { getErrorMessage } from '@/api/client'
import { useAuth } from '@/hooks/useAuth'
// ─── Utilita' scadenza ────────────────────────────────────────────────────────
type ScadenzaStatus = 'overdue' | 'imminent' | 'ok' | null
function getScadenzaStatus(scadenza: string | null): ScadenzaStatus {
if (!scadenza) return null
const now = new Date()
const d = new Date(scadenza)
if (d < now) return 'overdue'
const diff = (d.getTime() - now.getTime()) / (1000 * 60 * 60 * 24)
if (diff <= 7) return 'imminent'
return 'ok'
}
function getDaysLabel(scadenza: string): string {
const now = new Date()
const d = new Date(scadenza)
const diffMs = d.getTime() - now.getTime()
const diffDays = Math.round(diffMs / (1000 * 60 * 60 * 24))
if (diffDays < 0) return `Scaduto da ${Math.abs(diffDays)} gg`
if (diffDays === 0) return 'Scade oggi'
if (diffDays === 1) return 'Scade domani'
return `Scade tra ${diffDays} gg`
}
// ─── Badge stato ──────────────────────────────────────────────────────────────
function StatoBadge({ stato }: { stato: FascicoloResponse['stato'] }) {
@@ -52,6 +79,29 @@ function StatoBadge({ stato }: { stato: FascicoloResponse['stato'] }) {
)
}
// ─── Badge scadenza ───────────────────────────────────────────────────────────
function ScadenzaBadge({ scadenza }: { scadenza: string }) {
const status = getScadenzaStatus(scadenza)
if (status === 'overdue') {
return (
<span className="inline-flex items-center gap-1 px-2 py-0.5 rounded-full text-xs font-medium bg-destructive/10 text-destructive border border-destructive/20">
<AlertTriangle className="h-3 w-3" />
Scaduto
</span>
)
}
if (status === 'imminent') {
return (
<span className="inline-flex items-center gap-1 px-2 py-0.5 rounded-full text-xs font-medium bg-amber-100 text-amber-800 border border-amber-200">
<Clock className="h-3 w-3" />
Imminente
</span>
)
}
return null
}
// ─── Dialog crea / modifica fascicolo ─────────────────────────────────────────
interface FascicoloDialogProps {
@@ -208,6 +258,8 @@ function FascicoloDialog({ fascicolo, onClose }: FascicoloDialogProps) {
// ─── Pagina principale ────────────────────────────────────────────────────────
type ScadenzaFilter = '' | 'overdue' | 'imminent' | 'has_deadline'
export function FascicoliPage() {
const navigate = useNavigate()
const queryClient = useQueryClient()
@@ -215,11 +267,12 @@ export function FascicoliPage() {
const [search, setSearch] = useState('')
const [filterStato, setFilterStato] = useState('')
const [filterScadenza, setFilterScadenza] = useState<ScadenzaFilter>('')
const [showDialog, setShowDialog] = useState(false)
const [editFascicolo, setEditFascicolo] = useState<FascicoloResponse | null>(null)
const [deleteConfirm, setDeleteConfirm] = useState<FascicoloResponse | null>(null)
const { data: fascicoli = [], isLoading } = useQuery({
const { data: allFascicoli = [], isLoading } = useQuery({
queryKey: ['fascicoli', filterStato, search],
queryFn: () =>
fascicoliApi.list({
@@ -228,6 +281,25 @@ export function FascicoliPage() {
}),
})
// Filtro scadenza applicato lato client
const fascicoli = useMemo(() => {
if (!filterScadenza) return allFascicoli
const now = new Date()
const weekEnd = new Date(now)
weekEnd.setDate(weekEnd.getDate() + 7)
return allFascicoli.filter((f) => {
if (!f.scadenza) return false
const d = new Date(f.scadenza)
if (filterScadenza === 'overdue') return d < now
if (filterScadenza === 'imminent') return d >= now && d <= weekEnd
if (filterScadenza === 'has_deadline') return true
return true
})
}, [allFascicoli, filterScadenza])
const hasActiveFilters = search || filterStato || filterScadenza
const deleteMutation = useMutation({
mutationFn: (id: string) => fascicoliApi.delete(id),
onSuccess: () => {
@@ -249,6 +321,9 @@ export function FascicoliPage() {
</h1>
<p className="text-sm text-muted-foreground">
{fascicoli.length} {fascicoli.length === 1 ? 'fascicolo' : 'fascicoli'}
{hasActiveFilters && allFascicoli.length !== fascicoli.length && (
<span className="ml-1">su {allFascicoli.length} totali</span>
)}
</p>
</div>
<Button onClick={() => { setEditFascicolo(null); setShowDialog(true) }}>
@@ -258,7 +333,7 @@ export function FascicoliPage() {
</div>
{/* Filtri */}
<div className="border-b bg-background px-6 py-3 flex items-center gap-3">
<div className="border-b bg-background px-6 py-3 flex items-center gap-3 flex-wrap">
<div className="relative flex-1 max-w-xs">
<Search className="absolute left-2.5 top-2.5 h-4 w-4 text-muted-foreground" />
<Input
@@ -278,11 +353,21 @@ export function FascicoliPage() {
<option value="chiuso">Chiusi</option>
<option value="archiviato">Archiviati</option>
</select>
{(search || filterStato) && (
<select
value={filterScadenza}
onChange={(e) => setFilterScadenza(e.target.value as ScadenzaFilter)}
className="h-9 rounded-md border border-input bg-background px-3 text-sm focus:outline-none focus:ring-2 focus:ring-ring"
>
<option value="">Tutte le scadenze</option>
<option value="overdue">Solo scaduti</option>
<option value="imminent">Scadenza entro 7 giorni</option>
<option value="has_deadline">Con scadenza</option>
</select>
{hasActiveFilters && (
<Button
variant="ghost"
size="sm"
onClick={() => { setSearch(''); setFilterStato('') }}
onClick={() => { setSearch(''); setFilterStato(''); setFilterScadenza('') }}
>
<X className="h-4 w-4 mr-1" />
Pulisci
@@ -301,11 +386,11 @@ export function FascicoliPage() {
<FolderOpen className="h-14 w-14 mx-auto mb-4 opacity-20" />
<p className="text-lg font-medium">Nessun fascicolo trovato</p>
<p className="text-sm mt-1">
{search || filterStato
{hasActiveFilters
? 'Prova a modificare i filtri di ricerca.'
: 'Crea il primo fascicolo per raggruppare le comunicazioni PEC.'}
</p>
{!search && !filterStato && (
{!hasActiveFilters && (
<Button
className="mt-4"
onClick={() => { setEditFascicolo(null); setShowDialog(true) }}
@@ -317,85 +402,105 @@ export function FascicoliPage() {
</div>
) : (
<div className="max-w-4xl mx-auto space-y-3">
{fascicoli.map((f) => (
<div
key={f.id}
className="flex items-center gap-4 p-4 rounded-lg border bg-card hover:shadow-sm transition-shadow cursor-pointer group"
onClick={() => navigate(`/fascicoli/${f.id}`)}
>
{/* Icona stato */}
<div className="flex-shrink-0">
{f.stato === 'aperto' && <FolderOpen className="h-6 w-6 text-green-600" />}
{f.stato === 'chiuso' && <FolderCheck className="h-6 w-6 text-gray-500" />}
{f.stato === 'archiviato' && <FolderArchive className="h-6 w-6 text-amber-600" />}
</div>
{fascicoli.map((f) => {
const scadenzaStatus = getScadenzaStatus(f.scadenza)
const isOverdue = scadenzaStatus === 'overdue'
const isImminent = scadenzaStatus === 'imminent'
{/* Info principale */}
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2 flex-wrap">
<p className="font-semibold truncate">{f.titolo}</p>
<StatoBadge stato={f.stato} />
{f.numero_pratica && (
<span className="text-xs text-muted-foreground font-mono bg-muted px-1.5 py-0.5 rounded">
#{f.numero_pratica}
</span>
)}
{f.categoria && (
<span className="text-xs text-muted-foreground bg-blue-50 border border-blue-100 px-1.5 py-0.5 rounded">
{f.categoria}
return (
<div
key={f.id}
className={`flex items-center gap-4 p-4 rounded-lg border bg-card hover:shadow-sm transition-shadow cursor-pointer group ${
isOverdue
? 'border-destructive/30 bg-destructive/5'
: isImminent
? 'border-amber-300/50 bg-amber-50/30'
: ''
}`}
onClick={() => navigate(`/fascicoli/${f.id}`)}
>
{/* Icona stato */}
<div className="flex-shrink-0">
{f.stato === 'aperto' && <FolderOpen className={`h-6 w-6 ${isOverdue ? 'text-destructive' : 'text-green-600'}`} />}
{f.stato === 'chiuso' && <FolderCheck className="h-6 w-6 text-gray-500" />}
{f.stato === 'archiviato' && <FolderArchive className="h-6 w-6 text-amber-600" />}
</div>
{/* Info principale */}
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2 flex-wrap">
<p className="font-semibold truncate">{f.titolo}</p>
<StatoBadge stato={f.stato} />
{f.scadenza && <ScadenzaBadge scadenza={f.scadenza} />}
{f.numero_pratica && (
<span className="text-xs text-muted-foreground font-mono bg-muted px-1.5 py-0.5 rounded">
#{f.numero_pratica}
</span>
)}
{f.categoria && (
<span className="text-xs text-muted-foreground bg-blue-50 border border-blue-100 px-1.5 py-0.5 rounded">
{f.categoria}
</span>
)}
</div>
<div className="flex items-center gap-4 mt-1 text-xs text-muted-foreground">
<span className="flex items-center gap-1">
<MessageSquare className="h-3 w-3" />
{f.message_count} {f.message_count === 1 ? 'messaggio' : 'messaggi'}
</span>
{f.scadenza && (
<span className={`flex items-center gap-1 ${
isOverdue ? 'text-destructive font-medium' : isImminent ? 'text-amber-600 font-medium' : 'text-muted-foreground'
}`}>
{isOverdue
? <AlertTriangle className="h-3 w-3" />
: <Calendar className="h-3 w-3" />}
{getDaysLabel(f.scadenza)}
<span className="text-muted-foreground font-normal">
({formatDate(f.scadenza)})
</span>
</span>
)}
<span>Aggiornato: {formatDate(f.updated_at)}</span>
</div>
{f.note && (
<p className="text-xs text-muted-foreground italic mt-0.5 truncate">{f.note}</p>
)}
</div>
<div className="flex items-center gap-4 mt-1 text-xs text-muted-foreground">
<span className="flex items-center gap-1">
<MessageSquare className="h-3 w-3" />
{f.message_count} {f.message_count === 1 ? 'messaggio' : 'messaggi'}
</span>
{f.scadenza && (
<span className="flex items-center gap-1 text-amber-600">
<Calendar className="h-3 w-3" />
Scade: {formatDate(f.scadenza)}
</span>
)}
<span>Aggiornato: {formatDate(f.updated_at)}</span>
</div>
{f.note && (
<p className="text-xs text-muted-foreground italic mt-0.5 truncate">{f.note}</p>
)}
</div>
{/* Azioni */}
<div className="flex items-center gap-1 flex-shrink-0 opacity-0 group-hover:opacity-100 transition-opacity">
<button
type="button"
onClick={(e) => {
e.stopPropagation()
setEditFascicolo(f)
setShowDialog(true)
}}
className="p-1.5 rounded hover:bg-muted text-muted-foreground hover:text-foreground"
title="Modifica"
>
<Pencil className="h-4 w-4" />
</button>
{isAdmin && (
{/* Azioni */}
<div className="flex items-center gap-1 flex-shrink-0 opacity-0 group-hover:opacity-100 transition-opacity">
<button
type="button"
onClick={(e) => {
e.stopPropagation()
setDeleteConfirm(f)
setEditFascicolo(f)
setShowDialog(true)
}}
className="p-1.5 rounded hover:bg-destructive/10 text-muted-foreground hover:text-destructive"
title="Elimina"
className="p-1.5 rounded hover:bg-muted text-muted-foreground hover:text-foreground"
title="Modifica"
>
<Trash2 className="h-4 w-4" />
<Pencil className="h-4 w-4" />
</button>
)}
</div>
{isAdmin && (
<button
type="button"
onClick={(e) => {
e.stopPropagation()
setDeleteConfirm(f)
}}
className="p-1.5 rounded hover:bg-destructive/10 text-muted-foreground hover:text-destructive"
title="Elimina"
>
<Trash2 className="h-4 w-4" />
</button>
)}
</div>
<ChevronRight className="h-4 w-4 text-muted-foreground flex-shrink-0" />
</div>
))}
<ChevronRight className="h-4 w-4 text-muted-foreground flex-shrink-0" />
</div>
)
})}
</div>
)}
</div>
@@ -18,6 +18,7 @@ import {
ExternalLink,
AlertTriangle,
CheckCircle2,
Clock,
} from 'lucide-react'
import toast from 'react-hot-toast'
import { Button } from '@/components/ui/Button'
@@ -344,6 +345,27 @@ export function FascicoloDetailPage() {
? FolderCheck
: FolderArchive
// Calcola stato scadenza
const scadenzaStatus = (() => {
if (!fascicolo.scadenza) return null
const now = new Date()
const d = new Date(fascicolo.scadenza)
if (d < now) return 'overdue'
const diff = (d.getTime() - now.getTime()) / (1000 * 60 * 60 * 24)
if (diff <= 7) return 'imminent'
return 'ok'
})()
const getDaysFromNow = (dateStr: string): string => {
const now = new Date()
const d = new Date(dateStr)
const diffDays = Math.round((d.getTime() - now.getTime()) / (1000 * 60 * 60 * 24))
if (diffDays < 0) return `Scaduto da ${Math.abs(diffDays)} giorn${Math.abs(diffDays) === 1 ? 'o' : 'i'}`
if (diffDays === 0) return 'Scade oggi'
if (diffDays === 1) return 'Scade domani'
return `Scade tra ${diffDays} giorni`
}
return (
<div className="flex flex-col h-full">
{/* Toolbar */}
@@ -369,6 +391,29 @@ export function FascicoloDetailPage() {
</div>
</div>
{/* Banner alert scadenza */}
{scadenzaStatus === 'overdue' && fascicolo.scadenza && (
<div className="bg-destructive/10 border-b border-destructive/20 px-6 py-2.5">
<div className="max-w-4xl mx-auto flex items-center gap-2 text-sm text-destructive font-medium">
<AlertTriangle className="h-4 w-4 flex-shrink-0" />
<span>
{getDaysFromNow(fascicolo.scadenza)} scadenza del {formatDate(fascicolo.scadenza)}.
Aggiorna la scadenza o chiudi il fascicolo.
</span>
</div>
</div>
)}
{scadenzaStatus === 'imminent' && fascicolo.scadenza && (
<div className="bg-amber-50 border-b border-amber-200 px-6 py-2.5">
<div className="max-w-4xl mx-auto flex items-center gap-2 text-sm text-amber-700 font-medium">
<Clock className="h-4 w-4 flex-shrink-0" />
<span>
{getDaysFromNow(fascicolo.scadenza)} scadenza del {formatDate(fascicolo.scadenza)}.
</span>
</div>
</div>
)}
{/* Intestazione fascicolo */}
<div className="border-b bg-muted/30 px-6 py-5">
<div className="max-w-4xl mx-auto">