Initial commit: LexMind - Plataforma Jurídica Inteligente
This commit is contained in:
479
src/app/dashboard/prazos/page.tsx
Normal file
479
src/app/dashboard/prazos/page.tsx
Normal file
@@ -0,0 +1,479 @@
|
||||
'use client'
|
||||
|
||||
import { useState, useEffect } from 'react'
|
||||
import {
|
||||
Clock,
|
||||
Plus,
|
||||
X,
|
||||
Calendar,
|
||||
AlertTriangle,
|
||||
CheckCircle2,
|
||||
XCircle,
|
||||
Filter,
|
||||
Loader2,
|
||||
Trash2,
|
||||
Edit3,
|
||||
Building2,
|
||||
FileText,
|
||||
} from 'lucide-react'
|
||||
|
||||
interface Prazo {
|
||||
id: string
|
||||
title: string
|
||||
description: string | null
|
||||
processNumber: string | null
|
||||
court: string | null
|
||||
deadline: string
|
||||
alertDays: number
|
||||
status: 'PENDENTE' | 'CONCLUIDO' | 'VENCIDO' | 'CANCELADO'
|
||||
priority: 'ALTA' | 'MEDIA' | 'BAIXA'
|
||||
createdAt: string
|
||||
}
|
||||
|
||||
const priorityConfig = {
|
||||
ALTA: { label: 'Alta', color: 'text-red-400', bg: 'bg-red-500/15 border-red-500/30', dot: 'bg-red-500' },
|
||||
MEDIA: { label: 'Média', color: 'text-yellow-400', bg: 'bg-yellow-500/15 border-yellow-500/30', dot: 'bg-yellow-500' },
|
||||
BAIXA: { label: 'Baixa', color: 'text-green-400', bg: 'bg-green-500/15 border-green-500/30', dot: 'bg-green-500' },
|
||||
}
|
||||
|
||||
const statusConfig = {
|
||||
PENDENTE: { label: 'Pendente', color: 'text-blue-400', bg: 'bg-blue-500/15 border-blue-500/30', icon: Clock },
|
||||
CONCLUIDO: { label: 'Concluído', color: 'text-green-400', bg: 'bg-green-500/15 border-green-500/30', icon: CheckCircle2 },
|
||||
VENCIDO: { label: 'Vencido', color: 'text-red-400', bg: 'bg-red-500/15 border-red-500/30', icon: AlertTriangle },
|
||||
CANCELADO: { label: 'Cancelado', color: 'text-zinc-400', bg: 'bg-zinc-500/15 border-zinc-500/30', icon: XCircle },
|
||||
}
|
||||
|
||||
export default function PrazosPage() {
|
||||
const [prazos, setPrazos] = useState<Prazo[]>([])
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [showModal, setShowModal] = useState(false)
|
||||
const [editingPrazo, setEditingPrazo] = useState<Prazo | null>(null)
|
||||
const [filterStatus, setFilterStatus] = useState<string>('')
|
||||
const [filterPriority, setFilterPriority] = useState<string>('')
|
||||
const [saving, setSaving] = useState(false)
|
||||
|
||||
// Form state
|
||||
const [form, setForm] = useState({
|
||||
title: '',
|
||||
description: '',
|
||||
processNumber: '',
|
||||
court: '',
|
||||
deadline: '',
|
||||
alertDays: 3,
|
||||
priority: 'MEDIA' as 'ALTA' | 'MEDIA' | 'BAIXA',
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
fetchPrazos()
|
||||
}, [filterStatus, filterPriority])
|
||||
|
||||
async function fetchPrazos() {
|
||||
setLoading(true)
|
||||
try {
|
||||
const params = new URLSearchParams()
|
||||
if (filterStatus) params.set('status', filterStatus)
|
||||
if (filterPriority) params.set('priority', filterPriority)
|
||||
const res = await fetch(`/api/prazos?${params}`)
|
||||
if (res.ok) {
|
||||
const data = await res.json()
|
||||
setPrazos(data.prazos || [])
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Failed to load prazos:', e)
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
function openNewModal() {
|
||||
setEditingPrazo(null)
|
||||
setForm({ title: '', description: '', processNumber: '', court: '', deadline: '', alertDays: 3, priority: 'MEDIA' })
|
||||
setShowModal(true)
|
||||
}
|
||||
|
||||
function openEditModal(prazo: Prazo) {
|
||||
setEditingPrazo(prazo)
|
||||
setForm({
|
||||
title: prazo.title,
|
||||
description: prazo.description || '',
|
||||
processNumber: prazo.processNumber || '',
|
||||
court: prazo.court || '',
|
||||
deadline: prazo.deadline.split('T')[0],
|
||||
alertDays: prazo.alertDays,
|
||||
priority: prazo.priority,
|
||||
})
|
||||
setShowModal(true)
|
||||
}
|
||||
|
||||
async function handleSave() {
|
||||
if (!form.title || !form.deadline) return
|
||||
setSaving(true)
|
||||
try {
|
||||
if (editingPrazo) {
|
||||
await fetch(`/api/prazos/${editingPrazo.id}`, {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(form),
|
||||
})
|
||||
} else {
|
||||
await fetch('/api/prazos', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(form),
|
||||
})
|
||||
}
|
||||
setShowModal(false)
|
||||
fetchPrazos()
|
||||
} catch (e) {
|
||||
console.error('Failed to save:', e)
|
||||
} finally {
|
||||
setSaving(false)
|
||||
}
|
||||
}
|
||||
|
||||
async function handleStatusChange(id: string, status: string) {
|
||||
try {
|
||||
await fetch(`/api/prazos/${id}`, {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ status }),
|
||||
})
|
||||
fetchPrazos()
|
||||
} catch (e) {
|
||||
console.error('Failed to update status:', e)
|
||||
}
|
||||
}
|
||||
|
||||
async function handleDelete(id: string) {
|
||||
if (!confirm('Tem certeza que deseja excluir este prazo?')) return
|
||||
try {
|
||||
await fetch(`/api/prazos/${id}`, { method: 'DELETE' })
|
||||
fetchPrazos()
|
||||
} catch (e) {
|
||||
console.error('Failed to delete:', e)
|
||||
}
|
||||
}
|
||||
|
||||
function getDaysUntil(deadline: string) {
|
||||
const now = new Date()
|
||||
now.setHours(0, 0, 0, 0)
|
||||
const dl = new Date(deadline)
|
||||
dl.setHours(0, 0, 0, 0)
|
||||
return Math.ceil((dl.getTime() - now.getTime()) / (1000 * 60 * 60 * 24))
|
||||
}
|
||||
|
||||
function formatDate(dateStr: string) {
|
||||
return new Date(dateStr).toLocaleDateString('pt-BR', { day: '2-digit', month: '2-digit', year: 'numeric' })
|
||||
}
|
||||
|
||||
// Stats
|
||||
const stats = {
|
||||
total: prazos.length,
|
||||
pendentes: prazos.filter(p => p.status === 'PENDENTE').length,
|
||||
vencidos: prazos.filter(p => p.status === 'VENCIDO').length,
|
||||
concluidos: prazos.filter(p => p.status === 'CONCLUIDO').length,
|
||||
}
|
||||
|
||||
// Auto-mark overdue
|
||||
const processedPrazos = prazos.map(p => {
|
||||
if (p.status === 'PENDENTE' && getDaysUntil(p.deadline) < 0) {
|
||||
return { ...p, status: 'VENCIDO' as const }
|
||||
}
|
||||
return p
|
||||
})
|
||||
|
||||
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-teal-600/20">
|
||||
<Clock className="h-5 w-5 text-teal-400" />
|
||||
</div>
|
||||
Gestão de Prazos
|
||||
</h1>
|
||||
<p className="mt-1 text-sm text-zinc-500">Gerencie seus prazos processuais e compromissos</p>
|
||||
</div>
|
||||
<button
|
||||
onClick={openNewModal}
|
||||
className="flex items-center gap-2 rounded-xl bg-teal-600 px-4 py-2.5 text-sm font-semibold text-white transition-all hover:bg-teal-500 hover:shadow-lg hover:shadow-teal-500/20"
|
||||
>
|
||||
<Plus className="h-4 w-4" />
|
||||
Novo Prazo
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Stats Cards */}
|
||||
<div className="mb-6 grid grid-cols-2 gap-4 sm:grid-cols-4">
|
||||
{[
|
||||
{ label: 'Total', value: stats.total, color: 'text-white', bg: 'bg-white/5 border-white/10' },
|
||||
{ label: 'Pendentes', value: stats.pendentes, color: 'text-blue-400', bg: 'bg-blue-500/10 border-blue-500/20' },
|
||||
{ label: 'Vencidos', value: stats.vencidos, color: 'text-red-400', bg: 'bg-red-500/10 border-red-500/20' },
|
||||
{ label: 'Concluídos', value: stats.concluidos, color: 'text-green-400', bg: 'bg-green-500/10 border-green-500/20' },
|
||||
].map(s => (
|
||||
<div key={s.label} className={`rounded-xl border ${s.bg} p-4`}>
|
||||
<p className="text-xs font-medium text-zinc-500">{s.label}</p>
|
||||
<p className={`mt-1 text-2xl font-bold ${s.color}`}>{s.value}</p>
|
||||
</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={filterStatus}
|
||||
onChange={e => setFilterStatus(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-teal-500/40"
|
||||
>
|
||||
<option value="">Todos os status</option>
|
||||
<option value="PENDENTE">Pendente</option>
|
||||
<option value="CONCLUIDO">Concluído</option>
|
||||
<option value="VENCIDO">Vencido</option>
|
||||
<option value="CANCELADO">Cancelado</option>
|
||||
</select>
|
||||
<select
|
||||
value={filterPriority}
|
||||
onChange={e => setFilterPriority(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-teal-500/40"
|
||||
>
|
||||
<option value="">Todas as prioridades</option>
|
||||
<option value="ALTA">Alta</option>
|
||||
<option value="MEDIA">Média</option>
|
||||
<option value="BAIXA">Baixa</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{/* Prazos List */}
|
||||
{loading ? (
|
||||
<div className="flex items-center justify-center py-16">
|
||||
<Loader2 className="h-6 w-6 animate-spin text-teal-400" />
|
||||
</div>
|
||||
) : processedPrazos.length === 0 ? (
|
||||
<div className="flex flex-col items-center justify-center rounded-xl border border-white/5 bg-white/[0.02] py-16">
|
||||
<Calendar className="h-12 w-12 text-zinc-600" />
|
||||
<p className="mt-3 text-sm text-zinc-500">Nenhum prazo encontrado</p>
|
||||
<button onClick={openNewModal} className="mt-4 text-sm text-teal-400 hover:text-teal-300">
|
||||
Adicionar primeiro prazo
|
||||
</button>
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-3">
|
||||
{processedPrazos.map(prazo => {
|
||||
const days = getDaysUntil(prazo.deadline)
|
||||
const isUrgent = prazo.status === 'PENDENTE' && days >= 0 && days <= 3
|
||||
const isOverdue = days < 0 && prazo.status === 'PENDENTE'
|
||||
const prio = priorityConfig[prazo.priority]
|
||||
const stat = statusConfig[prazo.status]
|
||||
const StatusIcon = stat.icon
|
||||
|
||||
return (
|
||||
<div
|
||||
key={prazo.id}
|
||||
className={`group relative rounded-xl border bg-white/[0.02] p-4 transition-all hover:bg-white/[0.04] ${
|
||||
isUrgent || isOverdue ? 'border-red-500/30 shadow-[0_0_15px_rgba(239,68,68,0.1)]' : 'border-white/5'
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-start gap-4">
|
||||
{/* Priority indicator */}
|
||||
<div className={`mt-1 h-3 w-3 rounded-full ${prio.dot} flex-shrink-0`} />
|
||||
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-start justify-between gap-3">
|
||||
<div>
|
||||
<h3 className="font-semibold text-white">{prazo.title}</h3>
|
||||
{prazo.description && (
|
||||
<p className="mt-0.5 text-sm text-zinc-500 line-clamp-1">{prazo.description}</p>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-center gap-2 flex-shrink-0">
|
||||
<span className={`rounded-md border px-2 py-0.5 text-[10px] font-bold uppercase ${prio.bg} ${prio.color}`}>
|
||||
{prio.label}
|
||||
</span>
|
||||
<span className={`flex items-center gap-1 rounded-md border px-2 py-0.5 text-[10px] font-bold uppercase ${stat.bg} ${stat.color}`}>
|
||||
<StatusIcon className="h-3 w-3" />
|
||||
{stat.label}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-3 flex flex-wrap items-center gap-x-4 gap-y-1 text-xs text-zinc-500">
|
||||
<span className="flex items-center gap-1">
|
||||
<Calendar className="h-3.5 w-3.5" />
|
||||
{formatDate(prazo.deadline)}
|
||||
</span>
|
||||
{prazo.processNumber && (
|
||||
<span className="flex items-center gap-1">
|
||||
<FileText className="h-3.5 w-3.5" />
|
||||
{prazo.processNumber}
|
||||
</span>
|
||||
)}
|
||||
{prazo.court && (
|
||||
<span className="flex items-center gap-1">
|
||||
<Building2 className="h-3.5 w-3.5" />
|
||||
{prazo.court}
|
||||
</span>
|
||||
)}
|
||||
{prazo.status === 'PENDENTE' && (
|
||||
<span className={`font-semibold ${isOverdue ? 'text-red-400' : isUrgent ? 'text-red-400 animate-pulse' : days <= 7 ? 'text-yellow-400' : 'text-zinc-400'}`}>
|
||||
{isOverdue
|
||||
? `Vencido há ${Math.abs(days)} dia${Math.abs(days) !== 1 ? 's' : ''}`
|
||||
: days === 0
|
||||
? 'Vence hoje!'
|
||||
: days === 1
|
||||
? 'Vence amanhã'
|
||||
: `${days} dias restantes`
|
||||
}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Actions */}
|
||||
<div className="flex items-center gap-1 opacity-0 group-hover:opacity-100 transition-opacity">
|
||||
{prazo.status === 'PENDENTE' && (
|
||||
<button
|
||||
onClick={() => handleStatusChange(prazo.id, 'CONCLUIDO')}
|
||||
className="rounded-lg p-1.5 text-zinc-500 hover:bg-green-500/10 hover:text-green-400"
|
||||
title="Marcar como concluído"
|
||||
>
|
||||
<CheckCircle2 className="h-4 w-4" />
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
onClick={() => openEditModal(prazo)}
|
||||
className="rounded-lg p-1.5 text-zinc-500 hover:bg-white/10 hover:text-zinc-300"
|
||||
title="Editar"
|
||||
>
|
||||
<Edit3 className="h-4 w-4" />
|
||||
</button>
|
||||
<button
|
||||
onClick={() => handleDelete(prazo.id)}
|
||||
className="rounded-lg p-1.5 text-zinc-500 hover:bg-red-500/10 hover:text-red-400"
|
||||
title="Excluir"
|
||||
>
|
||||
<Trash2 className="h-4 w-4" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Modal */}
|
||||
{showModal && (
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/60 backdrop-blur-sm">
|
||||
<div className="mx-4 w-full max-w-lg rounded-2xl border border-white/10 bg-[#0f1620] p-6 shadow-2xl">
|
||||
<div className="flex items-center justify-between mb-6">
|
||||
<h2 className="text-lg font-bold text-white">
|
||||
{editingPrazo ? 'Editar Prazo' : 'Novo Prazo'}
|
||||
</h2>
|
||||
<button onClick={() => setShowModal(false)} className="rounded-lg p-1.5 text-zinc-400 hover:bg-white/5 hover:text-white">
|
||||
<X className="h-5 w-5" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-zinc-400 mb-1">Título *</label>
|
||||
<input
|
||||
type="text"
|
||||
value={form.title}
|
||||
onChange={e => setForm(f => ({ ...f, title: e.target.value }))}
|
||||
className="w-full rounded-lg border border-white/10 bg-white/5 px-3 py-2 text-sm text-white outline-none focus:border-teal-500/40"
|
||||
placeholder="Ex: Contestação - Processo 1234567"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-zinc-400 mb-1">Descrição</label>
|
||||
<textarea
|
||||
value={form.description}
|
||||
onChange={e => setForm(f => ({ ...f, description: e.target.value }))}
|
||||
className="w-full rounded-lg border border-white/10 bg-white/5 px-3 py-2 text-sm text-white outline-none focus:border-teal-500/40"
|
||||
rows={2}
|
||||
placeholder="Detalhes adicionais..."
|
||||
/>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-zinc-400 mb-1">Nº do Processo</label>
|
||||
<input
|
||||
type="text"
|
||||
value={form.processNumber}
|
||||
onChange={e => setForm(f => ({ ...f, processNumber: e.target.value }))}
|
||||
className="w-full rounded-lg border border-white/10 bg-white/5 px-3 py-2 text-sm text-white outline-none focus:border-teal-500/40"
|
||||
placeholder="0000000-00.0000.0.00.0000"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-zinc-400 mb-1">Tribunal/Vara</label>
|
||||
<input
|
||||
type="text"
|
||||
value={form.court}
|
||||
onChange={e => setForm(f => ({ ...f, court: e.target.value }))}
|
||||
className="w-full rounded-lg border border-white/10 bg-white/5 px-3 py-2 text-sm text-white outline-none focus:border-teal-500/40"
|
||||
placeholder="Ex: 1ª Vara Cível"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid grid-cols-3 gap-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-zinc-400 mb-1">Prazo *</label>
|
||||
<input
|
||||
type="date"
|
||||
value={form.deadline}
|
||||
onChange={e => setForm(f => ({ ...f, deadline: e.target.value }))}
|
||||
className="w-full rounded-lg border border-white/10 bg-white/5 px-3 py-2 text-sm text-white outline-none focus:border-teal-500/40"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-zinc-400 mb-1">Alerta (dias)</label>
|
||||
<input
|
||||
type="number"
|
||||
value={form.alertDays}
|
||||
onChange={e => setForm(f => ({ ...f, alertDays: parseInt(e.target.value) || 3 }))}
|
||||
className="w-full rounded-lg border border-white/10 bg-white/5 px-3 py-2 text-sm text-white outline-none focus:border-teal-500/40"
|
||||
min={1}
|
||||
max={30}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-zinc-400 mb-1">Prioridade</label>
|
||||
<select
|
||||
value={form.priority}
|
||||
onChange={e => setForm(f => ({ ...f, priority: e.target.value as 'ALTA' | 'MEDIA' | 'BAIXA' }))}
|
||||
className="w-full rounded-lg border border-white/10 bg-white/5 px-3 py-2 text-sm text-white outline-none focus:border-teal-500/40"
|
||||
>
|
||||
<option value="ALTA">Alta</option>
|
||||
<option value="MEDIA">Média</option>
|
||||
<option value="BAIXA">Baixa</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-6 flex justify-end gap-3">
|
||||
<button
|
||||
onClick={() => setShowModal(false)}
|
||||
className="rounded-lg border border-white/10 px-4 py-2 text-sm text-zinc-400 hover:bg-white/5"
|
||||
>
|
||||
Cancelar
|
||||
</button>
|
||||
<button
|
||||
onClick={handleSave}
|
||||
disabled={!form.title || !form.deadline || saving}
|
||||
className="flex items-center gap-2 rounded-lg bg-teal-600 px-4 py-2 text-sm font-semibold text-white hover:bg-teal-500 disabled:opacity-50"
|
||||
>
|
||||
{saving && <Loader2 className="h-4 w-4 animate-spin" />}
|
||||
{editingPrazo ? 'Salvar' : 'Criar Prazo'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user