diff --git a/backend/app/api/v1/fascicoli.py b/backend/app/api/v1/fascicoli.py index 660ff94..2e4c122 100644 --- a/backend/app/api/v1/fascicoli.py +++ b/backend/app/api/v1/fascicoli.py @@ -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, diff --git a/backend/app/schemas/fascicolo.py b/backend/app/schemas/fascicolo.py index d6dd60b..befb7a1 100644 --- a/backend/app/schemas/fascicolo.py +++ b/backend/app/schemas/fascicolo.py @@ -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} diff --git a/backend/app/services/fascicolo_service.py b/backend/app/services/fascicolo_service.py index e85a16c..a651dd9 100644 --- a/backend/app/services/fascicolo_service.py +++ b/backend/app/services/fascicolo_service.py @@ -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 + ] diff --git a/frontend/src/api/fascicoli.api.ts b/frontend/src/api/fascicoli.api.ts index a077843..c40f945 100644 --- a/frontend/src/api/fascicoli.api.ts +++ b/frontend/src/api/fascicoli.api.ts @@ -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(`/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('/fascicoli/scadenzario', { params }) + .then((r) => r.data), } diff --git a/frontend/src/pages/Deadlines/DeadlinesPage.tsx b/frontend/src/pages/Deadlines/DeadlinesPage.tsx index 358550d..1d1e0bb 100644 --- a/frontend/src/pages/Deadlines/DeadlinesPage.tsx +++ b/frontend/src/pages/Deadlines/DeadlinesPage.tsx @@ -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( + 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({ + 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 ( +
+
+ +

+ {title} ({items.length}) +

+
+
{items.map((item, i) =>
{renderItem(item)}
)}
+
+ ) +} + +// ─── 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 (
navigate(`/messages/${item.id}`)} >
@@ -55,7 +122,9 @@ function DeadlineItem({ item }: { item: DeadlineMessageResponse }) {

{item.subject || '(nessun oggetto)'}

- {item.direction === 'inbound' ? `Da: ${item.from_address}` : `A: ${(item.to_addresses ?? []).join(', ')}`} + {item.direction === 'inbound' + ? `Da: ${item.from_address}` + : `A: ${(item.to_addresses ?? []).join(', ')}`}

{item.deadline_note && (

{item.deadline_note}

@@ -65,51 +134,254 @@ function DeadlineItem({ item }: { item: DeadlineMessageResponse }) {

{deadlineDate ? formatDate(deadlineDate.toISOString()) : '-'}

- {item.is_overdue && ( -

Scaduto

- )} + {item.is_overdue &&

Scaduto

}
) } -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 ( -
-
- -

{title} ({items.length})

-
-
- {items.map((item) => ( - - ))} -
-
- ) -} - -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 ( +
+
+
+ ) + } + + if (data.length === 0) { + return ( +
+ +

Nessuna scadenza sui messaggi

+

+ Le scadenze si impostano dal dettaglio di ogni messaggio. +

+
+ ) + } + + return ( +
+ } + /> + } + /> + } + /> + } + /> +
+ ) +} + +// ─── 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 ( +
navigate(`/fascicoli/${item.id}`)} + > +
+ {item.is_overdue ? ( + + ) : ( + + )} +
+
+
+

{item.titolo}

+ {item.numero_pratica && ( + + #{item.numero_pratica} + + )} + {item.categoria && ( + + {item.categoria} + + )} +
+

+ + {item.message_count} {item.message_count === 1 ? 'messaggio' : 'messaggi'} + {item.note && {item.note}} +

+
+
+

+ {scadenzaDate ? formatDate(scadenzaDate.toISOString()) : '-'} +

+ {item.is_overdue &&

Scaduto

} +
+ +
+ ) +} + +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 ( +
+
+
+ ) + } + + if (data.length === 0) { + return ( +
+ +

Nessuna scadenza sui fascicoli

+

+ Le scadenze si impostano creando o modificando un fascicolo. +

+
+ ) + } + + return ( +
+ } + /> + } + /> + } + /> + } + /> +
+ ) +} + +// ─── Pagina principale ──────────────────────────────────────────────────────── + +type Tab = 'messaggi' | 'fascicoli' + +export function DeadlinesPage() { + const [activeTab, setActiveTab] = useState('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 (
+ {/* Header */}

@@ -117,7 +389,13 @@ export function DeadlinesPage() { Scadenzario

- {total} messaggi con scadenze + {totalOverdue > 0 ? ( + + {totalOverdue} {totalOverdue === 1 ? 'elemento scaduto' : 'elementi scaduti'} + + ) : ( + `${msgData.length + fascData.length} scadenze totali` + )}

@@ -143,46 +421,58 @@ export function DeadlinesPage() {
+ {/* Tab bar */} +
+
+ + +
+
+ + {/* Contenuto tab */}
- {isLoading ? ( -
-
-
- ) : total === 0 ? ( -
- -

Nessuna scadenza trovata

-

- Le scadenze si impostano dal dettaglio di ogni messaggio. -

-
+ {activeTab === 'messaggi' ? ( + ) : ( -
- - - - -
+ )}
diff --git a/frontend/src/pages/Fascicoli/FascicoliPage.tsx b/frontend/src/pages/Fascicoli/FascicoliPage.tsx index 17a7efd..ef64d74 100644 --- a/frontend/src/pages/Fascicoli/FascicoliPage.tsx +++ b/frontend/src/pages/Fascicoli/FascicoliPage.tsx @@ -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 ( + + + Scaduto + + ) + } + if (status === 'imminent') { + return ( + + + Imminente + + ) + } + 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('') const [showDialog, setShowDialog] = useState(false) const [editFascicolo, setEditFascicolo] = useState(null) const [deleteConfirm, setDeleteConfirm] = useState(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() {

{fascicoli.length} {fascicoli.length === 1 ? 'fascicolo' : 'fascicoli'} + {hasActiveFilters && allFascicoli.length !== fascicoli.length && ( + su {allFascicoli.length} totali + )}

{/* Filtri */} -
+
Chiusi - {(search || filterStato) && ( + + {hasActiveFilters && (
) : (
- {fascicoli.map((f) => ( -
navigate(`/fascicoli/${f.id}`)} - > - {/* Icona stato */} -
- {f.stato === 'aperto' && } - {f.stato === 'chiuso' && } - {f.stato === 'archiviato' && } -
+ {fascicoli.map((f) => { + const scadenzaStatus = getScadenzaStatus(f.scadenza) + const isOverdue = scadenzaStatus === 'overdue' + const isImminent = scadenzaStatus === 'imminent' - {/* Info principale */} -
-
-

{f.titolo}

- - {f.numero_pratica && ( - - #{f.numero_pratica} - - )} - {f.categoria && ( - - {f.categoria} + return ( +
navigate(`/fascicoli/${f.id}`)} + > + {/* Icona stato */} +
+ {f.stato === 'aperto' && } + {f.stato === 'chiuso' && } + {f.stato === 'archiviato' && } +
+ + {/* Info principale */} +
+
+

{f.titolo}

+ + {f.scadenza && } + {f.numero_pratica && ( + + #{f.numero_pratica} + + )} + {f.categoria && ( + + {f.categoria} + + )} +
+
+ + + {f.message_count} {f.message_count === 1 ? 'messaggio' : 'messaggi'} + {f.scadenza && ( + + {isOverdue + ? + : } + {getDaysLabel(f.scadenza)} + + ({formatDate(f.scadenza)}) + + + )} + Aggiornato: {formatDate(f.updated_at)} +
+ {f.note && ( +

{f.note}

)}
-
- - - {f.message_count} {f.message_count === 1 ? 'messaggio' : 'messaggi'} - - {f.scadenza && ( - - - Scade: {formatDate(f.scadenza)} - - )} - Aggiornato: {formatDate(f.updated_at)} -
- {f.note && ( -

{f.note}

- )} -
- {/* Azioni */} -
- - {isAdmin && ( + {/* Azioni */} +
- )} -
+ {isAdmin && ( + + )} +
- -
- ))} + +
+ ) + })}
)}
diff --git a/frontend/src/pages/Fascicoli/FascicoloDetailPage.tsx b/frontend/src/pages/Fascicoli/FascicoloDetailPage.tsx index 6ead6e8..1ab1788 100644 --- a/frontend/src/pages/Fascicoli/FascicoloDetailPage.tsx +++ b/frontend/src/pages/Fascicoli/FascicoloDetailPage.tsx @@ -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 (
{/* Toolbar */} @@ -369,6 +391,29 @@ export function FascicoloDetailPage() {
+ {/* Banner alert scadenza */} + {scadenzaStatus === 'overdue' && fascicolo.scadenza && ( +
+
+ + + {getDaysFromNow(fascicolo.scadenza)} — scadenza del {formatDate(fascicolo.scadenza)}. + Aggiorna la scadenza o chiudi il fascicolo. + +
+
+ )} + {scadenzaStatus === 'imminent' && fascicolo.scadenza && ( +
+
+ + + {getDaysFromNow(fascicolo.scadenza)} — scadenza del {formatDate(fascicolo.scadenza)}. + +
+
+ )} + {/* Intestazione fascicolo */}