sistema-de-chamados/src/components/portal/portal-shell.tsx

128 lines
4.9 KiB
TypeScript

"use client"
import { type ReactNode, useMemo, useState } from "react"
import Link from "next/link"
import { usePathname, useRouter } from "next/navigation"
import { LogOut, PlusCircle } from "lucide-react"
import { toast } from "sonner"
import { Button } from "@/components/ui/button"
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"
import { cn } from "@/lib/utils"
import { useAuth, signOut } from "@/lib/auth-client"
interface PortalShellProps {
children: ReactNode
}
const navItems = [
{ label: "Meus chamados", href: "/portal/tickets" },
{ label: "Abrir chamado", href: "/portal/tickets/new", icon: PlusCircle },
]
export function PortalShell({ children }: PortalShellProps) {
const pathname = usePathname()
const router = useRouter()
const { session, machineContext } = useAuth()
const [isSigningOut, setIsSigningOut] = useState(false)
const displayName = machineContext?.assignedUserName ?? session?.user.name ?? session?.user.email ?? "Cliente"
const displayEmail = machineContext?.assignedUserEmail ?? session?.user.email ?? ""
const personaLabel = machineContext?.persona === "manager" ? "Gestor" : "Colaborador"
const initials = useMemo(() => {
const name = displayName || displayEmail || "Cliente"
return name
.split(" ")
.slice(0, 2)
.map((part) => part.charAt(0).toUpperCase())
.join("")
}, [displayName, displayEmail])
async function handleSignOut() {
if (isSigningOut) return
setIsSigningOut(true)
toast.loading("Encerrando sessão...", { id: "portal-signout" })
try {
await signOut()
toast.success("Sessão encerrada", { id: "portal-signout" })
router.replace("/login")
} catch (error) {
console.error(error)
toast.error("Não foi possível encerrar a sessão", { id: "portal-signout" })
} finally {
setIsSigningOut(false)
}
}
return (
<div className="flex min-h-screen flex-col bg-gradient-to-b from-slate-50 via-slate-50 to-white">
<header className="border-b border-slate-200 bg-white/90 backdrop-blur">
<div className="mx-auto flex w-full max-w-6xl items-center justify-between gap-4 px-6 py-4">
<div className="flex flex-col">
<span className="text-xs font-semibold uppercase tracking-[0.28em] text-neutral-500">
Portal do cliente
</span>
<span className="text-lg font-semibold text-neutral-900">Raven</span>
</div>
<nav className="flex items-center gap-3 text-sm font-medium">
{navItems.map((item) => {
const isActive = pathname === item.href || pathname.startsWith(`${item.href}/`)
const Icon = item.icon
return (
<Link
key={item.href}
href={item.href}
className={cn(
"inline-flex items-center gap-2 rounded-full px-4 py-2 transition",
isActive
? "bg-neutral-900 text-white shadow-sm"
: "bg-transparent text-neutral-700 hover:bg-neutral-100"
)}
>
{Icon ? <Icon className="size-4" /> : null}
{item.label}
</Link>
)
})}
</nav>
<div className="flex items-center gap-3">
<div className="flex items-center gap-2 text-sm">
<Avatar className="size-9 border border-slate-200">
<AvatarImage src={session?.user.avatarUrl ?? undefined} alt={displayName ?? ""} />
<AvatarFallback>{initials}</AvatarFallback>
</Avatar>
<div className="flex flex-col leading-tight">
<span className="font-semibold text-neutral-900">{displayName}</span>
<span className="text-xs text-neutral-500">{displayEmail}</span>
{machineContext ? (
<span className="text-[10px] uppercase tracking-wide text-neutral-400">{personaLabel}</span>
) : null}
</div>
</div>
<Button
size="sm"
variant="outline"
onClick={handleSignOut}
disabled={isSigningOut}
className="inline-flex items-center gap-2"
>
<LogOut className="size-4" />
Sair
</Button>
</div>
</div>
</header>
<main className="mx-auto flex w-full max-w-6xl flex-1 flex-col gap-6 px-6 py-8">
{null}
{children}
</main>
<footer className="border-t border-slate-200 bg-white/70">
<div className="mx-auto flex w-full max-w-6xl items-center justify-between px-6 py-4 text-xs text-neutral-500">
<span>&copy; {new Date().getFullYear()} Raven</span>
<span>Suporte: suporte@sistema.dev</span>
</div>
</footer>
</div>
)
}