feat: implementa busca de produtos com API externa e corrige URL base
- Adiciona integração com API de produtos em http://10.1.1.212:8805/api/v1/data-consult/products/{id} - Corrige mapeamento de resposta da API para formato Product interface - Atualiza ProductSearchInput para usar description como nome do produto - Corrige API_BASE_URL adicionando protocolo http:// para evitar URLs relativas - Resolve erro 404 causado por URLs malformadas em requisições de API
This commit is contained in:
@@ -23,6 +23,7 @@ import { CustomerSearchInput } from "@/src/components/orders/CustomerSearchInput
|
|||||||
import { useCallback, useState, useEffect } from "react";
|
import { useCallback, useState, useEffect } from "react";
|
||||||
import { User, useAuthValidation } from "../../../hooks/useAuthValidation";
|
import { User, useAuthValidation } from "../../../hooks/useAuthValidation";
|
||||||
import { SellerSearchInput } from "@/src/components/orders/SellerSearchInput";
|
import { SellerSearchInput } from "@/src/components/orders/SellerSearchInput";
|
||||||
|
import { ProductSearchInput } from "@/src/components/orders/ProductSearchInput";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Página de Consulta de Pedidos
|
* Página de Consulta de Pedidos
|
||||||
@@ -161,6 +162,36 @@ export default function FindOrdersPage() {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div className="filter-field">
|
||||||
|
<ProductSearchInput
|
||||||
|
id="productSearch"
|
||||||
|
label="Produto"
|
||||||
|
placeholder="Buscar por código, nome ou descrição"
|
||||||
|
value={searchParams.productId || ""}
|
||||||
|
onChange={(value) => handleInputChange("productId", value)}
|
||||||
|
onSelect={(product) => {
|
||||||
|
if (product) {
|
||||||
|
handleInputChange("productId", product.id);
|
||||||
|
setSearchParams(prev => ({
|
||||||
|
...prev,
|
||||||
|
selectedProduct: product
|
||||||
|
}));
|
||||||
|
} else {
|
||||||
|
handleInputChange("productId", "");
|
||||||
|
setSearchParams(prev => ({
|
||||||
|
...prev,
|
||||||
|
selectedProduct: null
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
selectedProduct={searchParams.selectedProduct || null}
|
||||||
|
aria-label="Buscar Produto"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<div className="flex gap-4">
|
<div className="flex gap-4">
|
||||||
<div className="filter-small-field">
|
<div className="filter-small-field">
|
||||||
<FilterInput
|
<FilterInput
|
||||||
@@ -413,8 +444,6 @@ export default function FindOrdersPage() {
|
|||||||
visibleOrdersCount={visibleOrdersCount}
|
visibleOrdersCount={visibleOrdersCount}
|
||||||
stores={stores}
|
stores={stores}
|
||||||
transfers={[]}
|
transfers={[]}
|
||||||
status={status}
|
|
||||||
leadtime={leadtime}
|
|
||||||
/>
|
/>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|||||||
@@ -20,41 +20,44 @@ import { Badge } from "../../components/ui/badge";
|
|||||||
import { ordersApi } from "../../lib/api";
|
import { ordersApi } from "../../lib/api";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Represents a timeline event in an order's lifecycle
|
* Representa um evento na linha do tempo do ciclo de vida de um pedido
|
||||||
|
* @typedef {Object} Leadtime
|
||||||
|
* @property {string} etapas - Descrição da etapa
|
||||||
|
* @property {string} descricao - Descrição detalhada do evento
|
||||||
|
* @property {string} date - Data e hora do evento
|
||||||
|
* @property {string} codigoFuncionario - ID do funcionário responsável
|
||||||
|
* @property {string} nomeFuncionario - Nome do funcionário responsável
|
||||||
|
* @property {string} numeroPedido - Número do pedido associado
|
||||||
|
* @property {LucideIcon} [icon] - Ícone para o evento
|
||||||
|
* @property {string} [status] - Rótulo de status do evento
|
||||||
|
* @property {string} [color] - Tema de cor do evento
|
||||||
*/
|
*/
|
||||||
interface Leadtime {
|
interface Leadtime {
|
||||||
/** Description of the stage/step */
|
|
||||||
etapas: string;
|
etapas: string;
|
||||||
/** Detailed description of the event */
|
|
||||||
descricao: string;
|
descricao: string;
|
||||||
/** Date and time of the event */
|
|
||||||
date: string;
|
date: string;
|
||||||
/** Employee ID who handled this step */
|
|
||||||
codigoFuncionario: string;
|
codigoFuncionario: string;
|
||||||
/** Employee name who handled this step */
|
|
||||||
nomeFuncionario: string;
|
nomeFuncionario: string;
|
||||||
/** Order number associated with this event */
|
|
||||||
numeroPedido: string;
|
numeroPedido: string;
|
||||||
/** Icon to display for this event */
|
|
||||||
icon?: LucideIcon;
|
icon?: LucideIcon;
|
||||||
/** Status label for this event */
|
|
||||||
status?: string;
|
status?: string;
|
||||||
/** Color theme for this event */
|
|
||||||
color?: string;
|
color?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Props for the Timeline component
|
* Propriedades do componente Timeline
|
||||||
|
* @typedef {Object} TimelineProps
|
||||||
|
* @property {string} [orderId] - ID do pedido para buscar dados da linha do tempo
|
||||||
|
* @property {string} [className] - Classe CSS opcional
|
||||||
*/
|
*/
|
||||||
interface TimelineProps {
|
interface TimelineProps {
|
||||||
/** Order ID to fetch timeline data for */
|
|
||||||
orderId?: string;
|
orderId?: string;
|
||||||
/** Optional CSS class name */
|
|
||||||
className?: string;
|
className?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Configuration for different order status types
|
* Configuração para diferentes tipos de status do pedido
|
||||||
|
* @type {Record<string, {icon: LucideIcon, color: string, status: string}>}
|
||||||
*/
|
*/
|
||||||
const statusConfig: Record<
|
const statusConfig: Record<
|
||||||
string,
|
string,
|
||||||
@@ -75,7 +78,8 @@ const statusConfig: Record<
|
|||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Keywords used to match description text to status types
|
* Palavras-chave usadas para mapear descrições de etapas para tipos de status
|
||||||
|
* @type {Object}
|
||||||
*/
|
*/
|
||||||
const statusKeywords = {
|
const statusKeywords = {
|
||||||
pedido: ['venda realizada', 'pedido', 'receb'],
|
pedido: ['venda realizada', 'pedido', 'receb'],
|
||||||
@@ -87,9 +91,9 @@ const statusKeywords = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Determines the status configuration based on the event description
|
* Determina a configuração de status com base na descrição do evento
|
||||||
* @param descricao - The description text to analyze
|
* @param {string} descricao - Texto da descrição a ser analisado
|
||||||
* @returns The matching status configuration object
|
* @returns {{icon: LucideIcon, color: string, status: string}} Objeto de configuração de status correspondente
|
||||||
*/
|
*/
|
||||||
function getStatusConfig(descricao: string): { icon: LucideIcon; color: string; status: string } {
|
function getStatusConfig(descricao: string): { icon: LucideIcon; color: string; status: string } {
|
||||||
const desc = descricao?.toLowerCase().trim() || "";
|
const desc = descricao?.toLowerCase().trim() || "";
|
||||||
@@ -102,9 +106,9 @@ function getStatusConfig(descricao: string): { icon: LucideIcon; color: string;
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Formats a date string to localized date and time format
|
* Formata uma string de data para o formato local brasileiro (DD/MM/AAAA HH:MM)
|
||||||
* @param dateString - The date string to format
|
* @param {string} dateString - String de data a ser formatada
|
||||||
* @returns Formatted date string in Brazilian format (DD/MM/YYYY HH:MM)
|
* @returns {string} Data formatada
|
||||||
*/
|
*/
|
||||||
function formatDate(dateString: string): string {
|
function formatDate(dateString: string): string {
|
||||||
try {
|
try {
|
||||||
@@ -123,9 +127,9 @@ function formatDate(dateString: string): string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Formats a date string to show only the time portion
|
* Formata uma string de data para exibir apenas o horário (HH:MM)
|
||||||
* @param dateString - The date string to format
|
* @param {string} dateString - String de data a ser formatada
|
||||||
* @returns Formatted time string in Brazilian format (HH:MM)
|
* @returns {string} Horário formatado
|
||||||
*/
|
*/
|
||||||
function formatTimeOnly(dateString: string): string {
|
function formatTimeOnly(dateString: string): string {
|
||||||
try {
|
try {
|
||||||
@@ -141,10 +145,10 @@ function formatTimeOnly(dateString: string): string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Maps API data to the Leadtime interface with proper status configuration
|
* Mapeia os dados da API para o formato Leadtime com configuração de status
|
||||||
* @param apiData - Raw API data
|
* @param {any[]} apiData - Dados brutos da API
|
||||||
* @param defaultOrderId - Fallback order ID if not present in API data
|
* @param {string} [defaultOrderId] - ID do pedido padrão caso não esteja presente nos dados
|
||||||
* @returns Array of processed Leadtime objects
|
* @returns {Leadtime[]} Array de objetos Leadtime processados
|
||||||
*/
|
*/
|
||||||
function mapApiDataToLeadtimes(apiData: any[], defaultOrderId: string = ""): Leadtime[] {
|
function mapApiDataToLeadtimes(apiData: any[], defaultOrderId: string = ""): Leadtime[] {
|
||||||
return apiData.map((item) => {
|
return apiData.map((item) => {
|
||||||
@@ -164,7 +168,9 @@ function mapApiDataToLeadtimes(apiData: any[], defaultOrderId: string = ""): Lea
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Timeline component that displays the history of events for an order
|
* Componente Timeline que exibe o histórico de eventos de um pedido
|
||||||
|
* @param {TimelineProps} props
|
||||||
|
* @returns {JSX.Element}
|
||||||
*/
|
*/
|
||||||
export default function Timeline({ orderId = "", className }: TimelineProps) {
|
export default function Timeline({ orderId = "", className }: TimelineProps) {
|
||||||
const [leadtimes, setLeadtimes] = useState<Leadtime[]>([]);
|
const [leadtimes, setLeadtimes] = useState<Leadtime[]>([]);
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ export type ColumnKey =
|
|||||||
| "Motorista"
|
| "Motorista"
|
||||||
| "Placa"
|
| "Placa"
|
||||||
|
|
||||||
|
|
||||||
const ALL_COLUMNS: ColumnKey[] = [
|
const ALL_COLUMNS: ColumnKey[] = [
|
||||||
"Data", "Data previsão de entrega", "Agendamento", "Pedido", "Tipo Pedido", "Cliente", "Vendedor", "Filial", "Situação", "Dt Carregamento", "Rota", "Valor", "Peso(kg)", "Carregamento", "Cobrança", "Trasportadora", "Praça", "Nota fiscal", "Dt Faturamento", "TV 7", "Motorista", "Placa"
|
"Data", "Data previsão de entrega", "Agendamento", "Pedido", "Tipo Pedido", "Cliente", "Vendedor", "Filial", "Situação", "Dt Carregamento", "Rota", "Valor", "Peso(kg)", "Carregamento", "Cobrança", "Trasportadora", "Praça", "Nota fiscal", "Dt Faturamento", "TV 7", "Motorista", "Placa"
|
||||||
];
|
];
|
||||||
@@ -87,12 +88,10 @@ export function OrdersTable({
|
|||||||
}: OrdersTableProps) {
|
}: OrdersTableProps) {
|
||||||
const { getStoreName } = useStoreNameResolver(stores);
|
const { getStoreName } = useStoreNameResolver(stores);
|
||||||
|
|
||||||
// Find the selected order based on selectedOrderId
|
|
||||||
const selectedOrder = useMemo(() =>
|
const selectedOrder = useMemo(() =>
|
||||||
selectedOrderId ? orders.find(order => order.orderId === selectedOrderId) : undefined
|
selectedOrderId ? orders.find(order => order.orderId === selectedOrderId) : undefined
|
||||||
, [selectedOrderId, orders]);
|
, [selectedOrderId, orders]);
|
||||||
|
|
||||||
// Use custom hook for order-related data fetching
|
|
||||||
const {
|
const {
|
||||||
orderStatus,
|
orderStatus,
|
||||||
orderLeadtime,
|
orderLeadtime,
|
||||||
@@ -108,7 +107,6 @@ export function OrdersTable({
|
|||||||
"Peso(kg)": (order: Order) => parseFloat(String(order.totalWeigth || "0")),
|
"Peso(kg)": (order: Order) => parseFloat(String(order.totalWeigth || "0")),
|
||||||
"Dt Faturamento": (order: Order) => new Date(order.invoiceDate || ""),
|
"Dt Faturamento": (order: Order) => new Date(order.invoiceDate || ""),
|
||||||
"Dt Carregamento": (order: Order) => new Date(order.shipmentDate || ""),
|
"Dt Carregamento": (order: Order) => new Date(order.shipmentDate || ""),
|
||||||
// Add other column accessors as needed
|
|
||||||
}), []);
|
}), []);
|
||||||
|
|
||||||
// Use our custom sort hook
|
// Use our custom sort hook
|
||||||
|
|||||||
@@ -7,6 +7,16 @@ import {
|
|||||||
import { OrderRowExpandable } from "./tabela-pedidos/components/OrderRowExpandable";
|
import { OrderRowExpandable } from "./tabela-pedidos/components/OrderRowExpandable";
|
||||||
import { Order } from "@/src/components/types";
|
import { Order } from "@/src/components/types";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Propriedades para o componente OrdersTableBody
|
||||||
|
* @typedef {Object} OrdersTableBodyProps
|
||||||
|
* @property {Order[]} orders - Lista completa de pedidos
|
||||||
|
* @property {Order[]} currentOrders - Lista de pedidos filtrados/paginados
|
||||||
|
* @property {string|null} selectedOrderId - ID do pedido selecionado
|
||||||
|
* @property {string[]} visibleColumns - Colunas visíveis na tabela
|
||||||
|
* @property {(storeId?: string) => string} getStoreName - Função para obter o nome da loja
|
||||||
|
* @property {(orderId: string) => void} handleRowClick - Função chamada ao clicar em uma linha
|
||||||
|
*/
|
||||||
interface OrdersTableBodyProps {
|
interface OrdersTableBodyProps {
|
||||||
orders: Order[];
|
orders: Order[];
|
||||||
currentOrders: Order[];
|
currentOrders: Order[];
|
||||||
@@ -16,7 +26,11 @@ interface OrdersTableBodyProps {
|
|||||||
handleRowClick: (orderId: string) => void;
|
handleRowClick: (orderId: string) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Componente de linha vazia memoizado
|
/**
|
||||||
|
* Linha vazia exibida quando não há pedidos
|
||||||
|
* @param {{ colSpan: number }} props
|
||||||
|
* @returns {JSX.Element}
|
||||||
|
*/
|
||||||
const EmptyRow = memo(({ colSpan }: { colSpan: number }) => (
|
const EmptyRow = memo(({ colSpan }: { colSpan: number }) => (
|
||||||
<TableRow>
|
<TableRow>
|
||||||
<TableCell colSpan={colSpan} className="h-24 text-center text-xs">
|
<TableCell colSpan={colSpan} className="h-24 text-center text-xs">
|
||||||
@@ -25,7 +39,11 @@ const EmptyRow = memo(({ colSpan }: { colSpan: number }) => (
|
|||||||
</TableRow>
|
</TableRow>
|
||||||
));
|
));
|
||||||
|
|
||||||
// Componente de linha de pedido memoizado
|
/**
|
||||||
|
* Linha de pedido individual, expansível
|
||||||
|
* @param {{ order: Order, isSelected: boolean, visibleColumns: string[], onSelect: (orderId: string) => void, getStoreName: (storeId?: string) => string }} props
|
||||||
|
* @returns {JSX.Element}
|
||||||
|
*/
|
||||||
const OrderRow = memo(({
|
const OrderRow = memo(({
|
||||||
order,
|
order,
|
||||||
isSelected,
|
isSelected,
|
||||||
@@ -39,11 +57,11 @@ const OrderRow = memo(({
|
|||||||
onSelect: (orderId: string) => void;
|
onSelect: (orderId: string) => void;
|
||||||
getStoreName: (storeId?: string) => string;
|
getStoreName: (storeId?: string) => string;
|
||||||
}) => {
|
}) => {
|
||||||
// Create a new order object with the store name only (without ID prefix)
|
// Cria um novo objeto de pedido com o nome da loja
|
||||||
const orderWithStore = {
|
const orderWithStore = {
|
||||||
...order,
|
...order,
|
||||||
store: getStoreName(order.storeId),
|
store: getStoreName(order.storeId),
|
||||||
storeId: order.storeId, // Preserve original storeId
|
storeId: order.storeId, // Mantém o storeId original
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -60,7 +78,11 @@ const OrderRow = memo(({
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Componente principal memoizado
|
/**
|
||||||
|
* Corpo da tabela de pedidos, renderiza linhas de pedidos ou linha vazia
|
||||||
|
* @param {OrdersTableBodyProps} props
|
||||||
|
* @returns {JSX.Element}
|
||||||
|
*/
|
||||||
function OrdersTableBodyComponent({
|
function OrdersTableBodyComponent({
|
||||||
orders,
|
orders,
|
||||||
currentOrders,
|
currentOrders,
|
||||||
@@ -69,7 +91,7 @@ function OrdersTableBodyComponent({
|
|||||||
getStoreName,
|
getStoreName,
|
||||||
handleRowClick,
|
handleRowClick,
|
||||||
}: OrdersTableBodyProps) {
|
}: OrdersTableBodyProps) {
|
||||||
// Renderização condicional baseada na presença de orders, memoizada
|
// Renderização condicional baseada na presença de pedidos
|
||||||
const content = useMemo(() => {
|
const content = useMemo(() => {
|
||||||
if (orders.length === 0) {
|
if (orders.length === 0) {
|
||||||
return <EmptyRow colSpan={visibleColumns.length} />;
|
return <EmptyRow colSpan={visibleColumns.length} />;
|
||||||
@@ -90,4 +112,8 @@ function OrdersTableBodyComponent({
|
|||||||
return <TableBody>{content}</TableBody>;
|
return <TableBody>{content}</TableBody>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Exporta o componente memoizado do corpo da tabela de pedidos
|
||||||
|
* @type {React.NamedExoticComponent<OrdersTableBodyProps>}
|
||||||
|
*/
|
||||||
export const OrdersTableBody = memo(OrdersTableBodyComponent);
|
export const OrdersTableBody = memo(OrdersTableBodyComponent);
|
||||||
256
src/components/orders/ProductSearchInput.tsx
Normal file
256
src/components/orders/ProductSearchInput.tsx
Normal file
@@ -0,0 +1,256 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import * as React from "react";
|
||||||
|
import { Check, ChevronsUpDown, Package, Loader2, X } from "lucide-react";
|
||||||
|
import { cn } from "@/src/lib/utils";
|
||||||
|
import { Button } from "@/src/components/ui/button";
|
||||||
|
import {
|
||||||
|
Command,
|
||||||
|
CommandEmpty,
|
||||||
|
CommandGroup,
|
||||||
|
CommandInput,
|
||||||
|
CommandItem,
|
||||||
|
CommandList,
|
||||||
|
} from "@/src/components/ui/command";
|
||||||
|
import {
|
||||||
|
Popover,
|
||||||
|
PopoverContent,
|
||||||
|
PopoverTrigger,
|
||||||
|
} from "@/src/components/ui/popover";
|
||||||
|
import { dataConsultApi } from "@/src/lib/api";
|
||||||
|
import { Product } from "@/src/components/types";
|
||||||
|
import { Label } from "@/src/components/ui/label";
|
||||||
|
import { Badge } from "@/src/components/ui/badge";
|
||||||
|
|
||||||
|
interface ProductSearchInputProps {
|
||||||
|
id: string;
|
||||||
|
label: string;
|
||||||
|
placeholder: string;
|
||||||
|
value: string;
|
||||||
|
onChange: (value: string) => void;
|
||||||
|
onSelect: (product: Product | null) => void;
|
||||||
|
selectedProduct: Product | null;
|
||||||
|
disabled?: boolean;
|
||||||
|
"aria-label"?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Componente de busca de produtos com autocompletar
|
||||||
|
* Permite buscar produtos por código, nome ou descrição
|
||||||
|
*/
|
||||||
|
export function ProductSearchInput({
|
||||||
|
id,
|
||||||
|
label,
|
||||||
|
placeholder,
|
||||||
|
value,
|
||||||
|
onChange,
|
||||||
|
onSelect,
|
||||||
|
selectedProduct,
|
||||||
|
disabled = false,
|
||||||
|
"aria-label": ariaLabel,
|
||||||
|
}: ProductSearchInputProps) {
|
||||||
|
const [open, setOpen] = React.useState(false);
|
||||||
|
const [products, setProducts] = React.useState<Product[]>([]);
|
||||||
|
const [loading, setLoading] = React.useState(false);
|
||||||
|
const [searchQuery, setSearchQuery] = React.useState("");
|
||||||
|
const [searchError, setSearchError] = React.useState<string | null>(null);
|
||||||
|
const searchInputRef = React.useRef<HTMLInputElement>(null);
|
||||||
|
|
||||||
|
// Buscar produtos baseado na query
|
||||||
|
const searchProducts = React.useCallback(async (query: string) => {
|
||||||
|
if (!query || query.length < 2) {
|
||||||
|
setProducts([]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
setLoading(true);
|
||||||
|
setSearchError(null);
|
||||||
|
|
||||||
|
const response = await dataConsultApi.getProducts(query);
|
||||||
|
|
||||||
|
if (response.success && response.data && response.data.length > 0) {
|
||||||
|
// Mapear os dados da API para o formato Product
|
||||||
|
const mappedProducts = response.data.map((product: any) => ({
|
||||||
|
id: product.id?.toString() || "",
|
||||||
|
code: product.manufacturerCode || "",
|
||||||
|
name: product.description || product.name || "",
|
||||||
|
description: product.description || product.name || "",
|
||||||
|
brand: product.brand || "",
|
||||||
|
department: product.department || "",
|
||||||
|
color: product.color || "",
|
||||||
|
}));
|
||||||
|
|
||||||
|
setProducts(mappedProducts.slice(0, 50)); // Limitar a 50 resultados
|
||||||
|
} else {
|
||||||
|
setSearchError(`Nenhum produto encontrado para "${query}"`);
|
||||||
|
setProducts([]);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Erro ao buscar produtos:", error);
|
||||||
|
setSearchError("Erro ao buscar produtos");
|
||||||
|
setProducts([]);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// Debounce para a busca
|
||||||
|
React.useEffect(() => {
|
||||||
|
const timer = setTimeout(() => {
|
||||||
|
if (open && searchQuery.length >= 2) {
|
||||||
|
searchProducts(searchQuery);
|
||||||
|
}
|
||||||
|
}, 300);
|
||||||
|
|
||||||
|
return () => clearTimeout(timer);
|
||||||
|
}, [searchQuery, searchProducts, open]);
|
||||||
|
|
||||||
|
// Selecionar produto
|
||||||
|
const handleSelectProduct = (product: Product) => {
|
||||||
|
onSelect(product);
|
||||||
|
setOpen(false);
|
||||||
|
setSearchQuery("");
|
||||||
|
};
|
||||||
|
|
||||||
|
// Limpar seleção
|
||||||
|
const handleClearSelection = (e?: React.MouseEvent) => {
|
||||||
|
if (e) {
|
||||||
|
e.stopPropagation();
|
||||||
|
}
|
||||||
|
onSelect(null);
|
||||||
|
onChange("");
|
||||||
|
setSearchQuery("");
|
||||||
|
setProducts([]);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Quando o componente monta ou o valor muda
|
||||||
|
React.useEffect(() => {
|
||||||
|
// Se temos um value mas não temos selectedProduct, executar busca inicial
|
||||||
|
if (value && !selectedProduct && !loading && !open) {
|
||||||
|
searchProducts(value);
|
||||||
|
}
|
||||||
|
}, [value, selectedProduct, searchProducts, loading, open]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col space-y-1">
|
||||||
|
<div className="flex justify-between items-center">
|
||||||
|
<Label
|
||||||
|
htmlFor={id}
|
||||||
|
className="text-xs font-medium"
|
||||||
|
>
|
||||||
|
{label}
|
||||||
|
{selectedProduct && (
|
||||||
|
<Badge variant="outline" className="ml-2 text-xs py-0">
|
||||||
|
{selectedProduct.code ? `Cód: ${selectedProduct.code}` : `ID: ${selectedProduct.id}`}
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
|
</Label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{searchError && (
|
||||||
|
<p className="text-xs text-red-500 mt-0.5">{searchError}</p>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="relative">
|
||||||
|
<Popover open={open} onOpenChange={setOpen}>
|
||||||
|
<PopoverTrigger asChild>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
role="combobox"
|
||||||
|
aria-expanded={open}
|
||||||
|
aria-label={ariaLabel || "Selecione um produto"}
|
||||||
|
disabled={disabled}
|
||||||
|
className={cn(
|
||||||
|
"w-full justify-between h-8 transition-all",
|
||||||
|
selectedProduct ? "pr-8" : "text-muted-foreground",
|
||||||
|
disabled && "opacity-50 cursor-not-allowed"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{selectedProduct ? (
|
||||||
|
<div className="flex items-center gap-2 overflow-hidden">
|
||||||
|
<Package className="h-4 w-4 shrink-0" />
|
||||||
|
<span className="truncate">
|
||||||
|
{selectedProduct.name}
|
||||||
|
{selectedProduct.code ? ` (${selectedProduct.code})` : ""}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<span className="truncate">{placeholder}</span>
|
||||||
|
)}
|
||||||
|
{selectedProduct ? null : (
|
||||||
|
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
</PopoverTrigger>
|
||||||
|
{selectedProduct && (
|
||||||
|
<div
|
||||||
|
className="absolute right-0 top-0 h-full flex items-center pr-2"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
handleClearSelection();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<X className="h-4 w-4 cursor-pointer" />
|
||||||
|
<span className="sr-only">Limpar seleção</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<PopoverContent className="min-w-[400px] p-0">
|
||||||
|
<Command>
|
||||||
|
<CommandInput
|
||||||
|
ref={searchInputRef}
|
||||||
|
placeholder="Buscar produto por código, nome ou descrição..."
|
||||||
|
value={searchQuery}
|
||||||
|
onValueChange={setSearchQuery}
|
||||||
|
className="h-9"
|
||||||
|
/>
|
||||||
|
{loading && (
|
||||||
|
<div className="flex items-center justify-center py-6">
|
||||||
|
<Loader2 className="h-6 w-6 animate-spin text-gray-400" />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{!loading && (
|
||||||
|
<>
|
||||||
|
<CommandEmpty>
|
||||||
|
{searchQuery.length >= 2
|
||||||
|
? "Nenhum produto encontrado com esse termo"
|
||||||
|
: "Digite pelo menos 2 caracteres para buscar"}
|
||||||
|
</CommandEmpty>
|
||||||
|
{products.length > 0 && (
|
||||||
|
<CommandGroup heading="Produtos">
|
||||||
|
<CommandList>
|
||||||
|
{products.map((product, index) => (
|
||||||
|
<CommandItem
|
||||||
|
key={product.id || product.code || `product-${index}`}
|
||||||
|
value={product.name}
|
||||||
|
onSelect={() => handleSelectProduct(product)}
|
||||||
|
>
|
||||||
|
<Package className="mr-2 h-4 w-4" />
|
||||||
|
<div className="flex flex-col">
|
||||||
|
<span className="font-medium">{product.name}</span>
|
||||||
|
<div className="flex gap-2 text-xs text-gray-500">
|
||||||
|
{product.code && (
|
||||||
|
<span>Cód: {product.code}</span>
|
||||||
|
)}
|
||||||
|
{product.brand && (
|
||||||
|
<span>Marca: {product.brand}</span>
|
||||||
|
)}
|
||||||
|
{product.department && (
|
||||||
|
<span>Depto: {product.department}</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CommandItem>
|
||||||
|
))}
|
||||||
|
</CommandList>
|
||||||
|
</CommandGroup>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Command>
|
||||||
|
</PopoverContent>
|
||||||
|
</Popover>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -13,11 +13,23 @@ export interface TimelineEvent {
|
|||||||
description: string;
|
description: string;
|
||||||
date: string | Date;
|
date: string | Date;
|
||||||
status?: string;
|
status?: string;
|
||||||
icon?: LucideIcon;
|
icon?: LucideIcon;
|
||||||
color?: string;
|
color?: string;
|
||||||
user?: string;
|
user?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface productsSearchParams {
|
||||||
|
productId?: string;
|
||||||
|
description?: string;
|
||||||
|
pacth?: string;
|
||||||
|
color?: string;
|
||||||
|
stockId?: string;
|
||||||
|
quantity?: string;
|
||||||
|
salePrice?: string;
|
||||||
|
department?: string;
|
||||||
|
brand?: string;
|
||||||
|
}
|
||||||
|
|
||||||
export interface cutitens {
|
export interface cutitens {
|
||||||
productId: number;
|
productId: number;
|
||||||
description: string;
|
description: string;
|
||||||
@@ -81,7 +93,7 @@ export interface Order {
|
|||||||
sellerId: string;
|
sellerId: string;
|
||||||
stockId: string;
|
stockId: string;
|
||||||
storeName: string;
|
storeName: string;
|
||||||
store?: string;
|
store?: string;
|
||||||
createDate: string | Date;
|
createDate: string | Date;
|
||||||
updateDate?: string | Date;
|
updateDate?: string | Date;
|
||||||
status?: string;
|
status?: string;
|
||||||
@@ -159,7 +171,7 @@ export interface ClientData {
|
|||||||
totalPurchases: number;
|
totalPurchases: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type OrderStatus =
|
export type OrderStatus =
|
||||||
| 'Aguardando pagamento'
|
| 'Aguardando pagamento'
|
||||||
| 'Pagamento aprovado'
|
| 'Pagamento aprovado'
|
||||||
| 'Em separação'
|
| 'Em separação'
|
||||||
@@ -179,13 +191,13 @@ export type PaymentStatus =
|
|||||||
name: string;
|
name: string;
|
||||||
document: string;
|
document: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
export interface LoginDto {
|
export interface LoginDto {
|
||||||
username: string;
|
username: string;
|
||||||
password: string;
|
password: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface LoginResponseDto {
|
export interface LoginResponseDto {
|
||||||
id: number;
|
id: number;
|
||||||
sellerId: number;
|
sellerId: number;
|
||||||
@@ -201,9 +213,19 @@ export type PaymentStatus =
|
|||||||
name?: string;
|
name?: string;
|
||||||
document?: string;
|
document?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export interface Product {
|
||||||
|
id: string;
|
||||||
|
code: string;
|
||||||
|
name: string;
|
||||||
|
description?: string;
|
||||||
|
brand?: string;
|
||||||
|
department?: string;
|
||||||
|
color?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Interface para parâmetros de pesquisa de pedidos
|
* Interface para parâmetros de pesquisa de pedidos
|
||||||
* Define todos os campos possíveis usados no formulário de busca
|
* Define todos os campos possíveis usados no formulário de busca
|
||||||
@@ -241,6 +263,8 @@ export interface OrderSearchParams {
|
|||||||
deliveryLocal?: string;
|
deliveryLocal?: string;
|
||||||
/** ID do vendedor */
|
/** ID do vendedor */
|
||||||
sellerId?: string | number;
|
sellerId?: string | number;
|
||||||
|
/** Nome do vendedor */
|
||||||
|
sellerName?: string;
|
||||||
/** Documento do cliente (CPF/CNPJ) - duplicado para compatibilidade */
|
/** Documento do cliente (CPF/CNPJ) - duplicado para compatibilidade */
|
||||||
document?: string;
|
document?: string;
|
||||||
/** Número da nota fiscal */
|
/** Número da nota fiscal */
|
||||||
@@ -251,6 +275,10 @@ export interface OrderSearchParams {
|
|||||||
invoiceId?: string;
|
invoiceId?: string;
|
||||||
/** Número da nota fiscal (NFe) */
|
/** Número da nota fiscal (NFe) */
|
||||||
nfe?: string;
|
nfe?: string;
|
||||||
|
/** ID do produto para busca */
|
||||||
|
productId?: string;
|
||||||
|
/** Produto selecionado pelo componente de busca */
|
||||||
|
selectedProduct?: Product | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface CustomerSearchParams {
|
export interface CustomerSearchParams {
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import { clearUrlAndNavigate } from "@/src/utils/url-helpers";
|
|||||||
/**
|
/**
|
||||||
* Função de utilidade para aplicar debounce em funções
|
* Função de utilidade para aplicar debounce em funções
|
||||||
* Evita execuções repetidas em curtos intervalos de tempo
|
* Evita execuções repetidas em curtos intervalos de tempo
|
||||||
*
|
*
|
||||||
* @template T - Tipo da função a ser debounced
|
* @template T - Tipo da função a ser debounced
|
||||||
* @param {T} callback - Função a aplicar debounce
|
* @param {T} callback - Função a aplicar debounce
|
||||||
* @param {number} delay - Tempo de espera em ms
|
* @param {number} delay - Tempo de espera em ms
|
||||||
@@ -84,31 +84,32 @@ const paramMapping: Record<string, keyof OrderSearchParams> = {
|
|||||||
createDateEnd: "createDateEnd",
|
createDateEnd: "createDateEnd",
|
||||||
customerId: "customerId",
|
customerId: "customerId",
|
||||||
document: "document",
|
document: "document",
|
||||||
invoiceNumber: "invoiceNumber"
|
invoiceNumber: "invoiceNumber",
|
||||||
|
productId: "productId"
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Hook personalizado para gerenciar a consulta e exibição de pedidos
|
* Hook personalizado para gerenciar a consulta e exibição de pedidos
|
||||||
* Gerencia busca, paginação, armazenamento de parâmetros e detalhes de pedidos
|
* Gerencia busca, paginação, armazenamento de parâmetros e detalhes de pedidos
|
||||||
*
|
*
|
||||||
* @param {number} ordersPerPage - Número de pedidos a serem exibidos por página
|
* @param {number} ordersPerPage - Número de pedidos a serem exibidos por página
|
||||||
* @returns {UseOrderSearchReturn} Objeto contendo estado e funções para lidar com pedidos
|
* @returns {UseOrderSearchReturn} Objeto contendo estado e funções para lidar com pedidos
|
||||||
*/
|
*/
|
||||||
export function useOrderSearch(ordersPerPage: number = 8): UseOrderSearchReturn {
|
export function useOrderSearch(ordersPerPage: number = 8): UseOrderSearchReturn {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const urlSearchParams = useSearchParams();
|
const urlSearchParams = useSearchParams();
|
||||||
|
|
||||||
// Inicializar searchParams com valores da URL ou do localStorage ou um objeto vazio
|
// Inicializar searchParams com valores da URL ou do localStorage ou um objeto vazio
|
||||||
const [searchParams, setSearchParams] = useState<OrderSearchParams>(() => {
|
const [searchParams, setSearchParams] = useState<OrderSearchParams>(() => {
|
||||||
const params: OrderSearchParams = {};
|
const params: OrderSearchParams = {};
|
||||||
|
|
||||||
// Tentar recuperar do localStorage primeiro
|
// Tentar recuperar do localStorage primeiro
|
||||||
const savedParams = getStorage("orderSearchParams");
|
const savedParams = getStorage("orderSearchParams");
|
||||||
|
|
||||||
if (savedParams) {
|
if (savedParams) {
|
||||||
return savedParams;
|
return savedParams;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Se não houver no localStorage, extrair valores da URL para o estado
|
// Se não houver no localStorage, extrair valores da URL para o estado
|
||||||
Object.entries(paramMapping).forEach(([urlParam, stateParam]) => {
|
Object.entries(paramMapping).forEach(([urlParam, stateParam]) => {
|
||||||
const value = urlSearchParams.get(urlParam);
|
const value = urlSearchParams.get(urlParam);
|
||||||
@@ -117,7 +118,7 @@ export function useOrderSearch(ordersPerPage: number = 8): UseOrderSearchReturn
|
|||||||
(params as any)[stateParam] = value;
|
(params as any)[stateParam] = value;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Tratamento especial para parâmetros que podem ser arrays
|
// Tratamento especial para parâmetros que podem ser arrays
|
||||||
const typeParam = urlSearchParams.get('type');
|
const typeParam = urlSearchParams.get('type');
|
||||||
if (typeParam) {
|
if (typeParam) {
|
||||||
@@ -128,10 +129,10 @@ export function useOrderSearch(ordersPerPage: number = 8): UseOrderSearchReturn
|
|||||||
params.type = typeParam;
|
params.type = typeParam;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return params;
|
return params;
|
||||||
});
|
});
|
||||||
|
|
||||||
const [orders, setOrders] = useState<Order[]>([]);
|
const [orders, setOrders] = useState<Order[]>([]);
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
@@ -147,7 +148,7 @@ export function useOrderSearch(ordersPerPage: number = 8): UseOrderSearchReturn
|
|||||||
|
|
||||||
// Detectar parâmetro origem=detalhe para realizar pesquisa automática ao retornar
|
// Detectar parâmetro origem=detalhe para realizar pesquisa automática ao retornar
|
||||||
const origin = urlSearchParams.get("origem");
|
const origin = urlSearchParams.get("origem");
|
||||||
|
|
||||||
// Limpar todos os campos relacionados ao cliente
|
// Limpar todos os campos relacionados ao cliente
|
||||||
const clearCustomerFields = useCallback(() => {
|
const clearCustomerFields = useCallback(() => {
|
||||||
setSearchParams(prev => ({
|
setSearchParams(prev => ({
|
||||||
@@ -158,12 +159,21 @@ export function useOrderSearch(ordersPerPage: number = 8): UseOrderSearchReturn
|
|||||||
selectedCustomer: null,
|
selectedCustomer: null,
|
||||||
}));
|
}));
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// Efeito para realizar pesquisa automática quando a página é carregada
|
// Limpar todos os campos relacionados ao produto
|
||||||
|
const clearProductFields = useCallback(() => {
|
||||||
|
setSearchParams(prev => ({
|
||||||
|
...prev,
|
||||||
|
productId: "",
|
||||||
|
selectedProduct: null,
|
||||||
|
}));
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// Efeito para realizar pesquisa automática quando a página é carregada
|
||||||
// e há parâmetros de pesquisa na URL ou se estamos voltando da página de detalhes
|
// e há parâmetros de pesquisa na URL ou se estamos voltando da página de detalhes
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const hasSearchParams = Object.keys(searchParams).length > 0;
|
const hasSearchParams = Object.keys(searchParams).length > 0;
|
||||||
|
|
||||||
// Se temos parâmetros de busca e vamos retornando da página de detalhes,
|
// Se temos parâmetros de busca e vamos retornando da página de detalhes,
|
||||||
// ou se temos parâmetros e ainda não realizamos a pesquisa
|
// ou se temos parâmetros e ainda não realizamos a pesquisa
|
||||||
if ((hasSearchParams && origin === "detalhe") || (hasSearchParams && !hasSearched)) {
|
if ((hasSearchParams && origin === "detalhe") || (hasSearchParams && !hasSearched)) {
|
||||||
@@ -177,7 +187,7 @@ export function useOrderSearch(ordersPerPage: number = 8): UseOrderSearchReturn
|
|||||||
*/
|
*/
|
||||||
const updateUrlWithSearchParams = useCallback(() => {
|
const updateUrlWithSearchParams = useCallback(() => {
|
||||||
const urlParams = new URLSearchParams();
|
const urlParams = new URLSearchParams();
|
||||||
|
|
||||||
// Adicionar apenas parâmetros com valores
|
// Adicionar apenas parâmetros com valores
|
||||||
Object.entries(searchParams).forEach(([key, value]) => {
|
Object.entries(searchParams).forEach(([key, value]) => {
|
||||||
if (value) {
|
if (value) {
|
||||||
@@ -189,15 +199,15 @@ export function useOrderSearch(ordersPerPage: number = 8): UseOrderSearchReturn
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Preservar parâmetro origem se existir
|
// Preservar parâmetro origem se existir
|
||||||
if (origin) {
|
if (origin) {
|
||||||
urlParams.append("origem", origin);
|
urlParams.append("origem", origin);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Salvar no localStorage
|
// Salvar no localStorage
|
||||||
setStorage("orderSearchParams", searchParams);
|
setStorage("orderSearchParams", searchParams);
|
||||||
|
|
||||||
// Atualizar URL sem causar recarregamento da página
|
// Atualizar URL sem causar recarregamento da página
|
||||||
const url = window.location.pathname + (urlParams.toString() ? `?${urlParams.toString()}` : "");
|
const url = window.location.pathname + (urlParams.toString() ? `?${urlParams.toString()}` : "");
|
||||||
window.history.replaceState({ path: url }, "", url);
|
window.history.replaceState({ path: url }, "", url);
|
||||||
@@ -206,7 +216,7 @@ export function useOrderSearch(ordersPerPage: number = 8): UseOrderSearchReturn
|
|||||||
/**
|
/**
|
||||||
* Executa a busca de pedidos com base nos parâmetros definidos
|
* Executa a busca de pedidos com base nos parâmetros definidos
|
||||||
* Valida datas, formata parâmetros e faz a chamada à API
|
* Valida datas, formata parâmetros e faz a chamada à API
|
||||||
*
|
*
|
||||||
* @returns {Promise<void>}
|
* @returns {Promise<void>}
|
||||||
*/
|
*/
|
||||||
const handleSearch = async () => {
|
const handleSearch = async () => {
|
||||||
@@ -236,27 +246,30 @@ export function useOrderSearch(ordersPerPage: number = 8): UseOrderSearchReturn
|
|||||||
|
|
||||||
// Transform the ALL value to empty string for the API call
|
// Transform the ALL value to empty string for the API call
|
||||||
const paramsToSend: OrderSearchParams = { ...searchParams };
|
const paramsToSend: OrderSearchParams = { ...searchParams };
|
||||||
|
|
||||||
// Remover o campo selectedCustomer antes de enviar para a API
|
// Remover campos que não devem ser enviados para a API
|
||||||
if (paramsToSend.selectedCustomer) {
|
if (paramsToSend.selectedCustomer) {
|
||||||
delete paramsToSend.selectedCustomer;
|
delete paramsToSend.selectedCustomer;
|
||||||
}
|
}
|
||||||
|
if (paramsToSend.selectedProduct) {
|
||||||
|
delete paramsToSend.selectedProduct;
|
||||||
|
}
|
||||||
|
|
||||||
// Se tiver customerId definido, remover name e document para evitar conflito
|
// Se tiver customerId definido, remover name e document para evitar conflito
|
||||||
if (paramsToSend.customerId) {
|
if (paramsToSend.customerId) {
|
||||||
delete paramsToSend.name;
|
delete paramsToSend.name;
|
||||||
delete paramsToSend.document;
|
delete paramsToSend.document;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Se temos o nome do vendedor mas não temos o ID, tentar buscar o ID primeiro
|
// Se temos o nome do vendedor mas não temos o ID, tentar buscar o ID primeiro
|
||||||
if (paramsToSend.sellerName && !paramsToSend.sellerId) {
|
if (paramsToSend.sellerName && !paramsToSend.sellerId) {
|
||||||
try {
|
try {
|
||||||
const sellers = await dataConsultApi.getSellers();
|
const sellers = await dataConsultApi.getSellers();
|
||||||
if (Array.isArray(sellers)) {
|
if (Array.isArray(sellers)) {
|
||||||
const matchingSeller = sellers.find((seller: any) =>
|
const matchingSeller = sellers.find((seller: any) =>
|
||||||
seller.name.toLowerCase().includes(paramsToSend.sellerName?.toLowerCase() || '')
|
seller.name.toLowerCase().includes(paramsToSend.sellerName?.toLowerCase() || '')
|
||||||
);
|
);
|
||||||
|
|
||||||
if (matchingSeller) {
|
if (matchingSeller) {
|
||||||
// Extrair código numérico (887 de "887 - SAL-SIMONI")
|
// Extrair código numérico (887 de "887 - SAL-SIMONI")
|
||||||
if (matchingSeller.name && matchingSeller.name.includes("-")) {
|
if (matchingSeller.name && matchingSeller.name.includes("-")) {
|
||||||
@@ -276,18 +289,18 @@ export function useOrderSearch(ordersPerPage: number = 8): UseOrderSearchReturn
|
|||||||
console.error("Erro ao buscar vendedores:", err);
|
console.error("Erro ao buscar vendedores:", err);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Mapear invoiceNumber para invoiceId que é o que a API espera
|
// Mapear invoiceNumber para invoiceId que é o que a API espera
|
||||||
if (paramsToSend.invoiceNumber) {
|
if (paramsToSend.invoiceNumber) {
|
||||||
paramsToSend.invoiceId = paramsToSend.invoiceNumber;
|
paramsToSend.invoiceId = paramsToSend.invoiceNumber;
|
||||||
delete paramsToSend.invoiceNumber;
|
delete paramsToSend.invoiceNumber;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remover campos que a API não espera
|
// Remover campos que a API não espera
|
||||||
if ('nfe' in paramsToSend) {
|
if ('nfe' in paramsToSend) {
|
||||||
delete paramsToSend.nfe;
|
delete paramsToSend.nfe;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (paramsToSend.codfilial === "ALL") {
|
if (paramsToSend.codfilial === "ALL") {
|
||||||
paramsToSend.codfilial = "";
|
paramsToSend.codfilial = "";
|
||||||
}
|
}
|
||||||
@@ -305,7 +318,7 @@ export function useOrderSearch(ordersPerPage: number = 8): UseOrderSearchReturn
|
|||||||
|
|
||||||
// Construir manualmente o queryParams para lidar com arrays
|
// Construir manualmente o queryParams para lidar com arrays
|
||||||
const queryParams = new URLSearchParams();
|
const queryParams = new URLSearchParams();
|
||||||
|
|
||||||
Object.entries(paramsToSend).forEach(([key, value]) => {
|
Object.entries(paramsToSend).forEach(([key, value]) => {
|
||||||
if ((key === 'type' || key === 'status' || key === 'deliveryType') && Array.isArray(value)) {
|
if ((key === 'type' || key === 'status' || key === 'deliveryType') && Array.isArray(value)) {
|
||||||
// Unir os valores do array em uma única string separada por vírgula
|
// Unir os valores do array em uma única string separada por vírgula
|
||||||
@@ -314,21 +327,21 @@ export function useOrderSearch(ordersPerPage: number = 8): UseOrderSearchReturn
|
|||||||
queryParams.append(key, String(value));
|
queryParams.append(key, String(value));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const queryString = queryParams.toString();
|
const queryString = queryParams.toString();
|
||||||
const response = await ordersApi.findOrders(paramsToSend, queryString);
|
const response = await ordersApi.findOrders(paramsToSend, queryString);
|
||||||
setOrders(extractApiData<Order>(response));
|
setOrders(extractApiData<Order>(response));
|
||||||
setError(null);
|
setError(null);
|
||||||
|
|
||||||
// Reset selected order and items when new search is performed
|
// Reset selected order and items when new search is performed
|
||||||
setSelectedOrderId(null);
|
setSelectedOrderId(null);
|
||||||
setOrderItems([]);
|
setOrderItems([]);
|
||||||
setCutItems([]);
|
setCutItems([]);
|
||||||
|
|
||||||
// Reset pagination state
|
// Reset pagination state
|
||||||
setCurrentPage(1);
|
setCurrentPage(1);
|
||||||
setVisibleOrdersCount(ordersPerPage);
|
setVisibleOrdersCount(ordersPerPage);
|
||||||
|
|
||||||
// Atualizar a URL com os parâmetros de pesquisa
|
// Atualizar a URL com os parâmetros de pesquisa
|
||||||
updateUrlWithSearchParams();
|
updateUrlWithSearchParams();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@@ -342,13 +355,13 @@ export function useOrderSearch(ordersPerPage: number = 8): UseOrderSearchReturn
|
|||||||
/**
|
/**
|
||||||
* Manipula alterações em campos de entrada do formulário
|
* Manipula alterações em campos de entrada do formulário
|
||||||
* Atualiza o estado searchParams com o novo valor
|
* Atualiza o estado searchParams com o novo valor
|
||||||
*
|
*
|
||||||
* @param {keyof OrderSearchParams} field - Nome do campo a atualizar
|
* @param {keyof OrderSearchParams} field - Nome do campo a atualizar
|
||||||
* @param {any} value - Novo valor para o campo
|
* @param {any} value - Novo valor para o campo
|
||||||
*/
|
*/
|
||||||
const handleInputChange = (field: keyof OrderSearchParams, value: any) => {
|
const handleInputChange = (field: keyof OrderSearchParams, value: any) => {
|
||||||
// Garantir que valores apropriados sejam tratados como arrays
|
// Garantir que valores apropriados sejam tratados como arrays
|
||||||
if ((field === 'type' || field === 'status' || field === 'deliveryType') &&
|
if ((field === 'type' || field === 'status' || field === 'deliveryType') &&
|
||||||
typeof value === 'string' && value !== '') {
|
typeof value === 'string' && value !== '') {
|
||||||
// Converter string para array com um único item
|
// Converter string para array com um único item
|
||||||
setSearchParams((prev) => ({
|
setSearchParams((prev) => ({
|
||||||
@@ -370,7 +383,7 @@ export function useOrderSearch(ordersPerPage: number = 8): UseOrderSearchReturn
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Seleciona um cliente após busca, preenchendo campos relacionados
|
* Seleciona um cliente após busca, preenchendo campos relacionados
|
||||||
*
|
*
|
||||||
* @param {Cliente | null} customer - Cliente selecionado ou null para limpar
|
* @param {Cliente | null} customer - Cliente selecionado ou null para limpar
|
||||||
*/
|
*/
|
||||||
const handleCustomerSelect = (customer: Cliente | null) => {
|
const handleCustomerSelect = (customer: Cliente | null) => {
|
||||||
@@ -378,7 +391,7 @@ export function useOrderSearch(ordersPerPage: number = 8): UseOrderSearchReturn
|
|||||||
clearCustomerFields();
|
clearCustomerFields();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
setSearchParams((prev) => ({
|
setSearchParams((prev) => ({
|
||||||
...prev,
|
...prev,
|
||||||
selectedCustomer: customer,
|
selectedCustomer: customer,
|
||||||
@@ -391,7 +404,7 @@ export function useOrderSearch(ordersPerPage: number = 8): UseOrderSearchReturn
|
|||||||
/**
|
/**
|
||||||
* Manipula clique em linha da tabela de pedidos
|
* Manipula clique em linha da tabela de pedidos
|
||||||
* Carrega detalhes do pedido selecionado
|
* Carrega detalhes do pedido selecionado
|
||||||
*
|
*
|
||||||
* @param {string} orderId - ID do pedido selecionado
|
* @param {string} orderId - ID do pedido selecionado
|
||||||
* @returns {Promise<void>}
|
* @returns {Promise<void>}
|
||||||
*/
|
*/
|
||||||
@@ -435,14 +448,14 @@ export function useOrderSearch(ordersPerPage: number = 8): UseOrderSearchReturn
|
|||||||
|
|
||||||
// Pagination calculations - corrigir para garantir 8 itens por página
|
// Pagination calculations - corrigir para garantir 8 itens por página
|
||||||
const totalPages = Math.ceil(orders.length / ordersPerPage);
|
const totalPages = Math.ceil(orders.length / ordersPerPage);
|
||||||
|
|
||||||
// Calcular o índice inicial baseado na página atual
|
// Calcular o índice inicial baseado na página atual
|
||||||
const indexOfFirstOrder = (currentPage - 1) * ordersPerPage;
|
const indexOfFirstOrder = (currentPage - 1) * ordersPerPage;
|
||||||
// Calcular o índice final
|
// Calcular o índice final
|
||||||
const indexOfLastOrder = Math.min(indexOfFirstOrder + ordersPerPage, orders.length);
|
const indexOfLastOrder = Math.min(indexOfFirstOrder + ordersPerPage, orders.length);
|
||||||
// Selecionar apenas os itens da página atual
|
// Selecionar apenas os itens da página atual
|
||||||
const currentOrders = orders.slice(indexOfFirstOrder, indexOfLastOrder);
|
const currentOrders = orders.slice(indexOfFirstOrder, indexOfLastOrder);
|
||||||
|
|
||||||
// Navigate to next page
|
// Navigate to next page
|
||||||
const goToNextPage = useCallback(() => {
|
const goToNextPage = useCallback(() => {
|
||||||
if (currentPage < totalPages) {
|
if (currentPage < totalPages) {
|
||||||
@@ -490,7 +503,7 @@ export function useOrderSearch(ordersPerPage: number = 8): UseOrderSearchReturn
|
|||||||
/**
|
/**
|
||||||
* Identifica o tipo de entrada para busca de cliente
|
* Identifica o tipo de entrada para busca de cliente
|
||||||
* Diferencia entre ID, documento ou nome baseado no formato
|
* Diferencia entre ID, documento ou nome baseado no formato
|
||||||
*
|
*
|
||||||
* @param {string} value - Valor de entrada a ser analisado
|
* @param {string} value - Valor de entrada a ser analisado
|
||||||
* @returns {'id' | 'document' | 'name'} Tipo identificado da entrada
|
* @returns {'id' | 'document' | 'name'} Tipo identificado da entrada
|
||||||
*/
|
*/
|
||||||
@@ -503,7 +516,7 @@ export function useOrderSearch(ordersPerPage: number = 8): UseOrderSearchReturn
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Busca cliente por ID
|
* Busca cliente por ID
|
||||||
*
|
*
|
||||||
* @param {string} value - ID do cliente a ser buscado
|
* @param {string} value - ID do cliente a ser buscado
|
||||||
* @returns {Promise<void>}
|
* @returns {Promise<void>}
|
||||||
*/
|
*/
|
||||||
@@ -514,10 +527,10 @@ export function useOrderSearch(ordersPerPage: number = 8): UseOrderSearchReturn
|
|||||||
clearCustomerFields();
|
clearCustomerFields();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let customerData: any = null;
|
let customerData: any = null;
|
||||||
const inputType = identifyInputType(value);
|
const inputType = identifyInputType(value);
|
||||||
|
|
||||||
// Atualizar o campo apropriado com base no tipo de entrada
|
// Atualizar o campo apropriado com base no tipo de entrada
|
||||||
setSearchParams(prev => ({
|
setSearchParams(prev => ({
|
||||||
...prev,
|
...prev,
|
||||||
@@ -525,7 +538,7 @@ export function useOrderSearch(ordersPerPage: number = 8): UseOrderSearchReturn
|
|||||||
document: inputType === 'document' ? value : "",
|
document: inputType === 'document' ? value : "",
|
||||||
name: inputType === 'name' ? value : "",
|
name: inputType === 'name' ? value : "",
|
||||||
}));
|
}));
|
||||||
|
|
||||||
// Buscar cliente pelo tipo correto
|
// Buscar cliente pelo tipo correto
|
||||||
if (inputType === 'id') {
|
if (inputType === 'id') {
|
||||||
const response = await dataConsultApi.getCustomer(value);
|
const response = await dataConsultApi.getCustomer(value);
|
||||||
@@ -538,7 +551,7 @@ export function useOrderSearch(ordersPerPage: number = 8): UseOrderSearchReturn
|
|||||||
customerData = response[0];
|
customerData = response[0];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (customerData) {
|
if (customerData) {
|
||||||
// Create customer object from response
|
// Create customer object from response
|
||||||
const customer: Customer = {
|
const customer: Customer = {
|
||||||
@@ -546,7 +559,7 @@ export function useOrderSearch(ordersPerPage: number = 8): UseOrderSearchReturn
|
|||||||
name: customerData.name,
|
name: customerData.name,
|
||||||
document: customerData.document || ""
|
document: customerData.document || ""
|
||||||
};
|
};
|
||||||
|
|
||||||
// Update selected customer in form state
|
// Update selected customer in form state
|
||||||
setSearchParams(prev => ({
|
setSearchParams(prev => ({
|
||||||
...prev,
|
...prev,
|
||||||
@@ -564,21 +577,21 @@ export function useOrderSearch(ordersPerPage: number = 8): UseOrderSearchReturn
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Busca cliente por nome
|
* Busca cliente por nome
|
||||||
*
|
*
|
||||||
* @param {string} value - Nome parcial ou completo para busca
|
* @param {string} value - Nome parcial ou completo para busca
|
||||||
* @returns {Promise<void>}
|
* @returns {Promise<void>}
|
||||||
*/
|
*/
|
||||||
const searchCustomerByName = async (value: string) => {
|
const searchCustomerByName = async (value: string) => {
|
||||||
if (!value || value.length < 2) return;
|
if (!value || value.length < 2) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const matches = await clientesApi.search(value);
|
const matches = await clientesApi.search(value);
|
||||||
if (matches.length > 0) {
|
if (matches.length > 0) {
|
||||||
const c = matches[0];
|
const c = matches[0];
|
||||||
|
|
||||||
const clientId = c.id || "";
|
const clientId = c.id || "";
|
||||||
const clientName = c.name || "";
|
const clientName = c.name || "";
|
||||||
|
|
||||||
setSearchParams(prev => ({
|
setSearchParams(prev => ({
|
||||||
...prev,
|
...prev,
|
||||||
customerId: clientId,
|
customerId: clientId,
|
||||||
@@ -596,14 +609,14 @@ export function useOrderSearch(ordersPerPage: number = 8): UseOrderSearchReturn
|
|||||||
setError("Erro ao buscar cliente. Por favor, tente novamente.");
|
setError("Erro ao buscar cliente. Por favor, tente novamente.");
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Criar versão com debounce da função de busca
|
// Criar versão com debounce da função de busca
|
||||||
const debouncedSearchCustomer = useDebounce(searchCustomerByName, 500);
|
const debouncedSearchCustomer = useDebounce(searchCustomerByName, 500);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Manipula entrada de filtro de cliente
|
* Manipula entrada de filtro de cliente
|
||||||
* Identifica o tipo de filtro e direciona para a busca apropriada
|
* Identifica o tipo de filtro e direciona para a busca apropriada
|
||||||
*
|
*
|
||||||
* @param {string} value - Termo de busca (ID, documento ou nome)
|
* @param {string} value - Termo de busca (ID, documento ou nome)
|
||||||
* @returns {Promise<void>}
|
* @returns {Promise<void>}
|
||||||
*/
|
*/
|
||||||
@@ -613,7 +626,7 @@ export function useOrderSearch(ordersPerPage: number = 8): UseOrderSearchReturn
|
|||||||
clearCustomerFields();
|
clearCustomerFields();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Atualiza o campo de nome no estado
|
// Atualiza o campo de nome no estado
|
||||||
setSearchParams(prev => ({
|
setSearchParams(prev => ({
|
||||||
...prev,
|
...prev,
|
||||||
@@ -624,11 +637,11 @@ export function useOrderSearch(ordersPerPage: number = 8): UseOrderSearchReturn
|
|||||||
|
|
||||||
// Se ainda está digitando poucas letras, apenas atualiza o estado
|
// Se ainda está digitando poucas letras, apenas atualiza o estado
|
||||||
if (value.length < 2) return;
|
if (value.length < 2) return;
|
||||||
|
|
||||||
// Aciona a busca com debounce para evitar chamadas excessivas à API
|
// Aciona a busca com debounce para evitar chamadas excessivas à API
|
||||||
debouncedSearchCustomer(value);
|
debouncedSearchCustomer(value);
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Restaura o estado da pesquisa com dados previamente salvos
|
* Restaura o estado da pesquisa com dados previamente salvos
|
||||||
* Útil para preservar o estado ao voltar da página de detalhes
|
* Útil para preservar o estado ao voltar da página de detalhes
|
||||||
@@ -638,12 +651,12 @@ export function useOrderSearch(ordersPerPage: number = 8): UseOrderSearchReturn
|
|||||||
setCurrentPage(savedPage);
|
setCurrentPage(savedPage);
|
||||||
setHasSearched(savedHasSearched);
|
setHasSearched(savedHasSearched);
|
||||||
setVisibleOrdersCount(ordersPerPage * savedPage);
|
setVisibleOrdersCount(ordersPerPage * savedPage);
|
||||||
|
|
||||||
// Calcular total de páginas
|
// Calcular total de páginas
|
||||||
const calculatedTotalPages = Math.ceil(savedOrders.length / ordersPerPage);
|
const calculatedTotalPages = Math.ceil(savedOrders.length / ordersPerPage);
|
||||||
// Como não temos acesso direto à função setTotalPages, calcular localmente
|
// Como não temos acesso direto à função setTotalPages, calcular localmente
|
||||||
const totalPages = calculatedTotalPages;
|
const totalPages = calculatedTotalPages;
|
||||||
|
|
||||||
// Atualizar pedidos da página atual
|
// Atualizar pedidos da página atual
|
||||||
const indexOfLastOrder = savedPage * ordersPerPage;
|
const indexOfLastOrder = savedPage * ordersPerPage;
|
||||||
const indexOfFirstOrder = indexOfLastOrder - ordersPerPage;
|
const indexOfFirstOrder = indexOfLastOrder - ordersPerPage;
|
||||||
@@ -671,7 +684,7 @@ export function useOrderSearch(ordersPerPage: number = 8): UseOrderSearchReturn
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Manipula a seleção de múltiplos valores em filtros de seleção
|
* Manipula a seleção de múltiplos valores em filtros de seleção
|
||||||
*
|
*
|
||||||
* @param {string} field - Nome do campo no objeto searchParams
|
* @param {string} field - Nome do campo no objeto searchParams
|
||||||
* @param {string[]} values - Array de valores selecionados
|
* @param {string[]} values - Array de valores selecionados
|
||||||
*/
|
*/
|
||||||
@@ -716,4 +729,4 @@ export function useOrderSearch(ordersPerPage: number = 8): UseOrderSearchReturn
|
|||||||
clearAndNavigateToOrders,
|
clearAndNavigateToOrders,
|
||||||
handleMultiInputChange
|
handleMultiInputChange
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,10 @@ import { leadtime } from "../components/types";
|
|||||||
import { Cliente } from "../components/types";
|
import { Cliente } from "../components/types";
|
||||||
|
|
||||||
// URL base da API
|
// URL base da API
|
||||||
const API_BASE_URL = process.env.NEXT_PUBLIC_API_URL || "https://portalapi.jurunense.com";
|
//const API_BASE_URL = process.env.NEXT_PUBLIC_API_URL || "https://portalapi.jurunense.com";
|
||||||
|
const API_BASE_URL = process.env.NEXT_PUBLIC_API_URL || "http://10.1.1.212:8805";
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Classe de erro personalizada para requisições de API.
|
* Classe de erro personalizada para requisições de API.
|
||||||
@@ -29,7 +32,7 @@ export class ApiError extends Error {
|
|||||||
*/
|
*/
|
||||||
export async function fetchApi<T>(endpoint: string, options?: RequestInit): Promise<T> {
|
export async function fetchApi<T>(endpoint: string, options?: RequestInit): Promise<T> {
|
||||||
const url = `${API_BASE_URL}${endpoint}`;
|
const url = `${API_BASE_URL}${endpoint}`;
|
||||||
|
|
||||||
// Obtém o token de autenticação do localStorage
|
// Obtém o token de autenticação do localStorage
|
||||||
let authToken = null;
|
let authToken = null;
|
||||||
try {
|
try {
|
||||||
@@ -43,8 +46,8 @@ export async function fetchApi<T>(endpoint: string, options?: RequestInit): Prom
|
|||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
...(options?.headers as Record<string, string>),
|
...(options?.headers as Record<string, string>),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
if (authToken) {
|
if (authToken) {
|
||||||
headers["Authorization"] = `Bearer ${authToken}`;
|
headers["Authorization"] = `Bearer ${authToken}`;
|
||||||
}
|
}
|
||||||
@@ -98,7 +101,7 @@ export const dataConsultApi = {
|
|||||||
getStores: () => fetchApi<any[]>("/api/v1/data-consult/stores"),
|
getStores: () => fetchApi<any[]>("/api/v1/data-consult/stores"),
|
||||||
|
|
||||||
/** @returns {Promise<any[]>} Lista de vendedores */
|
/** @returns {Promise<any[]>} Lista de vendedores */
|
||||||
getSellers: () =>
|
getSellers: () =>
|
||||||
fetchApi<any>("/api/v1/data-consult/sellers")
|
fetchApi<any>("/api/v1/data-consult/sellers")
|
||||||
.then(response => {
|
.then(response => {
|
||||||
// Tentar extrair dados de vendedores em diferentes formatos
|
// Tentar extrair dados de vendedores em diferentes formatos
|
||||||
@@ -126,7 +129,7 @@ export const dataConsultApi = {
|
|||||||
* @param {string} name - Nome do vendedor para filtrar
|
* @param {string} name - Nome do vendedor para filtrar
|
||||||
* @returns {Promise<any[]>} Lista de vendedores filtrados
|
* @returns {Promise<any[]>} Lista de vendedores filtrados
|
||||||
*/
|
*/
|
||||||
searchSellers: (name: string) =>
|
searchSellers: (name: string) =>
|
||||||
fetchApi<any>(`/api/v1/data-consult/sellers/search?name=${encodeURIComponent(name)}`)
|
fetchApi<any>(`/api/v1/data-consult/sellers/search?name=${encodeURIComponent(name)}`)
|
||||||
.then(response => {
|
.then(response => {
|
||||||
// Tratar a resposta de forma similar a getSellers
|
// Tratar a resposta de forma similar a getSellers
|
||||||
@@ -179,10 +182,10 @@ export const dataConsultApi = {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {string} filter - Filtro de produto (nome ou código).
|
* @param {string} filter - Filtro de produto (nome ou código).
|
||||||
* @returns {Promise<any[]>} Produtos filtrados.
|
* @returns {Promise<{success: boolean, data: any[]}>} Produtos filtrados.
|
||||||
*/
|
*/
|
||||||
getProducts: (filter: string) =>
|
getProducts: (filter: string) =>
|
||||||
fetchApi<any[]>(`/api/v1/data-consult/products/${filter}`),
|
fetchApi<{ success: boolean; data: any[] }>(`/api/v1/data-consult/products/${filter}`),
|
||||||
|
|
||||||
/** @returns {Promise<any[]>} Todos os produtos */
|
/** @returns {Promise<any[]>} Todos os produtos */
|
||||||
getAllProducts: () => fetchApi<any[]>("/api/v1/data-consult/all"),
|
getAllProducts: () => fetchApi<any[]>("/api/v1/data-consult/all"),
|
||||||
|
|||||||
Reference in New Issue
Block a user