diff --git a/src/app/(authenticated)/admin/food/_components/dre-report.tsx b/src/app/(authenticated)/admin/food/_components/dre-report.tsx index be395e03..233309da 100644 --- a/src/app/(authenticated)/admin/food/_components/dre-report.tsx +++ b/src/app/(authenticated)/admin/food/_components/dre-report.tsx @@ -15,6 +15,7 @@ import { format, startOfMonth, endOfMonth } from "date-fns" import { ptBR } from "date-fns/locale" import { Calculator, FileText, Eye, FileSpreadsheet, ChevronDown, ChevronRight, ExternalLink } from "lucide-react" import * as XLSX from "xlsx" +import { formatOrderDate } from "@/lib/date-utils" import { Dialog, DialogContent, @@ -731,7 +732,7 @@ export default function DREReport({ selectedDate, onOpenOrder }: DREReportProps) {order.sector ?? "Não informado"} - {format(new Date(order.orderDate), "dd/MM/yyyy", { locale: ptBR })} + {formatOrderDate(order.orderDate)} {order.menuItemName} R$ {order.price.toFixed(2)} diff --git a/src/app/(authenticated)/admin/food/_components/orders-tab.tsx b/src/app/(authenticated)/admin/food/_components/orders-tab.tsx index c61ca9dc..577e1474 100644 --- a/src/app/(authenticated)/admin/food/_components/orders-tab.tsx +++ b/src/app/(authenticated)/admin/food/_components/orders-tab.tsx @@ -23,6 +23,7 @@ import { CheckCircle, Loader2 } from "lucide-react" import { useDebounce } from "@/hooks/use-debounce" import { type UserMinimal } from "@/trpc/react" import { cn } from "@/lib/utils" +import { formatOrderDate } from "@/lib/date-utils" interface OrdersTabProps { selectedDate: Date @@ -916,7 +917,7 @@ export default function OrdersTab({

{order.menuItem.name}

- Pedido do dia {format(new Date(order.orderDate), "dd/MM/yyyy", { locale: ptBR })} - feito às {format(new Date(order.orderTime), "HH:mm", { locale: ptBR })} + Pedido do dia {formatOrderDate(order.orderDate)} - feito às {format(new Date(order.orderTime), "HH:mm", { locale: ptBR })}

{order.observations && (

diff --git a/src/app/(authenticated)/food/page.tsx b/src/app/(authenticated)/food/page.tsx index 8996dd0d..39ded356 100644 --- a/src/app/(authenticated)/food/page.tsx +++ b/src/app/(authenticated)/food/page.tsx @@ -12,6 +12,7 @@ import { toast } from "sonner" import { format } from "date-fns" import { ptBR } from "date-fns/locale" import { addDays, startOfDay } from "date-fns" +import { formatOrderDate } from "@/lib/date-utils" const FOOD_ORDER_DEADLINE_HOUR = Number(process.env.NEXT_PUBLIC_FOOD_ORDER_DEADLINE_HOUR ?? 9) @@ -138,11 +139,10 @@ export default function FoodPage() { const restaurants = api.restaurant.listActive.useQuery() // Buscar itens do menu do restaurante selecionado - // Definir a data do pedido conforme a regra de horário (UTC-3) + // Definir a data do pedido conforme a regra de horário const now = new Date() - const brasiliaTime = new Date(now.getTime() - 3 * 60 * 60 * 1000) // UTC-3 - const today = startOfDay(brasiliaTime) - const tomorrow = startOfDay(addDays(brasiliaTime, 1)) + const today = startOfDay(now) + const tomorrow = startOfDay(addDays(now, 1)) const menuDate = now.getHours() < FOOD_ORDER_DEADLINE_HOUR ? today : tomorrow; const menuItems = api.menuItem.byRestaurant.useQuery( { restaurantId: selectedRestaurant, date: menuDate }, @@ -607,7 +607,7 @@ export default function FoodPage() {

{order.restaurant?.name}

{order.menuItem.name}

- {format(new Date(order.orderDate), "dd/MM/yyyy", { locale: ptBR })} + {formatOrderDate(order.orderDate)}

{order.observations && (

Obs: {order.observations}

diff --git a/src/components/forms/create-manual-response-dialog.tsx b/src/components/forms/create-manual-response-dialog.tsx index 2010f9d0..db1d08aa 100644 --- a/src/components/forms/create-manual-response-dialog.tsx +++ b/src/components/forms/create-manual-response-dialog.tsx @@ -1,6 +1,6 @@ "use client" - -import { useState } from "react" + +import { useState, useMemo, useEffect } from "react" import { useForm } from "react-hook-form" import { zodResolver } from "@hookform/resolvers/zod" import { z } from "zod" @@ -25,6 +25,7 @@ import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, Command import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover" import { Check, ChevronsUpDown } from "lucide-react" import { cn } from "@/lib/utils" +import { matchesSearch } from "@/lib/search-utils" import type { Field } from "@/lib/form-types" interface CreateManualResponseDialogProps { @@ -47,8 +48,17 @@ export function CreateManualResponseDialog({ const [userSearchOpen, setUserSearchOpen] = useState(false) const [userSearchValue, setUserSearchValue] = useState("") - // Buscar todos os usuários - const { data: allUsers = [], isLoading: isLoadingUsers } = api.user.listAll.useQuery() + // Buscar todos os usuários ativos com revalidação garantida + const { data: allUsers = [], isLoading: isLoadingUsers, refetch: refetchUsers } = api.user.listAll.useQuery(undefined, { + staleTime: 0, + refetchOnWindowFocus: true, + }) + + useEffect(() => { + if (open) { + void refetchUsers() + } + }, [open, refetchUsers]) // Criar schema dinâmico baseado nos campos do formulário const createSchema = () => { @@ -174,17 +184,29 @@ export function CreateManualResponseDialog({ } } - // Filtrar usuários baseado na busca - const filteredUsers = allUsers.filter((user) => { - if (!userSearchValue.trim()) return true - const search = userSearchValue.toLowerCase() - const fullName = `${user.firstName ?? ""} ${user.lastName ?? ""}`.trim().toLowerCase() - return ( - fullName.includes(search) || - user.email.toLowerCase().includes(search) || - (user.setor?.toLowerCase().includes(search)) - ) - }) + // Filtrar usuários baseado na busca normalizada (sem acento, case-insensitive, sem caracteres especiais) + const filteredUsers = useMemo(() => { + if (!userSearchValue.trim()) return allUsers + + return allUsers.filter((user) => { + const fullName = `${user.firstName ?? ""} ${user.lastName ?? ""}`.trim() + const filialName = user.filial?.name ?? "" + const filialCode = user.filial?.code ?? "" + const matricula = user.matricula ?? "" + const email = user.email ?? "" + const setor = user.setor ?? "" + + return matchesSearch( + userSearchValue, + fullName, + email, + setor, + matricula, + filialName, + filialCode + ) + }) + }, [allUsers, userSearchValue]) const selectedUser = allUsers.find((u) => u.id === selectedUserId) @@ -220,47 +242,61 @@ export function CreateManualResponseDialog({ - - + + - - - {isLoadingUsers ? "Carregando..." : "Nenhum usuário encontrado."} - - - {filteredUsers.map((user) => ( - { - setSelectedUserId(user.id) - setUserSearchOpen(false) - setUserSearchValue("") - }} - > - -
- - {user.firstName || user.lastName - ? `${user.firstName ?? ""} ${user.lastName ?? ""}`.trim() - : user.email} - - - {user.email} {user.setor && `• ${user.setor}`} - -
-
- ))} -
+ + {isLoadingUsers ? ( +
+ + Carregando usuários ativos... +
+ ) : filteredUsers.length === 0 ? ( + + Nenhum usuário ativo encontrado. + + ) : ( + + {filteredUsers.map((user) => { + const displayName = `${user.firstName ?? ""} ${user.lastName ?? ""}`.trim() || user.email + const isSelected = selectedUserId === user.id + return ( + { + setSelectedUserId(user.id) + setUserSearchOpen(false) + setUserSearchValue("") + }} + className="cursor-pointer flex items-center py-2 px-3 hover:bg-muted/80" + > + +
+ + {displayName} + + + {user.email} + {user.setor ? ` • ${user.setor}` : ""} + {user.filial?.code ? ` • ${user.filial.code}` : ""} + {user.matricula ? ` • Matrícula: ${user.matricula}` : ""} + +
+
+ ) + })} +
+ )}
diff --git a/src/lib/date-utils.ts b/src/lib/date-utils.ts index 58253266..be8cec4f 100644 --- a/src/lib/date-utils.ts +++ b/src/lib/date-utils.ts @@ -9,3 +9,10 @@ export function getUtcMonthDay(date: Date): { month: number; day: number } { } } +export function formatOrderDate(date: Date | string): string { + const d = typeof date === "string" ? new Date(date) : date + const day = String(d.getUTCDate()).padStart(2, "0") + const month = String(d.getUTCMonth() + 1).padStart(2, "0") + const year = d.getUTCFullYear() + return `${day}/${month}/${year}` +} \ No newline at end of file diff --git a/src/lib/search-utils.ts b/src/lib/search-utils.ts new file mode 100644 index 00000000..e77786a2 --- /dev/null +++ b/src/lib/search-utils.ts @@ -0,0 +1,23 @@ +export function normalizeSearch(text: string | null | undefined): string { + return (text ?? "") + .normalize("NFD") + .replace(/[\u0300-\u036f]/g, "") + .toLowerCase() + .replace(/[^\w\s]/g, " ") + .replace(/\s+/g, " ") + .trim() +} + +export function matchesSearch( + query: string | null | undefined, + ...targets: (string | null | undefined)[] +): boolean { + const normalizedQuery = normalizeSearch(query) + if (!normalizedQuery) return true + + const terms = normalizedQuery.split(/\s+/).filter(Boolean) + if (terms.length === 0) return true + + const combinedTarget = normalizeSearch(targets.filter(Boolean).join(" ")) + return terms.every((term) => combinedTarget.includes(term)) +} \ No newline at end of file diff --git a/src/server/api/routers/user.ts b/src/server/api/routers/user.ts index e402c4be..92ceba52 100644 --- a/src/server/api/routers/user.ts +++ b/src/server/api/routers/user.ts @@ -163,7 +163,32 @@ export const userRouter = createTRPCRouter({ return user; }), - listAll: adminProcedure.query(async ({ ctx }) => { + listAll: protectedProcedure.query(async ({ ctx }) => { + const currentUser = await ctx.db.user.findUnique({ + where: { id: ctx.auth.userId }, + select: { role_config: true, is_active: true }, + }); + + if (!currentUser?.is_active) { + throw new TRPCError({ + code: "FORBIDDEN", + message: "Usuário inativo", + }); + } + + const effectiveConfig = getEffectiveRoleConfig(currentUser); + const isSudo = !!effectiveConfig.sudo; + const hasAdminAccess = Array.isArray(effectiveConfig.admin_pages) && effectiveConfig.admin_pages.includes("/admin"); + const canCreateSolicitacoes = !!effectiveConfig.can_create_solicitacoes; + const canCreateForm = !!effectiveConfig.can_create_form; + + if (!isSudo && !hasAdminAccess && !canCreateSolicitacoes && !canCreateForm) { + throw new TRPCError({ + code: "FORBIDDEN", + message: "Você não tem permissão para listar usuários", + }); + } + return await ctx.db.user.findMany({ where: { is_active: true, @@ -178,10 +203,19 @@ export const userRouter = createTRPCRouter({ setor: true, extension: true, emailExtension: true, + matricula: true, + filial: { + select: { + id: true, + name: true, + code: true, + }, + }, }, - orderBy: { - firstName: "asc", - }, + orderBy: [ + { firstName: "asc" }, + { lastName: "asc" }, + ], }); }),