feat: improve company forms, phone input, and auth redirects

This commit is contained in:
Esdras Renan 2025-10-16 23:35:20 -03:00
parent 6962d5e5b5
commit 604216ddec
3 changed files with 57 additions and 17 deletions

View file

@ -15,7 +15,7 @@ export async function middleware(request: NextRequest) {
return new NextResponse("Invalid Host header", { status: 403 }) return new NextResponse("Invalid Host header", { status: 403 })
} }
const { pathname, search } = request.nextUrl const { pathname, searchParams, search } = request.nextUrl
if (pathname.startsWith("/api")) { if (pathname.startsWith("/api")) {
return NextResponse.next() return NextResponse.next()
@ -49,6 +49,18 @@ export async function middleware(request: NextRequest) {
? ((session.user as unknown as { machinePersona?: string }).machinePersona ?? "").toLowerCase() ? ((session.user as unknown as { machinePersona?: string }).machinePersona ?? "").toLowerCase()
: null : null
if (pathname === "/login") {
const callback = searchParams.get("callbackUrl") ?? undefined
const defaultDestination =
role === "machine"
? machinePersona === "manager"
? "/dashboard"
: "/portal/tickets"
: APP_HOME
const target = callback && !callback.startsWith("/login") ? callback : defaultDestination
return NextResponse.redirect(new URL(target, request.url))
}
// Ajusta destinos conforme persona da máquina para evitar loops login<->dashboard // Ajusta destinos conforme persona da máquina para evitar loops login<->dashboard
if (role === "machine") { if (role === "machine") {
// Evita enviar colaborador ao dashboard; redireciona para o Portal // Evita enviar colaborador ao dashboard; redireciona para o Portal
@ -56,11 +68,6 @@ export async function middleware(request: NextRequest) {
return NextResponse.redirect(new URL("/portal/tickets", request.url)) return NextResponse.redirect(new URL("/portal/tickets", request.url))
} }
// Evita mostrar login quando já há sessão de máquina // Evita mostrar login quando já há sessão de máquina
if (pathname === "/login") {
const target = machinePersona === "manager" ? "/dashboard" : "/portal/tickets"
const url = new URL(target, request.url)
return NextResponse.redirect(url)
}
} }
const isAdmin = role === "admin" const isAdmin = role === "admin"

View file

@ -1,6 +1,6 @@
"use client" "use client"
import { useCallback, useEffect, useMemo, useRef, useState, useTransition } from "react" import { useCallback, useEffect, useMemo, useRef, useState, useTransition, useId } from "react"
import { formatDistanceToNow } from "date-fns" import { formatDistanceToNow } from "date-fns"
import { ptBR } from "date-fns/locale" import { ptBR } from "date-fns/locale"
import { useQuery } from "convex/react" import { useQuery } from "convex/react"
@ -86,6 +86,15 @@ export function AdminCompaniesManager({ initialCompanies }: { initialCompanies:
const [searchTerm, setSearchTerm] = useState("") const [searchTerm, setSearchTerm] = useState("")
const isMobile = useIsMobile() const isMobile = useIsMobile()
const nameId = useId()
const slugId = useId()
const descriptionId = useId()
const cnpjId = useId()
const domainId = useId()
const phoneId = useId()
const addressId = useId()
const hoursId = useId()
const machinesQuery = useQuery(api.machines.listByTenant, { includeMetadata: false }) as MachineSummary[] | undefined const machinesQuery = useQuery(api.machines.listByTenant, { includeMetadata: false }) as MachineSummary[] | undefined
const machinesByCompanyId = useMemo(() => { const machinesByCompanyId = useMemo(() => {
const map = new Map<string, MachineSummary[]>() const map = new Map<string, MachineSummary[]>()
@ -304,60 +313,80 @@ export function AdminCompaniesManager({ initialCompanies }: { initialCompanies:
<CardContent> <CardContent>
<form onSubmit={handleSubmit} className="grid grid-cols-1 gap-4 md:grid-cols-2"> <form onSubmit={handleSubmit} className="grid grid-cols-1 gap-4 md:grid-cols-2">
<div className="grid gap-2"> <div className="grid gap-2">
<Label>Nome</Label> <Label htmlFor={nameId}>Nome</Label>
<Input <Input
id={nameId}
name="companyName"
value={form.name ?? ""} value={form.name ?? ""}
onChange={(e) => setForm((p) => ({ ...p, name: e.target.value }))} onChange={(e) => setForm((p) => ({ ...p, name: e.target.value }))}
placeholder="Nome da empresa ou apelido interno" placeholder="Nome da empresa ou apelido interno"
/> />
</div> </div>
<div className="grid gap-2"> <div className="grid gap-2">
<Label>Slug</Label> <Label htmlFor={slugId}>Slug</Label>
<Input <Input
id={slugId}
name="companySlug"
value={form.slug ?? ""} value={form.slug ?? ""}
onChange={(e) => setForm((p) => ({ ...p, slug: e.target.value }))} onChange={(e) => setForm((p) => ({ ...p, slug: e.target.value }))}
placeholder="empresa-exemplo" placeholder="empresa-exemplo"
/> />
</div> </div>
<div className="grid gap-2 md:col-span-2"> <div className="grid gap-2 md:col-span-2">
<Label>Descrição</Label> <Label htmlFor={descriptionId}>Descrição</Label>
<Input value={form.description ?? ""} onChange={(e) => setForm((p) => ({ ...p, description: e.target.value }))} placeholder="Resumo, segmento ou observações internas" /> <Input
id={descriptionId}
name="companyDescription"
value={form.description ?? ""}
onChange={(e) => setForm((p) => ({ ...p, description: e.target.value }))}
placeholder="Resumo, segmento ou observações internas"
/>
</div> </div>
<div className="grid gap-2"> <div className="grid gap-2">
<Label>CNPJ</Label> <Label htmlFor={cnpjId}>CNPJ</Label>
<Input <Input
id={cnpjId}
name="companyCnpj"
value={form.cnpj ?? ""} value={form.cnpj ?? ""}
onChange={(e) => setForm((p) => ({ ...p, cnpj: e.target.value }))} onChange={(e) => setForm((p) => ({ ...p, cnpj: e.target.value }))}
placeholder="00.000.000/0000-00" placeholder="00.000.000/0000-00"
/> />
</div> </div>
<div className="grid gap-2"> <div className="grid gap-2">
<Label>Domínio</Label> <Label htmlFor={domainId}>Domínio</Label>
<Input <Input
id={domainId}
name="companyDomain"
value={form.domain ?? ""} value={form.domain ?? ""}
onChange={(e) => setForm((p) => ({ ...p, domain: e.target.value }))} onChange={(e) => setForm((p) => ({ ...p, domain: e.target.value }))}
placeholder="empresa.com.br" placeholder="empresa.com.br"
/> />
</div> </div>
<div className="grid gap-2"> <div className="grid gap-2">
<Label>Telefone</Label> <Label htmlFor={phoneId}>Telefone</Label>
<PhoneInput <PhoneInput
id={phoneId}
name="companyPhone"
value={form.phone ?? ""} value={form.phone ?? ""}
onChange={(value) => setForm((p) => ({ ...p, phone: value || null }))} onChange={(value) => setForm((p) => ({ ...p, phone: value || null }))}
/> />
</div> </div>
<div className="grid gap-2 md:col-span-2"> <div className="grid gap-2 md:col-span-2">
<Label>Endereço</Label> <Label htmlFor={addressId}>Endereço</Label>
<Input <Input
id={addressId}
name="companyAddress"
value={form.address ?? ""} value={form.address ?? ""}
onChange={(e) => setForm((p) => ({ ...p, address: e.target.value }))} onChange={(e) => setForm((p) => ({ ...p, address: e.target.value }))}
placeholder="Rua, número, bairro, cidade/UF" placeholder="Rua, número, bairro, cidade/UF"
/> />
</div> </div>
<div className="grid gap-2"> <div className="grid gap-2">
<Label>Horas contratadas/mês</Label> <Label htmlFor={hoursId}>Horas contratadas/mês</Label>
<Input <Input
type="number" type="number"
id={hoursId}
name="companyHours"
min={0} min={0}
step="0.25" step="0.25"
value={form.contractedHoursPerMonth ?? ""} value={form.contractedHoursPerMonth ?? ""}

View file

@ -91,7 +91,7 @@ function placeholderFor(country: CountryOption): string {
return "Número de telefone" return "Número de telefone"
} }
export function PhoneInput({ value, onChange, className }: PhoneInputProps) { export function PhoneInput({ value, onChange, className, id, name }: PhoneInputProps) {
const [selectedCountry, setSelectedCountry] = useState<CountryOption>(DEFAULT_COUNTRY) const [selectedCountry, setSelectedCountry] = useState<CountryOption>(DEFAULT_COUNTRY)
const [localDigits, setLocalDigits] = useState<string>("") const [localDigits, setLocalDigits] = useState<string>("")
@ -144,6 +144,8 @@ export function PhoneInput({ value, onChange, className }: PhoneInputProps) {
</SelectContent> </SelectContent>
</Select> </Select>
<Input <Input
id={id}
name={name}
type="tel" type="tel"
value={displayValue} value={displayValue}
onChange={handleInputChange} onChange={handleInputChange}
@ -158,6 +160,8 @@ export type PhoneInputProps = {
value?: string | null value?: string | null
onChange?: (value: string) => void onChange?: (value: string) => void
className?: string className?: string
id?: string
name?: string
} }
export function formatPhoneDisplay(rawValue?: string | null): string | null { export function formatPhoneDisplay(rawValue?: string | null): string | null {