Fix company search filters and build regressions

This commit is contained in:
Esdras Renan 2025-10-13 14:18:57 -03:00
parent a8abb68e36
commit 11efad0312
3 changed files with 71 additions and 32 deletions

View file

@ -1,5 +1,6 @@
import { ConvexHttpClient } from "convex/browser" import { ConvexHttpClient } from "convex/browser"
import { Prisma } from "@prisma/client"
import { api } from "@/convex/_generated/api" import { api } from "@/convex/_generated/api"
import { DEFAULT_TENANT_ID } from "@/lib/constants" import { DEFAULT_TENANT_ID } from "@/lib/constants"
import { env } from "@/lib/env" import { env } from "@/lib/env"
@ -59,18 +60,31 @@ export async function GET(request: Request) {
const search = url.searchParams.get("search")?.trim() ?? "" const search = url.searchParams.get("search")?.trim() ?? ""
try { try {
const slugSearch = search ? normalizeSlug(search) ?? slugify(search) : null
const orFilters: Prisma.CompanyWhereInput[] = []
if (search) {
orFilters.push({
name: {
contains: search,
mode: Prisma.QueryMode.insensitive,
} as unknown as Prisma.StringFilter<"Company">,
})
if (slugSearch) {
orFilters.push({
slug: {
contains: slugSearch,
mode: Prisma.QueryMode.insensitive,
} as unknown as Prisma.StringFilter<"Company">,
})
}
}
const where: Prisma.CompanyWhereInput = {
tenantId,
...(orFilters.length > 0 ? { OR: orFilters } : {}),
}
const companies = await prisma.company.findMany({ const companies = await prisma.company.findMany({
where: { where,
tenantId,
...(search
? {
OR: [
{ name: { contains: search, mode: "insensitive" } },
{ slug: { contains: normalizeSlug(search) ?? slugify(search), mode: "insensitive" } },
],
}
: {}),
},
orderBy: { name: "asc" }, orderBy: { name: "asc" },
take: 20, take: 20,
}) })

View file

@ -21,11 +21,13 @@ import { Sheet, SheetContent, SheetDescription, SheetFooter, SheetHeader, SheetT
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs" import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
import { ROLE_OPTIONS, type RoleOption } from "@/lib/authz" import { ROLE_OPTIONS, type RoleOption } from "@/lib/authz"
type AdminRole = RoleOption | "machine"
type AdminUser = { type AdminUser = {
id: string id: string
email: string email: string
name: string name: string
role: RoleOption role: AdminRole
tenantId: string tenantId: string
createdAt: string createdAt: string
updatedAt: string | null updatedAt: string | null
@ -61,7 +63,7 @@ type CompanyOption = {
type Props = { type Props = {
initialUsers: AdminUser[] initialUsers: AdminUser[]
initialInvites: AdminInvite[] initialInvites: AdminInvite[]
roleOptions: readonly RoleOption[] roleOptions: readonly AdminRole[]
defaultTenantId: string defaultTenantId: string
} }
@ -113,6 +115,11 @@ function sanitizeInvite(invite: AdminInvite & { events?: unknown }): AdminInvite
return rest return rest
} }
function coerceRole(role: AdminRole | string | null | undefined): RoleOption {
const candidate = (role ?? "agent").toLowerCase()
return (ROLE_OPTIONS as readonly string[]).includes(candidate) ? (candidate as RoleOption) : "agent"
}
export function AdminUsersManager({ initialUsers, initialInvites, roleOptions, defaultTenantId }: Props) { export function AdminUsersManager({ initialUsers, initialInvites, roleOptions, defaultTenantId }: Props) {
const [users, setUsers] = useState<AdminUser[]>(initialUsers) const [users, setUsers] = useState<AdminUser[]>(initialUsers)
const [invites, setInvites] = useState<AdminInvite[]>(initialInvites) const [invites, setInvites] = useState<AdminInvite[]>(initialInvites)
@ -144,7 +151,17 @@ export function AdminUsersManager({ initialUsers, initialInvites, roleOptions, d
const [isResettingPassword, setIsResettingPassword] = useState(false) const [isResettingPassword, setIsResettingPassword] = useState(false)
const [passwordPreview, setPasswordPreview] = useState<string | null>(null) const [passwordPreview, setPasswordPreview] = useState<string | null>(null)
const normalizedRoles = useMemo(() => roleOptions ?? ROLE_OPTIONS, [roleOptions]) const normalizedRoles = useMemo<readonly AdminRole[]>(() => {
return (roleOptions && roleOptions.length > 0 ? roleOptions : ROLE_OPTIONS) as readonly AdminRole[]
}, [roleOptions])
const selectableRoles = useMemo(() => {
const unique = new Set<RoleOption>()
normalizedRoles.forEach((roleOption) => {
const coerced = coerceRole(roleOption)
unique.add(coerced)
})
return Array.from(unique)
}, [normalizedRoles])
const teamUsers = useMemo(() => users.filter((user) => user.role !== "machine"), [users]) const teamUsers = useMemo(() => users.filter((user) => user.role !== "machine"), [users])
const machineUsers = useMemo(() => users.filter((user) => user.role === "machine"), [users]) const machineUsers = useMemo(() => users.filter((user) => user.role === "machine"), [users])
@ -175,7 +192,7 @@ export function AdminUsersManager({ initialUsers, initialInvites, roleOptions, d
setEditForm({ setEditForm({
name: editUser.name || "", name: editUser.name || "",
email: editUser.email, email: editUser.email,
role: editUser.role, role: coerceRole(editUser.role),
tenantId: editUser.tenantId || defaultTenantId, tenantId: editUser.tenantId || defaultTenantId,
companyId: editUser.companyId ?? "", companyId: editUser.companyId ?? "",
}) })
@ -567,13 +584,11 @@ export function AdminUsersManager({ initialUsers, initialInvites, roleOptions, d
<SelectValue placeholder="Selecione" /> <SelectValue placeholder="Selecione" />
</SelectTrigger> </SelectTrigger>
<SelectContent> <SelectContent>
{normalizedRoles {selectableRoles.map((option) => (
.filter((option) => option !== "machine") <SelectItem key={option} value={option}>
.map((option) => ( {formatRole(option)}
<SelectItem key={option} value={option}> </SelectItem>
{formatRole(option)} ))}
</SelectItem>
))}
</SelectContent> </SelectContent>
</Select> </Select>
</div> </div>
@ -693,7 +708,7 @@ export function AdminUsersManager({ initialUsers, initialInvites, roleOptions, d
</Tabs> </Tabs>
<Sheet open={Boolean(editUser)} onOpenChange={(open) => (!open ? setEditUserId(null) : null)}> <Sheet open={Boolean(editUser)} onOpenChange={(open) => (!open ? setEditUserId(null) : null)}>
<SheetContent position="right" size="lg" className="space-y-6 overflow-y-auto"> <SheetContent side="right" className="space-y-6 overflow-y-auto sm:max-w-2xl">
<SheetHeader> <SheetHeader>
<SheetTitle>Editar usuário</SheetTitle> <SheetTitle>Editar usuário</SheetTitle>
<SheetDescription>Atualize os dados cadastrais, papel e vínculo do colaborador.</SheetDescription> <SheetDescription>Atualize os dados cadastrais, papel e vínculo do colaborador.</SheetDescription>
@ -733,13 +748,11 @@ export function AdminUsersManager({ initialUsers, initialInvites, roleOptions, d
<SelectValue placeholder="Selecione" /> <SelectValue placeholder="Selecione" />
</SelectTrigger> </SelectTrigger>
<SelectContent> <SelectContent>
{normalizedRoles {selectableRoles.map((option) => (
.filter((option) => option !== "machine") <SelectItem key={option} value={option}>
.map((option) => ( {formatRole(option)}
<SelectItem key={option} value={option}> </SelectItem>
{formatRole(option)} ))}
</SelectItem>
))}
</SelectContent> </SelectContent>
</Select> </Select>
</div> </div>

View file

@ -472,7 +472,11 @@ function TicketPdfDocument({ ticket, logoDataUrl }: { ticket: TicketWithDetails;
</View> </View>
<View style={styles.cardGroup}> <View style={styles.cardGroup}>
{comments.map((comment, index) => ( {comments.map((comment, index) => (
<View key={comment.id} style={[styles.card, index > 0 ? styles.cardSpacing : null]} wrap={false}> <View
key={comment.id}
style={index > 0 ? [styles.card, styles.cardSpacing] : [styles.card]}
wrap={false}
>
<View style={styles.cardHeader}> <View style={styles.cardHeader}>
<View> <View>
<Text style={styles.cardTitle}>{comment.author.name}</Text> <Text style={styles.cardTitle}>{comment.author.name}</Text>
@ -505,7 +509,15 @@ function TicketPdfDocument({ ticket, logoDataUrl }: { ticket: TicketWithDetails;
</View> </View>
<View style={styles.cardGroup}> <View style={styles.cardGroup}>
{timeline.map((event, index) => ( {timeline.map((event, index) => (
<View key={event.id} style={[styles.card, styles.timelineCard, index > 0 ? styles.cardSpacing : null]} wrap={false}> <View
key={event.id}
style={
index > 0
? [styles.card, styles.timelineCard, styles.cardSpacing]
: [styles.card, styles.timelineCard]
}
wrap={false}
>
<Text style={styles.cardTitle}>{event.label}</Text> <Text style={styles.cardTitle}>{event.label}</Text>
<Text style={styles.cardSubtitle}>{formatDateTime(event.createdAt)}</Text> <Text style={styles.cardSubtitle}>{formatDateTime(event.createdAt)}</Text>
{event.description ? ( {event.description ? (