Implementación de duración de precio por producto y mejoras en cálculo de tarifas
Detalles: - Se añadió PriceDurationDays a la entidad Product. - Actualización de base de datos y repositorios para soportar la nueva propiedad. - Ajuste en PricingService para calcular costos diarios basados en la duración del producto. - Mejora en UI: el selector de días en ventas se adapta a la duración base del producto. - Duración del precio ahora es opcional mediante un interruptor en el modal de producto. - Corrección en DeleteAsync usando SQL dinámico para mayor seguridad. - Cálculo de precio instantáneo al seleccionar el rubro sin requerir texto previo.
This commit is contained in:
@@ -4,6 +4,8 @@ import { useDebounce } from '../../hooks/useDebounce';
|
||||
import api from '../../services/api';
|
||||
import clsx from 'clsx';
|
||||
import { processCategories, type FlatCategory } from '../../utils/categoryTreeUtils';
|
||||
import { productService } from '../../services/productService';
|
||||
import type { Product } from '../../types/Product';
|
||||
|
||||
interface AdEditorModalProps {
|
||||
isOpen: boolean;
|
||||
@@ -22,6 +24,7 @@ interface PricingResult {
|
||||
export default function AdEditorModal({ isOpen, onClose, onConfirm, clientId, productId }: AdEditorModalProps) {
|
||||
const [flatCategories, setFlatCategories] = useState<FlatCategory[]>([]);
|
||||
const [operations, setOperations] = useState<any[]>([]);
|
||||
const [product, setProduct] = useState<Product | null>(null);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [calculating, setCalculating] = useState(false);
|
||||
|
||||
@@ -53,9 +56,21 @@ export default function AdEditorModal({ isOpen, onClose, onConfirm, clientId, pr
|
||||
|
||||
const loadData = async () => {
|
||||
try {
|
||||
const [catRes, opRes] = await Promise.all([api.get('/categories'), api.get('/operations')]);
|
||||
const [catRes, opRes, prodData] = await Promise.all([
|
||||
api.get('/categories'),
|
||||
api.get('/operations'),
|
||||
productService.getById(productId)
|
||||
]);
|
||||
setFlatCategories(processCategories(catRes.data));
|
||||
setOperations(opRes.data);
|
||||
setProduct(prodData);
|
||||
|
||||
// Ajustar días iniciales según la duración del producto
|
||||
if (prodData.priceDurationDays > 1) {
|
||||
setDays(prodData.priceDurationDays);
|
||||
} else {
|
||||
setDays(3);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("Error cargando configuración", e);
|
||||
}
|
||||
@@ -66,7 +81,10 @@ export default function AdEditorModal({ isOpen, onClose, onConfirm, clientId, pr
|
||||
|
||||
// Calculadora de Precio en Tiempo Real
|
||||
useEffect(() => {
|
||||
if (!categoryId || !text) return;
|
||||
if (!categoryId) {
|
||||
setPricing({ totalPrice: 0, wordCount: 0, details: '' });
|
||||
return;
|
||||
}
|
||||
|
||||
const calculate = async () => {
|
||||
setCalculating(true);
|
||||
@@ -88,7 +106,7 @@ export default function AdEditorModal({ isOpen, onClose, onConfirm, clientId, pr
|
||||
}
|
||||
};
|
||||
calculate();
|
||||
}, [debouncedText, categoryId, days, styles, startDate]);
|
||||
}, [debouncedText, categoryId, days, styles, startDate, productId]);
|
||||
|
||||
const handleSave = async () => {
|
||||
if (!categoryId || !operationId || !text) return alert("Complete los campos obligatorios");
|
||||
@@ -260,18 +278,42 @@ export default function AdEditorModal({ isOpen, onClose, onConfirm, clientId, pr
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-span-1">
|
||||
<label className="text-[10px] font-black text-blue-400 uppercase tracking-widest ml-1 mb-1 block">Cantidad Días</label>
|
||||
<div className="flex items-center bg-white border border-blue-200 rounded-xl overflow-hidden h-[38px]">
|
||||
<button onClick={() => setDays(Math.max(1, days - 1))} className="px-3 hover:bg-blue-50 text-blue-400 transition-colors">-</button>
|
||||
<input
|
||||
type="number"
|
||||
className="w-full text-center font-black text-slate-700 text-sm outline-none"
|
||||
value={days} onChange={e => setDays(parseInt(e.target.value) || 1)}
|
||||
/>
|
||||
<button onClick={() => setDays(days + 1)} className="px-3 hover:bg-blue-50 text-blue-400 transition-colors">+</button>
|
||||
{product?.productTypeId !== 4 && product?.productTypeId !== 6 && (
|
||||
<div className="col-span-1">
|
||||
<label className="text-[10px] font-black text-blue-400 uppercase tracking-widest ml-1 mb-1 block">
|
||||
{product && product.priceDurationDays > 1 ? `Cant. de Módulos (${product.priceDurationDays}d)` : 'Cantidad Días'}
|
||||
</label>
|
||||
<div className="flex items-center bg-white border border-blue-200 rounded-xl overflow-hidden h-[38px]">
|
||||
<button
|
||||
onClick={() => setDays(Math.max(product?.priceDurationDays || 1, days - (product?.priceDurationDays || 1)))}
|
||||
className="px-3 hover:bg-blue-50 text-blue-400 transition-colors"
|
||||
>
|
||||
-
|
||||
</button>
|
||||
<input
|
||||
type="number"
|
||||
className="w-full text-center font-black text-slate-700 text-sm outline-none"
|
||||
value={days}
|
||||
onChange={e => {
|
||||
const val = parseInt(e.target.value) || 0;
|
||||
const step = product?.priceDurationDays || 1;
|
||||
// Redondear al múltiplo más cercano si es necesario, o simplemente dejarlo
|
||||
setDays(Math.max(step, val));
|
||||
}}
|
||||
step={product?.priceDurationDays || 1}
|
||||
/>
|
||||
<button
|
||||
onClick={() => setDays(days + (product?.priceDurationDays || 1))}
|
||||
className="px-3 hover:bg-blue-50 text-blue-400 transition-colors"
|
||||
>
|
||||
+
|
||||
</button>
|
||||
</div>
|
||||
{product && product.priceDurationDays > 1 && (
|
||||
<p className="text-[9px] text-blue-400 font-bold mt-1 ml-1 uppercase">Mínimo: {product.priceDurationDays} días</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<div className="col-span-1 flex flex-col justify-center items-end">
|
||||
<span className="text-[9px] font-black text-slate-400 uppercase tracking-widest mb-0.5">Costo Estimado</span>
|
||||
<div className="text-2xl font-mono font-black text-slate-900">
|
||||
|
||||
Reference in New Issue
Block a user