Scadenzario aggiunta pratiche
This commit is contained in:
@@ -33,6 +33,7 @@ from app.models.message import Message
|
||||
from app.schemas.fascicolo import (
|
||||
FascicoloAddMessagesRequest,
|
||||
FascicoloCreate,
|
||||
FascicoloDeadlineResponse,
|
||||
FascicoloMessageItem,
|
||||
FascicoloRemoveMessagesRequest,
|
||||
FascicoloResponse,
|
||||
@@ -73,6 +74,33 @@ async def list_fascicoli(
|
||||
return [_to_response(f, cnt) for f, cnt in rows]
|
||||
|
||||
|
||||
@router.get("/fascicoli/scadenzario", response_model=list[FascicoloDeadlineResponse])
|
||||
async def list_fascicoli_scadenzario(
|
||||
current_user: CurrentUser,
|
||||
db: DB,
|
||||
days_ahead: int = Query(30, ge=1, le=365, description="Giorni da considerare in avanti"),
|
||||
include_overdue: bool = Query(True, description="Includi fascicoli gia' scaduti"),
|
||||
) -> list[FascicoloDeadlineResponse]:
|
||||
"""
|
||||
Scadenzario pratiche: fascicoli con scadenza imminente o scaduta.
|
||||
|
||||
Ordinati per scadenza ASC (scaduti prima, poi futuri).
|
||||
"""
|
||||
svc = FascicoloService(db)
|
||||
rows = await svc.list_fascicoli_scadenzario(
|
||||
current_user.tenant_id,
|
||||
days_ahead=days_ahead,
|
||||
include_overdue=include_overdue,
|
||||
)
|
||||
items = []
|
||||
for fascicolo, cnt, is_overdue in rows:
|
||||
resp = FascicoloDeadlineResponse.model_validate(fascicolo)
|
||||
resp.message_count = cnt
|
||||
resp.is_overdue = is_overdue
|
||||
items.append(resp)
|
||||
return items
|
||||
|
||||
|
||||
@router.post("/fascicoli", response_model=FascicoloResponse, status_code=status.HTTP_201_CREATED)
|
||||
async def create_fascicolo(
|
||||
data: FascicoloCreate,
|
||||
|
||||
@@ -90,3 +90,24 @@ class MessageFascicoloSummary(BaseModel):
|
||||
categoria: Optional[str] = None
|
||||
|
||||
model_config = {"from_attributes": True}
|
||||
|
||||
|
||||
# ─── Scadenzario fascicoli ────────────────────────────────────────────────────
|
||||
|
||||
class FascicoloDeadlineResponse(BaseModel):
|
||||
"""Fascicolo con scadenza per lo scadenzario pratiche."""
|
||||
id: uuid.UUID
|
||||
tenant_id: uuid.UUID
|
||||
titolo: str
|
||||
numero_pratica: Optional[str] = None
|
||||
stato: str
|
||||
categoria: Optional[str] = None
|
||||
responsabile_id: Optional[uuid.UUID] = None
|
||||
scadenza: Optional[datetime] = None
|
||||
note: Optional[str] = None
|
||||
created_at: datetime
|
||||
updated_at: datetime
|
||||
message_count: int = 0
|
||||
is_overdue: bool = False
|
||||
|
||||
model_config = {"from_attributes": True}
|
||||
|
||||
@@ -3,7 +3,7 @@ Service per la gestione dei Fascicoli (fascicolazione pratiche).
|
||||
"""
|
||||
|
||||
import uuid
|
||||
from datetime import datetime
|
||||
from datetime import datetime, timedelta, timezone
|
||||
|
||||
from sqlalchemy import delete, func, select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
@@ -268,3 +268,57 @@ class FascicoloService:
|
||||
.order_by(Fascicolo.titolo)
|
||||
)
|
||||
return list(result.scalars().all())
|
||||
|
||||
# ─── Scadenzario fascicoli ────────────────────────────────────────────────
|
||||
|
||||
async def list_fascicoli_scadenzario(
|
||||
self,
|
||||
tenant_id: uuid.UUID,
|
||||
days_ahead: int = 30,
|
||||
include_overdue: bool = True,
|
||||
) -> list[tuple[Fascicolo, int, bool]]:
|
||||
"""
|
||||
Restituisce lista di (Fascicolo, message_count, is_overdue) per lo scadenzario.
|
||||
|
||||
Filtra fascicoli con scadenza impostata nel range richiesto.
|
||||
Ordinati: scaduti prima (ASC scadenza), poi futuri (ASC scadenza).
|
||||
"""
|
||||
now = datetime.now(timezone.utc)
|
||||
future_limit = now + timedelta(days=days_ahead)
|
||||
|
||||
count_sub = (
|
||||
select(
|
||||
FascicoloMessage.fascicolo_id,
|
||||
func.count(FascicoloMessage.message_id).label("cnt"),
|
||||
)
|
||||
.group_by(FascicoloMessage.fascicolo_id)
|
||||
.subquery()
|
||||
)
|
||||
|
||||
stmt = (
|
||||
select(Fascicolo, func.coalesce(count_sub.c.cnt, 0).label("message_count"))
|
||||
.outerjoin(count_sub, Fascicolo.id == count_sub.c.fascicolo_id)
|
||||
.where(
|
||||
Fascicolo.tenant_id == tenant_id,
|
||||
Fascicolo.scadenza.is_not(None),
|
||||
)
|
||||
.order_by(Fascicolo.scadenza.asc())
|
||||
)
|
||||
|
||||
if include_overdue:
|
||||
# Scaduti (qualsiasi data passata) + futuri fino al limite
|
||||
stmt = stmt.where(Fascicolo.scadenza <= future_limit)
|
||||
else:
|
||||
# Solo scadenze future entro il limite
|
||||
stmt = stmt.where(
|
||||
Fascicolo.scadenza > now,
|
||||
Fascicolo.scadenza <= future_limit,
|
||||
)
|
||||
|
||||
result = await self.db.execute(stmt)
|
||||
rows = result.all()
|
||||
|
||||
return [
|
||||
(fascicolo, int(cnt), fascicolo.scadenza < now if fascicolo.scadenza else False)
|
||||
for fascicolo, cnt in rows
|
||||
]
|
||||
|
||||
@@ -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),
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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">
|
||||
|
||||
Reference in New Issue
Block a user