Files
lexmind/src/app/dashboard/publicacoes/page.tsx

470 lines
19 KiB
TypeScript

'use client'
import { useState, useEffect } from 'react'
import { useRouter } from 'next/navigation'
import {
Newspaper,
Plus,
Search,
Filter,
Loader2,
Eye,
EyeOff,
Calendar,
Building2,
FileText,
Clock,
AlertTriangle,
ChevronDown,
ChevronUp,
Trash2,
RefreshCw,
Bell,
} from 'lucide-react'
interface Processo {
id: string
numeroProcesso: string
tribunal: string
vara: string | null
comarca: string | null
parteAutora: string | null
parteRe: string | null
status: 'ATIVO' | 'ARQUIVADO' | 'SUSPENSO'
publicacoesNaoLidas: number
ultimaPublicacao: Publicacao | null
}
interface Publicacao {
id: string
processoId: string
dataPublicacao: string
diario: string
conteudo: string
tipo: 'INTIMACAO' | 'CITACAO' | 'SENTENCA' | 'DESPACHO' | 'ACORDAO' | 'OUTROS'
prazoCalculado: string | null
prazoTipo: string | null
visualizado: boolean
processo?: {
numeroProcesso: string
tribunal: string
parteAutora: string | null
parteRe: string | null
}
}
const tipoConfig = {
INTIMACAO: { label: 'Intimação', bg: 'bg-blue-500/15 border-blue-500/30', color: 'text-blue-400' },
CITACAO: { label: 'Citação', bg: 'bg-purple-500/15 border-purple-500/30', color: 'text-purple-400' },
SENTENCA: { label: 'Sentença', bg: 'bg-red-500/15 border-red-500/30', color: 'text-red-400' },
DESPACHO: { label: 'Despacho', bg: 'bg-yellow-500/15 border-yellow-500/30', color: 'text-yellow-400' },
ACORDAO: { label: 'Acórdão', bg: 'bg-orange-500/15 border-orange-500/30', color: 'text-orange-400' },
OUTROS: { label: 'Outros', bg: 'bg-zinc-500/15 border-zinc-500/30', color: 'text-zinc-400' },
}
export default function PublicacoesPage() {
const router = useRouter()
const [processos, setProcessos] = useState<Processo[]>([])
const [publicacoes, setPublicacoes] = useState<Publicacao[]>([])
const [loading, setLoading] = useState(true)
const [buscando, setBuscando] = useState(false)
const [expandedId, setExpandedId] = useState<string | null>(null)
// Filtros
const [filterTipo, setFilterTipo] = useState<string>('')
const [filterVisualizado, setFilterVisualizado] = useState<string>('')
const [filterProcesso, setFilterProcesso] = useState<string>('')
useEffect(() => {
fetchData()
}, [filterTipo, filterVisualizado, filterProcesso])
async function fetchData() {
setLoading(true)
try {
// Busca processos e publicações em paralelo
const [processosRes, publicacoesRes] = await Promise.all([
fetch('/api/processos'),
fetchPublicacoes(),
])
if (processosRes.ok) {
const data = await processosRes.json()
setProcessos(data.processos || [])
}
} catch (e) {
console.error('Failed to load data:', e)
} finally {
setLoading(false)
}
}
async function fetchPublicacoes() {
const params = new URLSearchParams()
if (filterTipo) params.set('tipo', filterTipo)
if (filterVisualizado) params.set('visualizado', filterVisualizado)
if (filterProcesso) params.set('processoId', filterProcesso)
const res = await fetch(`/api/publicacoes?${params}`)
if (res.ok) {
const data = await res.json()
setPublicacoes(data.publicacoes || [])
}
return res
}
async function buscarNovasPublicacoes() {
setBuscando(true)
try {
const res = await fetch('/api/publicacoes/buscar', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({}),
})
if (res.ok) {
await fetchData()
}
} catch (e) {
console.error('Failed to search:', e)
} finally {
setBuscando(false)
}
}
async function marcarComoLida(id: string) {
try {
await fetch(`/api/publicacoes/${id}/visualizar`, { method: 'PATCH' })
setPublicacoes(pubs => pubs.map(p => p.id === id ? { ...p, visualizado: true } : p))
} catch (e) {
console.error('Failed to mark as read:', e)
}
}
async function deleteProcesso(id: string) {
if (!confirm('Tem certeza que deseja remover este processo do monitoramento?')) return
try {
await fetch(`/api/processos/${id}`, { method: 'DELETE' })
fetchData()
} catch (e) {
console.error('Failed to delete:', e)
}
}
function formatDate(dateStr: string) {
return new Date(dateStr).toLocaleDateString('pt-BR', { day: '2-digit', month: '2-digit', year: 'numeric' })
}
function getDaysUntilPrazo(prazoStr: string | null) {
if (!prazoStr) return null
const now = new Date()
now.setHours(0, 0, 0, 0)
const prazo = new Date(prazoStr)
prazo.setHours(0, 0, 0, 0)
return Math.ceil((prazo.getTime() - now.getTime()) / (1000 * 60 * 60 * 24))
}
// Stats
const totalProcessos = processos.length
const totalNaoLidas = publicacoes.filter(p => !p.visualizado).length
const prazosProximos = publicacoes.filter(p => {
const days = getDaysUntilPrazo(p.prazoCalculado)
return days !== null && days >= 0 && days <= 5
}).length
return (
<div>
{/* Header */}
<div className="mb-8 flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
<div>
<h1 className="text-2xl font-bold text-white flex items-center gap-3">
<div className="flex h-10 w-10 items-center justify-center rounded-xl bg-indigo-600/20">
<Newspaper className="h-5 w-5 text-indigo-400" />
</div>
Monitoramento de Publicações
</h1>
<p className="mt-1 text-sm text-zinc-500">Acompanhe publicações dos Diários Oficiais</p>
</div>
<div className="flex items-center gap-3">
<button
onClick={buscarNovasPublicacoes}
disabled={buscando}
className="flex items-center gap-2 rounded-xl border border-white/10 bg-white/5 px-4 py-2.5 text-sm font-medium text-zinc-300 transition-all hover:bg-white/10 disabled:opacity-50"
>
<RefreshCw className={`h-4 w-4 ${buscando ? 'animate-spin' : ''}`} />
{buscando ? 'Buscando...' : 'Buscar Publicações'}
</button>
<button
onClick={() => router.push('/dashboard/publicacoes/novo')}
className="flex items-center gap-2 rounded-xl bg-indigo-600 px-4 py-2.5 text-sm font-semibold text-white transition-all hover:bg-indigo-500 hover:shadow-lg hover:shadow-indigo-500/20"
>
<Plus className="h-4 w-4" />
Adicionar Processo
</button>
</div>
</div>
{/* Stats Cards */}
<div className="mb-6 grid grid-cols-1 gap-4 sm:grid-cols-3">
<div className="rounded-xl border border-white/5 bg-white/[0.02] p-4">
<div className="flex items-center gap-3">
<div className="flex h-10 w-10 items-center justify-center rounded-lg bg-blue-500/10">
<FileText className="h-5 w-5 text-blue-400" />
</div>
<div>
<p className="text-xs font-medium text-zinc-500">Processos Monitorados</p>
<p className="text-2xl font-bold text-white">{totalProcessos}</p>
</div>
</div>
</div>
<div className="rounded-xl border border-white/5 bg-white/[0.02] p-4">
<div className="flex items-center gap-3">
<div className="flex h-10 w-10 items-center justify-center rounded-lg bg-yellow-500/10">
<Bell className="h-5 w-5 text-yellow-400" />
</div>
<div>
<p className="text-xs font-medium text-zinc-500">Publicações Não Lidas</p>
<p className="text-2xl font-bold text-yellow-400">{totalNaoLidas}</p>
</div>
</div>
</div>
<div className="rounded-xl border border-white/5 bg-white/[0.02] p-4">
<div className="flex items-center gap-3">
<div className="flex h-10 w-10 items-center justify-center rounded-lg bg-red-500/10">
<AlertTriangle className="h-5 w-5 text-red-400" />
</div>
<div>
<p className="text-xs font-medium text-zinc-500">Prazos Próximos (5 dias)</p>
<p className="text-2xl font-bold text-red-400">{prazosProximos}</p>
</div>
</div>
</div>
</div>
{/* Filters */}
<div className="mb-6 flex flex-wrap items-center gap-3">
<Filter className="h-4 w-4 text-zinc-500" />
<select
value={filterTipo}
onChange={e => setFilterTipo(e.target.value)}
className="rounded-lg border border-white/10 bg-white/5 px-3 py-1.5 text-sm text-zinc-300 outline-none focus:border-indigo-500/40"
>
<option value="">Todos os tipos</option>
<option value="INTIMACAO">Intimação</option>
<option value="CITACAO">Citação</option>
<option value="SENTENCA">Sentença</option>
<option value="DESPACHO">Despacho</option>
<option value="ACORDAO">Acórdão</option>
<option value="OUTROS">Outros</option>
</select>
<select
value={filterVisualizado}
onChange={e => setFilterVisualizado(e.target.value)}
className="rounded-lg border border-white/10 bg-white/5 px-3 py-1.5 text-sm text-zinc-300 outline-none focus:border-indigo-500/40"
>
<option value="">Todas</option>
<option value="false">Não lidas</option>
<option value="true">Lidas</option>
</select>
{processos.length > 0 && (
<select
value={filterProcesso}
onChange={e => setFilterProcesso(e.target.value)}
className="rounded-lg border border-white/10 bg-white/5 px-3 py-1.5 text-sm text-zinc-300 outline-none focus:border-indigo-500/40"
>
<option value="">Todos os processos</option>
{processos.map(p => (
<option key={p.id} value={p.id}>
{p.numeroProcesso} ({p.tribunal})
</option>
))}
</select>
)}
</div>
{/* Content */}
{loading ? (
<div className="flex items-center justify-center py-16">
<Loader2 className="h-6 w-6 animate-spin text-indigo-400" />
</div>
) : processos.length === 0 ? (
<div className="flex flex-col items-center justify-center rounded-xl border border-white/5 bg-white/[0.02] py-16">
<Newspaper className="h-12 w-12 text-zinc-600" />
<p className="mt-3 text-sm text-zinc-500">Nenhum processo sendo monitorado</p>
<button
onClick={() => router.push('/dashboard/publicacoes/novo')}
className="mt-4 text-sm text-indigo-400 hover:text-indigo-300"
>
Adicionar primeiro processo
</button>
</div>
) : publicacoes.length === 0 ? (
<div className="flex flex-col items-center justify-center rounded-xl border border-white/5 bg-white/[0.02] py-16">
<Search className="h-12 w-12 text-zinc-600" />
<p className="mt-3 text-sm text-zinc-500">Nenhuma publicação encontrada</p>
<button
onClick={buscarNovasPublicacoes}
className="mt-4 text-sm text-indigo-400 hover:text-indigo-300"
>
Buscar novas publicações
</button>
</div>
) : (
<div className="space-y-3">
{publicacoes.map(pub => {
const config = tipoConfig[pub.tipo]
const daysUntilPrazo = getDaysUntilPrazo(pub.prazoCalculado)
const isUrgent = daysUntilPrazo !== null && daysUntilPrazo >= 0 && daysUntilPrazo <= 3
const isOverdue = daysUntilPrazo !== null && daysUntilPrazo < 0
const isExpanded = expandedId === pub.id
return (
<div
key={pub.id}
className={`group rounded-xl border bg-white/[0.02] transition-all hover:bg-white/[0.04] ${
!pub.visualizado
? 'border-indigo-500/30 shadow-[0_0_15px_rgba(99,102,241,0.1)]'
: isUrgent || isOverdue
? 'border-red-500/30'
: 'border-white/5'
}`}
>
<div className="p-4">
<div className="flex items-start gap-4">
{/* Indicator */}
{!pub.visualizado && (
<div className="mt-1.5 h-2 w-2 rounded-full bg-indigo-500 flex-shrink-0" />
)}
<div className="flex-1 min-w-0">
<div className="flex items-start justify-between gap-3">
<div className="flex-1">
<div className="flex items-center gap-2 flex-wrap">
<span className={`rounded-md border px-2 py-0.5 text-[10px] font-bold uppercase ${config.bg} ${config.color}`}>
{config.label}
</span>
<span className="text-xs text-zinc-500">{pub.diario}</span>
<span className="text-xs text-zinc-600"></span>
<span className="text-xs text-zinc-500">{formatDate(pub.dataPublicacao)}</span>
</div>
{pub.processo && (
<p className="mt-1 text-sm font-medium text-white">
{pub.processo.numeroProcesso}
<span className="ml-2 text-zinc-500 font-normal">({pub.processo.tribunal})</span>
</p>
)}
</div>
<div className="flex items-center gap-2">
{!pub.visualizado && (
<button
onClick={() => marcarComoLida(pub.id)}
className="rounded-lg p-1.5 text-zinc-500 hover:bg-indigo-500/10 hover:text-indigo-400"
title="Marcar como lida"
>
<Eye className="h-4 w-4" />
</button>
)}
<button
onClick={() => setExpandedId(isExpanded ? null : pub.id)}
className="rounded-lg p-1.5 text-zinc-500 hover:bg-white/10 hover:text-zinc-300"
>
{isExpanded ? <ChevronUp className="h-4 w-4" /> : <ChevronDown className="h-4 w-4" />}
</button>
</div>
</div>
{/* Preview / Full content */}
<p className={`mt-2 text-sm text-zinc-400 ${isExpanded ? '' : 'line-clamp-2'}`}>
{pub.conteudo}
</p>
{/* Prazo */}
{pub.prazoCalculado && (
<div className="mt-3 flex items-center gap-2">
<Clock className="h-3.5 w-3.5 text-zinc-500" />
<span className={`text-xs font-medium ${
isOverdue
? 'text-red-400'
: isUrgent
? 'text-red-400 animate-pulse'
: daysUntilPrazo !== null && daysUntilPrazo <= 7
? 'text-yellow-400'
: 'text-zinc-400'
}`}>
Prazo: {formatDate(pub.prazoCalculado)}
{pub.prazoTipo && ` (${pub.prazoTipo})`}
{daysUntilPrazo !== null && (
<span className="ml-2">
{isOverdue
? `• Vencido há ${Math.abs(daysUntilPrazo)} dia(s)`
: daysUntilPrazo === 0
? '• Vence HOJE!'
: daysUntilPrazo === 1
? '• Vence AMANHÃ'
: `${daysUntilPrazo} dias restantes`
}
</span>
)}
</span>
</div>
)}
</div>
</div>
</div>
</div>
)
})}
</div>
)}
{/* Processos monitorados section */}
{processos.length > 0 && (
<div className="mt-10">
<h2 className="text-lg font-semibold text-white mb-4 flex items-center gap-2">
<FileText className="h-5 w-5 text-zinc-400" />
Processos Monitorados
</h2>
<div className="grid gap-3 sm:grid-cols-2 lg:grid-cols-3">
{processos.map(p => (
<div
key={p.id}
className="group rounded-xl border border-white/5 bg-white/[0.02] p-4 transition-all hover:bg-white/[0.04]"
>
<div className="flex items-start justify-between">
<div className="min-w-0 flex-1">
<p className="font-mono text-sm font-medium text-white truncate">
{p.numeroProcesso}
</p>
<p className="mt-0.5 text-xs text-zinc-500">{p.tribunal}</p>
{(p.parteAutora || p.parteRe) && (
<p className="mt-1 text-xs text-zinc-600 truncate">
{p.parteAutora && `Autor: ${p.parteAutora}`}
{p.parteAutora && p.parteRe && ' • '}
{p.parteRe && `Réu: ${p.parteRe}`}
</p>
)}
</div>
<div className="flex items-center gap-1 ml-2">
{p.publicacoesNaoLidas > 0 && (
<span className="flex h-5 min-w-5 items-center justify-center rounded-full bg-indigo-500 px-1.5 text-[10px] font-bold text-white">
{p.publicacoesNaoLidas}
</span>
)}
<button
onClick={() => deleteProcesso(p.id)}
className="rounded-lg p-1.5 text-zinc-600 opacity-0 group-hover:opacity-100 hover:bg-red-500/10 hover:text-red-400 transition-all"
title="Remover"
>
<Trash2 className="h-4 w-4" />
</button>
</div>
</div>
</div>
))}
</div>
</div>
)}
</div>
)
}