diff --git a/pacifictrans-app/public/css/style.css b/pacifictrans-app/public/css/style.css
index 0ae49cc..405fb41 100644
--- a/pacifictrans-app/public/css/style.css
+++ b/pacifictrans-app/public/css/style.css
@@ -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;
+}
+
+
diff --git a/pacifictrans-app/public/index.html b/pacifictrans-app/public/index.html
index e2e1ae5..d6070e4 100644
--- a/pacifictrans-app/public/index.html
+++ b/pacifictrans-app/public/index.html
@@ -242,6 +242,70 @@
+
+
+
+
+ ⚡ Salida Inmediata
+
+
+
+
+
+
+
+
+
+
+
+ 📅 Especifica tu tiempo de viaje:
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -350,6 +414,10 @@
Seguro Contractual:
Póliza y Planilla SOAT
+
+ Modalidad y Horario:
+ ⚡ Inmediato (Próxima Salida)
+
Subtotal:
@@ -453,6 +521,68 @@
+
+
+
+
+ 🚨 Envío Urgente Inmediato
+
+
+
+
+
+
+
+
+
+
+
+ 📅 Especifica fecha y hora de tu encomienda:
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/pacifictrans-app/public/js/app.js b/pacifictrans-app/public/js/app.js
index d6ebdcf..190b7bf 100644
--- a/pacifictrans-app/public/js/app.js
+++ b/pacifictrans-app/public/js/app.js
@@ -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 = `🚗💡
Planificación Inteligente: Para llegar a tu destino a las
${chosenTime}, tu vehículo te recogerá aproximadamente a las
${suggested.display} (trayecto ~3 hrs en el corredor).`;
+ }
+ if (poolSummaryScheduleText) {
+ poolSummaryScheduleText.innerHTML = `📅 Programado ${chosenDate || ''}
🏁 Destino: ${chosenTime} | 🚪 Recogida: ~${suggested ? suggested.display : ''}`;
+ }
+ } else {
+ if (poolTimeLabel) poolTimeLabel.textContent = 'Hora deseada de recogida';
+ const estimated = calculateSuggestedTime(chosenTime, +3);
+ if (poolSmartEta && estimated) {
+ poolSmartEta.style.display = 'block';
+ poolSmartEta.innerHTML = `🏁💡
Estimación de Ruta: Si te recogemos a las
${chosenTime}, tu llegada estimada a destino será alrededor de las
${estimated.display} (~3 hrs en carretera).`;
+ }
+ if (poolSummaryScheduleText) {
+ poolSummaryScheduleText.innerHTML = `📅 Programado ${chosenDate || ''}
🚪 Recogida: ${chosenTime} | 🏁 Llegada aprox: ~${estimated ? estimated.display : ''}`;
+ }
+ }
+ }
+
+ // 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 = `📦💡
Planificación Encomienda: Para entregar a las
${chosenTime}, el paquete se recogerá a las
${suggested.display} (~3 hrs Cali-Buenaventura).`;
+ }
+ if (freightSummaryScheduleText) {
+ freightSummaryScheduleText.innerHTML = `📅 ${chosenDate || ''}
🏁 Entrega: ${chosenTime} | 🚪 Recogida: ~${suggested ? suggested.display : ''}`;
+ }
+ } else {
+ if (freightTimeLabel) freightTimeLabel.textContent = 'Hora de recogida del paquete';
+ const estimated = calculateSuggestedTime(chosenTime, +3);
+ if (freightSmartEta && estimated) {
+ freightSmartEta.style.display = 'block';
+ freightSmartEta.innerHTML = `🏁💡
Estimación de Entrega: Si se recoge a las
${chosenTime}, la entrega estimada será a las
${estimated.display}.`;
+ }
+ if (freightSummaryScheduleText) {
+ freightSummaryScheduleText.innerHTML = `📅 ${chosenDate || ''}
🚪 Recogida: ${chosenTime} | 🏁 Entrega aprox: ~${estimated ? estimated.display : ''}`;
+ }
+ }
+ }
+
+ 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
};
diff --git a/scripts/check_docker_nets.py b/scripts/check_docker_nets.py
index 8b064cd..d7d3933 100644
--- a/scripts/check_docker_nets.py
+++ b/scripts/check_docker_nets.py
@@ -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()
diff --git a/scripts/check_pacific_logs.py b/scripts/check_pacific_logs.py
new file mode 100644
index 0000000..59d4d51
--- /dev/null
+++ b/scripts/check_pacific_logs.py
@@ -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()
diff --git a/scripts/inspect_vps_containers.py b/scripts/inspect_vps_containers.py
new file mode 100644
index 0000000..40959c4
--- /dev/null
+++ b/scripts/inspect_vps_containers.py
@@ -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()
diff --git a/scripts/verify_time_modes.py b/scripts/verify_time_modes.py
new file mode 100644
index 0000000..d2fd528
--- /dev/null
+++ b/scripts/verify_time_modes.py
@@ -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.")
+
+
diff --git a/transporte-cali-buenaventura/backend/public/css/style.css b/transporte-cali-buenaventura/backend/public/css/style.css
index 0ae49cc..405fb41 100644
--- a/transporte-cali-buenaventura/backend/public/css/style.css
+++ b/transporte-cali-buenaventura/backend/public/css/style.css
@@ -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;
+}
+
+
diff --git a/transporte-cali-buenaventura/backend/public/index.html b/transporte-cali-buenaventura/backend/public/index.html
index e2e1ae5..d6070e4 100644
--- a/transporte-cali-buenaventura/backend/public/index.html
+++ b/transporte-cali-buenaventura/backend/public/index.html
@@ -242,6 +242,70 @@
+
+
+
+
+ ⚡ Salida Inmediata
+
+
+
+
+
+
+
+
+
+
+
+ 📅 Especifica tu tiempo de viaje:
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -350,6 +414,10 @@
Seguro Contractual:
Póliza y Planilla SOAT
+
+ Modalidad y Horario:
+ ⚡ Inmediato (Próxima Salida)
+
Subtotal:
@@ -453,6 +521,68 @@
+
+
+
+
+ 🚨 Envío Urgente Inmediato
+
+
+
+
+
+
+
+
+
+
+
+ 📅 Especifica fecha y hora de tu encomienda:
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/transporte-cali-buenaventura/backend/public/js/app.js b/transporte-cali-buenaventura/backend/public/js/app.js
index d6ebdcf..190b7bf 100644
--- a/transporte-cali-buenaventura/backend/public/js/app.js
+++ b/transporte-cali-buenaventura/backend/public/js/app.js
@@ -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 = `🚗💡
Planificación Inteligente: Para llegar a tu destino a las
${chosenTime}, tu vehículo te recogerá aproximadamente a las
${suggested.display} (trayecto ~3 hrs en el corredor).`;
+ }
+ if (poolSummaryScheduleText) {
+ poolSummaryScheduleText.innerHTML = `📅 Programado ${chosenDate || ''}
🏁 Destino: ${chosenTime} | 🚪 Recogida: ~${suggested ? suggested.display : ''}`;
+ }
+ } else {
+ if (poolTimeLabel) poolTimeLabel.textContent = 'Hora deseada de recogida';
+ const estimated = calculateSuggestedTime(chosenTime, +3);
+ if (poolSmartEta && estimated) {
+ poolSmartEta.style.display = 'block';
+ poolSmartEta.innerHTML = `🏁💡
Estimación de Ruta: Si te recogemos a las
${chosenTime}, tu llegada estimada a destino será alrededor de las
${estimated.display} (~3 hrs en carretera).`;
+ }
+ if (poolSummaryScheduleText) {
+ poolSummaryScheduleText.innerHTML = `📅 Programado ${chosenDate || ''}
🚪 Recogida: ${chosenTime} | 🏁 Llegada aprox: ~${estimated ? estimated.display : ''}`;
+ }
+ }
+ }
+
+ // 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 = `📦💡
Planificación Encomienda: Para entregar a las
${chosenTime}, el paquete se recogerá a las
${suggested.display} (~3 hrs Cali-Buenaventura).`;
+ }
+ if (freightSummaryScheduleText) {
+ freightSummaryScheduleText.innerHTML = `📅 ${chosenDate || ''}
🏁 Entrega: ${chosenTime} | 🚪 Recogida: ~${suggested ? suggested.display : ''}`;
+ }
+ } else {
+ if (freightTimeLabel) freightTimeLabel.textContent = 'Hora de recogida del paquete';
+ const estimated = calculateSuggestedTime(chosenTime, +3);
+ if (freightSmartEta && estimated) {
+ freightSmartEta.style.display = 'block';
+ freightSmartEta.innerHTML = `🏁💡
Estimación de Entrega: Si se recoge a las
${chosenTime}, la entrega estimada será a las
${estimated.display}.`;
+ }
+ if (freightSummaryScheduleText) {
+ freightSummaryScheduleText.innerHTML = `📅 ${chosenDate || ''}
🚪 Recogida: ${chosenTime} | 🏁 Entrega aprox: ~${estimated ? estimated.display : ''}`;
+ }
+ }
+ }
+
+ 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
};
diff --git a/transporte-cali-buenaventura/backend/src/models/TarifasConfig.ts b/transporte-cali-buenaventura/backend/src/models/TarifasConfig.ts
index 03d1cc6..51c41d9 100644
--- a/transporte-cali-buenaventura/backend/src/models/TarifasConfig.ts
+++ b/transporte-cali-buenaventura/backend/src/models/TarifasConfig.ts
@@ -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
(
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,
diff --git a/transporte-cali-buenaventura/backend/src/models/Trip.ts b/transporte-cali-buenaventura/backend/src/models/Trip.ts
index 2ba5e95..5af6211 100644
--- a/transporte-cali-buenaventura/backend/src/models/Trip.ts
+++ b/transporte-cali-buenaventura/backend/src/models/Trip.ts
@@ -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(
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,
diff --git a/transporte-cali-buenaventura/backend/src/services/admin-approval.service.ts b/transporte-cali-buenaventura/backend/src/services/admin-approval.service.ts
index b62233c..a3ddf1b 100644
--- a/transporte-cali-buenaventura/backend/src/services/admin-approval.service.ts
+++ b/transporte-cali-buenaventura/backend/src/services/admin-approval.service.ts
@@ -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 {
- return Trip.find({ estado: 'PENDING_APPROVAL' })
- .populate('pasajeros.usuarioId', 'nombre telefono')
- .populate('conductorId', 'nombre telefono')
- .sort({ createdAt: 1 })
- .exec();
+ public async getTripsPendientesAprobacion(): Promise {
+ 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
*/
diff --git a/transporte-cali-buenaventura/backend/src/services/in-memory-trips.service.ts b/transporte-cali-buenaventura/backend/src/services/in-memory-trips.service.ts
new file mode 100644
index 0000000..1a687db
--- /dev/null
+++ b/transporte-cali-buenaventura/backend/src/services/in-memory-trips.service.ts
@@ -0,0 +1,40 @@
+export class InMemoryTripsService {
+ private static instance: InMemoryTripsService;
+ private trips: Map = 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 {
+ 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();
diff --git a/transporte-cali-buenaventura/backend/src/services/messaging-flete.service.ts b/transporte-cali-buenaventura/backend/src/services/messaging-flete.service.ts
index e167603..acb165d 100644
--- a/transporte-cali-buenaventura/backend/src/services/messaging-flete.service.ts
+++ b/transporte-cali-buenaventura/backend/src/services/messaging-flete.service.ts
@@ -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 {
- 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 {
- // 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,
};
}
}
diff --git a/transporte-cali-buenaventura/backend/src/services/pooling.service.ts b/transporte-cali-buenaventura/backend/src/services/pooling.service.ts
index a6c53ab..4c1c6d1 100644
--- a/transporte-cali-buenaventura/backend/src/services/pooling.service.ts
+++ b/transporte-cali-buenaventura/backend/src/services/pooling.service.ts
@@ -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';
diff --git a/transporte-cali-buenaventura/backend/src/services/tariffs.service.ts b/transporte-cali-buenaventura/backend/src/services/tariffs.service.ts
index bcf9116..21eda44 100644
--- a/transporte-cali-buenaventura/backend/src/services/tariffs.service.ts
+++ b/transporte-cali-buenaventura/backend/src/services/tariffs.service.ts
@@ -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 {
- 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 {
+ 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 {
+ 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 {
- 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;
}
}
diff --git a/transporte-cali-buenaventura/backend/src/services/whatsapp-notification.service.ts b/transporte-cali-buenaventura/backend/src/services/whatsapp-notification.service.ts
index 680eaee..72a1a91 100644
--- a/transporte-cali-buenaventura/backend/src/services/whatsapp-notification.service.ts
+++ b/transporte-cali-buenaventura/backend/src/services/whatsapp-notification.service.ts
@@ -315,12 +315,16 @@ export class WhatsAppNotificationService {
destino: string;
valorTotal: number;
pasajerosCount: number;
+ modalidad?: string;
+ detalleHorario?: string;
}): Promise {
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` +