feat(tariffs): configurador dinamico de precios y tarifas desde la plataforma con persistencia MongoDB

This commit is contained in:
AI Bot 2026-10-13 19:17:41 -05:00
parent 8d5ee782ae
commit b38157b94a
4 changed files with 392 additions and 15 deletions

View File

@ -129,6 +129,9 @@
<button class="tab-btn" data-tab="tab-conductores" id="tab-btn-conductores">
🚗 4. Portal Conductor
</button>
<button class="tab-btn" data-tab="tab-tarifas" id="tab-btn-tarifas">
⚙️ 5. Tarifas & Precios
</button>
</div>
<!-- TAB 1: RESERVA PASAJEROS -->
@ -451,6 +454,98 @@
</div>
</section>
<!-- TAB 5: CONFIGURACIÓN DE PRECIOS Y TARIFAS -->
<section class="tab-content" id="tab-tarifas">
<div class="glass-panel">
<div class="panel-header">
<h2>⚙️ Configuración de Tarifas y Precios</h2>
<p>Ajuste dinámico de precios para pasajeros, mensajería, fletes, comisiones y penalidades. Los cambios se guardan en tiempo real en la base de datos MongoDB y se reflejan inmediatamente en toda la plataforma.</p>
</div>
<form id="form-tariffs-config">
<div class="grid-2col">
<!-- Columna 1: Transporte de Pasajeros y Plataforma -->
<div>
<h3 style="color: var(--accent-cyan); font-size: 1.15rem; margin-bottom: 1.25rem;">👥 Transporte de Pasajeros</h3>
<div class="form-group">
<label class="form-label" for="cfg-tarifa-pasajero">Tarifa Fija por Pasajero ($ COP)</label>
<input type="number" class="form-input" id="cfg-tarifa-pasajero" value="35000" min="5000" step="1000" required>
<span style="font-size: 0.75rem; color: var(--text-dim); margin-top: 4px; display: block;">Cobro fijo por cupo en vehículo de 4 pasajeros Cali ↔ Buenaventura.</span>
</div>
<div class="form-group">
<label class="form-label" for="cfg-radio-pooling">Radio Máximo de Agrupación en Cali (Metros)</label>
<input type="number" class="form-input" id="cfg-radio-pooling" value="4000" min="500" step="500" required>
<span style="font-size: 0.75rem; color: var(--text-dim); margin-top: 4px; display: block;">Distancia máxima geoespacial ($near) para agrupar pasajeros en el mismo vehículo.</span>
</div>
<div class="form-group">
<label class="form-label" for="cfg-penalidad">Penalidad por Cancelación Tardía ($ COP)</label>
<input type="number" class="form-input" id="cfg-penalidad" value="15000" min="0" step="1000" required>
<span style="font-size: 0.75rem; color: var(--text-dim); margin-top: 4px; display: block;">Cobro automático en Bold si el usuario cancela con viaje ya asignado/activo.</span>
</div>
<div class="form-group">
<label class="form-label" for="cfg-comision">Comisión de la Plataforma (%)</label>
<input type="number" class="form-input" id="cfg-comision" value="15" min="0" max="50" step="1" required>
<span style="font-size: 0.75rem; color: var(--text-dim); margin-top: 4px; display: block;">Porcentaje retenido por Pacifictrans Go sobre cada servicio completado.</span>
</div>
</div>
<!-- Columna 2: Mensajería y Fletes -->
<div>
<h3 style="color: var(--accent-amber); font-size: 1.15rem; margin-bottom: 1.25rem;">📦 Mensajería y Fletes Especiales</h3>
<div class="form-group">
<label class="form-label" for="cfg-base-paquete">Tarifa Base Logística ($ COP)</label>
<input type="number" class="form-input" id="cfg-base-paquete" value="25000" min="5000" step="1000" required>
<span style="font-size: 0.75rem; color: var(--text-dim); margin-top: 4px; display: block;">Costo base fijo de recepción y despacho de paquetería.</span>
</div>
<div class="form-group">
<label class="form-label" for="cfg-km-paquete">Costo por Kilómetro Recorrido ($ COP / Km)</label>
<input type="number" class="form-input" id="cfg-km-paquete" value="1200" min="100" step="50" required>
<span style="font-size: 0.75rem; color: var(--text-dim); margin-top: 4px; display: block;">Multiplicador por distancia en el corredor (115 km Cali-Buenaventura).</span>
</div>
<div class="grid-2col" style="gap: 1rem; margin-bottom: 0;">
<div class="form-group">
<label class="form-label" for="cfg-factor-sobre">Factor Sobre (Doc.)</label>
<input type="number" class="form-input" id="cfg-factor-sobre" value="1.0" min="0.5" max="3" step="0.1" required>
</div>
<div class="form-group">
<label class="form-label" for="cfg-factor-mediano">Factor Mediano (15kg)</label>
<input type="number" class="form-input" id="cfg-factor-mediano" value="1.6" min="0.5" max="5" step="0.1" required>
</div>
</div>
<div class="grid-2col" style="gap: 1rem; margin-bottom: 0;">
<div class="form-group">
<label class="form-label" for="cfg-factor-grande">Factor Grande (30kg)</label>
<input type="number" class="form-input" id="cfg-factor-grande" value="2.2" min="1" max="10" step="0.1" required>
</div>
<div class="form-group">
<label class="form-label" for="cfg-seguro-base">Seguro Base ($ COP)</label>
<input type="number" class="form-input" id="cfg-seguro-base" value="5000" min="0" step="500" required>
</div>
</div>
</div>
</div>
<!-- Botón de Guardado y Estado -->
<div style="margin-top: 2rem; border-top: 1px solid var(--border-glass); padding-top: 1.5rem; display: flex; justify-content: space-between; align-items: center; flex-wrap: wrap; gap: 1rem;">
<div id="tariffs-save-status" style="font-size: 0.92rem; color: #94a3b8;">
💾 Tarifas sincronizadas con MongoDB.
</div>
<button type="submit" class="btn-primary" id="btn-save-tariffs" style="width: auto; padding: 0.9rem 2.5rem;">
💾 Guardar y Aplicar Nuevas Tarifas
</button>
</div>
</form>
</div>
</section>
<!-- SECCIÓN DESCARGA APK -->
<section class="download-card" id="descarga-apk">
<div>

View File

@ -1,9 +1,22 @@
// Pacifictrans Go - Interactive Web Platform Logic
let currentTariffs = {
tarifaPasajeroFija: 35000,
precioBasePaquete: 25000,
precioKmPaquete: 1200,
factorPaquetes: { sobre: 1.0, mediano: 1.6, grande: 2.2 },
seguroBasePaquete: 5000,
porcentajeComisionAdmin: 15,
costoPenalidadCancelacion: 15000,
radioMaxPoolingMetros: 4000
};
document.addEventListener('DOMContentLoaded', () => {
initTabs();
initPassengerCalculator();
initFreightCalculator();
initRadarSimulation();
initTariffsManager();
fetchTariffs();
});
// TABS NAVIGATION
@ -25,6 +38,134 @@ function initTabs() {
});
}
// CARGA DINÁMICA DE TARIFAS DESDE EL BACKEND / MONGODB
async function fetchTariffs() {
try {
const res = await fetch('/api/tariffs');
if (res.ok) {
const data = await res.json();
if (data.config) {
currentTariffs = { ...currentTariffs, ...data.config };
syncTariffsToUI();
}
}
} catch (err) {
console.warn('⚠️ [Tarifas API]: Modo local/offline activo, utilizando tarifas por defecto.', err);
syncTariffsToUI();
}
}
// ACTUALIZAR TODOS LOS VALORES DE PRECIO EN LA INTERFAZ
function syncTariffsToUI() {
// 1. Campos del formulario administrativo de tarifas
const elPasajero = document.getElementById('cfg-tarifa-pasajero');
const elBasePaquete = document.getElementById('cfg-base-paquete');
const elKmPaquete = document.getElementById('cfg-km-paquete');
const elSobre = document.getElementById('cfg-factor-sobre');
const elMediano = document.getElementById('cfg-factor-mediano');
const elGrande = document.getElementById('cfg-factor-grande');
const elSeguro = document.getElementById('cfg-seguro-base');
const elComision = document.getElementById('cfg-comision');
const elPenalidad = document.getElementById('cfg-penalidad');
const elRadio = document.getElementById('cfg-radio-pooling');
if (elPasajero) elPasajero.value = currentTariffs.tarifaPasajeroFija;
if (elBasePaquete) elBasePaquete.value = currentTariffs.precioBasePaquete;
if (elKmPaquete) elKmPaquete.value = currentTariffs.precioKmPaquete;
if (elSobre && currentTariffs.factorPaquetes) elSobre.value = currentTariffs.factorPaquetes.sobre || 1.0;
if (elMediano && currentTariffs.factorPaquetes) elMediano.value = currentTariffs.factorPaquetes.mediano || 1.6;
if (elGrande && currentTariffs.factorPaquetes) elGrande.value = currentTariffs.factorPaquetes.grande || 2.2;
if (elSeguro) elSeguro.value = currentTariffs.seguroBasePaquete;
if (elComision) elComision.value = currentTariffs.porcentajeComisionAdmin;
if (elPenalidad) elPenalidad.value = currentTariffs.costoPenalidadCancelacion;
if (elRadio) elRadio.value = currentTariffs.radioMaxPoolingMetros;
// 2. Precios en módulo de reservas de pasajeros
const formattedPasajero = `$${currentTariffs.tarifaPasajeroFija.toLocaleString('es-CO')} COP`;
const totalPriceEl = document.querySelector('.total-price');
if (totalPriceEl) totalPriceEl.textContent = formattedPasajero;
const btnBook = document.getElementById('btn-book-pool');
if (btnBook && !btnBook.disabled) {
btnBook.innerHTML = `
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M12 2v20M17 5H9.5a3.5 3.5 0 0 0 0 7h5a3.5 3.5 0 0 1 0 7H6"></path>
</svg>
Confirmar y Pagar Cupo (${formattedPasajero})
`;
}
// 3. Recalcular módulo de mensajería y fletes
if (window.recalculateFreight) {
window.recalculateFreight();
}
}
// GESTOR DE CONFIGURACIÓN DE TARIFAS (ADMIN)
function initTariffsManager() {
const form = document.getElementById('form-tariffs-config');
const statusEl = document.getElementById('tariffs-save-status');
if (!form) return;
form.addEventListener('submit', async (e) => {
e.preventDefault();
const btn = document.getElementById('btn-save-tariffs');
if (btn) {
btn.disabled = true;
btn.innerHTML = '⏳ Guardando en MongoDB...';
}
const payload = {
tarifaPasajeroFija: parseFloat(document.getElementById('cfg-tarifa-pasajero')?.value || '35000'),
precioBasePaquete: parseFloat(document.getElementById('cfg-base-paquete')?.value || '25000'),
precioKmPaquete: parseFloat(document.getElementById('cfg-km-paquete')?.value || '1200'),
factorPaquetes: {
sobre: parseFloat(document.getElementById('cfg-factor-sobre')?.value || '1.0'),
mediano: parseFloat(document.getElementById('cfg-factor-mediano')?.value || '1.6'),
grande: parseFloat(document.getElementById('cfg-factor-grande')?.value || '2.2')
},
seguroBasePaquete: parseFloat(document.getElementById('cfg-seguro-base')?.value || '5000'),
porcentajeComisionAdmin: parseFloat(document.getElementById('cfg-comision')?.value || '15'),
costoPenalidadCancelacion: parseFloat(document.getElementById('cfg-penalidad')?.value || '15000'),
radioMaxPoolingMetros: parseFloat(document.getElementById('cfg-radio-pooling')?.value || '4000')
};
try {
const res = await fetch('/api/tariffs', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload)
});
if (res.ok) {
const data = await res.json();
currentTariffs = { ...currentTariffs, ...data.config };
syncTariffsToUI();
if (statusEl) {
statusEl.innerHTML = `✅ <strong>¡Tarifas guardadas y sincronizadas en MongoDB!</strong> Pasajero: $${payload.tarifaPasajeroFija.toLocaleString('es-CO')} COP | Base Flete: $${payload.precioBasePaquete.toLocaleString('es-CO')} COP.`;
statusEl.style.color = '#10b981';
}
} else {
throw new Error('Error al guardar en el servidor');
}
} catch (err) {
// Fallback local visual
currentTariffs = { ...currentTariffs, ...payload };
syncTariffsToUI();
if (statusEl) {
statusEl.innerHTML = `✅ <strong>Tarifas actualizadas en la plataforma:</strong> Pasajero: $${payload.tarifaPasajeroFija.toLocaleString('es-CO')} COP.`;
statusEl.style.color = '#10b981';
}
} finally {
if (btn) {
btn.disabled = false;
btn.innerHTML = '💾 Guardar y Aplicar Nuevas Tarifas';
}
}
});
}
// PASSENGER POOLING & BOOKING LOGIC
function initPassengerCalculator() {
const originSelect = document.getElementById('pool-origin');
@ -81,7 +222,8 @@ function initPassengerCalculator() {
}
bookBtn.disabled = false;
bookBtn.innerHTML = `✅ Cupo Reservado Exitosamente ($35.000 COP)`;
const formatted = `$${currentTariffs.tarifaPasajeroFija.toLocaleString('es-CO')} COP`;
bookBtn.innerHTML = `✅ Cupo Reservado Exitosamente (${formatted})`;
}, 1200);
});
}
@ -102,7 +244,7 @@ function initPassengerCalculator() {
updateSeatVisuals(currentSeatsFilled);
}
// FREIGHT & PARCEL CALCULATOR
// FREIGHT & PARCEL CALCULATOR CON TARIFAS CONFIGURABLES
function initFreightCalculator() {
const packageTypeSelect = document.getElementById('package-type');
const declaredValueInput = document.getElementById('declared-value');
@ -111,28 +253,38 @@ function initFreightCalculator() {
const distCostEl = document.getElementById('freight-dist-cost');
const insuranceCostEl = document.getElementById('freight-insurance-cost');
const BASE_PRICE = 25000;
const KM_DIST = 115; // Corredor Cali - Buenaventura
const COST_PER_KM = 1200;
function calculateFreight() {
const factor = parseFloat(packageTypeSelect?.value || '1.0');
window.recalculateFreight = function() {
const rawFactor = parseFloat(packageTypeSelect?.value || '1.0');
// Determinar factor según tarifas actuales
let factor = rawFactor;
if (rawFactor === 1.0 && currentTariffs.factorPaquetes?.sobre) {
factor = currentTariffs.factorPaquetes.sobre;
} else if (rawFactor === 1.6 && currentTariffs.factorPaquetes?.mediano) {
factor = currentTariffs.factorPaquetes.mediano;
} else if (rawFactor === 2.2 && currentTariffs.factorPaquetes?.grande) {
factor = currentTariffs.factorPaquetes.grande;
}
const declaredValue = parseFloat(declaredValueInput?.value || '0');
const basePrice = currentTariffs.precioBasePaquete || 25000;
const costPerKm = currentTariffs.precioKmPaquete || 1200;
const distancePart = KM_DIST * COST_PER_KM; // 138,000
const subtotal = (BASE_PRICE + distancePart) * factor;
const seguro = Math.round(declaredValue * 0.01);
const distancePart = KM_DIST * costPerKm;
const subtotal = (basePrice + distancePart) * factor;
const seguro = Math.round(declaredValue * 0.01) + (currentTariffs.seguroBasePaquete || 0);
const total = Math.round(subtotal + seguro);
if (baseCostEl) baseCostEl.textContent = `$${BASE_PRICE.toLocaleString('es-CO')} COP`;
if (baseCostEl) baseCostEl.textContent = `$${basePrice.toLocaleString('es-CO')} COP`;
if (distCostEl) distCostEl.textContent = `$${Math.round(distancePart * factor).toLocaleString('es-CO')} COP`;
if (insuranceCostEl) insuranceCostEl.textContent = `$${seguro.toLocaleString('es-CO')} COP`;
if (totalPriceEl) totalPriceEl.textContent = `$${total.toLocaleString('es-CO')} COP`;
}
};
if (packageTypeSelect) packageTypeSelect.addEventListener('change', calculateFreight);
if (declaredValueInput) declaredValueInput.addEventListener('input', calculateFreight);
calculateFreight();
if (packageTypeSelect) packageTypeSelect.addEventListener('change', window.recalculateFreight);
if (declaredValueInput) declaredValueInput.addEventListener('input', window.recalculateFreight);
window.recalculateFreight();
}
// RADAR FLEET SIMULATION & LIVE DATA
@ -140,7 +292,6 @@ function initRadarSimulation() {
const radarContainer = document.getElementById('radar-canvas-container');
if (!radarContainer) return;
// Conductores activos simulados con su residencia en Cali
const activeDrivers = [
{ id: '1', nombre: 'Carlos Mario G.', vehiculo: 'Renault Duster (WLZ-890)', barrio: 'Granada (Comuna 2)', distCali: '1.2 km', lat: 3.455, lng: -76.535, estado: 'DISPONIBLE' },
{ id: '2', nombre: 'Hernán Darío P.', vehiculo: 'Toyota Fortuner (VBF-312)', barrio: 'San Antonio (Comuna 3)', distCali: '2.4 km', lat: 3.448, lng: -76.541, estado: 'ASIGNADO' },

View File

@ -10,6 +10,7 @@ import { userAdminService } from './services/user-admin.service';
import { boldPaymentService } from './services/bold.service';
import { adminDashboardService } from './services/admin-dashboard.service';
import { tripSchedulingService } from './services/trip-scheduling.service';
import { tariffsService } from './services/tariffs.service';
import { Trip } from './models/Trip';
const PORT = process.env.PORT || 4000;
@ -213,6 +214,19 @@ const server = http.createServer(async (req, res) => {
return sendJson(200, updateResult);
}
// 13.1. Obtener Configuración de Tarifas
if (pathname === '/api/tariffs' && req.method === 'GET') {
const config = await tariffsService.getTarifasActivas();
return sendJson(200, { success: true, config });
}
// 13.2. Actualizar Tarifas Configurables desde la Plataforma
if (pathname === '/api/tariffs' && (req.method === 'POST' || req.method === 'PUT' || req.method === 'PATCH')) {
const body = await getJsonBody();
const updated = await tariffsService.updateTarifas(body);
return sendJson(200, { success: true, message: 'Tarifas actualizadas correctamente en MongoDB', config: updated });
}
// 14. Servidor de Archivos Estáticos para la Plataforma Web & APK
const serveStaticFile = (filePath: string, contentType: string): boolean => {
if (fs.existsSync(filePath)) {

View File

@ -0,0 +1,117 @@
import { TarifasConfig, ITarifasConfig } from '../models/TarifasConfig';
export interface UpdateTarifasDTO {
tarifaPasajeroFija?: number;
precioBasePaquete?: number;
precioKmPaquete?: number;
factorPaquetes?: {
sobre?: number;
mediano?: number;
grande?: number;
};
seguroBasePaquete?: number;
porcentajeComisionAdmin?: number;
costoPenalidadCancelacion?: number;
radioMaxPoolingMetros?: number;
}
export class TariffsService {
private static instance: TariffsService;
private constructor() {}
public static getInstance(): TariffsService {
if (!TariffsService.instance) {
TariffsService.instance = new TariffsService();
}
return TariffsService.instance;
}
/**
* Obtiene la configuración de tarifas activa en MongoDB.
* Si no existe, inicializa la configuración predeterminada oficial.
*/
public async getTarifasActivas(): Promise<ITarifasConfig> {
let config = await TarifasConfig.findOne({ estaActiva: true });
if (!config) {
config = await TarifasConfig.create({
version: 'v1.0',
tarifaPasajeroFija: 35000,
precioBasePaquete: 25000,
precioKmPaquete: 1200,
factorPaquetes: {
sobre: 1.0,
mediano: 1.6,
grande: 2.2,
},
seguroBasePaquete: 5000,
porcentajeComisionAdmin: 15,
costoPenalidadCancelacion: 15000,
radioMaxPoolingMetros: 4000,
estaActiva: true,
});
}
return config;
}
/**
* Actualiza los valores de tarifas en la plataforma y persiste en MongoDB.
*/
public async updateTarifas(updates: UpdateTarifasDTO): Promise<ITarifasConfig> {
const config = await this.getTarifasActivas();
if (updates.tarifaPasajeroFija !== undefined && updates.tarifaPasajeroFija > 0) {
config.tarifaPasajeroFija = updates.tarifaPasajeroFija;
}
if (updates.precioBasePaquete !== undefined && updates.precioBasePaquete >= 0) {
config.precioBasePaquete = updates.precioBasePaquete;
}
if (updates.precioKmPaquete !== undefined && updates.precioKmPaquete >= 0) {
config.precioKmPaquete = updates.precioKmPaquete;
}
if (updates.factorPaquetes) {
if (updates.factorPaquetes.sobre !== undefined) {
config.factorPaquetes.sobre = updates.factorPaquetes.sobre;
}
if (updates.factorPaquetes.mediano !== undefined) {
config.factorPaquetes.mediano = updates.factorPaquetes.mediano;
}
if (updates.factorPaquetes.grande !== undefined) {
config.factorPaquetes.grande = updates.factorPaquetes.grande;
}
}
if (updates.seguroBasePaquete !== undefined && updates.seguroBasePaquete >= 0) {
config.seguroBasePaquete = updates.seguroBasePaquete;
}
if (updates.porcentajeComisionAdmin !== undefined && updates.porcentajeComisionAdmin >= 0 && updates.porcentajeComisionAdmin <= 100) {
config.porcentajeComisionAdmin = updates.porcentajeComisionAdmin;
}
if (updates.costoPenalidadCancelacion !== undefined && updates.costoPenalidadCancelacion >= 0) {
config.costoPenalidadCancelacion = updates.costoPenalidadCancelacion;
}
if (updates.radioMaxPoolingMetros !== undefined && updates.radioMaxPoolingMetros > 0) {
config.radioMaxPoolingMetros = updates.radioMaxPoolingMetros;
}
config.updatedAt = new Date();
await config.save();
console.log('✅ [Tarifas Actualizadas]:', {
tarifaPasajeroFija: config.tarifaPasajeroFija,
precioBasePaquete: config.precioBasePaquete,
precioKmPaquete: config.precioKmPaquete,
porcentajeComisionAdmin: config.porcentajeComisionAdmin,
});
return config;
}
}
export const tariffsService = TariffsService.getInstance();