diff --git a/src/app/api/profile/avatar/route.ts b/src/app/api/profile/avatar/route.ts index cdb4d38..7d7e9b6 100644 --- a/src/app/api/profile/avatar/route.ts +++ b/src/app/api/profile/avatar/route.ts @@ -5,7 +5,6 @@ */ import { NextRequest, NextResponse } from "next/server" -import { headers } from "next/headers" import { getServerSession } from "@/lib/auth-server" import { auth } from "@/lib/auth" @@ -18,6 +17,23 @@ import type { Id } from "@/convex/_generated/dataModel" const MAX_FILE_SIZE = 5 * 1024 * 1024 // 5MB const ALLOWED_TYPES = ["image/jpeg", "image/png", "image/webp", "image/gif"] +function extractSetCookies(headers: Headers) { + const headersWithGetSetCookie = headers as Headers & { getSetCookie?: () => string[] | undefined } + let setCookieHeaders = + typeof headersWithGetSetCookie.getSetCookie === "function" + ? headersWithGetSetCookie.getSetCookie() ?? [] + : [] + + if (setCookieHeaders.length === 0) { + const single = headers.get("set-cookie") + if (single) { + setCookieHeaders = [single] + } + } + + return setCookieHeaders +} + export async function POST(request: NextRequest) { try { const session = await getServerSession() @@ -75,15 +91,23 @@ export async function POST(request: NextRequest) { return NextResponse.json({ error: "Erro ao obter URL do avatar" }, { status: 500 }) } - // Usa updateUser do Better Auth para invalidar o cache da sessao - const headerList = await headers() - const updateResult = await auth.api.updateUser({ - headers: headerList, - body: { avatarUrl }, - }) + // Atualiza no Better Auth e propaga Set-Cookie para invalidar o cookieCache da sessao (avatarUrl, etc) + let authSetCookies: string[] = [] + try { + const updateResponse = await auth.api.updateUser({ + request, + headers: request.headers, + body: { avatarUrl }, + asResponse: true, + }) - if (!updateResult) { - // Fallback: atualiza diretamente no Prisma + if (!updateResponse?.ok) { + throw new Error(`Falha ao atualizar usuario no Better Auth (${updateResponse?.status ?? "sem status"})`) + } + + authSetCookies = extractSetCookies(updateResponse.headers) + } catch (error) { + console.warn("[profile/avatar] Falha ao atualizar sessao via Better Auth, usando fallback Prisma:", error) await prisma.authUser.update({ where: { id: session.user.id }, data: { avatarUrl }, @@ -102,17 +126,23 @@ export async function POST(request: NextRequest) { console.warn("[profile/avatar] Falha ao sincronizar avatar no Convex:", error) } - return NextResponse.json({ + const response = NextResponse.json({ success: true, avatarUrl, }) + + for (const cookie of authSetCookies) { + response.headers.append("set-cookie", cookie) + } + + return response } catch (error) { console.error("[profile/avatar] Erro:", error) return NextResponse.json({ error: "Erro interno do servidor" }, { status: 500 }) } } -export async function DELETE() { +export async function DELETE(request: NextRequest) { try { const session = await getServerSession() @@ -120,15 +150,23 @@ export async function DELETE() { return NextResponse.json({ error: "Não autorizado" }, { status: 401 }) } - // Usa updateUser do Better Auth para invalidar o cache da sessao - const headerList = await headers() - const updateResult = await auth.api.updateUser({ - headers: headerList, - body: { avatarUrl: null }, - }) + // Atualiza no Better Auth e propaga Set-Cookie para invalidar o cookieCache da sessao (avatarUrl, etc) + let authSetCookies: string[] = [] + try { + const updateResponse = await auth.api.updateUser({ + request, + headers: request.headers, + body: { avatarUrl: null }, + asResponse: true, + }) - if (!updateResult) { - // Fallback: atualiza diretamente no Prisma + if (!updateResponse?.ok) { + throw new Error(`Falha ao atualizar usuario no Better Auth (${updateResponse?.status ?? "sem status"})`) + } + + authSetCookies = extractSetCookies(updateResponse.headers) + } catch (error) { + console.warn("[profile/avatar] Falha ao atualizar sessao via Better Auth, usando fallback Prisma:", error) await prisma.authUser.update({ where: { id: session.user.id }, data: { avatarUrl: null }, @@ -148,10 +186,16 @@ export async function DELETE() { console.warn("[profile/avatar] Falha ao sincronizar remoção de avatar no Convex:", error) } - return NextResponse.json({ + const response = NextResponse.json({ success: true, message: "Foto removida com sucesso", }) + + for (const cookie of authSetCookies) { + response.headers.append("set-cookie", cookie) + } + + return response } catch (error) { console.error("[profile/avatar] Erro ao remover:", error) return NextResponse.json({ error: "Erro interno do servidor" }, { status: 500 }) diff --git a/src/components/portal/portal-ticket-form.tsx b/src/components/portal/portal-ticket-form.tsx index 081bb7d..06a0690 100644 --- a/src/components/portal/portal-ticket-form.tsx +++ b/src/components/portal/portal-ticket-form.tsx @@ -13,6 +13,7 @@ import type { TicketPriority } from "@/lib/schemas/ticket" import { sanitizeEditorHtml } from "@/components/ui/rich-text-editor" import { useAuth } from "@/lib/auth-client" import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card" +import { Skeleton } from "@/components/ui/skeleton" import { Input } from "@/components/ui/input" import { Button } from "@/components/ui/button" import { CategorySelectFields } from "@/components/tickets/category-select" @@ -144,6 +145,7 @@ export function PortalTicketForm() { return true }, [subject, description, categoryId, subcategoryId, machineInactive, customFieldsInvalid]) const isViewerReady = Boolean(viewerId) + const isFormLoading = machineContextLoading || (viewerId && formsRemote === undefined) const viewerErrorMessage = useMemo(() => { if (!machineContextError) return null const suffix = machineContextError.status ? ` (status ${machineContextError.status})` : "" @@ -239,6 +241,51 @@ export function PortalTicketForm() { } } + if (isFormLoading) { + return ( + + + Abrir novo chamado + + +
+ + + +
+
+
+ + +
+
+ + +
+
+
+
+ + +
+
+ + +
+
+
+ + +
+
+ + +
+
+
+ ) + } + return ( @@ -279,7 +326,7 @@ export function PortalTicketForm() { ))} - {selectedForm?.description ? ( + {selectedForm?.description && selectedFormKey !== "default" ? (

{selectedForm.description}

) : null} @@ -322,8 +369,8 @@ export function PortalTicketForm() { onCategoryChange={setCategoryId} onSubcategoryChange={setSubcategoryId} layout="stacked" - categoryLabel="Categoria *" - subcategoryLabel="Subcategoria *" + categoryRequired + subcategoryRequired secondaryEmptyLabel="Selecione uma categoria" /> {selectedForm.fields.length > 0 ? ( diff --git a/src/components/tickets/category-select.tsx b/src/components/tickets/category-select.tsx index 679e860..32bcd52 100644 --- a/src/components/tickets/category-select.tsx +++ b/src/components/tickets/category-select.tsx @@ -23,6 +23,8 @@ interface CategorySelectProps { disabled?: boolean categoryLabel?: string subcategoryLabel?: string + categoryRequired?: boolean + subcategoryRequired?: boolean className?: string secondaryEmptyLabel?: string layout?: "grid" | "stacked" @@ -41,9 +43,11 @@ export function CategorySelectFields({ onSubcategoryChange, autoSelectFirst = true, disabled = false, - categoryLabel = "Primária", - subcategoryLabel = "Secundária", - secondaryEmptyLabel = "Selecione uma categoria primária", + categoryLabel = "Categoria", + subcategoryLabel = "Subcategoria", + categoryRequired = false, + subcategoryRequired = false, + secondaryEmptyLabel = "Selecione uma categoria", className, layout = "grid", }: CategorySelectProps) { @@ -80,9 +84,11 @@ export function CategorySelectFields({ return (
-
-