285 lines
9.5 KiB
TypeScript
285 lines
9.5 KiB
TypeScript
|
|
import React, { useState, useEffect } from "react";
|
||
|
|
import { OrderItem } from "../../types";
|
||
|
|
import { shoppingService, ShoppingItem } from "../../src/services/shopping.service";
|
||
|
|
import { authService } from "../../src/services/auth.service";
|
||
|
|
|
||
|
|
interface DiscountItemModalProps {
|
||
|
|
isOpen: boolean;
|
||
|
|
item: OrderItem | null;
|
||
|
|
onClose: () => void;
|
||
|
|
onConfirm: () => void;
|
||
|
|
}
|
||
|
|
|
||
|
|
const DiscountItemModal: React.FC<DiscountItemModalProps> = ({
|
||
|
|
isOpen,
|
||
|
|
item,
|
||
|
|
onClose,
|
||
|
|
onConfirm,
|
||
|
|
}) => {
|
||
|
|
const [discount, setDiscount] = useState<number>(0);
|
||
|
|
const [discountValue, setDiscountValue] = useState<number>(0);
|
||
|
|
const [salePrice, setSalePrice] = useState<number>(0);
|
||
|
|
const [listPrice, setListPrice] = useState<number>(0);
|
||
|
|
const [isUpdating, setIsUpdating] = useState(false);
|
||
|
|
const [error, setError] = useState<string>("");
|
||
|
|
const [isAnimating, setIsAnimating] = useState(false);
|
||
|
|
const [shouldRender, setShouldRender] = useState(false);
|
||
|
|
|
||
|
|
useEffect(() => {
|
||
|
|
if (item && isOpen) {
|
||
|
|
const initialDiscount = item.discount || 0;
|
||
|
|
const initialListPrice = item.originalPrice || item.price || 0;
|
||
|
|
const initialDiscountValue = item.discountValue || 0;
|
||
|
|
const initialSalePrice = item.price || 0;
|
||
|
|
|
||
|
|
setDiscount(initialDiscount);
|
||
|
|
setDiscountValue(initialDiscountValue);
|
||
|
|
setSalePrice(initialSalePrice);
|
||
|
|
setListPrice(initialListPrice);
|
||
|
|
setError("");
|
||
|
|
}
|
||
|
|
}, [item, isOpen]);
|
||
|
|
|
||
|
|
// Animação de entrada/saída
|
||
|
|
useEffect(() => {
|
||
|
|
if (isOpen) {
|
||
|
|
setShouldRender(true);
|
||
|
|
setTimeout(() => setIsAnimating(true), 10);
|
||
|
|
} else {
|
||
|
|
setIsAnimating(false);
|
||
|
|
const timer = setTimeout(() => setShouldRender(false), 300);
|
||
|
|
return () => clearTimeout(timer);
|
||
|
|
}
|
||
|
|
}, [isOpen]);
|
||
|
|
|
||
|
|
const calcDiscountValue = () => {
|
||
|
|
if (!item) return;
|
||
|
|
const percent = discount;
|
||
|
|
const newDiscountValue = Number.parseFloat(
|
||
|
|
((listPrice * percent) / 100).toFixed(2)
|
||
|
|
);
|
||
|
|
const newSalePrice = Number.parseFloat(
|
||
|
|
(listPrice - newDiscountValue).toFixed(2)
|
||
|
|
);
|
||
|
|
setDiscountValue(newDiscountValue);
|
||
|
|
setSalePrice(newSalePrice);
|
||
|
|
};
|
||
|
|
|
||
|
|
const calcPercentDiscount = () => {
|
||
|
|
if (!item) return;
|
||
|
|
const newPercent =
|
||
|
|
Number.parseFloat((discountValue / listPrice).toFixed(2)) * 100;
|
||
|
|
const newSalePrice = Number.parseFloat(
|
||
|
|
(listPrice - discountValue).toFixed(2)
|
||
|
|
);
|
||
|
|
setDiscount(newPercent);
|
||
|
|
setSalePrice(newSalePrice);
|
||
|
|
};
|
||
|
|
|
||
|
|
const handleConfirm = async () => {
|
||
|
|
if (!item) return;
|
||
|
|
|
||
|
|
// Validações (paymentPlan já foi validado antes de abrir o modal)
|
||
|
|
const paymentPlan = localStorage.getItem("paymentPlan");
|
||
|
|
if (!paymentPlan) {
|
||
|
|
setError(
|
||
|
|
"Venda sem plano de pagamento informado, desconto não permitido!"
|
||
|
|
);
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
try {
|
||
|
|
setIsUpdating(true);
|
||
|
|
setError("");
|
||
|
|
|
||
|
|
// Converter OrderItem para ShoppingItem
|
||
|
|
const shoppingItem = shoppingService.productToShoppingItem(item);
|
||
|
|
shoppingItem.id = item.id;
|
||
|
|
shoppingItem.discount = discount;
|
||
|
|
shoppingItem.discountValue = discountValue;
|
||
|
|
shoppingItem.price = salePrice;
|
||
|
|
shoppingItem.userDiscount = authService.getUser();
|
||
|
|
|
||
|
|
// Atualizar no backend
|
||
|
|
await shoppingService.updatePriceItemShopping(shoppingItem);
|
||
|
|
|
||
|
|
setIsAnimating(false);
|
||
|
|
setTimeout(() => {
|
||
|
|
onConfirm();
|
||
|
|
onClose();
|
||
|
|
}, 300);
|
||
|
|
} catch (err: any) {
|
||
|
|
setError(err.message || "Erro ao aplicar desconto");
|
||
|
|
} finally {
|
||
|
|
setIsUpdating(false);
|
||
|
|
}
|
||
|
|
};
|
||
|
|
|
||
|
|
const handleCancel = () => {
|
||
|
|
setIsAnimating(false);
|
||
|
|
setTimeout(() => {
|
||
|
|
onClose();
|
||
|
|
}, 300);
|
||
|
|
};
|
||
|
|
|
||
|
|
if (!shouldRender || !item) return null;
|
||
|
|
|
||
|
|
return (
|
||
|
|
<div className="fixed inset-0 z-[200] flex items-center justify-center">
|
||
|
|
{/* Overlay */}
|
||
|
|
<div
|
||
|
|
className={`absolute inset-0 bg-[#001f3f]/60 backdrop-blur-sm transition-opacity duration-300 ${
|
||
|
|
isAnimating ? "opacity-100" : "opacity-0"
|
||
|
|
}`}
|
||
|
|
onClick={handleCancel}
|
||
|
|
></div>
|
||
|
|
|
||
|
|
{/* Dialog */}
|
||
|
|
<div
|
||
|
|
className={`relative bg-white rounded-3xl shadow-2xl max-w-md w-full mx-4 transform transition-all duration-300 ${
|
||
|
|
isAnimating ? "scale-100 opacity-100" : "scale-95 opacity-0"
|
||
|
|
}`}
|
||
|
|
>
|
||
|
|
{/* Header */}
|
||
|
|
<div className="p-6 bg-[#002147] text-white rounded-t-3xl relative overflow-hidden">
|
||
|
|
<div className="relative z-10">
|
||
|
|
<div className="flex items-center gap-3 mb-2">
|
||
|
|
<div className="w-12 h-12 bg-orange-500/20 rounded-2xl flex items-center justify-center">
|
||
|
|
<svg
|
||
|
|
className="w-6 h-6 text-orange-400"
|
||
|
|
fill="none"
|
||
|
|
stroke="currentColor"
|
||
|
|
viewBox="0 0 24 24"
|
||
|
|
>
|
||
|
|
<path
|
||
|
|
strokeLinecap="round"
|
||
|
|
strokeLinejoin="round"
|
||
|
|
strokeWidth="2.5"
|
||
|
|
d="M12 8c-1.657 0-3 .895-3 2s1.343 2 3 2 3 .895 3 2-1.343 2-3 2m0-8c1.11 0 2.08.402 2.599 1M12 8V7m0 1v8m0 0v1m0-1c-1.11 0-2.08-.402-2.599-1M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
|
||
|
|
/>
|
||
|
|
</svg>
|
||
|
|
</div>
|
||
|
|
<div className="flex-1">
|
||
|
|
<h3 className="text-xl font-black">Desconto por produto</h3>
|
||
|
|
<p className="text-xs text-orange-400 font-bold uppercase tracking-wider mt-0.5">
|
||
|
|
Desconto sobre item de venda
|
||
|
|
</p>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
<div className="absolute right-[-10%] top-[-10%] w-32 h-32 bg-orange-400/10 rounded-full blur-2xl"></div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
{/* Content */}
|
||
|
|
<div className="p-6">
|
||
|
|
<form className="space-y-4">
|
||
|
|
{/* Produto */}
|
||
|
|
<div>
|
||
|
|
<label className="block text-sm font-medium text-slate-700 mb-1">
|
||
|
|
Produto
|
||
|
|
</label>
|
||
|
|
<input
|
||
|
|
type="text"
|
||
|
|
value={item.name}
|
||
|
|
disabled
|
||
|
|
className="w-full px-3 py-2 border border-slate-300 rounded-md bg-slate-50 text-slate-600"
|
||
|
|
/>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
{/* Preço de tabela */}
|
||
|
|
<div>
|
||
|
|
<label className="block text-sm font-medium text-slate-700 mb-1">
|
||
|
|
Preço de tabela
|
||
|
|
</label>
|
||
|
|
<input
|
||
|
|
type="number"
|
||
|
|
value={listPrice.toFixed(2)}
|
||
|
|
disabled
|
||
|
|
className="w-full px-3 py-2 border border-slate-300 rounded-md bg-slate-50 text-slate-600"
|
||
|
|
/>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
{/* % Desconto e Valor de desconto */}
|
||
|
|
<div className="grid grid-cols-2 gap-4">
|
||
|
|
<div>
|
||
|
|
<label className="block text-sm font-medium text-slate-700 mb-1">
|
||
|
|
% Desconto
|
||
|
|
</label>
|
||
|
|
<input
|
||
|
|
type="number"
|
||
|
|
value={discount}
|
||
|
|
onChange={(e) => {
|
||
|
|
setDiscount(Number.parseFloat(e.target.value) || 0);
|
||
|
|
}}
|
||
|
|
onBlur={calcDiscountValue}
|
||
|
|
step="0.01"
|
||
|
|
min="0"
|
||
|
|
max="100"
|
||
|
|
className="w-full px-3 py-2 border border-slate-300 rounded-md focus:outline-none focus:ring-2 focus:ring-[#002147]"
|
||
|
|
/>
|
||
|
|
</div>
|
||
|
|
<div>
|
||
|
|
<label className="block text-sm font-medium text-slate-700 mb-1">
|
||
|
|
Valor de desconto
|
||
|
|
</label>
|
||
|
|
<input
|
||
|
|
type="number"
|
||
|
|
value={discountValue.toFixed(2)}
|
||
|
|
onChange={(e) => {
|
||
|
|
setDiscountValue(Number.parseFloat(e.target.value) || 0);
|
||
|
|
}}
|
||
|
|
onBlur={calcPercentDiscount}
|
||
|
|
step="0.01"
|
||
|
|
min="0"
|
||
|
|
className="w-full px-3 py-2 border border-slate-300 rounded-md focus:outline-none focus:ring-2 focus:ring-[#002147]"
|
||
|
|
/>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
{/* Preço de venda */}
|
||
|
|
<div>
|
||
|
|
<label className="block text-sm font-medium text-slate-700 mb-1">
|
||
|
|
Preço de venda
|
||
|
|
</label>
|
||
|
|
<input
|
||
|
|
type="number"
|
||
|
|
value={salePrice.toFixed(2)}
|
||
|
|
disabled
|
||
|
|
className="w-full px-3 py-2 border border-slate-300 rounded-md bg-slate-50 text-slate-600"
|
||
|
|
/>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
{/* Erro */}
|
||
|
|
{error && (
|
||
|
|
<div className="bg-red-50 border border-red-200 text-red-700 px-4 py-3 rounded-md text-sm">
|
||
|
|
{error}
|
||
|
|
</div>
|
||
|
|
)}
|
||
|
|
</form>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
{/* Actions */}
|
||
|
|
<div className="p-6 pt-0 flex gap-3">
|
||
|
|
<button
|
||
|
|
onClick={handleCancel}
|
||
|
|
disabled={isUpdating}
|
||
|
|
className="flex-1 py-3 px-4 bg-slate-100 text-slate-600 font-bold uppercase text-xs tracking-wider rounded-xl hover:bg-slate-200 transition-all disabled:opacity-50"
|
||
|
|
>
|
||
|
|
Cancelar
|
||
|
|
</button>
|
||
|
|
<button
|
||
|
|
onClick={handleConfirm}
|
||
|
|
disabled={isUpdating}
|
||
|
|
className="flex-1 py-3 px-4 text-white font-black uppercase text-xs tracking-wider rounded-xl transition-all shadow-lg bg-orange-500 hover:bg-orange-600 shadow-orange-500/20 active:scale-95 disabled:opacity-50"
|
||
|
|
>
|
||
|
|
{isUpdating ? "Aplicando..." : "Confirmar"}
|
||
|
|
</button>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
);
|
||
|
|
};
|
||
|
|
|
||
|
|
export default DiscountItemModal;
|
||
|
|
|