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 ( -
-
-

Backlog e Prioridades

-

- Avalie o volume de tickets em aberto, prioridades e filas mais pressionadas. -

-
- -
+ + } + > +
+ +
+
) } 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 ( -
-
-

Relatório de CSAT

-

- Visualize a satisfação dos clientes e identifique pontos de melhoria na entrega. -

-
- -
+ + } + > +
+ +
+
) } 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 ( -
-
-

Relatório de SLA

-

- Acompanhe tempos de resposta, resolução e balanço de filas em tempo real. -

-
- -
+ + } + > +
+ +
+
) } 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." />