994 lines
33 KiB
TypeScript
994 lines
33 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 ConfirmDialog from "../ConfirmDialog";
|
||
|
|
import OrderItemsModal from "../OrderItemsModal";
|
||
|
|
import PrintOrderDialog from "../PrintOrderDialog";
|
||
|
|
import StimulsoftViewer from "../StimulsoftViewer";
|
||
|
|
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, IconButton } from "@mui/material";
|
||
|
|
import { Edit, Visibility, Print } from "@mui/icons-material";
|
||
|
|
|
||
|
|
interface PreOrder {
|
||
|
|
data: string;
|
||
|
|
idPreOrder: number;
|
||
|
|
value: number;
|
||
|
|
listValue: number;
|
||
|
|
idCustomer: number;
|
||
|
|
customer: string | number;
|
||
|
|
idSeller: number;
|
||
|
|
seller: string | number;
|
||
|
|
status?: string;
|
||
|
|
cpfPreCustomer?: string;
|
||
|
|
namePreCustomer?: string;
|
||
|
|
}
|
||
|
|
|
||
|
|
interface PreOrderItem {
|
||
|
|
productId: number;
|
||
|
|
description: string;
|
||
|
|
package: string;
|
||
|
|
color?: string;
|
||
|
|
local: string;
|
||
|
|
quantity: number;
|
||
|
|
price: number;
|
||
|
|
subTotal: number;
|
||
|
|
}
|
||
|
|
|
||
|
|
interface Store {
|
||
|
|
id: string;
|
||
|
|
shortName: string;
|
||
|
|
name: string;
|
||
|
|
}
|
||
|
|
|
||
|
|
const PreorderView: React.FC = () => {
|
||
|
|
const [preOrders, setPreOrders] = useState<PreOrder[]>([]);
|
||
|
|
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 [preOrderId, setPreOrderId] = useState<string>("");
|
||
|
|
const [document, setDocument] = useState<string>("");
|
||
|
|
const [customerName, setCustomerName] = useState<string>("");
|
||
|
|
|
||
|
|
// Modais
|
||
|
|
const [showOrderItems, setShowOrderItems] = useState(false);
|
||
|
|
const [orderItems, setOrderItems] = useState<PreOrderItem[]>([]);
|
||
|
|
const [selectedPreOrder, setSelectedPreOrder] = useState<PreOrder | null>(
|
||
|
|
null
|
||
|
|
);
|
||
|
|
const [showInfoDialog, setShowInfoDialog] = useState(false);
|
||
|
|
const [infoMessage, setInfoMessage] = useState("");
|
||
|
|
const [infoDescription, setInfoDescription] = useState("");
|
||
|
|
const [showCartLoadedDialog, setShowCartLoadedDialog] = useState(false);
|
||
|
|
const [showPrintDialog, setShowPrintDialog] = useState(false);
|
||
|
|
const [preOrderToPrint, setPreOrderToPrint] = useState<PreOrder | null>(null);
|
||
|
|
const [showPrintViewer, setShowPrintViewer] = useState(false);
|
||
|
|
const [printUrl, setPrintUrl] = useState<string>("");
|
||
|
|
const [printPreOrderId, setPrintPreOrderId] = useState<number | undefined>(
|
||
|
|
undefined
|
||
|
|
);
|
||
|
|
const [printModel, setPrintModel] = useState<string | undefined>(undefined);
|
||
|
|
|
||
|
|
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 exatamente o padrão do Angular: primeiro obtém o seller, depois verifica se é gerente
|
||
|
|
// O Angular getSeller() retorna user.seller diretamente (pode ser number ou string)
|
||
|
|
let sellerId: string | number = authService.getSeller() || 0;
|
||
|
|
|
||
|
|
// Se for gerente, sellerId = 0 (como no Angular)
|
||
|
|
if (authService.isManager()) {
|
||
|
|
sellerId = 0;
|
||
|
|
}
|
||
|
|
|
||
|
|
// Converter para número se for string, mas manter como está se já for número
|
||
|
|
// O HttpParams do Angular aceita qualquer tipo e converte para string automaticamente
|
||
|
|
if (typeof sellerId === "string") {
|
||
|
|
const parsed = parseInt(sellerId, 10);
|
||
|
|
sellerId = isNaN(parsed) ? 0 : parsed;
|
||
|
|
}
|
||
|
|
|
||
|
|
// Converter datas do formato YYYY-MM-DD para strings completas de data
|
||
|
|
// O Angular envia objetos Date que são convertidos para strings completas pelo HttpParams
|
||
|
|
// Exemplo: "Wed Jan 01 2025 00:00:00 GMT-0300 (Horário Padrão de Brasília)"
|
||
|
|
let startDateValue: string = "";
|
||
|
|
let endDateValue: string = "";
|
||
|
|
|
||
|
|
if (startDate) {
|
||
|
|
// Criar Date a partir da string YYYY-MM-DD no timezone local
|
||
|
|
// Usar meia-noite local para garantir o formato correto
|
||
|
|
const [year, month, day] = startDate.split("-").map(Number);
|
||
|
|
const startDateObj = new Date(year, month - 1, day, 0, 0, 0, 0);
|
||
|
|
startDateValue = startDateObj.toString();
|
||
|
|
}
|
||
|
|
|
||
|
|
if (endDate) {
|
||
|
|
// Criar Date a partir da string YYYY-MM-DD no timezone local
|
||
|
|
// Usar meia-noite local para garantir o formato correto
|
||
|
|
const [year, month, day] = endDate.split("-").map(Number);
|
||
|
|
const endDateObj = new Date(year, month - 1, day, 0, 0, 0, 0);
|
||
|
|
endDateValue = endDateObj.toString();
|
||
|
|
}
|
||
|
|
|
||
|
|
// Seguindo exatamente o padrão do Angular: HttpParams.append() adiciona TODOS os parâmetros,
|
||
|
|
// mesmo quando são null ou strings vazias. Isso é importante para o backend processar corretamente.
|
||
|
|
// O Angular HttpParams usa uma codificação similar ao encodeURIComponent, mas preserva alguns caracteres
|
||
|
|
// como ':' (dois pontos) que são seguros em query strings
|
||
|
|
const encodeParam = (value: string): string => {
|
||
|
|
// Primeiro codifica tudo
|
||
|
|
let encoded = encodeURIComponent(value);
|
||
|
|
// Depois decodifica os caracteres que o Angular preserva (dois pontos são seguros em query strings)
|
||
|
|
encoded = encoded.replace(/%3A/g, ":");
|
||
|
|
return encoded;
|
||
|
|
};
|
||
|
|
|
||
|
|
// O Angular HttpParams.append() converte automaticamente qualquer tipo para string
|
||
|
|
// e sempre adiciona o parâmetro, mesmo quando o valor é null, undefined ou string vazia
|
||
|
|
const buildQueryParam = (
|
||
|
|
key: string,
|
||
|
|
value: string | number | null | undefined
|
||
|
|
): string => {
|
||
|
|
// Se o valor for null ou undefined, enviar como string vazia (não como "null" ou "undefined")
|
||
|
|
if (value === null || value === undefined) {
|
||
|
|
return `${key}=`;
|
||
|
|
}
|
||
|
|
const strValue = value.toString();
|
||
|
|
// Se a string estiver vazia, ainda enviar o parâmetro (como o Angular faz)
|
||
|
|
return strValue ? `${key}=${encodeParam(strValue)}` : `${key}=`;
|
||
|
|
};
|
||
|
|
|
||
|
|
// Seguindo exatamente a ordem do Angular HttpParams.append()
|
||
|
|
const queryParams: string[] = [];
|
||
|
|
queryParams.push(buildQueryParam("seller", sellerId));
|
||
|
|
queryParams.push(buildQueryParam("store", selectedStore || null));
|
||
|
|
queryParams.push(buildQueryParam("start", startDateValue || null));
|
||
|
|
queryParams.push(buildQueryParam("end", endDateValue || null));
|
||
|
|
queryParams.push(
|
||
|
|
buildQueryParam("idPreOrder", preOrderId ? parseInt(preOrderId, 10) : 0)
|
||
|
|
);
|
||
|
|
queryParams.push(buildQueryParam("document", document || null));
|
||
|
|
queryParams.push(buildQueryParam("nameCustomer", customerName || null));
|
||
|
|
|
||
|
|
const url = `${apiUrl}/preorder/list?${queryParams.join("&")}`;
|
||
|
|
|
||
|
|
const response = await fetch(url, {
|
||
|
|
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 orçamentos: ${response.statusText}`
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
const result = await response.json();
|
||
|
|
|
||
|
|
// Seguindo exatamente o padrão do Angular: verificar result.success antes de usar result.data
|
||
|
|
// O Angular faz: if (result.success) { this.preOrders = result.data; }
|
||
|
|
if (result.success) {
|
||
|
|
setPreOrders(result.data || []);
|
||
|
|
} else {
|
||
|
|
// Se result.success for false, não há dados para exibir
|
||
|
|
setPreOrders([]);
|
||
|
|
if (result.message) {
|
||
|
|
setError(result.message);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
} catch (err) {
|
||
|
|
console.error("Erro ao buscar orçamentos:", err);
|
||
|
|
setError(
|
||
|
|
err instanceof Error
|
||
|
|
? err.message
|
||
|
|
: "Erro ao buscar orçamentos. Tente novamente."
|
||
|
|
);
|
||
|
|
} finally {
|
||
|
|
setLoading(false);
|
||
|
|
}
|
||
|
|
};
|
||
|
|
|
||
|
|
const handleClear = () => {
|
||
|
|
setSelectedStore("");
|
||
|
|
setStartDate("");
|
||
|
|
setEndDate("");
|
||
|
|
setPreOrderId("");
|
||
|
|
setDocument("");
|
||
|
|
setCustomerName("");
|
||
|
|
setPreOrders([]);
|
||
|
|
setError(null);
|
||
|
|
};
|
||
|
|
|
||
|
|
const handleEditPreOrder = async (preOrder: PreOrder) => {
|
||
|
|
// Validação: não pode editar se já foi utilizado (exatamente como no Angular)
|
||
|
|
if (preOrder.status && preOrder.status === "ORÇAMENTO UTILIZADO") {
|
||
|
|
setInfoMessage("Alterar Orçamento");
|
||
|
|
setInfoDescription(
|
||
|
|
"Orçamento não pode ser editado.\nOrçamento já foi convertido em pedido de venda, alteração não permitida."
|
||
|
|
);
|
||
|
|
setShowInfoDialog(true);
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
try {
|
||
|
|
const token = authService.getToken();
|
||
|
|
const apiUrl = env.API_URL.replace(/\/$/, "");
|
||
|
|
|
||
|
|
// Seguindo exatamente o padrão do Angular: usar HttpParams com preOrderId
|
||
|
|
const response = await fetch(
|
||
|
|
`${apiUrl}/preorder/cart?preOrderId=${preOrder.idPreOrder}`,
|
||
|
|
{
|
||
|
|
method: "GET",
|
||
|
|
headers: {
|
||
|
|
"Content-Type": "application/json",
|
||
|
|
...(token && { Authorization: `Basic ${token}` }),
|
||
|
|
},
|
||
|
|
}
|
||
|
|
);
|
||
|
|
|
||
|
|
if (response.ok) {
|
||
|
|
const result = await response.json();
|
||
|
|
|
||
|
|
console.log("📦 [PREORDER] Dados recebidos do backend:", {
|
||
|
|
cartId: result.cartId,
|
||
|
|
hasCustomer: !!result.customer,
|
||
|
|
hasPaymentPlan: !!result.paymentPlan,
|
||
|
|
hasBilling: !!result.billing,
|
||
|
|
hasPartner: !!result.partner,
|
||
|
|
hasAddress: !!result.address,
|
||
|
|
hasPreCustomer: !!result.preCustomer,
|
||
|
|
hasInvoiceStore: !!result.invoiceStore,
|
||
|
|
});
|
||
|
|
|
||
|
|
// Salvar dados no localStorage exatamente como no Angular
|
||
|
|
console.log(
|
||
|
|
"📦 [PREORDER] Salvando cartId no localStorage:",
|
||
|
|
result.cartId
|
||
|
|
);
|
||
|
|
localStorage.setItem("cart", result.cartId);
|
||
|
|
localStorage.setItem("customer", JSON.stringify(result.customer));
|
||
|
|
localStorage.setItem("paymentPlan", JSON.stringify(result.paymentPlan));
|
||
|
|
localStorage.setItem("billing", JSON.stringify(result.billing));
|
||
|
|
|
||
|
|
if (result.partner) {
|
||
|
|
console.log("📦 [PREORDER] Salvando partner");
|
||
|
|
localStorage.setItem("partner", JSON.stringify(result.partner));
|
||
|
|
}
|
||
|
|
|
||
|
|
if (result.address) {
|
||
|
|
console.log("📦 [PREORDER] Salvando address");
|
||
|
|
localStorage.setItem("address", JSON.stringify(result.address));
|
||
|
|
}
|
||
|
|
|
||
|
|
if (result.preCustomer) {
|
||
|
|
console.log("📦 [PREORDER] Salvando preCustomer");
|
||
|
|
localStorage.setItem(
|
||
|
|
"preCustomer",
|
||
|
|
JSON.stringify(result.preCustomer)
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
console.log("📦 [PREORDER] Salvando invoiceStore");
|
||
|
|
localStorage.setItem(
|
||
|
|
"invoiceStore",
|
||
|
|
JSON.stringify(result.invoiceStore)
|
||
|
|
);
|
||
|
|
|
||
|
|
// Criar OrderDelivery exatamente como no Angular
|
||
|
|
const orderDelivery = {
|
||
|
|
notification: result.notification1 ?? "",
|
||
|
|
notification1: result.notification2 ?? "",
|
||
|
|
notification2: "",
|
||
|
|
notificationDelivery1: result.notificationDelivery1 ?? "",
|
||
|
|
notificationDelivery2: result.notificationDelivery2 ?? "",
|
||
|
|
notificationDelivery3: result.notificationDelivery3 ?? "",
|
||
|
|
dateDelivery: result.deliveryDate,
|
||
|
|
scheduleDelivery: result.squeduleDelivery,
|
||
|
|
priorityDelivery: result.priorityDelivery,
|
||
|
|
};
|
||
|
|
console.log("📦 [PREORDER] Salvando dataDelivery");
|
||
|
|
localStorage.setItem("dataDelivery", JSON.stringify(orderDelivery));
|
||
|
|
|
||
|
|
// Verificar se o cartId foi salvo corretamente
|
||
|
|
const savedCartId = localStorage.getItem("cart");
|
||
|
|
console.log("📦 [PREORDER] CartId salvo no localStorage:", savedCartId);
|
||
|
|
console.log("📦 [PREORDER] CartId recebido do backend:", result.cartId);
|
||
|
|
console.log(
|
||
|
|
"📦 [PREORDER] CartIds são iguais?",
|
||
|
|
savedCartId === result.cartId
|
||
|
|
);
|
||
|
|
|
||
|
|
// IMPORTANTE: Carregar os itens do carrinho ANTES de navegar
|
||
|
|
// No Angular, o componente home-sales dispara LoadShoppingAction no ngOnInit
|
||
|
|
// No React, precisamos garantir que os itens sejam carregados antes da navegação
|
||
|
|
console.log(
|
||
|
|
"📦 [PREORDER] Carregando itens do carrinho antes de navegar..."
|
||
|
|
);
|
||
|
|
try {
|
||
|
|
const { shoppingService } = await import(
|
||
|
|
"../../src/services/shopping.service"
|
||
|
|
);
|
||
|
|
const items = await shoppingService.getShoppingItems(result.cartId);
|
||
|
|
console.log(
|
||
|
|
"📦 [PREORDER] Itens do carrinho carregados:",
|
||
|
|
items.length
|
||
|
|
);
|
||
|
|
console.log("📦 [PREORDER] Itens:", items);
|
||
|
|
|
||
|
|
// Salvar os itens no sessionStorage para garantir que sejam carregados após navegação
|
||
|
|
sessionStorage.setItem("pendingCartItems", JSON.stringify(items));
|
||
|
|
sessionStorage.setItem("pendingCartId", result.cartId);
|
||
|
|
console.log(
|
||
|
|
"📦 [PREORDER] Itens salvos no sessionStorage para carregamento após navegação"
|
||
|
|
);
|
||
|
|
} catch (loadError) {
|
||
|
|
console.error(
|
||
|
|
"📦 [PREORDER] Erro ao carregar itens antes de navegar:",
|
||
|
|
loadError
|
||
|
|
);
|
||
|
|
// Continuar mesmo se houver erro, o useCart tentará carregar depois
|
||
|
|
}
|
||
|
|
|
||
|
|
// Disparar evento customizado para notificar mudança no cartId
|
||
|
|
console.log("📦 [PREORDER] Disparando evento cartUpdated");
|
||
|
|
const storageEvent = new Event("cartUpdated") as any;
|
||
|
|
storageEvent.key = "cart";
|
||
|
|
storageEvent.newValue = result.cartId;
|
||
|
|
window.dispatchEvent(storageEvent);
|
||
|
|
|
||
|
|
// Mostrar mensagem de sucesso informando que os dados do carrinho foram carregados
|
||
|
|
console.log("📦 [PREORDER] Dados do carrinho carregados com sucesso");
|
||
|
|
setShowCartLoadedDialog(true);
|
||
|
|
} else {
|
||
|
|
const errorData = await response.json().catch(() => ({}));
|
||
|
|
throw new Error(
|
||
|
|
errorData.message || "Erro ao carregar dados do orçamento"
|
||
|
|
);
|
||
|
|
}
|
||
|
|
} catch (err) {
|
||
|
|
console.error("Erro ao editar orçamento:", err);
|
||
|
|
// Tratamento de erro exatamente como no Angular
|
||
|
|
setInfoMessage("Consulta de orçamentos");
|
||
|
|
setInfoDescription(
|
||
|
|
err instanceof Error
|
||
|
|
? `Ops! Houve um erro ao consultar os orçamentos.\n${err.message}`
|
||
|
|
: "Ops! Houve um erro ao consultar os orçamentos."
|
||
|
|
);
|
||
|
|
setShowInfoDialog(true);
|
||
|
|
}
|
||
|
|
};
|
||
|
|
|
||
|
|
const handleViewItems = async (preOrder: PreOrder) => {
|
||
|
|
try {
|
||
|
|
const token = authService.getToken();
|
||
|
|
const apiUrl = env.API_URL.replace(/\/$/, "");
|
||
|
|
|
||
|
|
const response = await fetch(
|
||
|
|
`${apiUrl}/preorder/itens/${preOrder.idPreOrder}`,
|
||
|
|
{
|
||
|
|
method: "GET",
|
||
|
|
headers: {
|
||
|
|
"Content-Type": "application/json",
|
||
|
|
...(token && { Authorization: `Basic ${token}` }),
|
||
|
|
},
|
||
|
|
}
|
||
|
|
);
|
||
|
|
|
||
|
|
if (response.ok) {
|
||
|
|
const items = await response.json();
|
||
|
|
// Converter para o formato esperado pelo OrderItemsModal
|
||
|
|
const convertedItems = items.map((item: PreOrderItem) => ({
|
||
|
|
productId: item.productId,
|
||
|
|
description: item.description,
|
||
|
|
package: item.package,
|
||
|
|
color: item.color,
|
||
|
|
local: item.local,
|
||
|
|
quantity: item.quantity,
|
||
|
|
price: item.price,
|
||
|
|
subTotal: item.subTotal,
|
||
|
|
}));
|
||
|
|
setOrderItems(convertedItems);
|
||
|
|
setSelectedPreOrder(preOrder);
|
||
|
|
setShowOrderItems(true);
|
||
|
|
}
|
||
|
|
} catch (err) {
|
||
|
|
console.error("Erro ao buscar itens do orçamento:", err);
|
||
|
|
setInfoMessage("Erro");
|
||
|
|
setInfoDescription("Não foi possível carregar os itens do orçamento.");
|
||
|
|
setShowInfoDialog(true);
|
||
|
|
}
|
||
|
|
};
|
||
|
|
|
||
|
|
const handleCloseOrderItemsModal = () => {
|
||
|
|
setShowOrderItems(false);
|
||
|
|
setOrderItems([]);
|
||
|
|
setSelectedPreOrder(null);
|
||
|
|
};
|
||
|
|
|
||
|
|
const handlePrintPreOrder = (preOrder: PreOrder) => {
|
||
|
|
setPreOrderToPrint(preOrder);
|
||
|
|
setShowPrintDialog(true);
|
||
|
|
};
|
||
|
|
|
||
|
|
const handleConfirmPrint = (model: "A" | "B" | "P") => {
|
||
|
|
if (!preOrderToPrint) return;
|
||
|
|
|
||
|
|
// Construir URL do viewer seguindo o padrão do Angular
|
||
|
|
const viewerUrl = env.PRINT_VIEWER_URL.replace("{action}", "InitViewer");
|
||
|
|
const url = `${viewerUrl}?order=${preOrderToPrint.idPreOrder}&model=${model}`;
|
||
|
|
|
||
|
|
// Configurar e mostrar o viewer
|
||
|
|
setPrintUrl(url);
|
||
|
|
setPrintPreOrderId(preOrderToPrint.idPreOrder);
|
||
|
|
setPrintModel(model);
|
||
|
|
setShowPrintViewer(true);
|
||
|
|
setShowPrintDialog(false);
|
||
|
|
};
|
||
|
|
|
||
|
|
const formatDate = (dateString: string | null | undefined): string => {
|
||
|
|
if (!dateString || dateString === "null" || dateString === "undefined") {
|
||
|
|
return "";
|
||
|
|
}
|
||
|
|
|
||
|
|
try {
|
||
|
|
// Tentar criar a data
|
||
|
|
let date: Date;
|
||
|
|
|
||
|
|
// Se já for uma string de data válida, usar diretamente
|
||
|
|
if (typeof dateString === "string") {
|
||
|
|
// Tentar parsear diferentes formatos
|
||
|
|
date = new Date(dateString);
|
||
|
|
|
||
|
|
// Se falhar, tentar formatos alternativos
|
||
|
|
if (isNaN(date.getTime())) {
|
||
|
|
// Tentar formato brasileiro DD/MM/YYYY
|
||
|
|
const parts = dateString.split("/");
|
||
|
|
if (parts.length === 3) {
|
||
|
|
date = new Date(
|
||
|
|
parseInt(parts[2]),
|
||
|
|
parseInt(parts[1]) - 1,
|
||
|
|
parseInt(parts[0])
|
||
|
|
);
|
||
|
|
} else {
|
||
|
|
// Tentar formato ISO
|
||
|
|
date = new Date(
|
||
|
|
dateString.replace(/(\d{2})\/(\d{2})\/(\d{4})/, "$3-$2-$1")
|
||
|
|
);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
} else {
|
||
|
|
date = new Date(dateString);
|
||
|
|
}
|
||
|
|
|
||
|
|
// Verificar se a data é válida
|
||
|
|
if (isNaN(date.getTime())) {
|
||
|
|
console.warn("Data inválida:", dateString);
|
||
|
|
return "";
|
||
|
|
}
|
||
|
|
|
||
|
|
// Formatar no padrão DD/MM/YYYY
|
||
|
|
const day = String(date.getDate()).padStart(2, "0");
|
||
|
|
const month = String(date.getMonth() + 1).padStart(2, "0");
|
||
|
|
const year = date.getFullYear();
|
||
|
|
|
||
|
|
return `${day}/${month}/${year}`;
|
||
|
|
} catch (error) {
|
||
|
|
console.error("Erro ao formatar data:", dateString, error);
|
||
|
|
return "";
|
||
|
|
}
|
||
|
|
};
|
||
|
|
|
||
|
|
const getStatusColor = (status: string): string => {
|
||
|
|
switch (status) {
|
||
|
|
case "ORÇAMENTO UTILIZADO":
|
||
|
|
return "bg-green-100 text-green-800";
|
||
|
|
case "PENDENTE":
|
||
|
|
return "bg-yellow-100 text-yellow-800";
|
||
|
|
default:
|
||
|
|
return "bg-slate-100 text-slate-800";
|
||
|
|
}
|
||
|
|
};
|
||
|
|
|
||
|
|
const getCustomerDisplay = (preOrder: PreOrder): string => {
|
||
|
|
if (preOrder.cpfPreCustomer && preOrder.idCustomer === 1) {
|
||
|
|
return `${preOrder.namePreCustomer} (PRE)`;
|
||
|
|
}
|
||
|
|
return typeof preOrder.customer === "string"
|
||
|
|
? preOrder.customer
|
||
|
|
: String(preOrder.customer);
|
||
|
|
};
|
||
|
|
|
||
|
|
// Definir colunas do DataGrid
|
||
|
|
const columns: GridColDef[] = [
|
||
|
|
{
|
||
|
|
field: "actions",
|
||
|
|
headerName: "Ações",
|
||
|
|
width: 200,
|
||
|
|
sortable: false,
|
||
|
|
filterable: false,
|
||
|
|
disableColumnMenu: true,
|
||
|
|
renderCell: (params) => {
|
||
|
|
const preOrder = params.row as PreOrder;
|
||
|
|
return (
|
||
|
|
<Box sx={{ display: "flex", gap: 0.5 }}>
|
||
|
|
<IconButton
|
||
|
|
size="small"
|
||
|
|
onClick={() => handleEditPreOrder(preOrder)}
|
||
|
|
sx={{
|
||
|
|
color: "#64748b",
|
||
|
|
"&:hover": { backgroundColor: "#f1f5f9", color: "#475569" },
|
||
|
|
}}
|
||
|
|
title="Editar orçamento"
|
||
|
|
>
|
||
|
|
<Edit fontSize="small" />
|
||
|
|
</IconButton>
|
||
|
|
<IconButton
|
||
|
|
size="small"
|
||
|
|
onClick={() => handleViewItems(preOrder)}
|
||
|
|
sx={{
|
||
|
|
color: "#64748b",
|
||
|
|
"&:hover": { backgroundColor: "#f1f5f9", color: "#475569" },
|
||
|
|
}}
|
||
|
|
title="Ver itens do orçamento"
|
||
|
|
>
|
||
|
|
<Visibility fontSize="small" />
|
||
|
|
</IconButton>
|
||
|
|
<IconButton
|
||
|
|
size="small"
|
||
|
|
onClick={() => handlePrintPreOrder(preOrder)}
|
||
|
|
sx={{
|
||
|
|
color: "#64748b",
|
||
|
|
"&:hover": { backgroundColor: "#f1f5f9", color: "#475569" },
|
||
|
|
}}
|
||
|
|
title="Imprimir orçamento"
|
||
|
|
>
|
||
|
|
<Print fontSize="small" />
|
||
|
|
</IconButton>
|
||
|
|
</Box>
|
||
|
|
);
|
||
|
|
},
|
||
|
|
},
|
||
|
|
{
|
||
|
|
field: "data",
|
||
|
|
headerName: "Data",
|
||
|
|
width: 120,
|
||
|
|
valueFormatter: (value) => {
|
||
|
|
if (!value) return "";
|
||
|
|
return formatDate(String(value));
|
||
|
|
},
|
||
|
|
},
|
||
|
|
{
|
||
|
|
field: "idPreOrder",
|
||
|
|
headerName: "N.Orçamento",
|
||
|
|
width: 130,
|
||
|
|
headerAlign: "left",
|
||
|
|
},
|
||
|
|
{
|
||
|
|
field: "status",
|
||
|
|
headerName: "Situação",
|
||
|
|
width: 180,
|
||
|
|
renderCell: (params) => {
|
||
|
|
const status = (params.value as string) || "PENDENTE";
|
||
|
|
return (
|
||
|
|
<span
|
||
|
|
className={`px-2 py-1 rounded-md text-xs font-bold uppercase ${getStatusColor(
|
||
|
|
status
|
||
|
|
)}`}
|
||
|
|
>
|
||
|
|
{status}
|
||
|
|
</span>
|
||
|
|
);
|
||
|
|
},
|
||
|
|
},
|
||
|
|
{
|
||
|
|
field: "idCustomer",
|
||
|
|
headerName: "Cód.Cliente",
|
||
|
|
width: 120,
|
||
|
|
headerAlign: "left",
|
||
|
|
},
|
||
|
|
{
|
||
|
|
field: "customer",
|
||
|
|
headerName: "Cliente",
|
||
|
|
width: 300,
|
||
|
|
flex: 1,
|
||
|
|
renderCell: (params) => {
|
||
|
|
const preOrder = params.row as PreOrder;
|
||
|
|
return (
|
||
|
|
<div className="flex items-center gap-2">
|
||
|
|
<span>{getCustomerDisplay(preOrder)}</span>
|
||
|
|
{preOrder.cpfPreCustomer && preOrder.idCustomer === 1 && (
|
||
|
|
<span className="px-2 py-0.5 bg-red-100 text-red-800 text-xs font-bold rounded">
|
||
|
|
PRE
|
||
|
|
</span>
|
||
|
|
)}
|
||
|
|
</div>
|
||
|
|
);
|
||
|
|
},
|
||
|
|
},
|
||
|
|
{
|
||
|
|
field: "value",
|
||
|
|
headerName: "Valor",
|
||
|
|
width: 130,
|
||
|
|
headerAlign: "right",
|
||
|
|
align: "right",
|
||
|
|
valueFormatter: (value) => formatCurrency(value as number),
|
||
|
|
},
|
||
|
|
{
|
||
|
|
field: "seller",
|
||
|
|
headerName: "Vendedor",
|
||
|
|
width: 200,
|
||
|
|
valueFormatter: (value) => {
|
||
|
|
return typeof value === "string" ? value : String(value);
|
||
|
|
},
|
||
|
|
},
|
||
|
|
];
|
||
|
|
|
||
|
|
return (
|
||
|
|
<div className="space-y-0">
|
||
|
|
{/* Header */}
|
||
|
|
<header>
|
||
|
|
<h2 className="text-2xl font-black text-[#002147] mb-2">
|
||
|
|
Orçamentos Pendentes
|
||
|
|
</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 orçamento */}
|
||
|
|
<div>
|
||
|
|
<Label htmlFor="preOrderId">Número do orçamento</Label>
|
||
|
|
<Input
|
||
|
|
id="preOrderId"
|
||
|
|
type="text"
|
||
|
|
value={preOrderId}
|
||
|
|
onChange={(e) => setPreOrderId(e.target.value)}
|
||
|
|
placeholder="Informe o número do orçamento"
|
||
|
|
/>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
{/* Data inicial */}
|
||
|
|
<div>
|
||
|
|
<Label htmlFor="startDate">Data inicial</Label>
|
||
|
|
<DateInput
|
||
|
|
id="startDate"
|
||
|
|
value={startDate}
|
||
|
|
onChange={(e) => setStartDate(e.target.value)}
|
||
|
|
/>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
{/* Data final */}
|
||
|
|
<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>
|
||
|
|
</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 Orçamentos */}
|
||
|
|
{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 && preOrders.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]">
|
||
|
|
Orçamentos encontrados: {preOrders.length}
|
||
|
|
</h3>
|
||
|
|
</div>
|
||
|
|
<Box sx={{ height: 600, width: "100%" }}>
|
||
|
|
<DataGridPremium
|
||
|
|
rows={preOrders}
|
||
|
|
columns={columns}
|
||
|
|
getRowId={(row) => row.idPreOrder}
|
||
|
|
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='idPreOrder']": {
|
||
|
|
fontWeight: 700,
|
||
|
|
color: "#0f172a",
|
||
|
|
},
|
||
|
|
"& .MuiDataGrid-cell[data-field='value']": {
|
||
|
|
fontWeight: 700,
|
||
|
|
color: "#0f172a",
|
||
|
|
},
|
||
|
|
"& .MuiDataGrid-cell[data-field='data']": {
|
||
|
|
color: "#64748b",
|
||
|
|
},
|
||
|
|
"& .MuiDataGrid-cell[data-field='idCustomer']": {
|
||
|
|
color: "#64748b",
|
||
|
|
},
|
||
|
|
"& .MuiDataGrid-cell[data-field='customer']": {
|
||
|
|
color: "#64748b",
|
||
|
|
},
|
||
|
|
"& .MuiDataGrid-virtualScroller": {
|
||
|
|
overflowY: "auto",
|
||
|
|
},
|
||
|
|
"& .MuiDataGrid-virtualScrollerContent": {
|
||
|
|
height: "auto !important",
|
||
|
|
},
|
||
|
|
}}
|
||
|
|
/>
|
||
|
|
</Box>
|
||
|
|
</div>
|
||
|
|
)}
|
||
|
|
|
||
|
|
{!loading && preOrders.length === 0 && !error && (
|
||
|
|
<div className="bg-white rounded-2xl border border-slate-100 overflow-hidden">
|
||
|
|
<NoData
|
||
|
|
title="Nenhum orçamento encontrado"
|
||
|
|
description="Não foram encontrados orçamentos com os filtros informados. Tente ajustar os parâmetros de pesquisa ou verifique se há orçamentos no período selecionado."
|
||
|
|
icon="clipboard"
|
||
|
|
variant="outline"
|
||
|
|
/>
|
||
|
|
</div>
|
||
|
|
)}
|
||
|
|
|
||
|
|
{/* Modal de Itens do Orçamento */}
|
||
|
|
<OrderItemsModal
|
||
|
|
isOpen={showOrderItems}
|
||
|
|
onClose={handleCloseOrderItemsModal}
|
||
|
|
orderId={selectedPreOrder?.idPreOrder || 0}
|
||
|
|
orderItems={orderItems}
|
||
|
|
/>
|
||
|
|
|
||
|
|
{/* Dialog de Informação */}
|
||
|
|
<ConfirmDialog
|
||
|
|
isOpen={showInfoDialog}
|
||
|
|
onClose={() => setShowInfoDialog(false)}
|
||
|
|
onConfirm={() => setShowInfoDialog(false)}
|
||
|
|
type="info"
|
||
|
|
title={infoMessage}
|
||
|
|
message={infoDescription}
|
||
|
|
confirmText="OK"
|
||
|
|
showWarning={false}
|
||
|
|
/>
|
||
|
|
|
||
|
|
{/* Dialog de Carrinho Carregado */}
|
||
|
|
<ConfirmDialog
|
||
|
|
isOpen={showCartLoadedDialog}
|
||
|
|
onClose={() => {
|
||
|
|
setShowCartLoadedDialog(false);
|
||
|
|
// Navegar para página de produtos após fechar o dialog
|
||
|
|
setTimeout(() => {
|
||
|
|
window.location.href = "/#/sales/home";
|
||
|
|
}, 100);
|
||
|
|
}}
|
||
|
|
onConfirm={() => {
|
||
|
|
setShowCartLoadedDialog(false);
|
||
|
|
// Navegar para página de produtos após confirmar
|
||
|
|
setTimeout(() => {
|
||
|
|
window.location.href = "/#/sales/home";
|
||
|
|
}, 100);
|
||
|
|
}}
|
||
|
|
type="success"
|
||
|
|
title="Carrinho carregado"
|
||
|
|
message="Os dados do carrinho foram carregados com sucesso!\n\nVocê será redirecionado para a página de produtos."
|
||
|
|
confirmText="OK"
|
||
|
|
showWarning={false}
|
||
|
|
/>
|
||
|
|
|
||
|
|
{/* Dialog de Seleção de Modelo de Impressão */}
|
||
|
|
{preOrderToPrint && (
|
||
|
|
<PrintOrderDialog
|
||
|
|
isOpen={showPrintDialog}
|
||
|
|
onClose={() => {
|
||
|
|
setShowPrintDialog(false);
|
||
|
|
setPreOrderToPrint(null);
|
||
|
|
}}
|
||
|
|
onConfirm={handleConfirmPrint}
|
||
|
|
orderId={preOrderToPrint.idPreOrder}
|
||
|
|
includeModelP={true}
|
||
|
|
/>
|
||
|
|
)}
|
||
|
|
|
||
|
|
{/* Modal do Viewer de Impressão */}
|
||
|
|
{showPrintViewer && printUrl && (
|
||
|
|
<div className="fixed inset-0 z-[300] flex items-center justify-center bg-black/80">
|
||
|
|
<div className="relative bg-white rounded-3xl shadow-2xl w-[95%] h-[90vh] max-w-7xl overflow-hidden flex flex-col">
|
||
|
|
{/* Header */}
|
||
|
|
<div className="p-6 bg-[#002147] text-white rounded-t-3xl relative overflow-hidden flex items-center justify-between">
|
||
|
|
<div>
|
||
|
|
<h3 className="text-xl font-black">Orçamento de venda</h3>
|
||
|
|
<p className="text-xs text-blue-400 font-bold uppercase tracking-wider mt-0.5">
|
||
|
|
Visualização e Impressão
|
||
|
|
</p>
|
||
|
|
</div>
|
||
|
|
<button
|
||
|
|
onClick={() => {
|
||
|
|
setShowPrintViewer(false);
|
||
|
|
setPrintUrl("");
|
||
|
|
setPrintPreOrderId(undefined);
|
||
|
|
setPrintModel(undefined);
|
||
|
|
}}
|
||
|
|
className="p-2 text-white/70 hover:text-white hover:bg-white/10 rounded-lg transition-colors"
|
||
|
|
>
|
||
|
|
<svg
|
||
|
|
className="w-6 h-6"
|
||
|
|
fill="none"
|
||
|
|
stroke="currentColor"
|
||
|
|
viewBox="0 0 24 24"
|
||
|
|
>
|
||
|
|
<path
|
||
|
|
strokeLinecap="round"
|
||
|
|
strokeLinejoin="round"
|
||
|
|
strokeWidth={2}
|
||
|
|
d="M6 18L18 6M6 6l12 12"
|
||
|
|
/>
|
||
|
|
</svg>
|
||
|
|
</button>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
{/* Viewer Content */}
|
||
|
|
<div className="flex-1 overflow-hidden">
|
||
|
|
<StimulsoftViewer
|
||
|
|
requestUrl={printUrl}
|
||
|
|
action="InitViewer"
|
||
|
|
width="100%"
|
||
|
|
height="100%"
|
||
|
|
onClose={() => {
|
||
|
|
setShowPrintViewer(false);
|
||
|
|
setPrintUrl("");
|
||
|
|
setPrintPreOrderId(undefined);
|
||
|
|
setPrintModel(undefined);
|
||
|
|
}}
|
||
|
|
/>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
)}
|
||
|
|
</div>
|
||
|
|
);
|
||
|
|
};
|
||
|
|
|
||
|
|
export default PreorderView;
|