182 lines
7.6 KiB
TypeScript
182 lines
7.6 KiB
TypeScript
"use client"
|
|
|
|
import { type ReactNode, useMemo, useState } from "react"
|
|
import Image from "next/image"
|
|
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 },
|
|
{ label: "Perfil", href: "/portal/profile" },
|
|
]
|
|
|
|
export function PortalShell({ children }: PortalShellProps) {
|
|
const pathname = usePathname()
|
|
const router = useRouter()
|
|
const { session, machineContext, machineContextError, machineContextLoading } = useAuth()
|
|
const [isSigningOut, setIsSigningOut] = useState(false)
|
|
|
|
const isMachineSession = session?.user.role === "machine" || Boolean(machineContext)
|
|
const personaValue = machineContext?.persona ?? session?.user.machinePersona ?? null
|
|
const collaboratorName = machineContext?.assignedUserName?.trim() ?? ""
|
|
const collaboratorEmail = machineContext?.assignedUserEmail?.trim() ?? ""
|
|
const userName = session?.user.name?.trim() ?? ""
|
|
const userEmail = session?.user.email?.trim() ?? ""
|
|
const displayName = collaboratorName || userName || collaboratorEmail || userEmail || "Cliente"
|
|
const displayEmail = collaboratorEmail || userEmail
|
|
const personaLabel = personaValue === "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)
|
|
}
|
|
}
|
|
|
|
const isNavItemActive = (itemHref: string) => {
|
|
if (itemHref === "/portal/tickets") {
|
|
if (pathname === "/portal" || pathname === "/portal/tickets") return true
|
|
if (/^\/portal\/tickets\/[A-Za-z0-9_-]+$/.test(pathname) && !pathname.endsWith("/new")) return true
|
|
return false
|
|
}
|
|
return pathname === itemHref
|
|
}
|
|
|
|
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 flex-wrap items-center justify-between gap-4 px-6 py-4">
|
|
<div className="flex items-center gap-3">
|
|
<div className="flex h-12 w-12 items-center justify-center">
|
|
<Image
|
|
src="/logo-raven.png"
|
|
alt="Logotipo Raven"
|
|
width={48}
|
|
height={48}
|
|
className="h-12 w-12 object-contain"
|
|
priority
|
|
/>
|
|
</div>
|
|
<div className="flex flex-col">
|
|
<span className="text-sm text-neutral-500">
|
|
Portal do Cliente
|
|
</span>
|
|
<span className="text-lg font-semibold text-neutral-900">Raven</span>
|
|
</div>
|
|
</div>
|
|
<nav className="flex w-full flex-wrap items-center gap-2 text-sm font-medium sm:w-auto sm:justify-center">
|
|
{navItems.map((item) => {
|
|
const isActive = isNavItemActive(item.href)
|
|
const Icon = item.icon
|
|
return (
|
|
<Link
|
|
key={item.href}
|
|
href={item.href}
|
|
className={cn(
|
|
"inline-flex w-full items-center justify-center gap-2 rounded-full px-4 py-2 transition sm:w-auto",
|
|
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 w-full flex-col items-start gap-3 sm:w-auto sm:flex-row sm:items-center">
|
|
<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 || "Sem e-mail definido"}</span>
|
|
{personaValue ? (
|
|
<span className="text-[10px] uppercase tracking-wide text-neutral-400">{personaLabel}</span>
|
|
) : null}
|
|
</div>
|
|
</div>
|
|
{!isMachineSession ? (
|
|
<Button
|
|
size="sm"
|
|
variant="outline"
|
|
onClick={handleSignOut}
|
|
disabled={isSigningOut}
|
|
className="inline-flex items-center gap-2 self-stretch sm:self-auto"
|
|
>
|
|
<LogOut className="size-4" />
|
|
Sair
|
|
</Button>
|
|
) : null}
|
|
</div>
|
|
</div>
|
|
</header>
|
|
<main className="mx-auto flex w-full max-w-6xl flex-1 flex-col gap-6 px-6 py-8">
|
|
{machineContextError ? (
|
|
<div className="rounded-xl border border-red-200 bg-red-50 px-4 py-3 text-sm text-red-700 shadow-sm">
|
|
<p className="font-semibold text-red-800">Falha ao carregar os dados do colaborador vinculado.</p>
|
|
<p className="mt-1 text-xs text-red-700/80">
|
|
{machineContextError.message}
|
|
{machineContextError.status ? ` (status ${machineContextError.status})` : null}
|
|
</p>
|
|
{machineContextError.details && Object.keys(machineContextError.details).length > 0 ? (
|
|
<pre className="mt-2 overflow-x-auto rounded-lg bg-red-100 px-3 py-2 text-[11px] leading-tight text-red-800">
|
|
{JSON.stringify(machineContextError.details, null, 2)}
|
|
</pre>
|
|
) : null}
|
|
</div>
|
|
) : null}
|
|
{!machineContextError && machineContextLoading ? (
|
|
<div className="rounded-xl border border-slate-200 bg-slate-50 px-4 py-3 text-sm text-neutral-600 shadow-sm">
|
|
Recuperando dados do colaborador vinculado...
|
|
</div>
|
|
) : null}
|
|
{children}
|
|
</main>
|
|
<footer className="border-t border-slate-200 bg-white/70">
|
|
<div className="mx-auto flex w-full max-w-6xl flex-wrap items-center justify-between gap-3 px-6 py-4 text-xs text-neutral-500">
|
|
<span>© {new Date().getFullYear()} Raven — Desenvolvido pela Rever Tecnologia</span>
|
|
<span>
|
|
Suporte:{" "}
|
|
<a href="mailto:suporte@rever.com.br" className="font-medium text-neutral-600 hover:text-neutral-800">
|
|
suporte@rever.com.br
|
|
</a>
|
|
</span>
|
|
</div>
|
|
</footer>
|
|
</div>
|
|
)
|
|
}
|