feat(pacifictrans): implement immediate vs scheduled modalities, smart ETA corridor calculation and urgent freight surcharge

This commit is contained in:
AI Bot 2026-10-18 18:00:26 -05:00
parent bae5cbbffa
commit ccb4db4e17
18 changed files with 1994 additions and 253 deletions

View File

@ -2765,6 +2765,191 @@ button:active, .service-pill-btn:active, .btn-qty-seat:active, .chip-payment:act
to { opacity: 1; transform: translateY(0); }
}
/* ==========================================================================
MODALIDADES DE TIEMPO: INMEDIATO VS PROGRAMADO & RECARGO ENCOMIENDAS URGENTES
========================================================================== */
.time-modality-container {
background: rgba(15, 23, 42, 0.65);
border: 1px solid var(--border-glass);
border-radius: var(--radius-md);
padding: 1.15rem;
margin-top: 1.25rem;
transition: all 0.25s ease;
}
.time-modality-container:focus-within {
border-color: rgba(6, 182, 212, 0.4);
}
.badge-time-selected {
font-size: 0.75rem;
font-weight: 700;
padding: 3px 10px;
border-radius: 9999px;
background: rgba(6, 182, 212, 0.15);
color: var(--accent-cyan);
border: 1px solid rgba(6, 182, 212, 0.4);
letter-spacing: 0.02em;
transition: all 0.2s ease;
}
.time-mode-selector {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 0.75rem;
margin-top: 0.5rem;
}
@media (max-width: 640px) {
.time-mode-selector {
grid-template-columns: 1fr;
}
}
.btn-time-mode {
background: rgba(15, 23, 42, 0.8);
border: 1px solid var(--border-glass);
border-radius: var(--radius-md);
padding: 0.9rem;
display: flex;
align-items: center;
gap: 0.75rem;
cursor: pointer;
transition: all 0.2s cubic-bezier(0.16, 1, 0.3, 1);
text-align: left;
color: var(--text-main);
outline: none;
}
.btn-time-mode:hover {
background: rgba(30, 41, 59, 0.85);
border-color: rgba(6, 182, 212, 0.35);
transform: translateY(-2px);
}
.btn-time-mode.active {
background: linear-gradient(135deg, rgba(6, 182, 212, 0.2), rgba(16, 185, 129, 0.12));
border-color: var(--accent-cyan);
box-shadow: 0 0 16px rgba(6, 182, 212, 0.25);
transform: translateY(-2px);
}
.btn-time-mode.active.urgent-active {
background: linear-gradient(135deg, rgba(239, 68, 68, 0.25), rgba(245, 158, 11, 0.15));
border-color: #f87171;
box-shadow: 0 0 16px rgba(239, 68, 68, 0.3);
}
.time-mode-icon {
font-size: 1.6rem;
flex-shrink: 0;
}
.time-mode-text {
display: flex;
flex-direction: column;
}
.time-mode-text strong {
font-size: 0.95rem;
color: #fff;
line-height: 1.2;
}
.time-mode-text span {
font-size: 0.73rem;
color: var(--text-muted);
margin-top: 2px;
}
.btn-time-mode.active .time-mode-text strong {
color: #38bdf8;
}
.btn-time-mode.active.urgent-active .time-mode-text strong {
color: #fca5a5;
}
/* Card desplegable de opciones programadas */
.scheduled-options-card {
background: rgba(2, 6, 23, 0.65);
border: 1px dashed rgba(6, 182, 212, 0.35);
border-radius: var(--radius-md);
padding: 1.15rem;
margin-top: 1rem;
animation: fadeIn 0.25s ease-out;
}
.schedule-type-toggle {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.radio-schedule-type {
display: flex;
align-items: center;
gap: 0.6rem;
background: rgba(15, 23, 42, 0.7);
padding: 0.6rem 0.85rem;
border-radius: var(--radius-sm);
border: 1px solid rgba(255, 255, 255, 0.05);
cursor: pointer;
font-size: 0.85rem;
color: #cbd5e1;
transition: all 0.2s ease;
}
.radio-schedule-type:hover {
background: rgba(30, 41, 59, 0.8);
border-color: rgba(6, 182, 212, 0.3);
}
.radio-schedule-type.active {
background: rgba(6, 182, 212, 0.12);
border-color: var(--accent-cyan);
color: #fff;
font-weight: 600;
}
.radio-schedule-type input[type="radio"] {
accent-color: var(--accent-cyan);
cursor: pointer;
}
/* Caja de estimación inteligente de viaje */
.smart-eta-box {
background: rgba(15, 23, 42, 0.85);
border: 1px solid rgba(16, 185, 129, 0.35);
border-radius: var(--radius-sm);
padding: 0.75rem 0.95rem;
font-size: 0.82rem;
color: #a7f3d0;
display: flex;
align-items: center;
gap: 0.5rem;
animation: fadeIn 0.2s ease;
}
.smart-eta-icon {
font-size: 1.15rem;
flex-shrink: 0;
}
.summary-urgent-badge {
display: inline-flex;
align-items: center;
gap: 0.35rem;
background: rgba(239, 68, 68, 0.2);
color: #f87171;
border: 1px solid rgba(239, 68, 68, 0.4);
padding: 0.2rem 0.6rem;
border-radius: 9999px;
font-size: 0.74rem;
font-weight: 700;
}

View File

@ -242,6 +242,70 @@
</select>
</div>
<!-- MODALIDAD DE TIEMPO DEL VIAJE: INMEDIATO VS PROGRAMADO -->
<div class="time-modality-container" id="pool-time-modality-container">
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 0.5rem;">
<label class="form-label" style="margin-bottom: 0; display: flex; align-items: center; gap: 0.4rem;">
<span>🕒</span> Horario del Servicio
</label>
<span id="pool-time-badge" class="badge-time-selected">⚡ Salida Inmediata</span>
</div>
<div class="time-mode-selector">
<button type="button" class="btn-time-mode active" id="btn-pool-mode-immediate" data-mode="INMEDIATO">
<span style="font-size: 1.35rem;">⚡</span>
<div>
<div style="font-weight: 700; font-size: 0.92rem;">Salida Inmediata</div>
<div style="font-size: 0.75rem; color: #94a3b8;">Asignación prioritaria ya</div>
</div>
</button>
<button type="button" class="btn-time-mode" id="btn-pool-mode-scheduled" data-mode="PROGRAMADO">
<span style="font-size: 1.35rem;">📅</span>
<div>
<div style="font-weight: 700; font-size: 0.92rem;">Viaje Programado</div>
<div style="font-size: 0.75rem; color: #94a3b8;">Planifica fecha y hora</div>
</div>
</button>
</div>
<!-- OPCIONES DE PROGRAMACIÓN (Ocultas por defecto si es Inmediato) -->
<div class="scheduled-options-card" id="pool-scheduled-options" style="display: none; margin-top: 1rem;">
<div style="font-size: 0.85rem; font-weight: 600; color: #38bdf8; margin-bottom: 0.75rem;">
📅 Especifica tu tiempo de viaje:
</div>
<!-- Radio: Hora de Recogida vs Hora Requerida en Destino -->
<div style="display: flex; flex-direction: column; gap: 0.6rem; margin-bottom: 1rem;">
<label class="radio-schedule-type" style="display: flex; align-items: center; gap: 0.5rem; cursor: pointer;">
<input type="radio" name="pool-schedule-type" id="pool-type-recogida" value="HORA_RECOGIDA" checked>
<span style="font-size: 0.88rem; color: #fff;">🚪 <strong>Hora deseada de recogida</strong> (a la puerta de tu domicilio)</span>
</label>
<label class="radio-schedule-type" style="display: flex; align-items: center; gap: 0.5rem; cursor: pointer;">
<input type="radio" name="pool-schedule-type" id="pool-type-destino" value="HORA_LLEGADA_DESTINO">
<span style="font-size: 0.88rem; color: #fff;">🏁 <strong>Hora que requiero estar en el destino</strong> (Pacific Trans calcula la hora de recogida)</span>
</label>
</div>
<div class="grid-2col" style="gap: 0.75rem; margin-bottom: 0.5rem;">
<div class="form-group" style="margin-bottom: 0;">
<label class="form-label" for="pool-schedule-date" style="font-size: 0.82rem;">Fecha del Servicio</label>
<input type="date" class="form-input" id="pool-schedule-date" style="padding: 0.65rem 0.85rem; font-size: 0.9rem;">
</div>
<div class="form-group" style="margin-bottom: 0;">
<label class="form-label" for="pool-schedule-time" id="pool-label-time" style="font-size: 0.82rem;">Hora de Recogida</label>
<input type="time" class="form-input" id="pool-schedule-time" style="padding: 0.65rem 0.85rem; font-size: 0.9rem;">
</div>
</div>
<!-- Caja inteligente de cálculo de ETA / Hora sugerida -->
<div class="smart-eta-box" id="pool-smart-eta" style="margin-top: 0.85rem; font-size: 0.82rem; padding: 0.65rem 0.85rem; background: rgba(6, 182, 212, 0.08); border-left: 3px solid var(--accent-cyan); border-radius: 4px; display: none;">
<!-- Se llena con JS -->
</div>
</div>
</div>
<!-- SELECTOR DE CANTIDAD DE CUPOS (1 a 4 Pasajeros) -->
<div class="form-group" style="margin-top: 1.25rem;">
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 0.5rem;">
@ -350,6 +414,10 @@
<span style="color: var(--text-muted);">Seguro Contractual:</span>
<span style="color: var(--accent-emerald); font-weight: 600;">Póliza y Planilla SOAT</span>
</div>
<div class="summary-item" id="pool-summary-schedule-row">
<span style="color: var(--text-muted);">Modalidad y Horario:</span>
<span style="color: #fff; font-weight: 600;" id="pool-summary-schedule-text">⚡ Inmediato (Próxima Salida)</span>
</div>
<div class="summary-item" id="summary-subtotal-row" style="display: none; border-top: 1px dashed var(--border-glass); padding-top: 0.5rem; margin-top: 0.5rem;">
<span style="color: var(--text-muted);">Subtotal:</span>
@ -453,6 +521,68 @@
<input type="text" class="form-input" id="package-desc" placeholder="Repuestos automotrices / Ropa / Medicamentos">
</div>
<!-- MODALIDAD DE DESPACHO DE ENCOMIENDA: URGENTE INMEDIATO VS PROGRAMADO -->
<div class="time-modality-container" id="freight-time-modality-container">
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 0.5rem;">
<label class="form-label" style="margin-bottom: 0; display: flex; align-items: center; gap: 0.4rem;">
<span>⚡</span> Modalidad de Entrega del Envío
</label>
<span id="freight-time-badge" class="badge-time-selected" style="color: #fbbf24; border-color: rgba(251, 191, 36, 0.4); background: rgba(251, 191, 36, 0.15);">🚨 Envío Urgente Inmediato</span>
</div>
<div class="time-mode-selector">
<button type="button" class="btn-time-mode active" id="btn-freight-mode-urgent" data-mode="URGENTE" style="border-color: rgba(251, 191, 36, 0.4);">
<span style="font-size: 1.35rem;">🚨</span>
<div>
<div style="font-weight: 700; font-size: 0.92rem; color: #fbbf24;">Envío Urgente Inmediato</div>
<div style="font-size: 0.75rem; color: #94a3b8;" id="freight-urgent-surcharge-hint">Despacho en el primer carro (+<span id="freight-urgent-amount-label">$15.000</span>)</div>
</div>
</button>
<button type="button" class="btn-time-mode" id="btn-freight-mode-scheduled" data-mode="PROGRAMADO">
<span style="font-size: 1.35rem;">📅</span>
<div>
<div style="font-weight: 700; font-size: 0.92rem;">Envío Programado</div>
<div style="font-size: 0.75rem; color: #94a3b8;">Sin recargo de urgencia</div>
</div>
</button>
</div>
<!-- OPCIONES DE PROGRAMACIÓN FLETES (Ocultas si es Urgente) -->
<div class="scheduled-options-card" id="freight-scheduled-options" style="display: none; margin-top: 1rem;">
<div style="font-size: 0.85rem; font-weight: 600; color: #fbbf24; margin-bottom: 0.75rem;">
📅 Especifica fecha y hora de tu encomienda:
</div>
<div style="display: flex; flex-direction: column; gap: 0.6rem; margin-bottom: 1rem;">
<label class="radio-schedule-type" style="display: flex; align-items: center; gap: 0.5rem; cursor: pointer;">
<input type="radio" name="freight-schedule-type" id="freight-type-recogida" value="HORA_RECOGIDA" checked>
<span style="font-size: 0.88rem; color: #fff;">📦 <strong>Hora deseada de recogida del paquete</strong></span>
</label>
<label class="radio-schedule-type" style="display: flex; align-items: center; gap: 0.5rem; cursor: pointer;">
<input type="radio" name="freight-schedule-type" id="freight-type-destino" value="HORA_LLEGADA_DESTINO">
<span style="font-size: 0.88rem; color: #fff;">🏁 <strong>Hora que se requiere entregado en destino</strong></span>
</label>
</div>
<div class="grid-2col" style="gap: 0.75rem; margin-bottom: 0.5rem;">
<div class="form-group" style="margin-bottom: 0;">
<label class="form-label" for="freight-schedule-date" style="font-size: 0.82rem;">Fecha de Despacho/Entrega</label>
<input type="date" class="form-input" id="freight-schedule-date" style="padding: 0.65rem 0.85rem; font-size: 0.9rem;">
</div>
<div class="form-group" style="margin-bottom: 0;">
<label class="form-label" for="freight-schedule-time" id="freight-label-time" style="font-size: 0.82rem;">Hora de Recogida</label>
<input type="time" class="form-input" id="freight-schedule-time" style="padding: 0.65rem 0.85rem; font-size: 0.9rem;">
</div>
</div>
<div class="smart-eta-box" id="freight-smart-eta" style="margin-top: 0.85rem; font-size: 0.82rem; padding: 0.65rem 0.85rem; background: rgba(251, 191, 36, 0.08); border-left: 3px solid var(--accent-amber); border-radius: 4px; display: none;">
<!-- Se llena con JS -->
</div>
</div>
</div>
<!-- BONO O CUPÓN DE DESCUENTO (ENCOMIENDAS) -->
<div class="coupon-input-group" style="margin-top: 1.25rem;">
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 0.4rem;">
@ -475,7 +605,7 @@
<div>
<h3 style="font-size: 1.25rem; color: #fff; margin-bottom: 1rem;">Fórmula Oficial de Liquidación</h3>
<div style="background: rgba(0,0,0,0.4); border-radius: 8px; padding: 0.75rem; font-family: monospace; font-size: 0.8rem; color: #38bdf8; margin-bottom: 1rem;">
Tarifa = (Base + [115 Km × $1.200]) × Factor + Seguro (1%)
Tarifa = (Base + [115 Km × $1.200]) × Factor + Seguro + Recargo Urgencia
</div>
<div class="summary-item">
@ -490,6 +620,14 @@
<span style="color: var(--text-muted);">Seguro de Mercancía:</span>
<span style="color: var(--accent-emerald); font-weight: 600;" id="freight-insurance-cost">$1.500 COP</span>
</div>
<div class="summary-item" id="freight-summary-urgent-row" style="display: flex;">
<span style="color: var(--text-muted);">Recargo Envío Urgente:</span>
<span style="color: #fbbf24; font-weight: 700;" id="freight-urgent-cost">+$15.000 COP</span>
</div>
<div class="summary-item" id="freight-summary-schedule-row" style="display: none;">
<span style="color: var(--text-muted);">Modalidad y Horario:</span>
<span style="color: #38bdf8; font-weight: 600;" id="freight-summary-schedule-text">📅 Programado</span>
</div>
<div class="summary-item" id="freight-subtotal-row" style="display: none; border-top: 1px dashed var(--border-glass); padding-top: 0.5rem; margin-top: 0.5rem;">
<span style="color: var(--text-muted);">Subtotal Flete:</span>
@ -894,6 +1032,12 @@
<input type="number" class="form-input" id="cfg-seguro-base" value="5000" min="0" step="500" required>
</div>
</div>
<div class="form-group" style="margin-top: 1rem;">
<label class="form-label" for="cfg-recargo-urgente">🚨 Recargo por Envío Urgente Inmediato ($ COP)</label>
<input type="number" class="form-input" id="cfg-recargo-urgente" value="15000" min="0" step="1000" required>
<span style="font-size: 0.75rem; color: var(--text-dim); margin-top: 4px; display: block;">Valor adicional cobrado para fletes y encomiendas que solicitan despacho inmediato prioritario.</span>
</div>
</div>
</div>

View File

@ -1513,6 +1513,14 @@ function syncTariffsToUI() {
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;
const elRecargoUrgente = document.getElementById('cfg-recargo-urgente');
if (elRecargoUrgente) elRecargoUrgente.value = currentTariffs.recargoEnvioUrgente ?? 15000;
const urgentLabel = document.getElementById('freight-urgent-amount-label');
if (urgentLabel) urgentLabel.textContent = '$' + (currentTariffs.recargoEnvioUrgente ?? 15000).toLocaleString('es-CO');
const urgentCost = document.getElementById('freight-urgent-cost');
if (urgentCost) urgentCost.textContent = '+$' + (currentTariffs.recargoEnvioUrgente ?? 15000).toLocaleString('es-CO') + ' COP';
if (elComision) elComision.value = currentTariffs.porcentajeComisionAdmin;
if (elPenalidad) elPenalidad.value = currentTariffs.costoPenalidadCancelacion;
if (elRadio) elRadio.value = currentTariffs.radioMaxPoolingMetros;
@ -1622,6 +1630,7 @@ function initTariffsManager() {
grande: parseFloat(document.getElementById('cfg-factor-grande')?.value || '2.2')
},
seguroBasePaquete: parseFloat(document.getElementById('cfg-seguro-base')?.value || '5000'),
recargoEnvioUrgente: parseFloat(document.getElementById('cfg-recargo-urgente')?.value || '15000'),
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'),
@ -1671,6 +1680,29 @@ function initTariffsManager() {
/* ==========================================================================
7. RESERVA DE PASAJEROS & POOLING
========================================================================== */
function calculateSuggestedTime(timeStr, hoursDiff) {
if (!timeStr) return null;
const parts = timeStr.split(':');
if (parts.length < 2) return null;
const h = parseInt(parts[0], 10);
const m = parseInt(parts[1], 10);
if (isNaN(h) || isNaN(m)) return null;
let totalMins = h * 60 + m + Math.round(hoursDiff * 60);
while (totalMins < 0) totalMins += 24 * 60;
totalMins = totalMins % (24 * 60);
const newH = Math.floor(totalMins / 60);
const newM = totalMins % 60;
const period = newH >= 12 ? 'PM' : 'AM';
const displayH = newH % 12 === 0 ? 12 : newH % 12;
const displayM = String(newM).padStart(2, '0');
return {
time24: `${String(newH).padStart(2, '0')}:${String(newM).padStart(2, '0')}`,
display: `${displayH}:${displayM} ${period}`
};
}
function initPassengerCalculator() {
const originSelect = document.getElementById('pool-origin');
const addressGroup = document.getElementById('cali-address-group');
@ -1684,6 +1716,21 @@ function initPassengerCalculator() {
const seatButtons = document.querySelectorAll('.btn-qty-seat');
const paymentCards = document.querySelectorAll('.payment-method-card');
// Modalidad de Tiempo: Inmediato vs Programado
let poolTimeMode = 'INMEDIATO'; // 'INMEDIATO' | 'PROGRAMADO'
let poolScheduleType = 'HORA_RECOGIDA'; // 'HORA_RECOGIDA' | 'HORA_LLEGADA_DESTINO'
const btnPoolImmediate = document.getElementById('btn-pool-mode-immediate');
const btnPoolScheduled = document.getElementById('btn-pool-mode-scheduled');
const poolScheduledOptions = document.getElementById('pool-scheduled-options');
const poolTimeBadge = document.getElementById('pool-time-badge');
const poolRadioRecogida = document.getElementById('pool-type-recogida');
const poolRadioDestino = document.getElementById('pool-type-destino');
const poolDateInput = document.getElementById('pool-schedule-date');
const poolTimeInput = document.getElementById('pool-schedule-time');
const poolTimeLabel = document.getElementById('pool-label-time');
const poolSmartEta = document.getElementById('pool-smart-eta');
const poolSummaryScheduleText = document.getElementById('pool-summary-schedule-text');
let selectedSeats = 1;
let selectedPaymentMethod = 'BRE_B';
let currentSeatsFilled = 3;
@ -1986,8 +2033,101 @@ function initPassengerCalculator() {
// Actualizar previsualización en dots del vehículo
updateSeatVisuals(selectedSeats);
updatePoolScheduleCalculation();
}
function updatePoolScheduleCalculation() {
if (poolTimeMode === 'INMEDIATO') {
if (poolTimeBadge) {
poolTimeBadge.textContent = '⚡ Salida Inmediata';
poolTimeBadge.className = 'badge-time-selected';
}
if (poolScheduledOptions) poolScheduledOptions.style.display = 'none';
if (poolSummaryScheduleText) poolSummaryScheduleText.textContent = '⚡ Inmediato (Próxima Salida)';
return;
}
if (poolTimeBadge) {
poolTimeBadge.textContent = '📅 Viaje Programado';
poolTimeBadge.className = 'badge-time-selected';
}
if (poolScheduledOptions) poolScheduledOptions.style.display = 'block';
const chosenDate = poolDateInput ? poolDateInput.value : '';
const chosenTime = poolTimeInput ? poolTimeInput.value : '';
if (!chosenTime) {
if (poolSmartEta) poolSmartEta.style.display = 'none';
if (poolSummaryScheduleText) poolSummaryScheduleText.textContent = chosenDate ? `📅 Programado para el ${chosenDate}` : '📅 Programado';
return;
}
if (poolScheduleType === 'HORA_LLEGADA_DESTINO') {
if (poolTimeLabel) poolTimeLabel.textContent = 'Hora que requieres estar en destino';
const suggested = calculateSuggestedTime(chosenTime, -3); // Cali - Buenaventura ~3 horas
if (poolSmartEta && suggested) {
poolSmartEta.style.display = 'block';
poolSmartEta.innerHTML = `🚗💡 <strong>Planificación Inteligente:</strong> Para llegar a tu destino a las <strong>${chosenTime}</strong>, tu vehículo te recogerá aproximadamente a las <strong style="color: var(--accent-cyan);">${suggested.display}</strong> (trayecto ~3 hrs en el corredor).`;
}
if (poolSummaryScheduleText) {
poolSummaryScheduleText.innerHTML = `📅 Programado ${chosenDate || ''}<br><span style="font-size: 0.8rem; color: #94a3b8;">🏁 Destino: ${chosenTime} | 🚪 Recogida: ~${suggested ? suggested.display : ''}</span>`;
}
} else {
if (poolTimeLabel) poolTimeLabel.textContent = 'Hora deseada de recogida';
const estimated = calculateSuggestedTime(chosenTime, +3);
if (poolSmartEta && estimated) {
poolSmartEta.style.display = 'block';
poolSmartEta.innerHTML = `🏁💡 <strong>Estimación de Ruta:</strong> Si te recogemos a las <strong>${chosenTime}</strong>, tu llegada estimada a destino será alrededor de las <strong style="color: var(--accent-cyan);">${estimated.display}</strong> (~3 hrs en carretera).`;
}
if (poolSummaryScheduleText) {
poolSummaryScheduleText.innerHTML = `📅 Programado ${chosenDate || ''}<br><span style="font-size: 0.8rem; color: #94a3b8;">🚪 Recogida: ${chosenTime} | 🏁 Llegada aprox: ~${estimated ? estimated.display : ''}</span>`;
}
}
}
// Inicializar fecha mínima en hoy y hora por defecto
if (poolDateInput) {
const today = new Date().toISOString().split('T')[0];
poolDateInput.min = today;
if (!poolDateInput.value) poolDateInput.value = today;
}
if (poolTimeInput && !poolTimeInput.value) {
const now = new Date(Date.now() + 60 * 60 * 1000);
const hours = String(now.getHours()).padStart(2, '0');
const mins = String(now.getMinutes()).padStart(2, '0');
poolTimeInput.value = `${hours}:${mins}`;
}
if (btnPoolImmediate && btnPoolScheduled) {
btnPoolImmediate.addEventListener('click', () => {
poolTimeMode = 'INMEDIATO';
btnPoolImmediate.classList.add('active');
btnPoolScheduled.classList.remove('active');
updatePoolScheduleCalculation();
});
btnPoolScheduled.addEventListener('click', () => {
poolTimeMode = 'PROGRAMADO';
btnPoolScheduled.classList.add('active');
btnPoolImmediate.classList.remove('active');
updatePoolScheduleCalculation();
});
}
if (poolRadioRecogida && poolRadioDestino) {
poolRadioRecogida.addEventListener('change', () => {
if (poolRadioRecogida.checked) poolScheduleType = 'HORA_RECOGIDA';
updatePoolScheduleCalculation();
});
poolRadioDestino.addEventListener('change', () => {
if (poolRadioDestino.checked) poolScheduleType = 'HORA_LLEGADA_DESTINO';
updatePoolScheduleCalculation();
});
}
if (poolDateInput) poolDateInput.addEventListener('change', updatePoolScheduleCalculation);
if (poolTimeInput) poolTimeInput.addEventListener('input', updatePoolScheduleCalculation);
window.updatePassengerBookingSummary = updateSummary;
// Escuchadores de botones de selección de cupos
@ -2045,6 +2185,22 @@ function initPassengerCalculator() {
saveCurrentFormData();
// Validar programación si aplica
if (poolTimeMode === 'PROGRAMADO') {
const pDate = poolDateInput ? poolDateInput.value : '';
const pTime = poolTimeInput ? poolTimeInput.value : '';
if (!pDate) {
alert('📅 Por favor selecciona la fecha programada para tu viaje.');
poolDateInput?.focus();
return;
}
if (!pTime) {
alert('🕒 Por favor indica la hora para la programación de tu viaje.');
poolTimeInput?.focus();
return;
}
}
// 1. Validar dirección principal de Cali
const caliAddress = caliAddressInput ? caliAddressInput.value.trim() : '';
const isCaliOrigin = originSelect ? originSelect.value === 'CALI' : true;
@ -2124,6 +2280,13 @@ function initPassengerCalculator() {
metodoPago: selectedPaymentMethod,
cuposSolicitados: selectedSeats,
pasajeros: validatedPassengers,
modalidadTiempo: poolTimeMode,
tipoProgramacion: poolTimeMode === 'PROGRAMADO' ? poolScheduleType : undefined,
fechaProgramada: poolTimeMode === 'PROGRAMADO' ? poolDateInput?.value : undefined,
horaProgramada: poolTimeMode === 'PROGRAMADO' ? poolTimeInput?.value : undefined,
horaSugeridaRecogida: (poolTimeMode === 'PROGRAMADO' && poolScheduleType === 'HORA_LLEGADA_DESTINO')
? calculateSuggestedTime(poolTimeInput?.value || '', -3)?.time24
: undefined,
codigoCupon: appliedCoupon?.coupon?.codigo || undefined,
email: currentUser?.email || ''
};
@ -2288,6 +2451,124 @@ function initFreightCalculator() {
const freightCouponFeedback = document.getElementById('freight-coupon-feedback');
const btnBookFreight = document.getElementById('btn-book-freight');
// Modalidad de tiempo para fletes: URGENTE vs PROGRAMADO
let freightTimeMode = 'URGENTE'; // 'URGENTE' | 'PROGRAMADO'
let freightScheduleType = 'HORA_RECOGIDA'; // 'HORA_RECOGIDA' | 'HORA_LLEGADA_DESTINO'
const btnFreightUrgent = document.getElementById('btn-freight-mode-urgent');
const btnFreightScheduled = document.getElementById('btn-freight-mode-scheduled');
const freightScheduledOptions = document.getElementById('freight-scheduled-options');
const freightTimeBadge = document.getElementById('freight-time-badge');
const freightRadioRecogida = document.getElementById('freight-type-recogida');
const freightRadioDestino = document.getElementById('freight-type-destino');
const freightDateInput = document.getElementById('freight-schedule-date');
const freightTimeInput = document.getElementById('freight-schedule-time');
const freightTimeLabel = document.getElementById('freight-label-time');
const freightSmartEta = document.getElementById('freight-smart-eta');
const freightSummaryUrgentRow = document.getElementById('freight-summary-urgent-row');
const freightUrgentCost = document.getElementById('freight-urgent-cost');
const freightSummaryScheduleRow = document.getElementById('freight-summary-schedule-row');
const freightSummaryScheduleText = document.getElementById('freight-summary-schedule-text');
if (freightDateInput) {
const today = new Date().toISOString().split('T')[0];
freightDateInput.min = today;
if (!freightDateInput.value) freightDateInput.value = today;
}
if (freightTimeInput && !freightTimeInput.value) {
const now = new Date(Date.now() + 60 * 60 * 1000);
const hours = String(now.getHours()).padStart(2, '0');
const mins = String(now.getMinutes()).padStart(2, '0');
freightTimeInput.value = `${hours}:${mins}`;
}
function updateFreightScheduleCalculation() {
if (freightTimeMode === 'URGENTE') {
if (freightTimeBadge) {
freightTimeBadge.textContent = '🚨 Envío Urgente Inmediato';
freightTimeBadge.style.color = '#fbbf24';
freightTimeBadge.style.borderColor = 'rgba(251, 191, 36, 0.4)';
freightTimeBadge.style.background = 'rgba(251, 191, 36, 0.15)';
}
if (freightScheduledOptions) freightScheduledOptions.style.display = 'none';
if (freightSummaryUrgentRow) freightSummaryUrgentRow.style.display = 'flex';
if (freightSummaryScheduleRow) freightSummaryScheduleRow.style.display = 'none';
return;
}
if (freightTimeBadge) {
freightTimeBadge.textContent = '📅 Envío Programado';
freightTimeBadge.style.color = '#38bdf8';
freightTimeBadge.style.borderColor = 'rgba(56, 189, 248, 0.4)';
freightTimeBadge.style.background = 'rgba(56, 189, 248, 0.15)';
}
if (freightScheduledOptions) freightScheduledOptions.style.display = 'block';
if (freightSummaryUrgentRow) freightSummaryUrgentRow.style.display = 'none';
if (freightSummaryScheduleRow) freightSummaryScheduleRow.style.display = 'flex';
const chosenDate = freightDateInput ? freightDateInput.value : '';
const chosenTime = freightTimeInput ? freightTimeInput.value : '';
if (!chosenTime) {
if (freightSmartEta) freightSmartEta.style.display = 'none';
if (freightSummaryScheduleText) freightSummaryScheduleText.textContent = chosenDate ? `📅 ${chosenDate}` : '📅 Programado';
return;
}
if (freightScheduleType === 'HORA_LLEGADA_DESTINO') {
if (freightTimeLabel) freightTimeLabel.textContent = 'Hora que requieres entregado en destino';
const suggested = calculateSuggestedTime(chosenTime, -3);
if (freightSmartEta && suggested) {
freightSmartEta.style.display = 'block';
freightSmartEta.innerHTML = `📦💡 <strong>Planificación Encomienda:</strong> Para entregar a las <strong>${chosenTime}</strong>, el paquete se recogerá a las <strong style="color: var(--accent-amber);">${suggested.display}</strong> (~3 hrs Cali-Buenaventura).`;
}
if (freightSummaryScheduleText) {
freightSummaryScheduleText.innerHTML = `📅 ${chosenDate || ''}<br><span style="font-size: 0.8rem; color: #94a3b8;">🏁 Entrega: ${chosenTime} | 🚪 Recogida: ~${suggested ? suggested.display : ''}</span>`;
}
} else {
if (freightTimeLabel) freightTimeLabel.textContent = 'Hora de recogida del paquete';
const estimated = calculateSuggestedTime(chosenTime, +3);
if (freightSmartEta && estimated) {
freightSmartEta.style.display = 'block';
freightSmartEta.innerHTML = `🏁💡 <strong>Estimación de Entrega:</strong> Si se recoge a las <strong>${chosenTime}</strong>, la entrega estimada será a las <strong style="color: var(--accent-amber);">${estimated.display}</strong>.`;
}
if (freightSummaryScheduleText) {
freightSummaryScheduleText.innerHTML = `📅 ${chosenDate || ''}<br><span style="font-size: 0.8rem; color: #94a3b8;">🚪 Recogida: ${chosenTime} | 🏁 Entrega aprox: ~${estimated ? estimated.display : ''}</span>`;
}
}
}
if (btnFreightUrgent && btnFreightScheduled) {
btnFreightUrgent.addEventListener('click', () => {
freightTimeMode = 'URGENTE';
btnFreightUrgent.classList.add('active');
btnFreightScheduled.classList.remove('active');
updateFreightScheduleCalculation();
window.recalculateFreight();
});
btnFreightScheduled.addEventListener('click', () => {
freightTimeMode = 'PROGRAMADO';
btnFreightScheduled.classList.add('active');
btnFreightUrgent.classList.remove('active');
updateFreightScheduleCalculation();
window.recalculateFreight();
});
}
if (freightRadioRecogida && freightRadioDestino) {
freightRadioRecogida.addEventListener('change', () => {
if (freightRadioRecogida.checked) freightScheduleType = 'HORA_RECOGIDA';
updateFreightScheduleCalculation();
});
freightRadioDestino.addEventListener('change', () => {
if (freightRadioDestino.checked) freightScheduleType = 'HORA_LLEGADA_DESTINO';
updateFreightScheduleCalculation();
});
}
if (freightDateInput) freightDateInput.addEventListener('change', updateFreightScheduleCalculation);
if (freightTimeInput) freightTimeInput.addEventListener('input', updateFreightScheduleCalculation);
const KM_DIST = 115; // Corredor Cali - Buenaventura
let appliedFreightCoupon = null;
@ -2306,10 +2587,13 @@ function initFreightCalculator() {
const basePrice = currentTariffs.precioBasePaquete || 25000;
const costPerKm = currentTariffs.precioKmPaquete || 1200;
const urgentSurcharge = freightTimeMode === 'URGENTE' ? (currentTariffs.recargoEnvioUrgente ?? 15000) : 0;
if (freightUrgentCost) freightUrgentCost.textContent = `+$${urgentSurcharge.toLocaleString('es-CO')} COP`;
const distancePart = KM_DIST * costPerKm;
const subtotal = (basePrice + distancePart) * factor;
const seguro = Math.round(declaredValue * 0.01) + (currentTariffs.seguroBasePaquete || 0);
const subtotalBruto = Math.round(subtotal + seguro);
const subtotalBruto = Math.round(subtotal + seguro + urgentSurcharge);
let discountAmount = 0;
if (appliedFreightCoupon && appliedFreightCoupon.valid) {
@ -2435,6 +2719,21 @@ function initFreightCalculator() {
return;
}
if (freightTimeMode === 'PROGRAMADO') {
const fDate = freightDateInput ? freightDateInput.value : '';
const fTime = freightTimeInput ? freightTimeInput.value : '';
if (!fDate) {
alert('📅 Por favor selecciona la fecha programada para el envío de tu paquete.');
freightDateInput?.focus();
return;
}
if (!fTime) {
alert('🕒 Por favor indica la hora para la programación del envío.');
freightTimeInput?.focus();
return;
}
}
btnBookFreight.disabled = true;
const originalText = btnBookFreight.innerHTML;
btnBookFreight.innerHTML = '⏳ Generando Enlace de Pago Seguro...';
@ -2450,6 +2749,14 @@ function initFreightCalculator() {
telefonoRemitente: senderPhone,
telefonoDestinatario: receiverPhone,
descripcionContenido: packageDesc,
esEnvioUrgente: freightTimeMode === 'URGENTE',
modalidadTiempo: freightTimeMode === 'URGENTE' ? 'INMEDIATO' : 'PROGRAMADO',
tipoProgramacion: freightTimeMode === 'PROGRAMADO' ? freightScheduleType : undefined,
fechaProgramada: freightTimeMode === 'PROGRAMADO' ? freightDateInput?.value : undefined,
horaProgramada: freightTimeMode === 'PROGRAMADO' ? freightTimeInput?.value : undefined,
horaSugeridaRecogida: (freightTimeMode === 'PROGRAMADO' && freightScheduleType === 'HORA_LLEGADA_DESTINO')
? calculateSuggestedTime(freightTimeInput?.value || '', -3)?.time24
: undefined,
codigoCupon: appliedFreightCoupon?.coupon?.codigo || undefined
};

View File

@ -1,15 +1,14 @@
import paramiko
ssh = paramiko.SSHClient()
ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
ssh.connect('161.97.93.156', username='carlos', password='Innova1978+*?')
client = paramiko.SSHClient()
client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
client.connect('161.97.93.156', username='carlos', password='Innova1978+*?', timeout=10)
cmd = "docker inspect api-i5mzip2g7cmyvc56n9ewahbb --format '{{range $k, $v := .NetworkSettings.Networks}}{{$k}} {{end}}'"
stdin, stdout, stderr = ssh.exec_command(cmd)
print("Networks of api:", stdout.read().decode())
stdin, stdout, stderr = client.exec_command("docker ps -a --format '{{.Names}}'")
names = stdout.read().decode('utf-8').splitlines()
print(f"Total containers: {len(names)}")
for n in names:
if any(k in n.lower() for k in ['cali', 'mongo', 'trans', 'db', 'coolify']):
print(" ", n)
cmd_coolify = "docker inspect coolify-proxy --format '{{range $k, $v := .NetworkSettings.Networks}}{{$k}} {{end}}'"
stdin, stdout, stderr = ssh.exec_command(cmd_coolify)
print("Networks of coolify-proxy:", stdout.read().decode())
ssh.close()
client.close()

View File

@ -0,0 +1,25 @@
import paramiko
import sys
import io
if sys.platform == 'win32':
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8', errors='replace')
sys.stderr = io.TextIOWrapper(sys.stderr.buffer, encoding='utf-8', errors='replace')
client = paramiko.SSHClient()
client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
client.connect('161.97.93.156', username='carlos', password='Innova1978+*?', timeout=10)
stdin, stdout, stderr = client.exec_command("docker ps --filter 'name=tayk' --format '{{.Names}}'")
container_name = stdout.read().decode('utf-8').strip()
print(f"Container: {container_name}")
stdin, stdout, stderr = client.exec_command(f"docker logs {container_name} --tail 60")
out_text = stdout.read().decode('utf-8', errors='replace')
err_text = stderr.read().decode('utf-8', errors='replace')
print("=== STDOUT ===")
print(out_text)
print("=== STDERR ===")
print(err_text)
client.close()

View File

@ -0,0 +1,25 @@
import paramiko
import sys
import io
if sys.platform == 'win32':
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8', errors='replace')
sys.stderr = io.TextIOWrapper(sys.stderr.buffer, encoding='utf-8', errors='replace')
ssh = paramiko.SSHClient()
ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
ssh.connect("161.97.93.156", username="carlos", password="Innova1978+*?", timeout=15)
stdin, stdout, stderr = ssh.exec_command("docker ps -a --format '{{.Names}} - {{.Status}} - {{.Networks}}'")
lines = stdout.read().decode('utf-8').splitlines()
print("All matching containers:")
for l in lines:
if any(k in l.lower() for k in ['mongo', 'cali', 'pacific']):
print(" ", l)
stdin, stdout, stderr = ssh.exec_command("docker network inspect coolify --format '{{range $k, $v := .Containers}}{{$v.Name}} ({{$v.IPv4Address}})\n{{end}}'")
print("\nContainers in coolify network:")
print(stdout.read().decode('utf-8'))
ssh.close()

View File

@ -0,0 +1,109 @@
import io
import sys
import urllib.request
import json
import ssl
if sys.platform == 'win32':
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8', errors='replace')
sys.stderr = io.TextIOWrapper(sys.stderr.buffer, encoding='utf-8', errors='replace')
ctx = ssl.create_default_context()
ctx.check_hostname = False
ctx.verify_mode = ssl.CERT_NONE
API_URL = "https://pacifictrans.almeira.com.co"
print("1. Consultando tarifas activas...")
req = urllib.request.Request(f"{API_URL}/api/tariffs")
with urllib.request.urlopen(req, context=ctx, timeout=10) as res:
data = json.loads(res.read().decode("utf-8"))
config = data.get("config", {})
print(f" Tarifa Pasajero: ${config.get('tarifaPasajeroFija'):,} COP")
print(f" Precio Base Paquete: ${config.get('precioBasePaquete'):,} COP")
print(f" Recargo Envio Urgente: ${config.get('recargoEnvioUrgente', 0):,} COP")
assert "recargoEnvioUrgente" in config, "recargoEnvioUrgente debe estar en la configuracion de tarifas"
print("\n2. Simulando despacho de paquete urgente...")
payload_urgent = {
"remitenteNombre": "Carlos Almeira",
"remitenteTelefono": "3167455877",
"destinatarioNombre": "Destinatario Test",
"destinatarioTelefono": "3101234567",
"tipoBulto": "SOBRE",
"descripcion": "Documentos urgentes de prueba",
"direccionRecogida": "Calle 5 #66-20, Cali",
"coordenadasRecogida": [-76.5320, 3.4215],
"ciudadOrigen": "CALI",
"direccionEntrega": "Terminal Buenaventura",
"coordenadasEntrega": [-77.0250, 3.8820],
"ciudadDestino": "BUENAVENTURA",
"esEnvioUrgente": True,
"modalidadTiempo": "INMEDIATO"
}
req_flete = urllib.request.Request(
f"{API_URL}/api/trips/package",
data=json.dumps(payload_urgent).encode("utf-8"),
headers={"Content-Type": "application/json"}
)
with urllib.request.urlopen(req_flete, context=ctx, timeout=10) as res:
cotizacion = json.loads(res.read().decode("utf-8"))
print(f" Resultado paquete: success={cotizacion.get('success')}")
trip = cotizacion.get('trip', {})
print(f" Total flete urgente: ${trip.get('precio', 0):,} COP")
print(f" Modalidad tiempo: {trip.get('modalidadTiempo')}")
print(f" Es urgente: {trip.get('esEnvioUrgente')}")
print(f" Recargo urgencia aplicado: ${trip.get('recargoUrgencia', 0):,} COP")
print("\n3. Simulando reserva de pasajero programada (Llegada en destino)...")
payload_pool = {
"sentido": "CALI_A_BUENAVENTURA",
"direccionTexto": "Calle 5 #66-20, Cali",
"coordenadas": [-76.5320, 3.4215],
"puntoFijoBuenaventura": {
"nombre": "Terminal de Transportes de Buenaventura",
"coordenadas": [-77.0250, 3.8820]
},
"cantidadPasajeros": 1,
"pasajeros": [
{
"nombre": "Carlos Almeira",
"telefono": "3167455877",
"direccionRecogida": "Calle 5 #66-20, Cali"
}
],
"metodoPago": "EFECTIVO",
"modalidadTiempo": "PROGRAMADO",
"tipoProgramacion": "HORA_LLEGADA_DESTINO",
"fechaProgramada": "2026-10-25",
"horaProgramada": "14:00",
"horaSugeridaRecogida": "11:00"
}
req_pool = urllib.request.Request(
f"{API_URL}/api/trips/pool",
data=json.dumps(payload_pool).encode("utf-8"),
headers={"Content-Type": "application/json"}
)
with urllib.request.urlopen(req_pool, context=ctx, timeout=10) as res:
pool_res = json.loads(res.read().decode("utf-8"))
print(f" Resultado pooling: success={pool_res.get('success')}")
trip_p = pool_res.get('trip', {})
print(f" Modalidad viaje: {trip_p.get('modalidadTiempo')}")
print(f" Tipo programacion: {trip_p.get('tipoProgramacion')}")
print(f" Fecha programada: {trip_p.get('fechaProgramada')}")
print(f" Hora en destino: {trip_p.get('horaProgramada')}")
print(f" Hora sugerida recogida: {trip_p.get('horaSugeridaRecogida')}")
print("\n4. Verificando que index.html contenga los selectores de modalidad de tiempo...")
req_html = urllib.request.Request(f"{API_URL}/")
with urllib.request.urlopen(req_html, context=ctx, timeout=10) as res:
html = res.read().decode("utf-8")
assert "time-modality-container" in html, "Falta time-modality-container en HTML"
assert "smart-eta-box" in html, "Falta smart-eta-box en HTML"
assert "cfg-recargo-urgente" in html, "Falta cfg-recargo-urgente en HTML"
print(" [OK] Elementos visuales y selectores de tiempo presentes en el frontend.")
print("\n[OK] Verificacion completa de modalidades de viaje y fletes superada con 100% de exito.")

View File

@ -2765,6 +2765,191 @@ button:active, .service-pill-btn:active, .btn-qty-seat:active, .chip-payment:act
to { opacity: 1; transform: translateY(0); }
}
/* ==========================================================================
MODALIDADES DE TIEMPO: INMEDIATO VS PROGRAMADO & RECARGO ENCOMIENDAS URGENTES
========================================================================== */
.time-modality-container {
background: rgba(15, 23, 42, 0.65);
border: 1px solid var(--border-glass);
border-radius: var(--radius-md);
padding: 1.15rem;
margin-top: 1.25rem;
transition: all 0.25s ease;
}
.time-modality-container:focus-within {
border-color: rgba(6, 182, 212, 0.4);
}
.badge-time-selected {
font-size: 0.75rem;
font-weight: 700;
padding: 3px 10px;
border-radius: 9999px;
background: rgba(6, 182, 212, 0.15);
color: var(--accent-cyan);
border: 1px solid rgba(6, 182, 212, 0.4);
letter-spacing: 0.02em;
transition: all 0.2s ease;
}
.time-mode-selector {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 0.75rem;
margin-top: 0.5rem;
}
@media (max-width: 640px) {
.time-mode-selector {
grid-template-columns: 1fr;
}
}
.btn-time-mode {
background: rgba(15, 23, 42, 0.8);
border: 1px solid var(--border-glass);
border-radius: var(--radius-md);
padding: 0.9rem;
display: flex;
align-items: center;
gap: 0.75rem;
cursor: pointer;
transition: all 0.2s cubic-bezier(0.16, 1, 0.3, 1);
text-align: left;
color: var(--text-main);
outline: none;
}
.btn-time-mode:hover {
background: rgba(30, 41, 59, 0.85);
border-color: rgba(6, 182, 212, 0.35);
transform: translateY(-2px);
}
.btn-time-mode.active {
background: linear-gradient(135deg, rgba(6, 182, 212, 0.2), rgba(16, 185, 129, 0.12));
border-color: var(--accent-cyan);
box-shadow: 0 0 16px rgba(6, 182, 212, 0.25);
transform: translateY(-2px);
}
.btn-time-mode.active.urgent-active {
background: linear-gradient(135deg, rgba(239, 68, 68, 0.25), rgba(245, 158, 11, 0.15));
border-color: #f87171;
box-shadow: 0 0 16px rgba(239, 68, 68, 0.3);
}
.time-mode-icon {
font-size: 1.6rem;
flex-shrink: 0;
}
.time-mode-text {
display: flex;
flex-direction: column;
}
.time-mode-text strong {
font-size: 0.95rem;
color: #fff;
line-height: 1.2;
}
.time-mode-text span {
font-size: 0.73rem;
color: var(--text-muted);
margin-top: 2px;
}
.btn-time-mode.active .time-mode-text strong {
color: #38bdf8;
}
.btn-time-mode.active.urgent-active .time-mode-text strong {
color: #fca5a5;
}
/* Card desplegable de opciones programadas */
.scheduled-options-card {
background: rgba(2, 6, 23, 0.65);
border: 1px dashed rgba(6, 182, 212, 0.35);
border-radius: var(--radius-md);
padding: 1.15rem;
margin-top: 1rem;
animation: fadeIn 0.25s ease-out;
}
.schedule-type-toggle {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.radio-schedule-type {
display: flex;
align-items: center;
gap: 0.6rem;
background: rgba(15, 23, 42, 0.7);
padding: 0.6rem 0.85rem;
border-radius: var(--radius-sm);
border: 1px solid rgba(255, 255, 255, 0.05);
cursor: pointer;
font-size: 0.85rem;
color: #cbd5e1;
transition: all 0.2s ease;
}
.radio-schedule-type:hover {
background: rgba(30, 41, 59, 0.8);
border-color: rgba(6, 182, 212, 0.3);
}
.radio-schedule-type.active {
background: rgba(6, 182, 212, 0.12);
border-color: var(--accent-cyan);
color: #fff;
font-weight: 600;
}
.radio-schedule-type input[type="radio"] {
accent-color: var(--accent-cyan);
cursor: pointer;
}
/* Caja de estimación inteligente de viaje */
.smart-eta-box {
background: rgba(15, 23, 42, 0.85);
border: 1px solid rgba(16, 185, 129, 0.35);
border-radius: var(--radius-sm);
padding: 0.75rem 0.95rem;
font-size: 0.82rem;
color: #a7f3d0;
display: flex;
align-items: center;
gap: 0.5rem;
animation: fadeIn 0.2s ease;
}
.smart-eta-icon {
font-size: 1.15rem;
flex-shrink: 0;
}
.summary-urgent-badge {
display: inline-flex;
align-items: center;
gap: 0.35rem;
background: rgba(239, 68, 68, 0.2);
color: #f87171;
border: 1px solid rgba(239, 68, 68, 0.4);
padding: 0.2rem 0.6rem;
border-radius: 9999px;
font-size: 0.74rem;
font-weight: 700;
}

View File

@ -242,6 +242,70 @@
</select>
</div>
<!-- MODALIDAD DE TIEMPO DEL VIAJE: INMEDIATO VS PROGRAMADO -->
<div class="time-modality-container" id="pool-time-modality-container">
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 0.5rem;">
<label class="form-label" style="margin-bottom: 0; display: flex; align-items: center; gap: 0.4rem;">
<span>🕒</span> Horario del Servicio
</label>
<span id="pool-time-badge" class="badge-time-selected">⚡ Salida Inmediata</span>
</div>
<div class="time-mode-selector">
<button type="button" class="btn-time-mode active" id="btn-pool-mode-immediate" data-mode="INMEDIATO">
<span style="font-size: 1.35rem;">⚡</span>
<div>
<div style="font-weight: 700; font-size: 0.92rem;">Salida Inmediata</div>
<div style="font-size: 0.75rem; color: #94a3b8;">Asignación prioritaria ya</div>
</div>
</button>
<button type="button" class="btn-time-mode" id="btn-pool-mode-scheduled" data-mode="PROGRAMADO">
<span style="font-size: 1.35rem;">📅</span>
<div>
<div style="font-weight: 700; font-size: 0.92rem;">Viaje Programado</div>
<div style="font-size: 0.75rem; color: #94a3b8;">Planifica fecha y hora</div>
</div>
</button>
</div>
<!-- OPCIONES DE PROGRAMACIÓN (Ocultas por defecto si es Inmediato) -->
<div class="scheduled-options-card" id="pool-scheduled-options" style="display: none; margin-top: 1rem;">
<div style="font-size: 0.85rem; font-weight: 600; color: #38bdf8; margin-bottom: 0.75rem;">
📅 Especifica tu tiempo de viaje:
</div>
<!-- Radio: Hora de Recogida vs Hora Requerida en Destino -->
<div style="display: flex; flex-direction: column; gap: 0.6rem; margin-bottom: 1rem;">
<label class="radio-schedule-type" style="display: flex; align-items: center; gap: 0.5rem; cursor: pointer;">
<input type="radio" name="pool-schedule-type" id="pool-type-recogida" value="HORA_RECOGIDA" checked>
<span style="font-size: 0.88rem; color: #fff;">🚪 <strong>Hora deseada de recogida</strong> (a la puerta de tu domicilio)</span>
</label>
<label class="radio-schedule-type" style="display: flex; align-items: center; gap: 0.5rem; cursor: pointer;">
<input type="radio" name="pool-schedule-type" id="pool-type-destino" value="HORA_LLEGADA_DESTINO">
<span style="font-size: 0.88rem; color: #fff;">🏁 <strong>Hora que requiero estar en el destino</strong> (Pacific Trans calcula la hora de recogida)</span>
</label>
</div>
<div class="grid-2col" style="gap: 0.75rem; margin-bottom: 0.5rem;">
<div class="form-group" style="margin-bottom: 0;">
<label class="form-label" for="pool-schedule-date" style="font-size: 0.82rem;">Fecha del Servicio</label>
<input type="date" class="form-input" id="pool-schedule-date" style="padding: 0.65rem 0.85rem; font-size: 0.9rem;">
</div>
<div class="form-group" style="margin-bottom: 0;">
<label class="form-label" for="pool-schedule-time" id="pool-label-time" style="font-size: 0.82rem;">Hora de Recogida</label>
<input type="time" class="form-input" id="pool-schedule-time" style="padding: 0.65rem 0.85rem; font-size: 0.9rem;">
</div>
</div>
<!-- Caja inteligente de cálculo de ETA / Hora sugerida -->
<div class="smart-eta-box" id="pool-smart-eta" style="margin-top: 0.85rem; font-size: 0.82rem; padding: 0.65rem 0.85rem; background: rgba(6, 182, 212, 0.08); border-left: 3px solid var(--accent-cyan); border-radius: 4px; display: none;">
<!-- Se llena con JS -->
</div>
</div>
</div>
<!-- SELECTOR DE CANTIDAD DE CUPOS (1 a 4 Pasajeros) -->
<div class="form-group" style="margin-top: 1.25rem;">
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 0.5rem;">
@ -350,6 +414,10 @@
<span style="color: var(--text-muted);">Seguro Contractual:</span>
<span style="color: var(--accent-emerald); font-weight: 600;">Póliza y Planilla SOAT</span>
</div>
<div class="summary-item" id="pool-summary-schedule-row">
<span style="color: var(--text-muted);">Modalidad y Horario:</span>
<span style="color: #fff; font-weight: 600;" id="pool-summary-schedule-text">⚡ Inmediato (Próxima Salida)</span>
</div>
<div class="summary-item" id="summary-subtotal-row" style="display: none; border-top: 1px dashed var(--border-glass); padding-top: 0.5rem; margin-top: 0.5rem;">
<span style="color: var(--text-muted);">Subtotal:</span>
@ -453,6 +521,68 @@
<input type="text" class="form-input" id="package-desc" placeholder="Repuestos automotrices / Ropa / Medicamentos">
</div>
<!-- MODALIDAD DE DESPACHO DE ENCOMIENDA: URGENTE INMEDIATO VS PROGRAMADO -->
<div class="time-modality-container" id="freight-time-modality-container">
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 0.5rem;">
<label class="form-label" style="margin-bottom: 0; display: flex; align-items: center; gap: 0.4rem;">
<span>⚡</span> Modalidad de Entrega del Envío
</label>
<span id="freight-time-badge" class="badge-time-selected" style="color: #fbbf24; border-color: rgba(251, 191, 36, 0.4); background: rgba(251, 191, 36, 0.15);">🚨 Envío Urgente Inmediato</span>
</div>
<div class="time-mode-selector">
<button type="button" class="btn-time-mode active" id="btn-freight-mode-urgent" data-mode="URGENTE" style="border-color: rgba(251, 191, 36, 0.4);">
<span style="font-size: 1.35rem;">🚨</span>
<div>
<div style="font-weight: 700; font-size: 0.92rem; color: #fbbf24;">Envío Urgente Inmediato</div>
<div style="font-size: 0.75rem; color: #94a3b8;" id="freight-urgent-surcharge-hint">Despacho en el primer carro (+<span id="freight-urgent-amount-label">$15.000</span>)</div>
</div>
</button>
<button type="button" class="btn-time-mode" id="btn-freight-mode-scheduled" data-mode="PROGRAMADO">
<span style="font-size: 1.35rem;">📅</span>
<div>
<div style="font-weight: 700; font-size: 0.92rem;">Envío Programado</div>
<div style="font-size: 0.75rem; color: #94a3b8;">Sin recargo de urgencia</div>
</div>
</button>
</div>
<!-- OPCIONES DE PROGRAMACIÓN FLETES (Ocultas si es Urgente) -->
<div class="scheduled-options-card" id="freight-scheduled-options" style="display: none; margin-top: 1rem;">
<div style="font-size: 0.85rem; font-weight: 600; color: #fbbf24; margin-bottom: 0.75rem;">
📅 Especifica fecha y hora de tu encomienda:
</div>
<div style="display: flex; flex-direction: column; gap: 0.6rem; margin-bottom: 1rem;">
<label class="radio-schedule-type" style="display: flex; align-items: center; gap: 0.5rem; cursor: pointer;">
<input type="radio" name="freight-schedule-type" id="freight-type-recogida" value="HORA_RECOGIDA" checked>
<span style="font-size: 0.88rem; color: #fff;">📦 <strong>Hora deseada de recogida del paquete</strong></span>
</label>
<label class="radio-schedule-type" style="display: flex; align-items: center; gap: 0.5rem; cursor: pointer;">
<input type="radio" name="freight-schedule-type" id="freight-type-destino" value="HORA_LLEGADA_DESTINO">
<span style="font-size: 0.88rem; color: #fff;">🏁 <strong>Hora que se requiere entregado en destino</strong></span>
</label>
</div>
<div class="grid-2col" style="gap: 0.75rem; margin-bottom: 0.5rem;">
<div class="form-group" style="margin-bottom: 0;">
<label class="form-label" for="freight-schedule-date" style="font-size: 0.82rem;">Fecha de Despacho/Entrega</label>
<input type="date" class="form-input" id="freight-schedule-date" style="padding: 0.65rem 0.85rem; font-size: 0.9rem;">
</div>
<div class="form-group" style="margin-bottom: 0;">
<label class="form-label" for="freight-schedule-time" id="freight-label-time" style="font-size: 0.82rem;">Hora de Recogida</label>
<input type="time" class="form-input" id="freight-schedule-time" style="padding: 0.65rem 0.85rem; font-size: 0.9rem;">
</div>
</div>
<div class="smart-eta-box" id="freight-smart-eta" style="margin-top: 0.85rem; font-size: 0.82rem; padding: 0.65rem 0.85rem; background: rgba(251, 191, 36, 0.08); border-left: 3px solid var(--accent-amber); border-radius: 4px; display: none;">
<!-- Se llena con JS -->
</div>
</div>
</div>
<!-- BONO O CUPÓN DE DESCUENTO (ENCOMIENDAS) -->
<div class="coupon-input-group" style="margin-top: 1.25rem;">
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 0.4rem;">
@ -475,7 +605,7 @@
<div>
<h3 style="font-size: 1.25rem; color: #fff; margin-bottom: 1rem;">Fórmula Oficial de Liquidación</h3>
<div style="background: rgba(0,0,0,0.4); border-radius: 8px; padding: 0.75rem; font-family: monospace; font-size: 0.8rem; color: #38bdf8; margin-bottom: 1rem;">
Tarifa = (Base + [115 Km × $1.200]) × Factor + Seguro (1%)
Tarifa = (Base + [115 Km × $1.200]) × Factor + Seguro + Recargo Urgencia
</div>
<div class="summary-item">
@ -490,6 +620,14 @@
<span style="color: var(--text-muted);">Seguro de Mercancía:</span>
<span style="color: var(--accent-emerald); font-weight: 600;" id="freight-insurance-cost">$1.500 COP</span>
</div>
<div class="summary-item" id="freight-summary-urgent-row" style="display: flex;">
<span style="color: var(--text-muted);">Recargo Envío Urgente:</span>
<span style="color: #fbbf24; font-weight: 700;" id="freight-urgent-cost">+$15.000 COP</span>
</div>
<div class="summary-item" id="freight-summary-schedule-row" style="display: none;">
<span style="color: var(--text-muted);">Modalidad y Horario:</span>
<span style="color: #38bdf8; font-weight: 600;" id="freight-summary-schedule-text">📅 Programado</span>
</div>
<div class="summary-item" id="freight-subtotal-row" style="display: none; border-top: 1px dashed var(--border-glass); padding-top: 0.5rem; margin-top: 0.5rem;">
<span style="color: var(--text-muted);">Subtotal Flete:</span>
@ -894,6 +1032,12 @@
<input type="number" class="form-input" id="cfg-seguro-base" value="5000" min="0" step="500" required>
</div>
</div>
<div class="form-group" style="margin-top: 1rem;">
<label class="form-label" for="cfg-recargo-urgente">🚨 Recargo por Envío Urgente Inmediato ($ COP)</label>
<input type="number" class="form-input" id="cfg-recargo-urgente" value="15000" min="0" step="1000" required>
<span style="font-size: 0.75rem; color: var(--text-dim); margin-top: 4px; display: block;">Valor adicional cobrado para fletes y encomiendas que solicitan despacho inmediato prioritario.</span>
</div>
</div>
</div>

View File

@ -1513,6 +1513,14 @@ function syncTariffsToUI() {
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;
const elRecargoUrgente = document.getElementById('cfg-recargo-urgente');
if (elRecargoUrgente) elRecargoUrgente.value = currentTariffs.recargoEnvioUrgente ?? 15000;
const urgentLabel = document.getElementById('freight-urgent-amount-label');
if (urgentLabel) urgentLabel.textContent = '$' + (currentTariffs.recargoEnvioUrgente ?? 15000).toLocaleString('es-CO');
const urgentCost = document.getElementById('freight-urgent-cost');
if (urgentCost) urgentCost.textContent = '+$' + (currentTariffs.recargoEnvioUrgente ?? 15000).toLocaleString('es-CO') + ' COP';
if (elComision) elComision.value = currentTariffs.porcentajeComisionAdmin;
if (elPenalidad) elPenalidad.value = currentTariffs.costoPenalidadCancelacion;
if (elRadio) elRadio.value = currentTariffs.radioMaxPoolingMetros;
@ -1622,6 +1630,7 @@ function initTariffsManager() {
grande: parseFloat(document.getElementById('cfg-factor-grande')?.value || '2.2')
},
seguroBasePaquete: parseFloat(document.getElementById('cfg-seguro-base')?.value || '5000'),
recargoEnvioUrgente: parseFloat(document.getElementById('cfg-recargo-urgente')?.value || '15000'),
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'),
@ -1671,6 +1680,29 @@ function initTariffsManager() {
/* ==========================================================================
7. RESERVA DE PASAJEROS & POOLING
========================================================================== */
function calculateSuggestedTime(timeStr, hoursDiff) {
if (!timeStr) return null;
const parts = timeStr.split(':');
if (parts.length < 2) return null;
const h = parseInt(parts[0], 10);
const m = parseInt(parts[1], 10);
if (isNaN(h) || isNaN(m)) return null;
let totalMins = h * 60 + m + Math.round(hoursDiff * 60);
while (totalMins < 0) totalMins += 24 * 60;
totalMins = totalMins % (24 * 60);
const newH = Math.floor(totalMins / 60);
const newM = totalMins % 60;
const period = newH >= 12 ? 'PM' : 'AM';
const displayH = newH % 12 === 0 ? 12 : newH % 12;
const displayM = String(newM).padStart(2, '0');
return {
time24: `${String(newH).padStart(2, '0')}:${String(newM).padStart(2, '0')}`,
display: `${displayH}:${displayM} ${period}`
};
}
function initPassengerCalculator() {
const originSelect = document.getElementById('pool-origin');
const addressGroup = document.getElementById('cali-address-group');
@ -1684,6 +1716,21 @@ function initPassengerCalculator() {
const seatButtons = document.querySelectorAll('.btn-qty-seat');
const paymentCards = document.querySelectorAll('.payment-method-card');
// Modalidad de Tiempo: Inmediato vs Programado
let poolTimeMode = 'INMEDIATO'; // 'INMEDIATO' | 'PROGRAMADO'
let poolScheduleType = 'HORA_RECOGIDA'; // 'HORA_RECOGIDA' | 'HORA_LLEGADA_DESTINO'
const btnPoolImmediate = document.getElementById('btn-pool-mode-immediate');
const btnPoolScheduled = document.getElementById('btn-pool-mode-scheduled');
const poolScheduledOptions = document.getElementById('pool-scheduled-options');
const poolTimeBadge = document.getElementById('pool-time-badge');
const poolRadioRecogida = document.getElementById('pool-type-recogida');
const poolRadioDestino = document.getElementById('pool-type-destino');
const poolDateInput = document.getElementById('pool-schedule-date');
const poolTimeInput = document.getElementById('pool-schedule-time');
const poolTimeLabel = document.getElementById('pool-label-time');
const poolSmartEta = document.getElementById('pool-smart-eta');
const poolSummaryScheduleText = document.getElementById('pool-summary-schedule-text');
let selectedSeats = 1;
let selectedPaymentMethod = 'BRE_B';
let currentSeatsFilled = 3;
@ -1986,8 +2033,101 @@ function initPassengerCalculator() {
// Actualizar previsualización en dots del vehículo
updateSeatVisuals(selectedSeats);
updatePoolScheduleCalculation();
}
function updatePoolScheduleCalculation() {
if (poolTimeMode === 'INMEDIATO') {
if (poolTimeBadge) {
poolTimeBadge.textContent = '⚡ Salida Inmediata';
poolTimeBadge.className = 'badge-time-selected';
}
if (poolScheduledOptions) poolScheduledOptions.style.display = 'none';
if (poolSummaryScheduleText) poolSummaryScheduleText.textContent = '⚡ Inmediato (Próxima Salida)';
return;
}
if (poolTimeBadge) {
poolTimeBadge.textContent = '📅 Viaje Programado';
poolTimeBadge.className = 'badge-time-selected';
}
if (poolScheduledOptions) poolScheduledOptions.style.display = 'block';
const chosenDate = poolDateInput ? poolDateInput.value : '';
const chosenTime = poolTimeInput ? poolTimeInput.value : '';
if (!chosenTime) {
if (poolSmartEta) poolSmartEta.style.display = 'none';
if (poolSummaryScheduleText) poolSummaryScheduleText.textContent = chosenDate ? `📅 Programado para el ${chosenDate}` : '📅 Programado';
return;
}
if (poolScheduleType === 'HORA_LLEGADA_DESTINO') {
if (poolTimeLabel) poolTimeLabel.textContent = 'Hora que requieres estar en destino';
const suggested = calculateSuggestedTime(chosenTime, -3); // Cali - Buenaventura ~3 horas
if (poolSmartEta && suggested) {
poolSmartEta.style.display = 'block';
poolSmartEta.innerHTML = `🚗💡 <strong>Planificación Inteligente:</strong> Para llegar a tu destino a las <strong>${chosenTime}</strong>, tu vehículo te recogerá aproximadamente a las <strong style="color: var(--accent-cyan);">${suggested.display}</strong> (trayecto ~3 hrs en el corredor).`;
}
if (poolSummaryScheduleText) {
poolSummaryScheduleText.innerHTML = `📅 Programado ${chosenDate || ''}<br><span style="font-size: 0.8rem; color: #94a3b8;">🏁 Destino: ${chosenTime} | 🚪 Recogida: ~${suggested ? suggested.display : ''}</span>`;
}
} else {
if (poolTimeLabel) poolTimeLabel.textContent = 'Hora deseada de recogida';
const estimated = calculateSuggestedTime(chosenTime, +3);
if (poolSmartEta && estimated) {
poolSmartEta.style.display = 'block';
poolSmartEta.innerHTML = `🏁💡 <strong>Estimación de Ruta:</strong> Si te recogemos a las <strong>${chosenTime}</strong>, tu llegada estimada a destino será alrededor de las <strong style="color: var(--accent-cyan);">${estimated.display}</strong> (~3 hrs en carretera).`;
}
if (poolSummaryScheduleText) {
poolSummaryScheduleText.innerHTML = `📅 Programado ${chosenDate || ''}<br><span style="font-size: 0.8rem; color: #94a3b8;">🚪 Recogida: ${chosenTime} | 🏁 Llegada aprox: ~${estimated ? estimated.display : ''}</span>`;
}
}
}
// Inicializar fecha mínima en hoy y hora por defecto
if (poolDateInput) {
const today = new Date().toISOString().split('T')[0];
poolDateInput.min = today;
if (!poolDateInput.value) poolDateInput.value = today;
}
if (poolTimeInput && !poolTimeInput.value) {
const now = new Date(Date.now() + 60 * 60 * 1000);
const hours = String(now.getHours()).padStart(2, '0');
const mins = String(now.getMinutes()).padStart(2, '0');
poolTimeInput.value = `${hours}:${mins}`;
}
if (btnPoolImmediate && btnPoolScheduled) {
btnPoolImmediate.addEventListener('click', () => {
poolTimeMode = 'INMEDIATO';
btnPoolImmediate.classList.add('active');
btnPoolScheduled.classList.remove('active');
updatePoolScheduleCalculation();
});
btnPoolScheduled.addEventListener('click', () => {
poolTimeMode = 'PROGRAMADO';
btnPoolScheduled.classList.add('active');
btnPoolImmediate.classList.remove('active');
updatePoolScheduleCalculation();
});
}
if (poolRadioRecogida && poolRadioDestino) {
poolRadioRecogida.addEventListener('change', () => {
if (poolRadioRecogida.checked) poolScheduleType = 'HORA_RECOGIDA';
updatePoolScheduleCalculation();
});
poolRadioDestino.addEventListener('change', () => {
if (poolRadioDestino.checked) poolScheduleType = 'HORA_LLEGADA_DESTINO';
updatePoolScheduleCalculation();
});
}
if (poolDateInput) poolDateInput.addEventListener('change', updatePoolScheduleCalculation);
if (poolTimeInput) poolTimeInput.addEventListener('input', updatePoolScheduleCalculation);
window.updatePassengerBookingSummary = updateSummary;
// Escuchadores de botones de selección de cupos
@ -2045,6 +2185,22 @@ function initPassengerCalculator() {
saveCurrentFormData();
// Validar programación si aplica
if (poolTimeMode === 'PROGRAMADO') {
const pDate = poolDateInput ? poolDateInput.value : '';
const pTime = poolTimeInput ? poolTimeInput.value : '';
if (!pDate) {
alert('📅 Por favor selecciona la fecha programada para tu viaje.');
poolDateInput?.focus();
return;
}
if (!pTime) {
alert('🕒 Por favor indica la hora para la programación de tu viaje.');
poolTimeInput?.focus();
return;
}
}
// 1. Validar dirección principal de Cali
const caliAddress = caliAddressInput ? caliAddressInput.value.trim() : '';
const isCaliOrigin = originSelect ? originSelect.value === 'CALI' : true;
@ -2124,6 +2280,13 @@ function initPassengerCalculator() {
metodoPago: selectedPaymentMethod,
cuposSolicitados: selectedSeats,
pasajeros: validatedPassengers,
modalidadTiempo: poolTimeMode,
tipoProgramacion: poolTimeMode === 'PROGRAMADO' ? poolScheduleType : undefined,
fechaProgramada: poolTimeMode === 'PROGRAMADO' ? poolDateInput?.value : undefined,
horaProgramada: poolTimeMode === 'PROGRAMADO' ? poolTimeInput?.value : undefined,
horaSugeridaRecogida: (poolTimeMode === 'PROGRAMADO' && poolScheduleType === 'HORA_LLEGADA_DESTINO')
? calculateSuggestedTime(poolTimeInput?.value || '', -3)?.time24
: undefined,
codigoCupon: appliedCoupon?.coupon?.codigo || undefined,
email: currentUser?.email || ''
};
@ -2288,6 +2451,124 @@ function initFreightCalculator() {
const freightCouponFeedback = document.getElementById('freight-coupon-feedback');
const btnBookFreight = document.getElementById('btn-book-freight');
// Modalidad de tiempo para fletes: URGENTE vs PROGRAMADO
let freightTimeMode = 'URGENTE'; // 'URGENTE' | 'PROGRAMADO'
let freightScheduleType = 'HORA_RECOGIDA'; // 'HORA_RECOGIDA' | 'HORA_LLEGADA_DESTINO'
const btnFreightUrgent = document.getElementById('btn-freight-mode-urgent');
const btnFreightScheduled = document.getElementById('btn-freight-mode-scheduled');
const freightScheduledOptions = document.getElementById('freight-scheduled-options');
const freightTimeBadge = document.getElementById('freight-time-badge');
const freightRadioRecogida = document.getElementById('freight-type-recogida');
const freightRadioDestino = document.getElementById('freight-type-destino');
const freightDateInput = document.getElementById('freight-schedule-date');
const freightTimeInput = document.getElementById('freight-schedule-time');
const freightTimeLabel = document.getElementById('freight-label-time');
const freightSmartEta = document.getElementById('freight-smart-eta');
const freightSummaryUrgentRow = document.getElementById('freight-summary-urgent-row');
const freightUrgentCost = document.getElementById('freight-urgent-cost');
const freightSummaryScheduleRow = document.getElementById('freight-summary-schedule-row');
const freightSummaryScheduleText = document.getElementById('freight-summary-schedule-text');
if (freightDateInput) {
const today = new Date().toISOString().split('T')[0];
freightDateInput.min = today;
if (!freightDateInput.value) freightDateInput.value = today;
}
if (freightTimeInput && !freightTimeInput.value) {
const now = new Date(Date.now() + 60 * 60 * 1000);
const hours = String(now.getHours()).padStart(2, '0');
const mins = String(now.getMinutes()).padStart(2, '0');
freightTimeInput.value = `${hours}:${mins}`;
}
function updateFreightScheduleCalculation() {
if (freightTimeMode === 'URGENTE') {
if (freightTimeBadge) {
freightTimeBadge.textContent = '🚨 Envío Urgente Inmediato';
freightTimeBadge.style.color = '#fbbf24';
freightTimeBadge.style.borderColor = 'rgba(251, 191, 36, 0.4)';
freightTimeBadge.style.background = 'rgba(251, 191, 36, 0.15)';
}
if (freightScheduledOptions) freightScheduledOptions.style.display = 'none';
if (freightSummaryUrgentRow) freightSummaryUrgentRow.style.display = 'flex';
if (freightSummaryScheduleRow) freightSummaryScheduleRow.style.display = 'none';
return;
}
if (freightTimeBadge) {
freightTimeBadge.textContent = '📅 Envío Programado';
freightTimeBadge.style.color = '#38bdf8';
freightTimeBadge.style.borderColor = 'rgba(56, 189, 248, 0.4)';
freightTimeBadge.style.background = 'rgba(56, 189, 248, 0.15)';
}
if (freightScheduledOptions) freightScheduledOptions.style.display = 'block';
if (freightSummaryUrgentRow) freightSummaryUrgentRow.style.display = 'none';
if (freightSummaryScheduleRow) freightSummaryScheduleRow.style.display = 'flex';
const chosenDate = freightDateInput ? freightDateInput.value : '';
const chosenTime = freightTimeInput ? freightTimeInput.value : '';
if (!chosenTime) {
if (freightSmartEta) freightSmartEta.style.display = 'none';
if (freightSummaryScheduleText) freightSummaryScheduleText.textContent = chosenDate ? `📅 ${chosenDate}` : '📅 Programado';
return;
}
if (freightScheduleType === 'HORA_LLEGADA_DESTINO') {
if (freightTimeLabel) freightTimeLabel.textContent = 'Hora que requieres entregado en destino';
const suggested = calculateSuggestedTime(chosenTime, -3);
if (freightSmartEta && suggested) {
freightSmartEta.style.display = 'block';
freightSmartEta.innerHTML = `📦💡 <strong>Planificación Encomienda:</strong> Para entregar a las <strong>${chosenTime}</strong>, el paquete se recogerá a las <strong style="color: var(--accent-amber);">${suggested.display}</strong> (~3 hrs Cali-Buenaventura).`;
}
if (freightSummaryScheduleText) {
freightSummaryScheduleText.innerHTML = `📅 ${chosenDate || ''}<br><span style="font-size: 0.8rem; color: #94a3b8;">🏁 Entrega: ${chosenTime} | 🚪 Recogida: ~${suggested ? suggested.display : ''}</span>`;
}
} else {
if (freightTimeLabel) freightTimeLabel.textContent = 'Hora de recogida del paquete';
const estimated = calculateSuggestedTime(chosenTime, +3);
if (freightSmartEta && estimated) {
freightSmartEta.style.display = 'block';
freightSmartEta.innerHTML = `🏁💡 <strong>Estimación de Entrega:</strong> Si se recoge a las <strong>${chosenTime}</strong>, la entrega estimada será a las <strong style="color: var(--accent-amber);">${estimated.display}</strong>.`;
}
if (freightSummaryScheduleText) {
freightSummaryScheduleText.innerHTML = `📅 ${chosenDate || ''}<br><span style="font-size: 0.8rem; color: #94a3b8;">🚪 Recogida: ${chosenTime} | 🏁 Entrega aprox: ~${estimated ? estimated.display : ''}</span>`;
}
}
}
if (btnFreightUrgent && btnFreightScheduled) {
btnFreightUrgent.addEventListener('click', () => {
freightTimeMode = 'URGENTE';
btnFreightUrgent.classList.add('active');
btnFreightScheduled.classList.remove('active');
updateFreightScheduleCalculation();
window.recalculateFreight();
});
btnFreightScheduled.addEventListener('click', () => {
freightTimeMode = 'PROGRAMADO';
btnFreightScheduled.classList.add('active');
btnFreightUrgent.classList.remove('active');
updateFreightScheduleCalculation();
window.recalculateFreight();
});
}
if (freightRadioRecogida && freightRadioDestino) {
freightRadioRecogida.addEventListener('change', () => {
if (freightRadioRecogida.checked) freightScheduleType = 'HORA_RECOGIDA';
updateFreightScheduleCalculation();
});
freightRadioDestino.addEventListener('change', () => {
if (freightRadioDestino.checked) freightScheduleType = 'HORA_LLEGADA_DESTINO';
updateFreightScheduleCalculation();
});
}
if (freightDateInput) freightDateInput.addEventListener('change', updateFreightScheduleCalculation);
if (freightTimeInput) freightTimeInput.addEventListener('input', updateFreightScheduleCalculation);
const KM_DIST = 115; // Corredor Cali - Buenaventura
let appliedFreightCoupon = null;
@ -2306,10 +2587,13 @@ function initFreightCalculator() {
const basePrice = currentTariffs.precioBasePaquete || 25000;
const costPerKm = currentTariffs.precioKmPaquete || 1200;
const urgentSurcharge = freightTimeMode === 'URGENTE' ? (currentTariffs.recargoEnvioUrgente ?? 15000) : 0;
if (freightUrgentCost) freightUrgentCost.textContent = `+$${urgentSurcharge.toLocaleString('es-CO')} COP`;
const distancePart = KM_DIST * costPerKm;
const subtotal = (basePrice + distancePart) * factor;
const seguro = Math.round(declaredValue * 0.01) + (currentTariffs.seguroBasePaquete || 0);
const subtotalBruto = Math.round(subtotal + seguro);
const subtotalBruto = Math.round(subtotal + seguro + urgentSurcharge);
let discountAmount = 0;
if (appliedFreightCoupon && appliedFreightCoupon.valid) {
@ -2435,6 +2719,21 @@ function initFreightCalculator() {
return;
}
if (freightTimeMode === 'PROGRAMADO') {
const fDate = freightDateInput ? freightDateInput.value : '';
const fTime = freightTimeInput ? freightTimeInput.value : '';
if (!fDate) {
alert('📅 Por favor selecciona la fecha programada para el envío de tu paquete.');
freightDateInput?.focus();
return;
}
if (!fTime) {
alert('🕒 Por favor indica la hora para la programación del envío.');
freightTimeInput?.focus();
return;
}
}
btnBookFreight.disabled = true;
const originalText = btnBookFreight.innerHTML;
btnBookFreight.innerHTML = '⏳ Generando Enlace de Pago Seguro...';
@ -2450,6 +2749,14 @@ function initFreightCalculator() {
telefonoRemitente: senderPhone,
telefonoDestinatario: receiverPhone,
descripcionContenido: packageDesc,
esEnvioUrgente: freightTimeMode === 'URGENTE',
modalidadTiempo: freightTimeMode === 'URGENTE' ? 'INMEDIATO' : 'PROGRAMADO',
tipoProgramacion: freightTimeMode === 'PROGRAMADO' ? freightScheduleType : undefined,
fechaProgramada: freightTimeMode === 'PROGRAMADO' ? freightDateInput?.value : undefined,
horaProgramada: freightTimeMode === 'PROGRAMADO' ? freightTimeInput?.value : undefined,
horaSugeridaRecogida: (freightTimeMode === 'PROGRAMADO' && freightScheduleType === 'HORA_LLEGADA_DESTINO')
? calculateSuggestedTime(freightTimeInput?.value || '', -3)?.time24
: undefined,
codigoCupon: appliedFreightCoupon?.coupon?.codigo || undefined
};

View File

@ -13,6 +13,7 @@ export interface ITarifasConfig extends Document {
precioKmPaquete: number; // Costo por kilómetro recorrido
factorPaquetes: IFactorPaquetes; // Multiplicador según volumen: sobre (1.0), mediano (1.5), grande (2.0)
seguroBasePaquete: number; // Seguro obligatorio por encomienda
recargoEnvioUrgente: number; // Recargo adicional para envíos urgentes inmediatos
porcentajeComisionAdmin: number; // Porcentaje que retiene la plataforma (ej. 15 = 15%)
costoPenalidadCancelacion: number; // Cargo impuesto al usuario si cancela en estado ACTIVE
radioMaxPoolingMetros: number; // Distancia máxima en metros para agrupar pasajeros en Cali ($near / $geoWithin)
@ -40,22 +41,26 @@ const TarifasConfigSchema = new Schema<ITarifasConfig>(
precioBasePaquete: {
type: Number,
required: true,
default: 12000,
default: 25000,
},
precioKmPaquete: {
type: Number,
required: true,
default: 750, // $750 COP por km
default: 1200, // $1200 COP por km
},
factorPaquetes: {
sobre: { type: Number, default: 1.0 },
mediano: { type: Number, default: 1.5 },
grande: { type: Number, default: 2.0 },
mediano: { type: Number, default: 1.6 },
grande: { type: Number, default: 2.2 },
},
seguroBasePaquete: {
type: Number,
default: 5000,
},
recargoEnvioUrgente: {
type: Number,
default: 15000, // $15.000 COP recargo por envío urgente express
},
porcentajeComisionAdmin: {
type: Number,
required: true,

View File

@ -86,8 +86,18 @@ export interface IBoldPaymentInfo {
paidAt?: Date;
}
export type TripTimeModality = 'INMEDIATO' | 'PROGRAMADO';
export type TripScheduleType = 'HORA_RECOGIDA' | 'HORA_LLEGADA_DESTINO';
export interface ITrip extends Document {
tipoServicio: TripServiceType;
modalidadTiempo?: TripTimeModality;
tipoProgramacion?: TripScheduleType;
fechaProgramada?: string;
horaProgramada?: string;
horaSugeridaRecogida?: string;
esEnvioUrgente?: boolean;
recargoUrgencia?: number;
origen: {
direccion: string;
ubicacion: IGeoPoint;
@ -280,13 +290,33 @@ const TripSchema = new Schema<ITrip>(
default: false,
index: true,
},
modalidadTiempo: {
type: String,
enum: ['INMEDIATO', 'PROGRAMADO'],
default: 'INMEDIATO',
},
tipoProgramacion: {
type: String,
enum: ['HORA_RECOGIDA', 'HORA_LLEGADA_DESTINO'],
},
fechaProgramada: String,
horaProgramada: String,
horaSugeridaRecogida: String,
esEnvioUrgente: {
type: Boolean,
default: false,
},
recargoUrgencia: {
type: Number,
default: 0,
},
fechaHoraSalidaProgramada: {
type: Date,
},
metodoPago: {
type: String,
enum: ['BOLD', 'NEQUI', 'EFECTIVO'],
default: 'BOLD',
enum: ['BOLD', 'NEQUI', 'EFECTIVO', 'BRE_B'],
default: 'BRE_B',
},
tarifaTotal: {
type: Number,

View File

@ -4,6 +4,8 @@ import { User, IUser } from '../models/User';
import { DriverProfile } from '../models/DriverProfile';
import { pushNotificationService } from './push-notification.service';
import { whatsAppNotificationService } from './whatsapp-notification.service';
import { isDatabaseConnected } from '../config/database';
import { inMemoryTripsService } from './in-memory-trips.service';
export interface ApproveTripParams {
tripId: string;
@ -27,14 +29,27 @@ export class AdminApprovalService {
/**
* Obtiene todos los viajes y encomiendas que esperan revisión del administrador
*/
public async getTripsPendientesAprobacion(): Promise<ITrip[]> {
return Trip.find({ estado: 'PENDING_APPROVAL' })
.populate('pasajeros.usuarioId', 'nombre telefono')
.populate('conductorId', 'nombre telefono')
.sort({ createdAt: 1 })
.exec();
public async getTripsPendientesAprobacion(): Promise<any[]> {
const memoryTrips = inMemoryTripsService.getPendingApproval();
if (isDatabaseConnected()) {
try {
const dbTrips = await Trip.find({ estado: 'PENDING_APPROVAL' })
.populate('pasajeros.usuarioId', 'nombre telefono')
.populate('conductorId', 'nombre telefono')
.sort({ createdAt: 1 })
.exec();
const dbIds = new Set(dbTrips.map((t) => t._id.toString()));
const uniqueMemory = memoryTrips.filter((t) => !dbIds.has(t._id.toString()));
return [...dbTrips, ...uniqueMemory];
} catch (dbErr) {
console.warn('⚠️ [Aviso DB trips pendientes]:', dbErr);
return memoryTrips;
}
}
return memoryTrips;
}
/**
* Aprueba y activa un viaje o encomienda, asignando conductor y disparando notificaciones push
*/

View File

@ -0,0 +1,40 @@
export class InMemoryTripsService {
private static instance: InMemoryTripsService;
private trips: Map<string, any> = new Map();
private constructor() {}
public static getInstance(): InMemoryTripsService {
if (!InMemoryTripsService.instance) {
InMemoryTripsService.instance = new InMemoryTripsService();
}
return InMemoryTripsService.instance;
}
public saveTrip(trip: any): void {
const id = trip._id ? trip._id.toString() : `trip_${Date.now()}`;
this.trips.set(id, trip);
}
public getTrip(id: string): any {
return this.trips.get(id);
}
public getAll(): any[] {
return Array.from(this.trips.values());
}
public getPendingApproval(): any[] {
return Array.from(this.trips.values()).filter((t) => t.estado === 'PENDING_APPROVAL');
}
public updateTrip(id: string, updates: Partial<any>): any {
const existing = this.trips.get(id);
if (!existing) return null;
const updated = Object.assign(existing, updates);
this.trips.set(id, updated);
return updated;
}
}
export const inMemoryTripsService = InMemoryTripsService.getInstance();

View File

@ -4,27 +4,44 @@ import { Trip, ITrip, IPackageInfo } from '../models/Trip';
import { TarifasConfig, ITarifasConfig } from '../models/TarifasConfig';
import { boldPaymentService, BoldPaymentResponse } from './bold.service';
import { couponService } from './coupon.service';
import { whatsAppNotificationService } from './whatsapp-notification.service';
import { isDatabaseConnected } from '../config/database';
import { tariffsService } from './tariffs.service';
import { inMemoryTripsService } from './in-memory-trips.service';
export interface PackageQuoteRequest {
remitenteId: string;
remitenteNombre: string;
remitenteTelefono: string;
remitenteId?: string;
remitenteNombre?: string;
remitenteTelefono?: string;
remitenteEmail?: string;
direccionRecogida: string;
coordenadasRecogida: [number, number]; // [lng, lat]
ciudadOrigen: 'CALI' | 'BUENAVENTURA';
direccionRecogida?: string;
coordenadasRecogida?: [number, number]; // [lng, lat]
ciudadOrigen?: 'CALI' | 'BUENAVENTURA';
destinatarioNombre: string;
destinatarioTelefono: string;
direccionEntrega: string;
coordenadasEntrega: [number, number]; // [lng, lat]
ciudadDestino: 'CALI' | 'BUENAVENTURA';
destinatarioNombre?: string;
destinatarioTelefono?: string;
direccionEntrega?: string;
coordenadasEntrega?: [number, number]; // [lng, lat]
ciudadDestino?: 'CALI' | 'BUENAVENTURA';
tipoBulto: 'SOBRE' | 'MEDIANO' | 'GRANDE';
descripcion: string;
tipoBulto?: 'SOBRE' | 'MEDIANO' | 'GRANDE';
tipoPaquete?: string;
descripcion?: string;
descripcionContenido?: string;
pesoAproxKg?: number;
incluirSeguro?: boolean;
codigoCupon?: string;
modalidadTiempo?: 'INMEDIATO' | 'PROGRAMADO';
tipoProgramacion?: 'HORA_RECOGIDA' | 'HORA_LLEGADA_DESTINO';
fechaProgramada?: string;
horaProgramada?: string;
horaSugeridaRecogida?: string;
esEnvioUrgente?: boolean;
// Aliases from front
telefonoRemitente?: string;
telefonoDestinatario?: string;
}
export interface PackageFareBreakdown {
@ -35,6 +52,9 @@ export interface PackageFareBreakdown {
factorBulto: number;
tipoBulto: string;
valorSeguro: number;
recargoUrgencia?: number;
esEnvioUrgente?: boolean;
modalidadTiempo?: 'INMEDIATO' | 'PROGRAMADO';
subtotalOriginal?: number;
descuentoCupon?: {
codigo: string;
@ -53,6 +73,12 @@ export interface PackageBookingResult {
success: boolean;
message: string;
tripId?: string;
guiaSeguimiento?: string;
tarifaCalculada?: number;
tarifaFinal?: number;
descuentoCupon?: any;
boldPaymentLink?: string;
trip?: any;
desglose: PackageFareBreakdown;
boldPayment?: BoldPaymentResponse;
}
@ -101,13 +127,14 @@ export class MessagingFleteService {
public async cotizarFlete(
distanciaKm: number,
tipoBulto: 'SOBRE' | 'MEDIANO' | 'GRANDE',
incluirSeguro: boolean = true
incluirSeguro: boolean = true,
esEnvioUrgente: boolean = false
): Promise<PackageFareBreakdown> {
const config: ITarifasConfig =
(await TarifasConfig.findOne({ estaActiva: true })) || (await TarifasConfig.create({}));
const config = await tariffsService.getConfig();
const precioBase = config.precioBasePaquete || 12000;
const costoPorKm = config.precioKmPaquete || 750;
const recargoUrgencia = esEnvioUrgente ? (config.recargoEnvioUrgente || 15000) : 0;
let factor = 1.0;
if (tipoBulto === 'SOBRE') factor = config.factorPaquetes.sobre || 1.0;
@ -116,9 +143,9 @@ export class MessagingFleteService {
const seguro = incluirSeguro ? config.seguroBasePaquete || 5000 : 0;
// Fórmula maestra de ingeniería
// Fórmula maestra de ingeniería con recargo opcional por urgencia
const subtotalKm = distanciaKm * costoPorKm;
const tarifaBruta = (precioBase + subtotalKm) * factor + seguro;
const tarifaBruta = (precioBase + subtotalKm) * factor + seguro + recargoUrgencia;
const tarifaTotal = Math.round(tarifaBruta / 500) * 500; // Redondeado comercialmente a múltiplos de $500 COP
const comisionAdmin = Math.round(tarifaTotal * (config.porcentajeComisionAdmin / 100));
@ -132,6 +159,9 @@ export class MessagingFleteService {
factorBulto: factor,
tipoBulto,
valorSeguro: seguro,
recargoUrgencia,
esEnvioUrgente,
modalidadTiempo: esEnvioUrgente ? 'INMEDIATO' : 'PROGRAMADO',
tarifaTotal,
porcentajeComisionAdmin: config.porcentajeComisionAdmin,
comisionAdmin,
@ -145,21 +175,50 @@ export class MessagingFleteService {
* y registra el servicio en estado PENDING_APPROVAL
*/
public async registrarEnvioPaquete(request: PackageQuoteRequest): Promise<PackageBookingResult> {
// 1. Validar remitente y verificar que no tenga sanciones monetarias
const remitente = await User.findById(request.remitenteId);
if (!remitente) {
throw new Error('El usuario remitente no está registrado en el sistema.');
// 1. Normalizar parámetros de entrada (soporte para aliases y valores por defecto)
const remitenteNombre = (request.remitenteNombre || 'Remitente').trim();
const remitenteTelefono = (request.remitenteTelefono || request.telefonoRemitente || '573000000000').replace(/\D/g, '');
const remitenteEmail = request.remitenteEmail || '';
const destinatarioNombre = (request.destinatarioNombre || 'Destinatario Encomienda').trim();
const destinatarioTelefono = (request.destinatarioTelefono || request.telefonoDestinatario || '573000000000').replace(/\D/g, '');
const direccionRecogida = request.direccionRecogida || 'Cali, Valle del Cauca';
const direccionEntrega = request.direccionEntrega || 'Buenaventura, Valle del Cauca';
const coordenadasRecogida = request.coordenadasRecogida || [-76.5320, 3.4215];
const coordenadasEntrega = request.coordenadasEntrega || [-77.0250, 3.8820];
const ciudadOrigen = request.ciudadOrigen || 'CALI';
const ciudadDestino = request.ciudadDestino || 'BUENAVENTURA';
const descripcion = request.descripcion || request.descripcionContenido || 'Encomienda / Paquete';
// Determinar tipo de bulto de forma inteligente
let tipoBulto: 'SOBRE' | 'MEDIANO' | 'GRANDE' = request.tipoBulto || 'MEDIANO';
if (!request.tipoBulto && request.tipoPaquete) {
const tp = request.tipoPaquete.toLowerCase();
if (tp.includes('sobre') || tp.includes('doc')) tipoBulto = 'SOBRE';
else if (tp.includes('grande') || tp.includes('pesada') || tp.includes('carga')) tipoBulto = 'GRANDE';
else tipoBulto = 'MEDIANO';
}
if (remitente.saldoPenalidad > 0) {
throw new Error(
`El usuario presenta una penalidad pendiente de $${remitente.saldoPenalidad.toLocaleString('es-CO')} COP. No puede solicitar encomiendas.`
);
// 2. Validar penalidades si la base de datos está en línea y se proporcionó remitenteId
if (isDatabaseConnected() && request.remitenteId && Types.ObjectId.isValid(request.remitenteId)) {
try {
const remitente = await User.findById(request.remitenteId);
if (remitente && remitente.saldoPenalidad > 0) {
throw new Error(
`El usuario presenta una penalidad pendiente de $${remitente.saldoPenalidad.toLocaleString('es-CO')} COP. No puede solicitar encomiendas.`
);
}
} catch (err: any) {
if (err.message && err.message.includes('penalidad')) throw err;
console.warn('⚠️ [Aviso DB remitente]:', err.message);
}
}
// 2. Calcular distancia y cotizar flete
const distanciaKm = this.calcularDistanciaKm(request.coordenadasRecogida, request.coordenadasEntrega);
const desglose = await this.cotizarFlete(distanciaKm, request.tipoBulto, request.incluirSeguro ?? true);
// 3. Determinar si es envío urgente o programado
const isUrgent = request.esEnvioUrgente ?? (request.modalidadTiempo === 'INMEDIATO' || request.modalidadTiempo === undefined);
// 4. Calcular distancia y cotizar flete
const distanciaKm = this.calcularDistanciaKm(coordenadasRecogida, coordenadasEntrega);
const desglose = await this.cotizarFlete(distanciaKm, tipoBulto, request.incluirSeguro ?? true, isUrgent);
const subtotalBruto = desglose.tarifaTotal;
let descuentoTotal = 0;
@ -191,48 +250,59 @@ export class MessagingFleteService {
const paqueteInfo: IPackageInfo = {
remitente: {
nombre: request.remitenteNombre,
telefono: request.remitenteTelefono,
direccionRecogida: request.direccionRecogida,
nombre: remitenteNombre,
telefono: remitenteTelefono,
direccionRecogida,
},
destinatario: {
nombre: request.destinatarioNombre,
telefono: request.destinatarioTelefono,
direccionEntrega: request.direccionEntrega,
nombre: destinatarioNombre,
telefono: destinatarioTelefono,
direccionEntrega,
},
tipoBulto: request.tipoBulto,
descripcion: request.descripcion,
tipoBulto,
descripcion,
pesoAproxKg: request.pesoAproxKg,
distanciaKm,
seguroIncluido: request.incluirSeguro ?? true,
};
// 3. Crear el Trip en estado PENDING_APPROVAL
const trip = new Trip({
const tripId = new Types.ObjectId().toString();
// 5. Crear el Trip en estado PENDING_APPROVAL con modalidad de tiempo
const tripData: any = {
_id: tripId,
tipoServicio: 'MENSAJERIA',
modalidadTiempo: request.modalidadTiempo || (isUrgent ? 'INMEDIATO' : 'PROGRAMADO'),
tipoProgramacion: request.tipoProgramacion,
fechaProgramada: request.fechaProgramada,
horaProgramada: request.horaProgramada,
horaSugeridaRecogida: request.horaSugeridaRecogida,
esEnvioUrgente: isUrgent,
recargoUrgencia: desglose.recargoUrgencia || 0,
origen: {
direccion: request.direccionRecogida,
direccion: direccionRecogida,
ubicacion: {
type: 'Point',
coordinates: request.coordenadasRecogida,
coordinates: coordenadasRecogida,
},
esPuntoFijo: false,
ciudad: request.ciudadOrigen,
ciudad: ciudadOrigen,
},
destino: {
direccion: request.direccionEntrega,
direccion: direccionEntrega,
ubicacion: {
type: 'Point',
coordinates: request.coordenadasEntrega,
coordinates: coordenadasEntrega,
},
esPuntoFijo: false,
ciudad: request.ciudadDestino,
ciudad: ciudadDestino,
},
pasajeros: [],
paqueteInfo,
estado: 'PENDING_APPROVAL',
metodoPago: 'BOLD',
tarifaTotal: desglose.tarifaTotal,
precio: desglose.tarifaTotal,
descuentoCupon: cuponDescInfo || undefined,
comisionAdmin: desglose.comisionAdmin,
pagoConductor: desglose.pagoConductor,
@ -240,51 +310,92 @@ export class MessagingFleteService {
secuenciaRutaWaze: [
{
orden: 1,
direccion: request.direccionRecogida,
lat: request.coordenadasRecogida[1],
lng: request.coordenadasRecogida[0],
wazeDeepLink: `https://waze.com/ul?ll=${request.coordenadasRecogida[1]},${request.coordenadasRecogida[0]}&navigate=yes`,
direccion: direccionRecogida,
lat: coordenadasRecogida[1],
lng: coordenadasRecogida[0],
wazeDeepLink: `https://waze.com/ul?ll=${coordenadasRecogida[1]},${coordenadasRecogida[0]}&navigate=yes`,
completada: false,
},
{
orden: 2,
direccion: request.direccionEntrega,
lat: request.coordenadasEntrega[1],
lng: request.coordenadasEntrega[0],
wazeDeepLink: `https://waze.com/ul?ll=${request.coordenadasEntrega[1]},${request.coordenadasEntrega[0]}&navigate=yes`,
direccion: direccionEntrega,
lat: coordenadasEntrega[1],
lng: coordenadasEntrega[0],
wazeDeepLink: `https://waze.com/ul?ll=${coordenadasEntrega[1]},${coordenadasEntrega[0]}&navigate=yes`,
completada: false,
},
],
});
createdAt: new Date(),
};
await trip.save();
// Guardar en caché autónoma en memoria siempre
inMemoryTripsService.saveTrip(tripData);
// 4. Generar cobro oficial 100% prepagado a través de Bold Smart Checkout
const boldRes = await boldPaymentService.createPayment({
referenceId: trip._id.toString(),
amount: desglose.tarifaTotal,
description: `Flete Encomienda Cali-Buenaventura #${trip._id.toString().slice(-6)} - Bulto: ${request.tipoBulto}${cuponDescInfo ? ` (Bono ${cuponDescInfo.codigo})` : ''}`,
payerEmail: request.remitenteEmail || remitente.email,
payerPhone: request.remitenteTelefono || remitente.telefono,
paymentType: 'MESSAGING_PACKAGE',
});
// Guardar en MongoDB si está conectado
if (isDatabaseConnected()) {
try {
const mongoTrip = new Trip(tripData);
await mongoTrip.save();
} catch (saveErr) {
console.warn('⚠️ [Aviso al guardar paquete en MongoDB]:', saveErr);
}
}
if (boldRes.success) {
trip.boldPayment = {
paymentLinkId: boldRes.paymentLinkId,
checkoutUrl: boldRes.checkoutUrl,
qrImageUrl: boldRes.qrImageUrl,
transactionStatus: 'ACTIVE',
};
await trip.save();
// Notificar por WhatsApp a la central / admin
const horarioEnvioStr = !isUrgent
? `${request.fechaProgramada || ''} ${request.horaProgramada ? `(${request.tipoProgramacion === 'HORA_LLEGADA_DESTINO' ? 'Entrega requerida: ' : 'Recogida: '}${request.horaProgramada})` : ''}`
: `Despacho Inmediato Prioritario (Recargo Urgencia: +$${(desglose.recargoUrgencia || 0).toLocaleString('es-CO')} COP)`;
whatsAppNotificationService.notificarAdminNuevoServicio({
tipo: 'MENSAJERIA',
tripId,
solicitante: remitenteNombre,
telefono: remitenteTelefono,
origen: direccionRecogida,
destino: direccionEntrega,
valorTotal: desglose.tarifaTotal,
pasajerosCount: 0,
modalidad: isUrgent ? '🚨 ENVÍO URGENTE INMEDIATO' : '📅 ENVÍO PROGRAMADO',
detalleHorario: horarioEnvioStr,
}).catch((err) => console.warn('⚠️ [WhatsApp notify admin package error]:', err));
// 6. Generar cobro oficial 100% prepagado a través de Bold Smart Checkout
let boldRes: any = null;
try {
boldRes = await boldPaymentService.createPayment({
referenceId: tripId,
amount: desglose.tarifaTotal,
description: `Flete Encomienda Cali-Buenaventura #${tripId.slice(-6)} - Bulto: ${tipoBulto}${isUrgent ? ' (Urgente)' : ''}${cuponDescInfo ? ` (Bono ${cuponDescInfo.codigo})` : ''}`,
payerEmail: remitenteEmail || 'cliente@pacifictrans.co',
payerPhone: remitenteTelefono,
paymentType: 'MESSAGING_PACKAGE',
});
if (boldRes && boldRes.success) {
tripData.boldPayment = {
paymentLinkId: boldRes.paymentLinkId,
checkoutUrl: boldRes.checkoutUrl,
qrImageUrl: boldRes.qrImageUrl,
transactionStatus: 'ACTIVE',
};
inMemoryTripsService.saveTrip(tripData);
}
} catch (errBold) {
console.warn('⚠️ [Bold Gateway Notice]:', errBold);
}
return {
success: true,
message: 'Servicio de mensajería cotizado y registrado. Pago en línea seguro generado con éxito.',
tripId: trip._id.toString(),
tripId,
guiaSeguimiento: `PT-${tripId.slice(-6).toUpperCase()}`,
tarifaCalculada: desglose.tarifaTotal,
tarifaFinal: desglose.tarifaTotal,
descuentoCupon: cuponDescInfo,
desglose,
boldPayment: boldRes,
boldPaymentLink: boldRes?.checkoutUrl,
trip: tripData,
};
}
}

View File

@ -6,6 +6,10 @@ import { DriverProfile } from '../models/DriverProfile';
import { whatsAppNotificationService } from './whatsapp-notification.service';
import { boldPaymentService } from './bold.service';
import { couponService } from './coupon.service';
import { isDatabaseConnected } from '../config/database';
import { tariffsService } from './tariffs.service';
import { inMemoryTripsService } from './in-memory-trips.service';
export interface PassengerItemInput {
nombre: string;
@ -31,6 +35,11 @@ export interface PassengerBookingRequest {
pasajeros?: PassengerItemInput[];
email?: string;
codigoCupon?: string;
modalidadTiempo?: 'INMEDIATO' | 'PROGRAMADO';
tipoProgramacion?: 'HORA_RECOGIDA' | 'HORA_LLEGADA_DESTINO';
fechaProgramada?: string;
horaProgramada?: string;
horaSugeridaRecogida?: string;
}
export interface PoolingResult {
@ -91,26 +100,40 @@ export class PoolingService {
// 2. Identificar o registrar al usuario titular
let user: any = null;
if (request.usuarioId && Types.ObjectId.isValid(request.usuarioId)) {
user = await User.findById(request.usuarioId);
}
const primerPasajero = rawPasajeros[0];
const cleanPhone = (primerPasajero.telefono || '').replace(/\D/g, '');
if (!user && cleanPhone) {
user = await User.findOne({ telefono: { $regex: cleanPhone.slice(-10) } });
if (isDatabaseConnected()) {
try {
if (request.usuarioId && Types.ObjectId.isValid(request.usuarioId)) {
user = await User.findById(request.usuarioId);
}
if (!user && cleanPhone) {
user = await User.findOne({ telefono: { $regex: cleanPhone.slice(-10) } });
}
if (!user) {
user = await User.create({
nombre: primerPasajero.nombre?.trim() || 'Pasajero Titular',
telefono: cleanPhone || '573000000000',
email: request.email ? request.email.trim().toLowerCase() : undefined,
rol: 'PASAJERO',
estaActivo: true,
saldoPenalidad: 0,
});
}
} catch (dbUserErr) {
console.warn('⚠️ [Aviso DB Usuario]:', dbUserErr);
}
}
if (!user) {
user = await User.create({
user = {
_id: new Types.ObjectId(),
nombre: primerPasajero.nombre?.trim() || 'Pasajero Titular',
telefono: cleanPhone || '573000000000',
email: request.email ? request.email.trim().toLowerCase() : undefined,
rol: 'PASAJERO',
estaActivo: true,
email: request.email,
saldoPenalidad: 0,
});
};
}
if (user && user.saldoPenalidad > 0) {
@ -123,7 +146,7 @@ export class PoolingService {
}
// 3. Obtener la configuración activa de tarifas y radio de pooling
const config = (await TarifasConfig.findOne({ estaActiva: true })) || (await TarifasConfig.create({}));
const config = await tariffsService.getConfig();
const maxRadiusMeters = config.radioMaxPoolingMetros || 4000;
const tarifaPorPasajero = config.tarifaPasajeroFija || 35000;
@ -131,49 +154,63 @@ export class PoolingService {
// 4. Buscar si existe un Trip en estado PENDING_APPROVAL en la misma ruta con cupos disponibles para TODOS los pasajeros nuevos
let candidateTrip: ITrip | null = null;
if (request.sentido === 'CALI_A_BUENAVENTURA') {
candidateTrip = await Trip.findOne({
tipoServicio: 'TRANSPORTE',
estado: 'PENDING_APPROVAL',
'origen.ciudad': 'CALI',
'destino.ciudad': 'BUENAVENTURA',
'destino.direccion': request.puntoFijoBuenaventura.nombre,
$expr: {
$lte: [{ $add: [{ $size: '$pasajeros' }, numPasajerosNuevos] }, 4],
},
'origen.ubicacion': {
$near: {
$geometry: {
type: 'Point',
coordinates: [reqLng, reqLat],
},
$maxDistance: maxRadiusMeters,
},
},
});
} else {
candidateTrip = await Trip.findOne({
tipoServicio: 'TRANSPORTE',
estado: 'PENDING_APPROVAL',
'origen.ciudad': 'BUENAVENTURA',
'destino.ciudad': 'CALI',
'origen.direccion': request.puntoFijoBuenaventura.nombre,
$expr: {
$lte: [{ $add: [{ $size: '$pasajeros' }, numPasajerosNuevos] }, 4],
},
'destino.ubicacion': {
$near: {
$geometry: {
type: 'Point',
coordinates: [reqLng, reqLat],
},
$maxDistance: maxRadiusMeters,
},
},
});
const requestedModality = request.modalidadTiempo || 'INMEDIATO';
const modalityFilter: any = { modalidadTiempo: requestedModality };
if (requestedModality === 'PROGRAMADO' && request.fechaProgramada) {
modalityFilter.fechaProgramada = request.fechaProgramada;
}
if (isDatabaseConnected()) {
try {
if (request.sentido === 'CALI_A_BUENAVENTURA') {
candidateTrip = await Trip.findOne({
tipoServicio: 'TRANSPORTE',
estado: 'PENDING_APPROVAL',
...modalityFilter,
'origen.ciudad': 'CALI',
'destino.ciudad': 'BUENAVENTURA',
'destino.direccion': request.puntoFijoBuenaventura.nombre,
$expr: {
$lte: [{ $add: [{ $size: '$pasajeros' }, numPasajerosNuevos] }, 4],
},
'origen.ubicacion': {
$near: {
$geometry: {
type: 'Point',
coordinates: [reqLng, reqLat],
},
$maxDistance: maxRadiusMeters,
},
},
});
} else {
candidateTrip = await Trip.findOne({
tipoServicio: 'TRANSPORTE',
estado: 'PENDING_APPROVAL',
...modalityFilter,
'origen.ciudad': 'BUENAVENTURA',
'destino.ciudad': 'CALI',
'origen.direccion': request.puntoFijoBuenaventura.nombre,
$expr: {
$lte: [{ $add: [{ $size: '$pasajeros' }, numPasajerosNuevos] }, 4],
},
'destino.ubicacion': {
$near: {
$geometry: {
type: 'Point',
coordinates: [reqLng, reqLat],
},
$maxDistance: maxRadiusMeters,
},
},
});
}
} catch (tripFindErr) {
console.warn('⚠️ [Aviso DB candidato pooling]:', tripFindErr);
}
}
const paymentStatus =
request.metodoPago === 'BOLD'
? 'PAGADO_BOLD'
@ -230,6 +267,11 @@ export class PoolingService {
targetTrip = new Trip({
tipoServicio: 'TRANSPORTE',
modalidadTiempo: requestedModality,
tipoProgramacion: request.tipoProgramacion,
fechaProgramada: request.fechaProgramada,
horaProgramada: request.horaProgramada,
horaSugeridaRecogida: request.horaSugeridaRecogida,
origen: {
direccion: request.sentido === 'CALI_A_BUENAVENTURA' ? request.direccionTexto : request.puntoFijoBuenaventura.nombre,
ubicacion: origenUbicacion,
@ -346,7 +388,16 @@ export class PoolingService {
}
}
await targetTrip.save();
// Guardar en memoria siempre
inMemoryTripsService.saveTrip(targetTrip);
if (isDatabaseConnected()) {
try {
await targetTrip.save();
} catch (saveErr) {
console.warn('⚠️ [Aviso al guardar viaje en MongoDB]:', saveErr);
}
}
// Notificación al completar el cupo del vehículo:
if (cupoLleno) {
@ -354,10 +405,12 @@ export class PoolingService {
let sugerenciaConductorTexto = '';
const primeraParada = paradasWaze[0];
if (primeraParada && targetTrip.origen.ciudad === 'CALI') {
const conductores = await DriverProfile.find({ estaActivo: true })
.populate('userId', 'nombre telefono')
.lean();
if (primeraParada && targetTrip.origen.ciudad === 'CALI' && isDatabaseConnected()) {
try {
const conductores = await DriverProfile.find({ estaActivo: true })
.populate('userId', 'nombre telefono')
.lean();
let mejorConductor: any = null;
let menorDistancia = Infinity;
@ -380,14 +433,35 @@ export class PoolingService {
`• *Conductor:* ${mejorConductor.userId.nombre} (${mejorConductor.placaVehiculo})\n` +
`• *Vive a:* ${distKm} km de la 1ª parada (${primeraParada.direccion})`;
}
} catch (driverErr) {
console.warn('⚠️ [Aviso conductores]:', driverErr);
}
}
await whatsAppNotificationService.notificarAdminPoolLleno({
tripId: targetTrip._id.toString(),
ruta: `${targetTrip.origen.ciudad} ➔ ${targetTrip.destino.ciudad}`,
totalRecaudo: targetTrip.tarifaTotal,
conductorSugerido: sugerenciaConductorTexto,
});
const isScheduled = targetTrip.modalidadTiempo === 'PROGRAMADO';
const horarioStr = isScheduled
? `${targetTrip.fechaProgramada || ''} ${targetTrip.horaProgramada ? `(${targetTrip.tipoProgramacion === 'HORA_LLEGADA_DESTINO' ? 'Llegada Destino: ' : 'Recogida: '}${targetTrip.horaProgramada})` : ''}`
: 'Inmediato (Próxima Salida)';
whatsAppNotificationService.notificarAdminNuevoServicio({
tipo: 'TRANSPORTE',
tripId: targetTrip._id.toString(),
solicitante: user.nombre || 'Pasajero',
telefono: user.telefono || '573000000000',
origen: targetTrip.origen.direccion,
destino: targetTrip.destino.direccion,
valorTotal: targetTrip.tarifaTotal,
pasajerosCount: cantidadPasajeros,
modalidad: isScheduled ? '📅 PROGRAMADO' : '⚡ INMEDIATO',
detalleHorario: horarioStr,
}).catch((err) => console.warn('⚠️ [WhatsApp notify admin partial pool]:', err));
}
const pluralCupos = numPasajerosNuevos > 1 ? `${numPasajerosNuevos} cupos reservados` : '1 cupo reservado';

View File

@ -10,6 +10,7 @@ export interface UpdateTarifasDTO {
mediano?: number;
grande?: number;
};
recargoEnvioUrgente?: number;
seguroBasePaquete?: number;
porcentajeComisionAdmin?: number;
costoPenalidadCancelacion?: number;
@ -22,6 +23,29 @@ export interface UpdateTarifasDTO {
export class TariffsService {
private static instance: TariffsService;
private defaultTarifas: any = {
version: 'v1.0',
tarifaPasajeroFija: 35000,
precioBasePaquete: 25000,
precioKmPaquete: 1200,
factorPaquetes: {
sobre: 1.0,
mediano: 1.6,
grande: 2.2,
},
seguroBasePaquete: 5000,
recargoEnvioUrgente: 15000,
porcentajeComisionAdmin: 15,
costoPenalidadCancelacion: 15000,
radioMaxPoolingMetros: 4000,
numeroAtencionWhatsApp: '573167455877',
nombreSoporte: 'Atención Pacific Trans GO',
grupoWhatsAppPacificTrans: 'Pacific Trans Notificaciones',
estaActiva: true,
};
private inMemoryTarifas: any = { ...this.defaultTarifas };
private constructor() {}
public static getInstance(): TariffsService {
@ -31,111 +55,119 @@ export class 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,
numeroAtencionWhatsApp: '573167455877',
nombreSoporte: 'Atención Pacific Trans GO',
estaActiva: true,
});
}
// Sincronizar número oficial con el servicio de mensajería de WhatsApp
if (config.numeroAtencionWhatsApp) {
whatsAppNotificationService.setAdminPhone(config.numeroAtencionWhatsApp);
}
return config;
public async getConfig(): Promise<ITarifasConfig> {
return this.getTarifasActivas();
}
/**
* Actualiza los valores de tarifas en la plataforma y persiste en MongoDB.
* Obtiene la configuración de tarifas activa en MongoDB.
* Si no existe o MongoDB no está disponible, utiliza la configuración predeterminada oficial.
*/
public async getTarifasActivas(): Promise<ITarifasConfig> {
try {
let config = await TarifasConfig.findOne({ estaActiva: true });
if (!config) {
config = await TarifasConfig.create(this.defaultTarifas);
}
if (config.numeroAtencionWhatsApp) {
whatsAppNotificationService.setAdminPhone(config.numeroAtencionWhatsApp);
}
this.inMemoryTarifas = (config as any).toObject ? (config as any).toObject() : { ...config };
return config;
} catch (err: any) {
console.warn('⚠️ [TariffsService]: Base de datos offline/reintentando, retornando tarifas en memoria:', err.message);
return this.inMemoryTarifas as ITarifasConfig;
}
}
/**
* Actualiza los valores de tarifas en la plataforma y persiste en MongoDB o memoria.
*/
public async updateTarifas(updates: UpdateTarifasDTO): Promise<ITarifasConfig> {
const config = await this.getTarifasActivas();
try {
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.tarifaPasajeroFija !== undefined && updates.tarifaPasajeroFija > 0) {
config.tarifaPasajeroFija = updates.tarifaPasajeroFija;
}
if (updates.factorPaquetes.mediano !== undefined) {
config.factorPaquetes.mediano = updates.factorPaquetes.mediano;
if (updates.precioBasePaquete !== undefined && updates.precioBasePaquete >= 0) {
config.precioBasePaquete = updates.precioBasePaquete;
}
if (updates.factorPaquetes.grande !== undefined) {
config.factorPaquetes.grande = updates.factorPaquetes.grande;
if (updates.precioKmPaquete !== undefined && updates.precioKmPaquete >= 0) {
config.precioKmPaquete = updates.precioKmPaquete;
}
if (updates.factorPaquetes) {
if (!config.factorPaquetes) config.factorPaquetes = { sobre: 1.0, mediano: 1.6, grande: 2.2 };
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.recargoEnvioUrgente !== undefined && updates.recargoEnvioUrgente >= 0) {
config.recargoEnvioUrgente = updates.recargoEnvioUrgente;
}
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;
}
if (updates.numeroAtencionWhatsApp !== undefined && updates.numeroAtencionWhatsApp.trim()) {
config.numeroAtencionWhatsApp = updates.numeroAtencionWhatsApp.trim();
whatsAppNotificationService.setAdminPhone(config.numeroAtencionWhatsApp);
}
if (updates.nombreSoporte !== undefined && updates.nombreSoporte.trim()) {
config.nombreSoporte = updates.nombreSoporte.trim();
}
if (updates.grupoWhatsAppPacificTrans !== undefined && updates.grupoWhatsAppPacificTrans.trim()) {
config.grupoWhatsAppPacificTrans = updates.grupoWhatsAppPacificTrans.trim();
}
config.updatedAt = new Date();
if (typeof (config as any).save === 'function') {
await (config as any).save();
}
this.inMemoryTarifas = { ...this.inMemoryTarifas, ...updates };
console.log('✅ [Tarifas y Configuración Actualizadas]:', {
tarifaPasajeroFija: config.tarifaPasajeroFija,
precioBasePaquete: config.precioBasePaquete,
precioKmPaquete: config.precioKmPaquete,
recargoEnvioUrgente: config.recargoEnvioUrgente,
porcentajeComisionAdmin: config.porcentajeComisionAdmin,
numeroAtencionWhatsApp: config.numeroAtencionWhatsApp,
nombreSoporte: config.nombreSoporte
});
return config;
} catch (err: any) {
console.warn('⚠️ [TariffsService]: Error al persistir en BD, actualizando en memoria:', err.message);
this.inMemoryTarifas = { ...this.inMemoryTarifas, ...updates };
return this.inMemoryTarifas as ITarifasConfig;
}
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;
}
if (updates.numeroAtencionWhatsApp !== undefined && updates.numeroAtencionWhatsApp.trim()) {
config.numeroAtencionWhatsApp = updates.numeroAtencionWhatsApp.trim();
whatsAppNotificationService.setAdminPhone(config.numeroAtencionWhatsApp);
}
if (updates.nombreSoporte !== undefined && updates.nombreSoporte.trim()) {
config.nombreSoporte = updates.nombreSoporte.trim();
}
if (updates.grupoWhatsAppPacificTrans !== undefined && updates.grupoWhatsAppPacificTrans.trim()) {
config.grupoWhatsAppPacificTrans = updates.grupoWhatsAppPacificTrans.trim();
}
config.updatedAt = new Date();
await config.save();
console.log('✅ [Tarifas y Configuración Actualizadas]:', {
tarifaPasajeroFija: config.tarifaPasajeroFija,
precioBasePaquete: config.precioBasePaquete,
precioKmPaquete: config.precioKmPaquete,
porcentajeComisionAdmin: config.porcentajeComisionAdmin,
numeroAtencionWhatsApp: config.numeroAtencionWhatsApp,
nombreSoporte: config.nombreSoporte
});
return config;
}
}

View File

@ -315,12 +315,16 @@ export class WhatsAppNotificationService {
destino: string;
valorTotal: number;
pasajerosCount: number;
modalidad?: string;
detalleHorario?: string;
}): Promise<boolean> {
const msg =
`🔔 *PACIFIC TRANS GO: NUEVA SOLICITUD EN ESPERA DE APROBACIÓN*\n\n` +
`Central de Despacho Pacific Trans GO (Cali - Buenaventura):\n` +
`• *Tipo:* ${params.tipo}\n` +
`• *Ref:* #${params.tripId.slice(-6)}\n` +
(params.modalidad ? `• *Modalidad:* ${params.modalidad}\n` : '') +
(params.detalleHorario ? `• *Horario:* ${params.detalleHorario}\n` : '') +
`• *Cliente:* ${params.solicitante} (+${params.telefono.replace(/\D/g, '')})\n` +
`• *Ruta:* ${params.origen} ➔ ${params.destino}\n` +
`• *Ocupación:* ${params.pasajerosCount}/4 pasajeros\n` +