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

480 lines
19 KiB
TypeScript

'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"> 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>
)
}