sistema-de-chamados/src/app/ticket-view/[token]/page.tsx
esdrasrenan 7ecb4c1110 fix: corrige tipo JSON para String no SQLite e acentuação nos textos
- Altera typePreferences e categoryPreferences de Json para String no Prisma
- Atualiza API de preferências para fazer parse/stringify de JSON
- Corrige todos os textos sem acentuação nos componentes de notificação

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-07 21:09:02 -03:00

340 lines
11 KiB
TypeScript

"use client"
import { useEffect, useState, useCallback } from "react"
import { useParams } from "next/navigation"
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
import { Badge } from "@/components/ui/badge"
import { Button } from "@/components/ui/button"
import { Loader2, ExternalLink, CheckCircle, AlertCircle, Clock, MessageSquare } from "lucide-react"
import { getTicketStatusMeta } from "@/lib/ticket-status-style"
import { getTicketPriorityMeta } from "@/lib/ticket-priority-style"
interface TicketData {
id: string
reference: number
subject: string
summary: string | null
status: string
priority: string
channel: string
createdAt: string
resolvedAt: string | null
requester: {
id: string
name: string
}
assignee: {
id: string
name: string
} | null
company: {
id: string
name: string
} | null
comments: Array<{
id: string
body: string
createdAt: string
author: {
id: string
name: string
}
}>
rating: {
rating: number
comment: string | null
createdAt: string
} | null
}
interface TokenData {
id: string
scope: string
expiresAt: string
used: boolean
}
export default function TicketViewPage() {
const params = useParams()
const token = params.token as string
const [loading, setLoading] = useState(true)
const [error, setError] = useState<string | null>(null)
const [ticket, setTicket] = useState<TicketData | null>(null)
const [tokenData, setTokenData] = useState<TokenData | null>(null)
const [tryingRaven, setTryingRaven] = useState(true)
const [ravenAvailable, setRavenAvailable] = useState(false)
const loadTicket = useCallback(async () => {
try {
setLoading(true)
const response = await fetch(`/api/ticket-access/${token}`)
if (!response.ok) {
const data = await response.json()
throw new Error(data.error || "Erro ao carregar chamado")
}
const data = await response.json()
setTicket(data.ticket)
setTokenData(data.token)
} catch (err) {
setError(err instanceof Error ? err.message : "Erro desconhecido")
} finally {
setLoading(false)
}
}, [token])
// Tenta abrir o protocolo raven://
useEffect(() => {
if (!token) return
const tryRavenProtocol = async () => {
setTryingRaven(true)
// Tenta abrir o protocolo raven://
const ravenUrl = `raven://ticket/${token}`
// Cria um iframe oculto para detectar se o protocolo foi aceito
const iframe = document.createElement("iframe")
iframe.style.display = "none"
document.body.appendChild(iframe)
let protocolHandled = false
// Listener para detectar se o navegador aceitou o protocolo
const handleBlur = () => {
protocolHandled = true
setRavenAvailable(true)
}
window.addEventListener("blur", handleBlur)
// Tenta navegar para o protocolo
try {
window.location.href = ravenUrl
} catch {
// Protocolo não suportado
}
// Aguarda 2 segundos para ver se o protocolo foi aceito
setTimeout(() => {
window.removeEventListener("blur", handleBlur)
document.body.removeChild(iframe)
if (!protocolHandled) {
// Protocolo não foi aceito, carrega o ticket no navegador
setTryingRaven(false)
loadTicket()
}
}, 2000)
}
tryRavenProtocol()
}, [token, loadTicket])
const formatDate = (dateStr: string) => {
return new Date(dateStr).toLocaleString("pt-BR", {
timeZone: "America/Sao_Paulo",
day: "2-digit",
month: "2-digit",
year: "numeric",
hour: "2-digit",
minute: "2-digit",
})
}
// Mostra tela de "Abrindo no Raven..."
if (tryingRaven) {
return (
<div className="min-h-screen bg-background flex items-center justify-center p-4">
<Card className="w-full max-w-md">
<CardContent className="pt-6 text-center">
<Loader2 className="h-12 w-12 animate-spin mx-auto mb-4 text-primary" />
<h2 className="text-xl font-semibold mb-2">Abrindo no Raven...</h2>
<p className="text-muted-foreground text-sm">
Se o aplicativo Raven estiver instalado, ele abrirá automaticamente.
</p>
<p className="text-muted-foreground text-sm mt-2">
Caso contrário, o chamado será exibido aqui em instantes.
</p>
</CardContent>
</Card>
</div>
)
}
// Mostra loading
if (loading) {
return (
<div className="min-h-screen bg-background flex items-center justify-center p-4">
<Card className="w-full max-w-md">
<CardContent className="pt-6 text-center">
<Loader2 className="h-12 w-12 animate-spin mx-auto mb-4 text-primary" />
<h2 className="text-xl font-semibold">Carregando chamado...</h2>
</CardContent>
</Card>
</div>
)
}
// Mostra erro
if (error) {
return (
<div className="min-h-screen bg-background flex items-center justify-center p-4">
<Card className="w-full max-w-md">
<CardContent className="pt-6 text-center">
<AlertCircle className="h-12 w-12 mx-auto mb-4 text-destructive" />
<h2 className="text-xl font-semibold mb-2">Erro ao carregar</h2>
<p className="text-muted-foreground">{error}</p>
<Button className="mt-4" onClick={() => loadTicket()}>
Tentar novamente
</Button>
</CardContent>
</Card>
</div>
)
}
if (!ticket) return null
const statusMeta = getTicketStatusMeta(ticket.status)
const priorityMeta = getTicketPriorityMeta(ticket.priority)
return (
<div className="min-h-screen bg-background p-4">
<div className="max-w-3xl mx-auto space-y-6">
{/* Header */}
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<div className="w-10 h-10 bg-primary rounded-lg flex items-center justify-center">
<span className="text-primary-foreground font-bold text-lg">R</span>
</div>
<span className="text-xl font-semibold">Raven</span>
</div>
{ravenAvailable && (
<Button variant="outline" size="sm" asChild>
<a href={`raven://ticket/${token}`}>
<ExternalLink className="h-4 w-4 mr-2" />
Abrir no Raven
</a>
</Button>
)}
</div>
{/* Ticket Card */}
<Card>
<CardHeader>
<div className="flex items-start justify-between gap-4">
<div>
<div className="flex items-center gap-2 mb-2">
<Badge variant="outline" className="font-mono">
#{ticket.reference}
</Badge>
<Badge className={statusMeta.badgeClass}>
{statusMeta.label}
</Badge>
<Badge className={priorityMeta.badgeClass}>
{priorityMeta.label}
</Badge>
</div>
<CardTitle className="text-xl">{ticket.subject}</CardTitle>
</div>
</div>
</CardHeader>
<CardContent className="space-y-6">
{/* Informações */}
<div className="grid grid-cols-2 gap-4 text-sm">
<div>
<span className="text-muted-foreground">Solicitante</span>
<p className="font-medium">{ticket.requester.name}</p>
</div>
<div>
<span className="text-muted-foreground">Responsável</span>
<p className="font-medium">{ticket.assignee?.name ?? "Não atribuído"}</p>
</div>
<div>
<span className="text-muted-foreground">Criado em</span>
<p className="font-medium">{formatDate(ticket.createdAt)}</p>
</div>
{ticket.resolvedAt && (
<div>
<span className="text-muted-foreground">Resolvido em</span>
<p className="font-medium">{formatDate(ticket.resolvedAt)}</p>
</div>
)}
{ticket.company && (
<div>
<span className="text-muted-foreground">Empresa</span>
<p className="font-medium">{ticket.company.name}</p>
</div>
)}
</div>
{/* Resumo */}
{ticket.summary && (
<div>
<h3 className="text-sm font-medium text-muted-foreground mb-2">Descrição</h3>
<p className="text-sm whitespace-pre-wrap">{ticket.summary}</p>
</div>
)}
{/* Avaliação */}
{ticket.rating && (
<div className="bg-muted/50 rounded-lg p-4">
<div className="flex items-center gap-2 mb-2">
<CheckCircle className="h-4 w-4 text-emerald-500" />
<span className="text-sm font-medium">Avaliação</span>
</div>
<div className="flex items-center gap-1 mb-2">
{[1, 2, 3, 4, 5].map((star) => (
<span
key={star}
className={`text-xl ${star <= ticket.rating!.rating ? "text-amber-400" : "text-muted"}`}
>
</span>
))}
</div>
{ticket.rating.comment && (
<p className="text-sm text-muted-foreground">{ticket.rating.comment}</p>
)}
</div>
)}
{/* Comentários */}
{ticket.comments.length > 0 && (
<div>
<h3 className="text-sm font-medium text-muted-foreground mb-4 flex items-center gap-2">
<MessageSquare className="h-4 w-4" />
Últimas atualizações
</h3>
<div className="space-y-4">
{ticket.comments.map((comment) => (
<div key={comment.id} className="border-l-2 border-primary pl-4">
<div className="flex items-center gap-2 mb-1">
<span className="text-sm font-medium">{comment.author.name}</span>
<span className="text-xs text-muted-foreground flex items-center gap-1">
<Clock className="h-3 w-3" />
{formatDate(comment.createdAt)}
</span>
</div>
<p className="text-sm whitespace-pre-wrap">{comment.body}</p>
</div>
))}
</div>
</div>
)}
</CardContent>
</Card>
{/* Footer */}
<p className="text-center text-xs text-muted-foreground">
Este link expira em {tokenData ? formatDate(tokenData.expiresAt) : "breve"}.
<br />
Para acesso completo, utilize o aplicativo Raven ou acesse o portal.
</p>
</div>
</div>
)
}