Vendaweb-portal/components/EditItemModal.tsx

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;