Fix company search filters and build regressions
This commit is contained in:
parent
a8abb68e36
commit
11efad0312
3 changed files with 71 additions and 32 deletions
|
|
@ -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,
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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 ? (
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue