feat(pacifictrans): implement immediate vs scheduled modalities, smart ETA corridor calculation and urgent freight surcharge
This commit is contained in:
parent
bae5cbbffa
commit
ccb4db4e17
@ -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;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@ -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>
|
||||
|
||||
|
||||
@ -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
|
||||
};
|
||||
|
||||
|
||||
@ -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()
|
||||
|
||||
25
scripts/check_pacific_logs.py
Normal file
25
scripts/check_pacific_logs.py
Normal 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()
|
||||
25
scripts/inspect_vps_containers.py
Normal file
25
scripts/inspect_vps_containers.py
Normal 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()
|
||||
109
scripts/verify_time_modes.py
Normal file
109
scripts/verify_time_modes.py
Normal 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.")
|
||||
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@ -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>
|
||||
|
||||
|
||||
@ -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
|
||||
};
|
||||
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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
|
||||
*/
|
||||
|
||||
@ -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();
|
||||
@ -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,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@ -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';
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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` +
|
||||
|
||||
Loading…
Reference in New Issue
Block a user