diff --git a/web/src/app/login/page.tsx b/web/src/app/login/page.tsx
index 72c7ecb..b21ded6 100644
--- a/web/src/app/login/page.tsx
+++ b/web/src/app/login/page.tsx
@@ -23,10 +23,11 @@ export default function LoginPage() {
const [isHydrated, setIsHydrated] = useState(false)
useEffect(() => {
+ if (isPending) return
if (!session?.user) return
const destination = callbackUrl ?? "/dashboard"
router.replace(destination)
- }, [callbackUrl, router, session?.user])
+ }, [callbackUrl, isPending, router, session?.user])
useEffect(() => {
setIsHydrated(true)
diff --git a/web/src/app/reports/backlog/page.tsx b/web/src/app/reports/backlog/page.tsx
index a8e1448..77715c5 100644
--- a/web/src/app/reports/backlog/page.tsx
+++ b/web/src/app/reports/backlog/page.tsx
@@ -1,17 +1,22 @@
+import { AppShell } from "@/components/app-shell"
import { BacklogReport } from "@/components/reports/backlog-report"
+import { SiteHeader } from "@/components/site-header"
export const dynamic = "force-dynamic"
export default function ReportsBacklogPage() {
return (
-
-
-
-
+
+ }
+ >
+
+
+
+
)
}
diff --git a/web/src/app/reports/csat/page.tsx b/web/src/app/reports/csat/page.tsx
index c16086a..83d0715 100644
--- a/web/src/app/reports/csat/page.tsx
+++ b/web/src/app/reports/csat/page.tsx
@@ -1,17 +1,22 @@
+import { AppShell } from "@/components/app-shell"
import { CsatReport } from "@/components/reports/csat-report"
+import { SiteHeader } from "@/components/site-header"
export const dynamic = "force-dynamic"
export default function ReportsCsatPage() {
return (
-
-
-
-
+
+ }
+ >
+
+
+
+
)
}
diff --git a/web/src/app/reports/sla/page.tsx b/web/src/app/reports/sla/page.tsx
index 32c8341..824f935 100644
--- a/web/src/app/reports/sla/page.tsx
+++ b/web/src/app/reports/sla/page.tsx
@@ -1,17 +1,22 @@
+import { AppShell } from "@/components/app-shell"
import { SlaReport } from "@/components/reports/sla-report"
+import { SiteHeader } from "@/components/site-header"
export const dynamic = "force-dynamic"
export default function ReportsSlaPage() {
return (
-
-
-
-
+
+ }
+ >
+
+
+
+
)
}
diff --git a/web/src/app/tickets/new/page.tsx b/web/src/app/tickets/new/page.tsx
index 334d7c3..868099d 100644
--- a/web/src/app/tickets/new/page.tsx
+++ b/web/src/app/tickets/new/page.tsx
@@ -150,6 +150,7 @@ export default function NewTicketPage() {
className="min-h-[96px] w-full rounded-lg border border-slate-300 bg-white px-3 py-2 text-sm text-neutral-800 shadow-sm outline-none transition-colors focus-visible:border-[#00d6eb] focus-visible:ring-[3px] focus-visible:ring-[#00e8ff]/20"
value={summary}
onChange={(event) => setSummary(event.target.value)}
+ placeholder="Resuma rapidamente o cenário ou impacto do ticket."
/>
diff --git a/web/src/components/app-shell.tsx b/web/src/components/app-shell.tsx
index cb69bb4..acf8b32 100644
--- a/web/src/components/app-shell.tsx
+++ b/web/src/components/app-shell.tsx
@@ -1,6 +1,7 @@
import type { ReactNode } from "react"
-import { AppSidebar } from "@/components/app-sidebar"
+import { AppSidebar } from "@/components/app-sidebar"
+import { AuthGuard } from "@/components/auth/auth-guard"
import { SidebarInset, SidebarProvider } from "@/components/ui/sidebar"
interface AppShellProps {
@@ -13,6 +14,7 @@ export function AppShell({ header, children }: AppShellProps) {
+
{header}
{children}
diff --git a/web/src/components/app-sidebar.tsx b/web/src/components/app-sidebar.tsx
index 8563f66..b878c8d 100644
--- a/web/src/components/app-sidebar.tsx
+++ b/web/src/components/app-sidebar.tsx
@@ -21,19 +21,21 @@ import { usePathname } from "next/navigation"
import { SearchForm } from "@/components/search-form"
import { VersionSwitcher } from "@/components/version-switcher"
-import {
- Sidebar,
- SidebarContent,
- SidebarGroup,
- SidebarGroupContent,
- SidebarGroupLabel,
- SidebarHeader,
- SidebarMenu,
- SidebarMenuButton,
- SidebarMenuItem,
- SidebarRail,
-} from "@/components/ui/sidebar"
+import {
+ Sidebar,
+ SidebarContent,
+ SidebarFooter,
+ SidebarGroup,
+ SidebarGroupContent,
+ SidebarGroupLabel,
+ SidebarHeader,
+ SidebarMenu,
+ SidebarMenuButton,
+ SidebarMenuItem,
+ SidebarRail,
+} from "@/components/ui/sidebar"
import { Skeleton } from "@/components/ui/skeleton"
+import { NavUser } from "@/components/nav-user"
import { useAuth } from "@/lib/auth-client"
import type { LucideIcon } from "lucide-react"
@@ -103,7 +105,7 @@ const navigation: { versions: string[]; navMain: NavigationGroup[] } = {
export function AppSidebar({ ...props }: React.ComponentProps) {
const pathname = usePathname()
- const { isAdmin, isStaff, isCustomer } = useAuth()
+ const { session, isLoading, isAdmin, isStaff, isCustomer } = useAuth()
const [isHydrated, setIsHydrated] = React.useState(false)
React.useEffect(() => {
@@ -194,6 +196,25 @@ export function AppSidebar({ ...props }: React.ComponentProps) {
)
})}
+
+ {isLoading ? (
+
+ ) : (
+
+ )}
+
)
diff --git a/web/src/components/auth/auth-guard.tsx b/web/src/components/auth/auth-guard.tsx
new file mode 100644
index 0000000..07d2c4d
--- /dev/null
+++ b/web/src/components/auth/auth-guard.tsx
@@ -0,0 +1,29 @@
+"use client"
+
+import { useEffect } from "react"
+import { usePathname, useRouter, useSearchParams } from "next/navigation"
+
+import { useAuth } from "@/lib/auth-client"
+
+export function AuthGuard() {
+ const { session, isLoading } = useAuth()
+ const router = useRouter()
+ const pathname = usePathname()
+ const searchParams = useSearchParams()
+
+ useEffect(() => {
+ if (isLoading) return
+ if (session?.user) return
+
+ const search = searchParams?.toString()
+ const callbackUrl = pathname
+ ? search && search.length > 0
+ ? `${pathname}?${search}`
+ : pathname
+ : undefined
+ const nextUrl = callbackUrl ? `/login?callbackUrl=${encodeURIComponent(callbackUrl)}` : "/login"
+ router.replace(nextUrl)
+ }, [isLoading, session?.user, pathname, searchParams, router])
+
+ return null
+}
diff --git a/web/src/components/nav-user.tsx b/web/src/components/nav-user.tsx
index 71e822c..dec4676 100644
--- a/web/src/components/nav-user.tsx
+++ b/web/src/components/nav-user.tsx
@@ -1,110 +1,154 @@
-"use client"
-
-import {
- IconCreditCard,
- IconDotsVertical,
- IconLogout,
- IconNotification,
- IconUserCircle,
-} from "@tabler/icons-react"
-
-import {
- Avatar,
- AvatarFallback,
- AvatarImage,
-} from "@/components/ui/avatar"
-import {
- DropdownMenu,
- DropdownMenuContent,
- DropdownMenuGroup,
- DropdownMenuItem,
- DropdownMenuLabel,
- DropdownMenuSeparator,
- DropdownMenuTrigger,
-} from "@/components/ui/dropdown-menu"
-import {
- SidebarMenu,
- SidebarMenuButton,
- SidebarMenuItem,
- useSidebar,
-} from "@/components/ui/sidebar"
-
-export function NavUser({
- user,
-}: {
- user: {
- name: string
- email: string
- avatar: string
- }
-}) {
- const { isMobile } = useSidebar()
-
- return (
-
-
-
-
-
-
-
- CN
-
-
- {user.name}
-
- {user.email}
-
-
-
-
-
-
-
-
-
-
- CN
-
-
- {user.name}
-
- {user.email}
-
-
-
-
-
-
-
-
- Account
-
-
-
- Billing
-
-
-
- Notifications
-
-
-
-
-
- Log out
-
-
-
-
-
- )
-}
+"use client"
+
+import { useCallback, useMemo, useState } from "react"
+import { useRouter } from "next/navigation"
+import {
+ IconDotsVertical,
+ IconLogout,
+ IconNotification,
+ IconUserCircle,
+} from "@tabler/icons-react"
+import { toast } from "sonner"
+
+import {
+ Avatar,
+ AvatarFallback,
+ AvatarImage,
+} from "@/components/ui/avatar"
+import {
+ DropdownMenu,
+ DropdownMenuContent,
+ DropdownMenuGroup,
+ DropdownMenuItem,
+ DropdownMenuLabel,
+ DropdownMenuSeparator,
+ DropdownMenuTrigger,
+} from "@/components/ui/dropdown-menu"
+import {
+ SidebarMenu,
+ SidebarMenuButton,
+ SidebarMenuItem,
+ useSidebar,
+} from "@/components/ui/sidebar"
+import { signOut } from "@/lib/auth-client"
+
+type NavUserProps = {
+ user?: {
+ name?: string | null
+ email?: string | null
+ avatarUrl?: string | null
+ } | null
+}
+
+export function NavUser({ user }: NavUserProps) {
+ const normalizedUser = user ?? { name: null, email: null, avatarUrl: null }
+ const { isMobile } = useSidebar()
+ const router = useRouter()
+ const [isSigningOut, setIsSigningOut] = useState(false)
+
+ const initials = useMemo(() => {
+ const source = normalizedUser.name?.trim() || normalizedUser.email?.trim() || ""
+ if (!source) return "US"
+ const parts = source.split(" ").filter(Boolean)
+ const firstTwo = parts.slice(0, 2).map((part) => part[0]).join("")
+ if (firstTwo) return firstTwo.toUpperCase()
+ return source.slice(0, 2).toUpperCase()
+ }, [normalizedUser.name, normalizedUser.email])
+
+ const displayName = normalizedUser.name?.trim() || "Usuário"
+ const displayEmail = normalizedUser.email?.trim() || "Sem e-mail definido"
+
+ const handleProfile = useCallback(() => {
+ router.push("/settings")
+ }, [router])
+
+ const handleSignOut = useCallback(async () => {
+ if (isSigningOut) return
+ setIsSigningOut(true)
+ try {
+ await signOut()
+ toast.success("Sessão encerrada.")
+ router.replace("/login")
+ } catch (error) {
+ console.error("Erro ao encerrar sessão", error)
+ toast.error("Não foi possível encerrar a sessão.")
+ } finally {
+ setIsSigningOut(false)
+ }
+ }, [isSigningOut, router])
+
+ return (
+
+
+
+
+
+
+
+ {initials}
+
+
+ {displayName}
+
+ {displayEmail}
+
+
+
+
+
+
+
+
+
+
+ {initials}
+
+
+ {displayName}
+
+ {displayEmail}
+
+
+
+
+
+
+ {
+ event.preventDefault()
+ handleProfile()
+ }}
+ >
+
+ Meu perfil
+
+
+
+ Notificações (em breve)
+
+
+
+ {
+ event.preventDefault()
+ handleSignOut()
+ }}
+ disabled={isSigningOut}
+ >
+
+ {isSigningOut ? "Encerrando…" : "Encerrar sessão"}
+
+
+
+
+
+ )
+}
diff --git a/web/src/components/tickets/new-ticket-dialog.tsx b/web/src/components/tickets/new-ticket-dialog.tsx
index f28602b..5a30e9b 100644
--- a/web/src/components/tickets/new-ticket-dialog.tsx
+++ b/web/src/components/tickets/new-ticket-dialog.tsx
@@ -199,6 +199,7 @@ export function NewTicketDialog() {
id="summary"
className="min-h-[96px] w-full rounded-lg border border-slate-300 bg-background p-3 text-sm shadow-sm focus-visible:border-[#00d6eb] focus-visible:outline-none"
{...form.register("summary")}
+ placeholder="Explique em poucas linhas o contexto do chamado."
/>