474 lines
14 KiB
TypeScript
474 lines
14 KiB
TypeScript
import React, { useState, useEffect } from "react";
|
|
import LoadingSpinner from "../LoadingSpinner";
|
|
import { env } from "../../src/config/env";
|
|
import { authService } from "../../src/services/auth.service";
|
|
import { formatCurrency } from "../../utils/formatters";
|
|
import NoData from "../NoData";
|
|
import { Input } from "../ui/input";
|
|
import { Label } from "../ui/label";
|
|
import { Button } from "../ui/button";
|
|
import { CustomAutocomplete } from "../ui/autocomplete";
|
|
import { DateInput } from "../ui/date-input";
|
|
import { DataGridPremium, GridColDef } from "@mui/x-data-grid-premium";
|
|
import "../../lib/mui-license";
|
|
import { Box } from "@mui/material";
|
|
|
|
interface ProductOrder {
|
|
date: string;
|
|
orderId: number;
|
|
invoice?: any;
|
|
customerId: number;
|
|
customer: string;
|
|
seller: string;
|
|
productId: number;
|
|
product: string;
|
|
package: string;
|
|
quantity: number;
|
|
color?: string;
|
|
local?: string;
|
|
deliveryType: string;
|
|
itemId: string;
|
|
}
|
|
|
|
interface Store {
|
|
id: string;
|
|
shortName: string;
|
|
name: string;
|
|
}
|
|
|
|
interface ProductFilterOption {
|
|
id: string;
|
|
option: string;
|
|
}
|
|
|
|
const ProductsSoldView: React.FC = () => {
|
|
const [products, setProducts] = useState<ProductOrder[]>([]);
|
|
const [stores, setStores] = useState<Store[]>([]);
|
|
const [loading, setLoading] = useState(false);
|
|
const [error, setError] = useState<string | null>(null);
|
|
|
|
// Filtros
|
|
const [selectedStore, setSelectedStore] = useState<string>("");
|
|
const [startDate, setStartDate] = useState<string>("");
|
|
const [endDate, setEndDate] = useState<string>("");
|
|
const [orderId, setOrderId] = useState<string>("");
|
|
const [document, setDocument] = useState<string>("");
|
|
const [customerName, setCustomerName] = useState<string>("");
|
|
const [productFilterType, setProductFilterType] = useState<string>("");
|
|
const [productText, setProductText] = useState<string>("");
|
|
|
|
// Opções de filtro de produto
|
|
const productFilterOptions: ProductFilterOption[] = [
|
|
{ id: "ID", option: "Código" },
|
|
{ id: "EAN", option: "Ean" },
|
|
{ id: "TEXT", option: "Descrição" },
|
|
{ id: "PARTNER", option: "Código Fábrica" },
|
|
];
|
|
|
|
useEffect(() => {
|
|
fetchStores();
|
|
}, []);
|
|
|
|
const fetchStores = async () => {
|
|
try {
|
|
const token = authService.getToken();
|
|
const apiUrl = env.API_URL.replace(/\/$/, "");
|
|
|
|
const response = await fetch(`${apiUrl}/lists/store`, {
|
|
method: "GET",
|
|
headers: {
|
|
"Content-Type": "application/json",
|
|
...(token && { Authorization: `Basic ${token}` }),
|
|
},
|
|
});
|
|
|
|
if (response.ok) {
|
|
const data = await response.json();
|
|
setStores(data);
|
|
}
|
|
} catch (err) {
|
|
console.error("Erro ao buscar filiais:", err);
|
|
}
|
|
};
|
|
|
|
const handleSearch = async () => {
|
|
setLoading(true);
|
|
setError(null);
|
|
|
|
try {
|
|
const token = authService.getToken();
|
|
const apiUrl = env.API_URL.replace(/\/$/, "");
|
|
|
|
// Seguindo o padrão do Angular: sellerId sempre 0
|
|
let sellerId = 0;
|
|
if (authService.isManager()) {
|
|
sellerId = 0;
|
|
}
|
|
|
|
// Se não selecionou filial, usar '99' como padrão (seguindo o Angular)
|
|
const store = selectedStore || "99";
|
|
|
|
const params = new URLSearchParams({
|
|
"x-store": store,
|
|
initialDate: startDate || "",
|
|
finalDate: endDate || "",
|
|
document: document || "",
|
|
name: customerName.toUpperCase() || "",
|
|
sellerId: sellerId.toString(),
|
|
idOrder: orderId || "",
|
|
typeFilterProduct: productFilterType || "",
|
|
productText: productText || "",
|
|
});
|
|
|
|
const response = await fetch(
|
|
`${apiUrl}/order/products-order?${params.toString()}`,
|
|
{
|
|
method: "GET",
|
|
headers: {
|
|
Accept: "application/json, text/plain, */*",
|
|
"Content-Type": "application/json",
|
|
...(token && { Authorization: `Basic ${token}` }),
|
|
},
|
|
}
|
|
);
|
|
|
|
if (!response.ok) {
|
|
const errorData = await response.json().catch(() => ({}));
|
|
throw new Error(
|
|
errorData.message ||
|
|
`Erro ao buscar produtos vendidos: ${response.statusText}`
|
|
);
|
|
}
|
|
|
|
const data = await response.json();
|
|
setProducts(data || []);
|
|
} catch (err) {
|
|
console.error("Erro ao buscar produtos vendidos:", err);
|
|
setError(
|
|
err instanceof Error
|
|
? err.message
|
|
: "Erro ao buscar produtos vendidos. Tente novamente."
|
|
);
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
const handleClear = () => {
|
|
setSelectedStore("");
|
|
setStartDate("");
|
|
setEndDate("");
|
|
setOrderId("");
|
|
setDocument("");
|
|
setCustomerName("");
|
|
setProductFilterType("");
|
|
setProductText("");
|
|
setProducts([]);
|
|
setError(null);
|
|
};
|
|
|
|
const formatDate = (dateString: string): string => {
|
|
if (!dateString) return "";
|
|
const date = new Date(dateString);
|
|
return date.toLocaleDateString("pt-BR");
|
|
};
|
|
|
|
// Definir colunas do DataGrid
|
|
const columns: GridColDef[] = [
|
|
{
|
|
field: "date",
|
|
headerName: "Data",
|
|
width: 120,
|
|
valueFormatter: (value) => formatDate(value as string),
|
|
},
|
|
{
|
|
field: "orderId",
|
|
headerName: "N.Pedido",
|
|
width: 130,
|
|
headerAlign: "left",
|
|
},
|
|
{
|
|
field: "customerId",
|
|
headerName: "Cód.Cliente",
|
|
width: 120,
|
|
headerAlign: "left",
|
|
},
|
|
{
|
|
field: "customer",
|
|
headerName: "Cliente",
|
|
width: 250,
|
|
flex: 1,
|
|
},
|
|
{
|
|
field: "seller",
|
|
headerName: "Vendedor",
|
|
width: 150,
|
|
},
|
|
{
|
|
field: "productId",
|
|
headerName: "Cód.Produto",
|
|
width: 130,
|
|
headerAlign: "left",
|
|
},
|
|
{
|
|
field: "product",
|
|
headerName: "Produto",
|
|
width: 300,
|
|
flex: 1,
|
|
},
|
|
{
|
|
field: "package",
|
|
headerName: "Embalagem",
|
|
width: 120,
|
|
},
|
|
{
|
|
field: "quantity",
|
|
headerName: "Quantidade",
|
|
width: 120,
|
|
headerAlign: "right",
|
|
align: "right",
|
|
},
|
|
{
|
|
field: "deliveryType",
|
|
headerName: "Tipo Entrega",
|
|
width: 150,
|
|
},
|
|
];
|
|
|
|
return (
|
|
<div className="space-y-6">
|
|
{/* Header */}
|
|
<header>
|
|
<h2 className="text-2xl font-black text-[#002147] mb-2">
|
|
Consulta vendas por produto
|
|
</h2>
|
|
</header>
|
|
|
|
{/* Filtros */}
|
|
<div className="bg-white p-6 rounded-2xl border border-slate-100 shadow-sm">
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
{/* Filial de venda */}
|
|
<div>
|
|
<Label htmlFor="store">Filial de venda</Label>
|
|
<CustomAutocomplete
|
|
id="store"
|
|
options={stores.map((store) => ({
|
|
value: store.id,
|
|
label: store.shortName,
|
|
}))}
|
|
value={selectedStore}
|
|
onValueChange={setSelectedStore}
|
|
placeholder="Selecione a filial de venda..."
|
|
/>
|
|
</div>
|
|
|
|
{/* Número do pedido */}
|
|
<div>
|
|
<Label htmlFor="orderId">Número do pedido</Label>
|
|
<Input
|
|
id="orderId"
|
|
type="text"
|
|
value={orderId}
|
|
onChange={(e) => setOrderId(e.target.value)}
|
|
placeholder="Informe o número do pedido"
|
|
/>
|
|
</div>
|
|
|
|
{/* Data do pedido - Início */}
|
|
<div>
|
|
<Label htmlFor="startDate">Data inicial</Label>
|
|
<DateInput
|
|
id="startDate"
|
|
value={startDate}
|
|
onChange={(e) => setStartDate(e.target.value)}
|
|
/>
|
|
</div>
|
|
|
|
{/* Data do pedido - Fim */}
|
|
<div>
|
|
<Label htmlFor="endDate">Data final</Label>
|
|
<DateInput
|
|
id="endDate"
|
|
value={endDate}
|
|
onChange={(e) => setEndDate(e.target.value)}
|
|
/>
|
|
</div>
|
|
|
|
{/* CPF/CNPJ */}
|
|
<div>
|
|
<Label htmlFor="document">CPF / CNPJ</Label>
|
|
<Input
|
|
id="document"
|
|
type="text"
|
|
value={document}
|
|
onChange={(e) => setDocument(e.target.value)}
|
|
placeholder="Informe o CPF ou CNPJ do cliente"
|
|
/>
|
|
</div>
|
|
|
|
{/* Nome do cliente */}
|
|
<div>
|
|
<Label htmlFor="customerName">Nome do cliente</Label>
|
|
<Input
|
|
id="customerName"
|
|
type="text"
|
|
value={customerName}
|
|
onChange={(e) => setCustomerName(e.target.value)}
|
|
placeholder="Informe o nome ou razão social do cliente"
|
|
/>
|
|
</div>
|
|
|
|
{/* Tipo de pesquisa do produto */}
|
|
<div>
|
|
<Label htmlFor="productFilterType">Tipo Pesquisa Produto</Label>
|
|
<CustomAutocomplete
|
|
id="productFilterType"
|
|
options={productFilterOptions.map((option) => ({
|
|
value: option.id,
|
|
label: option.option,
|
|
}))}
|
|
value={productFilterType}
|
|
onValueChange={setProductFilterType}
|
|
placeholder="Selecione o tipo de pesquisa..."
|
|
/>
|
|
</div>
|
|
|
|
{/* Produto */}
|
|
<div>
|
|
<Label htmlFor="productText">Produto</Label>
|
|
<Input
|
|
id="productText"
|
|
type="text"
|
|
value={productText}
|
|
onChange={(e) => setProductText(e.target.value)}
|
|
placeholder="Informe o texto para pesquisa do produto"
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Botões de Ação */}
|
|
<div className="mt-6 flex gap-3">
|
|
<Button onClick={handleSearch} disabled={loading} className="flex-1">
|
|
{loading ? "Pesquisando..." : "Pesquisar"}
|
|
</Button>
|
|
<Button
|
|
onClick={handleClear}
|
|
disabled={loading}
|
|
variant="outline"
|
|
className="flex-1"
|
|
>
|
|
Limpar
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Tabela de Produtos Vendidos */}
|
|
{error && (
|
|
<div className="bg-red-50 border border-red-200 rounded-2xl p-4">
|
|
<p className="text-red-600 text-sm font-medium">{error}</p>
|
|
</div>
|
|
)}
|
|
|
|
{loading && (
|
|
<div className="flex items-center justify-center py-20">
|
|
<LoadingSpinner />
|
|
</div>
|
|
)}
|
|
|
|
{!loading && products.length > 0 && (
|
|
<div className="bg-white rounded-2xl border border-slate-100 overflow-hidden">
|
|
<div className="p-5 border-b border-slate-50">
|
|
<h3 className="text-[9px] font-black text-slate-400 uppercase tracking-[0.2em]">
|
|
Produtos encontrados: {products.length}
|
|
</h3>
|
|
</div>
|
|
<Box sx={{ height: 600, width: "100%" }}>
|
|
<DataGridPremium
|
|
rows={products}
|
|
columns={columns}
|
|
getRowId={(row) => `${row.orderId}-${row.itemId}`}
|
|
disableRowSelectionOnClick
|
|
hideFooter
|
|
sx={{
|
|
border: "none",
|
|
"& .MuiDataGrid-columnHeaders": {
|
|
backgroundColor: "#f8fafc",
|
|
borderBottom: "1px solid #e2e8f0",
|
|
"& .MuiDataGrid-columnHeader": {
|
|
fontSize: "9px",
|
|
fontWeight: 900,
|
|
color: "#94a3b8",
|
|
textTransform: "uppercase",
|
|
letterSpacing: "0.2em",
|
|
padding: "12px 16px",
|
|
"& .MuiDataGrid-columnHeaderTitle": {
|
|
fontWeight: 900,
|
|
},
|
|
},
|
|
},
|
|
"& .MuiDataGrid-row": {
|
|
"&:hover": {
|
|
backgroundColor: "#f8fafc",
|
|
},
|
|
},
|
|
"& .MuiDataGrid-cell": {
|
|
fontSize: "12px",
|
|
color: "#475569",
|
|
padding: "16px",
|
|
borderBottom: "1px solid #f1f5f9",
|
|
"&:focus": {
|
|
outline: "none",
|
|
},
|
|
"&:focus-within": {
|
|
outline: "none",
|
|
},
|
|
},
|
|
"& .MuiDataGrid-cell[data-field='orderId']": {
|
|
fontWeight: 700,
|
|
color: "#0f172a",
|
|
},
|
|
"& .MuiDataGrid-cell[data-field='productId']": {
|
|
fontWeight: 700,
|
|
color: "#0f172a",
|
|
},
|
|
"& .MuiDataGrid-cell[data-field='quantity']": {
|
|
fontWeight: 700,
|
|
color: "#0f172a",
|
|
},
|
|
"& .MuiDataGrid-cell[data-field='date']": {
|
|
color: "#64748b",
|
|
},
|
|
"& .MuiDataGrid-cell[data-field='customerId']": {
|
|
color: "#64748b",
|
|
},
|
|
"& .MuiDataGrid-cell[data-field='customer']": {
|
|
color: "#64748b",
|
|
},
|
|
"& .MuiDataGrid-virtualScroller": {
|
|
overflowY: "auto",
|
|
},
|
|
"& .MuiDataGrid-virtualScrollerContent": {
|
|
height: "auto !important",
|
|
},
|
|
}}
|
|
/>
|
|
</Box>
|
|
</div>
|
|
)}
|
|
|
|
{!loading && products.length === 0 && !error && (
|
|
<div className="bg-white rounded-2xl border border-slate-100 overflow-hidden">
|
|
<NoData
|
|
title="Nenhum produto encontrado"
|
|
description="Não foram encontrados produtos vendidos com os filtros informados. Tente ajustar os parâmetros de pesquisa ou verifique se há produtos no período selecionado."
|
|
icon="search"
|
|
variant="outline"
|
|
/>
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default ProductsSoldView;
|