493 lines
18 KiB
TypeScript
493 lines
18 KiB
TypeScript
|
|
import React, { useState, useEffect } from "react";
|
||
|
|
import { OrderItem, Product } from "../types";
|
||
|
|
import { productService, SaleProduct } from "../src/services/product.service";
|
||
|
|
import { authService } from "../src/services/auth.service";
|
||
|
|
import { shoppingService } from "../src/services/shopping.service";
|
||
|
|
import FilialSelector from "./FilialSelector";
|
||
|
|
import { X, Minus, Plus, Edit2 } from "lucide-react";
|
||
|
|
import { validateMinValue, validateRequired } from "../lib/utils";
|
||
|
|
|
||
|
|
interface EditItemModalProps {
|
||
|
|
item: OrderItem;
|
||
|
|
isOpen: boolean;
|
||
|
|
onClose: () => void;
|
||
|
|
onConfirm: (item: OrderItem) => Promise<void>;
|
||
|
|
}
|
||
|
|
|
||
|
|
const EditItemModal: React.FC<EditItemModalProps> = ({
|
||
|
|
item,
|
||
|
|
isOpen,
|
||
|
|
onClose,
|
||
|
|
onConfirm,
|
||
|
|
}) => {
|
||
|
|
const [productDetail, setProductDetail] = useState<SaleProduct | null>(null);
|
||
|
|
const [loading, setLoading] = useState(false);
|
||
|
|
const [quantity, setQuantity] = useState(item.quantity || 1);
|
||
|
|
const [selectedStore, setSelectedStore] = useState(
|
||
|
|
item.stockStore?.toString() || ""
|
||
|
|
);
|
||
|
|
const [deliveryType, setDeliveryType] = useState(item.deliveryType || "");
|
||
|
|
const [environment, setEnvironment] = useState(item.environment || "");
|
||
|
|
const [stocks, setStocks] = useState<
|
||
|
|
Array<{
|
||
|
|
store: string;
|
||
|
|
storeName: string;
|
||
|
|
quantity: number;
|
||
|
|
work: boolean;
|
||
|
|
blocked: string;
|
||
|
|
breakdown: number;
|
||
|
|
transfer: number;
|
||
|
|
allowDelivery: number;
|
||
|
|
}>
|
||
|
|
>([]);
|
||
|
|
const [showDescription, setShowDescription] = useState(false);
|
||
|
|
const [formErrors, setFormErrors] = useState<{
|
||
|
|
quantity?: string;
|
||
|
|
deliveryType?: string;
|
||
|
|
selectedStore?: string;
|
||
|
|
}>({});
|
||
|
|
|
||
|
|
// Tipos de entrega
|
||
|
|
const deliveryTypes = [
|
||
|
|
{ type: "RI", description: "Retira Imediata" },
|
||
|
|
{ type: "RP", description: "Retira Posterior" },
|
||
|
|
{ type: "EN", description: "Entrega" },
|
||
|
|
{ type: "EF", description: "Encomenda" },
|
||
|
|
];
|
||
|
|
|
||
|
|
useEffect(() => {
|
||
|
|
if (isOpen && item) {
|
||
|
|
setQuantity(item.quantity || 1);
|
||
|
|
setSelectedStore(item.stockStore?.toString() || "");
|
||
|
|
setDeliveryType(item.deliveryType || "");
|
||
|
|
setEnvironment(item.environment || "");
|
||
|
|
setFormErrors({});
|
||
|
|
loadProductDetail();
|
||
|
|
} else if (!isOpen) {
|
||
|
|
// Reset states when modal closes
|
||
|
|
setQuantity(1);
|
||
|
|
setSelectedStore("");
|
||
|
|
setDeliveryType("");
|
||
|
|
setEnvironment("");
|
||
|
|
setFormErrors({});
|
||
|
|
setProductDetail(null);
|
||
|
|
setStocks([]);
|
||
|
|
setShowDescription(false);
|
||
|
|
}
|
||
|
|
}, [isOpen, item]);
|
||
|
|
|
||
|
|
const loadProductDetail = async () => {
|
||
|
|
if (!item.code) return;
|
||
|
|
|
||
|
|
try {
|
||
|
|
setLoading(true);
|
||
|
|
const store = authService.getStore();
|
||
|
|
if (!store) {
|
||
|
|
throw new Error("Loja não encontrada");
|
||
|
|
}
|
||
|
|
|
||
|
|
const productId = parseInt(item.code);
|
||
|
|
if (isNaN(productId)) {
|
||
|
|
throw new Error("ID do produto inválido");
|
||
|
|
}
|
||
|
|
|
||
|
|
// Buscar detalhes do produto
|
||
|
|
const detail = await productService.getProductDetail(store, productId);
|
||
|
|
setProductDetail(detail);
|
||
|
|
|
||
|
|
// Buscar estoques
|
||
|
|
try {
|
||
|
|
const stocksData = await productService.getProductStocks(
|
||
|
|
store,
|
||
|
|
productId
|
||
|
|
);
|
||
|
|
setStocks(stocksData);
|
||
|
|
} catch (err) {
|
||
|
|
console.warn("Erro ao carregar estoques:", err);
|
||
|
|
}
|
||
|
|
} catch (err: any) {
|
||
|
|
console.error("Erro ao carregar detalhes do produto:", err);
|
||
|
|
} finally {
|
||
|
|
setLoading(false);
|
||
|
|
}
|
||
|
|
};
|
||
|
|
|
||
|
|
const validateForm = (): boolean => {
|
||
|
|
const errors: typeof formErrors = {};
|
||
|
|
|
||
|
|
if (!validateRequired(quantity) || !validateMinValue(quantity, 0.01)) {
|
||
|
|
errors.quantity = "A quantidade deve ser maior que zero";
|
||
|
|
}
|
||
|
|
|
||
|
|
if (!validateRequired(deliveryType)) {
|
||
|
|
errors.deliveryType = "O tipo de entrega é obrigatório";
|
||
|
|
}
|
||
|
|
|
||
|
|
if (stocks.length > 0 && !validateRequired(selectedStore)) {
|
||
|
|
errors.selectedStore = "A filial de estoque é obrigatória";
|
||
|
|
}
|
||
|
|
|
||
|
|
setFormErrors(errors);
|
||
|
|
return Object.keys(errors).length === 0;
|
||
|
|
};
|
||
|
|
|
||
|
|
const handleConfirm = async () => {
|
||
|
|
if (!validateForm()) {
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
// Criar OrderItem atualizado
|
||
|
|
const updatedItem: OrderItem = {
|
||
|
|
...item,
|
||
|
|
quantity,
|
||
|
|
deliveryType,
|
||
|
|
stockStore: selectedStore || item.stockStore,
|
||
|
|
environment: environment || undefined,
|
||
|
|
};
|
||
|
|
|
||
|
|
await onConfirm(updatedItem);
|
||
|
|
};
|
||
|
|
|
||
|
|
const calculateTotal = () => {
|
||
|
|
const price = productDetail?.salePrice || item.price || 0;
|
||
|
|
return price * quantity;
|
||
|
|
};
|
||
|
|
|
||
|
|
const calculateDiscount = () => {
|
||
|
|
const listPrice = productDetail?.listPrice || item.originalPrice || 0;
|
||
|
|
const salePrice = productDetail?.salePrice || item.price || 0;
|
||
|
|
if (listPrice > 0 && salePrice < listPrice) {
|
||
|
|
return Math.round(((listPrice - salePrice) / listPrice) * 100);
|
||
|
|
}
|
||
|
|
return 0;
|
||
|
|
};
|
||
|
|
|
||
|
|
const discount = calculateDiscount();
|
||
|
|
const total = calculateTotal();
|
||
|
|
const listPrice = productDetail?.listPrice || item.originalPrice || 0;
|
||
|
|
const salePrice = productDetail?.salePrice || item.price || 0;
|
||
|
|
|
||
|
|
if (!isOpen) return null;
|
||
|
|
|
||
|
|
return (
|
||
|
|
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/60 backdrop-blur-sm p-4">
|
||
|
|
<div className="bg-white rounded-2xl shadow-2xl max-w-4xl w-full max-h-[90vh] overflow-hidden flex flex-col">
|
||
|
|
{/* Header */}
|
||
|
|
<div className="flex items-center justify-between p-4 border-b border-slate-200 bg-[#002147]">
|
||
|
|
<h2 className="text-lg font-black text-white">
|
||
|
|
Editar Item do Pedido
|
||
|
|
</h2>
|
||
|
|
<button
|
||
|
|
onClick={onClose}
|
||
|
|
className="p-2 hover:bg-white/10 rounded-lg transition-colors"
|
||
|
|
>
|
||
|
|
<X className="w-5 h-5 text-white" />
|
||
|
|
</button>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
{/* Content */}
|
||
|
|
<div className="flex-1 overflow-y-auto p-6">
|
||
|
|
{loading ? (
|
||
|
|
<div className="flex items-center justify-center py-20">
|
||
|
|
<div className="animate-spin rounded-full h-12 w-12 border-4 border-slate-200 border-t-orange-500"></div>
|
||
|
|
</div>
|
||
|
|
) : (
|
||
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||
|
|
{/* Left: Image */}
|
||
|
|
<div className="flex items-center justify-center bg-slate-50 rounded-xl p-8 min-h-[400px]">
|
||
|
|
{item.image && item.image.trim() !== "" ? (
|
||
|
|
<img
|
||
|
|
src={item.image}
|
||
|
|
alt={item.name}
|
||
|
|
className="max-h-full max-w-full object-contain mix-blend-multiply"
|
||
|
|
/>
|
||
|
|
) : (
|
||
|
|
<div className="text-slate-300 text-center">
|
||
|
|
<svg
|
||
|
|
className="w-24 h-24 mx-auto mb-2"
|
||
|
|
fill="none"
|
||
|
|
stroke="currentColor"
|
||
|
|
viewBox="0 0 24 24"
|
||
|
|
>
|
||
|
|
<path
|
||
|
|
strokeLinecap="round"
|
||
|
|
strokeLinejoin="round"
|
||
|
|
strokeWidth="2"
|
||
|
|
d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z"
|
||
|
|
/>
|
||
|
|
</svg>
|
||
|
|
<p className="text-sm font-medium">Sem imagem</p>
|
||
|
|
</div>
|
||
|
|
)}
|
||
|
|
</div>
|
||
|
|
|
||
|
|
{/* Right: Product Info */}
|
||
|
|
<div className="space-y-4">
|
||
|
|
{/* Product Title */}
|
||
|
|
<div>
|
||
|
|
<h3 className="text-base font-black text-[#002147] mb-1">
|
||
|
|
#{productDetail?.title || item.name}
|
||
|
|
</h3>
|
||
|
|
<p className="text-xs text-slate-600 mb-2">
|
||
|
|
{productDetail?.smallDescription ||
|
||
|
|
productDetail?.description ||
|
||
|
|
item.description ||
|
||
|
|
""}
|
||
|
|
</p>
|
||
|
|
<div className="flex flex-wrap gap-1.5 text-xs text-slate-500 mb-2">
|
||
|
|
<span className="font-medium">
|
||
|
|
{productDetail?.brand || item.mark}
|
||
|
|
</span>
|
||
|
|
<span>•</span>
|
||
|
|
<span>{productDetail?.idProduct || item.code}</span>
|
||
|
|
{productDetail?.ean && (
|
||
|
|
<>
|
||
|
|
<span>•</span>
|
||
|
|
<span>{productDetail.ean}</span>
|
||
|
|
</>
|
||
|
|
)}
|
||
|
|
</div>
|
||
|
|
{productDetail?.productType === "A" && (
|
||
|
|
<span className="inline-block bg-green-500 text-white px-2 py-0.5 rounded text-[10px] font-bold uppercase">
|
||
|
|
AUTOSSERVIÇO
|
||
|
|
</span>
|
||
|
|
)}
|
||
|
|
</div>
|
||
|
|
|
||
|
|
{/* Price */}
|
||
|
|
<div className="space-y-1">
|
||
|
|
{listPrice > 0 && listPrice > salePrice && (
|
||
|
|
<p className="text-xs text-slate-500">
|
||
|
|
de R$ {listPrice.toLocaleString("pt-BR", {
|
||
|
|
minimumFractionDigits: 2,
|
||
|
|
maximumFractionDigits: 2,
|
||
|
|
})}{" "}
|
||
|
|
por
|
||
|
|
</p>
|
||
|
|
)}
|
||
|
|
<div className="flex items-baseline gap-2">
|
||
|
|
<p className="text-2xl font-black text-orange-600">
|
||
|
|
R$ {salePrice.toLocaleString("pt-BR", {
|
||
|
|
minimumFractionDigits: 2,
|
||
|
|
maximumFractionDigits: 2,
|
||
|
|
})}
|
||
|
|
</p>
|
||
|
|
{discount > 0 && (
|
||
|
|
<span className="bg-orange-500 text-white px-2 py-0.5 rounded text-[10px] font-black">
|
||
|
|
-{discount}%
|
||
|
|
</span>
|
||
|
|
)}
|
||
|
|
</div>
|
||
|
|
{productDetail?.installments &&
|
||
|
|
productDetail.installments.length > 0 && (
|
||
|
|
<p className="text-[10px] text-slate-600">
|
||
|
|
POR UN EM {productDetail.installments[0].installment}X DE
|
||
|
|
R${" "}
|
||
|
|
{productDetail.installments[0].installmentValue.toLocaleString(
|
||
|
|
"pt-BR",
|
||
|
|
{
|
||
|
|
minimumFractionDigits: 2,
|
||
|
|
maximumFractionDigits: 2,
|
||
|
|
}
|
||
|
|
)}
|
||
|
|
</p>
|
||
|
|
)}
|
||
|
|
</div>
|
||
|
|
|
||
|
|
{/* Store Selector */}
|
||
|
|
{stocks.length > 0 && (
|
||
|
|
<div>
|
||
|
|
<label className="block text-xs font-bold text-slate-700 uppercase tracking-wide mb-2">
|
||
|
|
LOCAL DE ESTOQUE
|
||
|
|
</label>
|
||
|
|
<FilialSelector
|
||
|
|
stocks={stocks}
|
||
|
|
value={selectedStore}
|
||
|
|
onValueChange={(value) => {
|
||
|
|
setSelectedStore(value);
|
||
|
|
if (formErrors.selectedStore) {
|
||
|
|
setFormErrors((prev) => ({
|
||
|
|
...prev,
|
||
|
|
selectedStore: undefined,
|
||
|
|
}));
|
||
|
|
}
|
||
|
|
}}
|
||
|
|
placeholder="Digite para buscar..."
|
||
|
|
/>
|
||
|
|
{formErrors.selectedStore && (
|
||
|
|
<p className="text-red-600 text-xs mt-1">
|
||
|
|
{formErrors.selectedStore}
|
||
|
|
</p>
|
||
|
|
)}
|
||
|
|
</div>
|
||
|
|
)}
|
||
|
|
|
||
|
|
{/* Description (Collapsible) */}
|
||
|
|
<div className="border-t border-slate-200 pt-3">
|
||
|
|
<div className="flex items-center justify-between">
|
||
|
|
<span className="text-xs font-bold text-slate-700 uppercase tracking-wide">
|
||
|
|
DESCRIÇÃO DO PRODUTO
|
||
|
|
</span>
|
||
|
|
<button
|
||
|
|
onClick={() => setShowDescription(!showDescription)}
|
||
|
|
className="text-xs text-blue-600 hover:underline"
|
||
|
|
>
|
||
|
|
Ver mais
|
||
|
|
</button>
|
||
|
|
</div>
|
||
|
|
{showDescription && (
|
||
|
|
<p className="text-xs text-slate-600 mt-2">
|
||
|
|
{productDetail?.description ||
|
||
|
|
item.description ||
|
||
|
|
"Sem descrição disponível"}
|
||
|
|
</p>
|
||
|
|
)}
|
||
|
|
</div>
|
||
|
|
|
||
|
|
{/* Quantity */}
|
||
|
|
<div>
|
||
|
|
<label className="block text-xs font-bold text-slate-700 uppercase tracking-wide mb-2">
|
||
|
|
Quantidade
|
||
|
|
</label>
|
||
|
|
<div className="flex items-center border-2 border-[#002147] rounded-lg overflow-hidden w-fit">
|
||
|
|
<button
|
||
|
|
onClick={() => {
|
||
|
|
const newQty = Math.max(1, quantity - 1);
|
||
|
|
setQuantity(newQty);
|
||
|
|
if (formErrors.quantity) {
|
||
|
|
setFormErrors((prev) => ({
|
||
|
|
...prev,
|
||
|
|
quantity: undefined,
|
||
|
|
}));
|
||
|
|
}
|
||
|
|
}}
|
||
|
|
className="bg-[#002147] text-white px-4 py-2.5 hover:bg-[#003366] transition-colors font-bold"
|
||
|
|
>
|
||
|
|
<Minus className="w-4 h-4" />
|
||
|
|
</button>
|
||
|
|
<input
|
||
|
|
type="number"
|
||
|
|
min="1"
|
||
|
|
step="1"
|
||
|
|
value={quantity}
|
||
|
|
onChange={(e) => {
|
||
|
|
const val = parseInt(e.target.value);
|
||
|
|
if (!isNaN(val) && val > 0) {
|
||
|
|
setQuantity(val);
|
||
|
|
if (formErrors.quantity) {
|
||
|
|
setFormErrors((prev) => ({
|
||
|
|
...prev,
|
||
|
|
quantity: undefined,
|
||
|
|
}));
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}}
|
||
|
|
className="w-16 text-center text-sm font-bold border-0 focus:outline-none bg-white"
|
||
|
|
/>
|
||
|
|
<button
|
||
|
|
onClick={() => {
|
||
|
|
setQuantity((q) => q + 1);
|
||
|
|
if (formErrors.quantity) {
|
||
|
|
setFormErrors((prev) => ({
|
||
|
|
...prev,
|
||
|
|
quantity: undefined,
|
||
|
|
}));
|
||
|
|
}
|
||
|
|
}}
|
||
|
|
className="bg-[#002147] text-white px-4 py-2.5 hover:bg-[#003366] transition-colors font-bold"
|
||
|
|
>
|
||
|
|
<Plus className="w-4 h-4" />
|
||
|
|
</button>
|
||
|
|
</div>
|
||
|
|
{formErrors.quantity && (
|
||
|
|
<p className="text-red-600 text-xs mt-1">
|
||
|
|
{formErrors.quantity}
|
||
|
|
</p>
|
||
|
|
)}
|
||
|
|
</div>
|
||
|
|
|
||
|
|
{/* Environment */}
|
||
|
|
<div>
|
||
|
|
<label className="block text-xs font-bold text-slate-700 uppercase tracking-wide mb-2">
|
||
|
|
Ambiente
|
||
|
|
</label>
|
||
|
|
<input
|
||
|
|
type="text"
|
||
|
|
value={environment}
|
||
|
|
onChange={(e) => setEnvironment(e.target.value)}
|
||
|
|
className="w-full px-4 py-2 border border-slate-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-orange-500/20 text-sm"
|
||
|
|
placeholder=""
|
||
|
|
/>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
{/* Delivery Type */}
|
||
|
|
<div>
|
||
|
|
<label className="block text-xs font-bold text-slate-700 uppercase tracking-wide mb-2">
|
||
|
|
Tipo de Entrega
|
||
|
|
</label>
|
||
|
|
<select
|
||
|
|
value={deliveryType}
|
||
|
|
onChange={(e) => {
|
||
|
|
setDeliveryType(e.target.value);
|
||
|
|
if (formErrors.deliveryType) {
|
||
|
|
setFormErrors((prev) => ({
|
||
|
|
...prev,
|
||
|
|
deliveryType: undefined,
|
||
|
|
}));
|
||
|
|
}
|
||
|
|
}}
|
||
|
|
className={`w-full px-4 py-2 border rounded-lg focus:outline-none focus:ring-2 focus:ring-orange-500/20 text-sm ${
|
||
|
|
formErrors.deliveryType
|
||
|
|
? "border-red-500"
|
||
|
|
: "border-slate-300"
|
||
|
|
}`}
|
||
|
|
>
|
||
|
|
<option value="">Selecione tipo de entrega</option>
|
||
|
|
{deliveryTypes.map((dt) => (
|
||
|
|
<option key={dt.type} value={dt.type}>
|
||
|
|
{dt.description}
|
||
|
|
</option>
|
||
|
|
))}
|
||
|
|
</select>
|
||
|
|
{formErrors.deliveryType && (
|
||
|
|
<p className="text-red-600 text-xs mt-1">
|
||
|
|
{formErrors.deliveryType}
|
||
|
|
</p>
|
||
|
|
)}
|
||
|
|
</div>
|
||
|
|
|
||
|
|
{/* Total Price and Confirm Button */}
|
||
|
|
<div className="pt-4 border-t border-slate-200 space-y-3">
|
||
|
|
<div className="flex items-center gap-2">
|
||
|
|
<span className="text-base font-bold text-[#002147]">
|
||
|
|
R$ {total.toLocaleString("pt-BR", {
|
||
|
|
minimumFractionDigits: 2,
|
||
|
|
maximumFractionDigits: 2,
|
||
|
|
})}
|
||
|
|
</span>
|
||
|
|
<button
|
||
|
|
className="p-1 hover:bg-slate-100 rounded transition-colors"
|
||
|
|
title="Editar preço"
|
||
|
|
>
|
||
|
|
<Edit2 className="w-4 h-4 text-slate-400" />
|
||
|
|
</button>
|
||
|
|
</div>
|
||
|
|
<button
|
||
|
|
onClick={handleConfirm}
|
||
|
|
className="w-full bg-orange-500 text-white py-3.5 rounded-lg font-black uppercase text-xs tracking-wider hover:bg-orange-600 transition-all shadow-lg"
|
||
|
|
>
|
||
|
|
ADICIONAR AO CARRINHO
|
||
|
|
</button>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
)}
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
);
|
||
|
|
};
|
||
|
|
|
||
|
|
export default EditItemModal;
|
||
|
|
|