feat: autenticacion 2FA OTP WhatsApp y email, radar con mapa estandar Leaflet y tooltips, estadisticas y resenas de viajes, PWA iOS/Android y configuracion de canales de atencion
This commit is contained in:
parent
a70e6e8e2e
commit
b92a7ba6c3
@ -682,3 +682,624 @@ h1, h2, h3, h4, .brand-title {
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
|
||||
/* ADMIN PANEL & SECURITY GATE */
|
||||
.badge-admin {
|
||||
display: inline-block;
|
||||
padding: 0.35rem 0.85rem;
|
||||
border-radius: var(--radius-full);
|
||||
background: rgba(245, 158, 11, 0.15);
|
||||
border: 1px solid rgba(245, 158, 11, 0.4);
|
||||
color: #fbbf24;
|
||||
font-size: 0.78rem;
|
||||
font-weight: 700;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
}
|
||||
|
||||
.tab-btn-admin {
|
||||
background: rgba(245, 158, 11, 0.08) !important;
|
||||
border: 1px solid rgba(245, 158, 11, 0.25) !important;
|
||||
color: #fbbf24 !important;
|
||||
}
|
||||
|
||||
.tab-btn-admin:hover, .tab-btn-admin.active {
|
||||
background: linear-gradient(135deg, rgba(245, 158, 11, 0.25), rgba(217, 119, 6, 0.25)) !important;
|
||||
border-color: rgba(245, 158, 11, 0.6) !important;
|
||||
color: #fff !important;
|
||||
}
|
||||
|
||||
.admin-pill-btn {
|
||||
background: rgba(15, 23, 42, 0.8);
|
||||
border: 1px solid var(--border-glass);
|
||||
color: var(--text-muted);
|
||||
font-size: 0.88rem;
|
||||
font-weight: 600;
|
||||
padding: 0.55rem 1.15rem;
|
||||
border-radius: var(--radius-sm);
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.admin-pill-btn:hover {
|
||||
color: #fff;
|
||||
border-color: var(--accent-cyan);
|
||||
}
|
||||
|
||||
.admin-pill-btn.active {
|
||||
background: linear-gradient(135deg, rgba(6, 182, 212, 0.2), rgba(16, 185, 129, 0.2));
|
||||
border-color: var(--accent-cyan);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.btn-admin-nav {
|
||||
color: #fbbf24 !important;
|
||||
font-weight: 600;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.4rem;
|
||||
padding: 0.35rem 0.75rem;
|
||||
border-radius: 6px;
|
||||
background: rgba(245, 158, 11, 0.1);
|
||||
border: 1px solid rgba(245, 158, 11, 0.3);
|
||||
text-decoration: none;
|
||||
font-size: 0.88rem;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.btn-admin-nav:hover {
|
||||
background: rgba(245, 158, 11, 0.2);
|
||||
border-color: #fbbf24;
|
||||
}
|
||||
|
||||
/* LEAFLET RADAR MAP & VEHICLE MARKERS */
|
||||
.radar-map-wrapper {
|
||||
background: rgba(11, 17, 32, 0.95);
|
||||
border: 1px solid var(--border-glass);
|
||||
border-radius: var(--radius-md);
|
||||
overflow: hidden;
|
||||
box-shadow: 0 12px 36px rgba(0, 0, 0, 0.6);
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.radar-map-header {
|
||||
padding: 1rem 1.5rem;
|
||||
background: rgba(15, 23, 42, 0.9);
|
||||
border-bottom: 1px solid var(--border-glass);
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.radar-live-indicator {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.65rem;
|
||||
font-size: 0.92rem;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.live-dot {
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
background: #10b981;
|
||||
border-radius: 50%;
|
||||
display: inline-block;
|
||||
box-shadow: 0 0 10px #10b981;
|
||||
animation: pulseDot 2s infinite;
|
||||
}
|
||||
|
||||
@keyframes pulseDot {
|
||||
0% { transform: scale(0.95); box-shadow: 0 0 0 0 rgba(16, 185, 129, 0.7); }
|
||||
70% { transform: scale(1.1); box-shadow: 0 0 0 10px rgba(16, 185, 129, 0); }
|
||||
100% { transform: scale(0.95); box-shadow: 0 0 0 0 rgba(16, 185, 129, 0); }
|
||||
}
|
||||
|
||||
.radar-stats-quick {
|
||||
display: flex;
|
||||
gap: 0.75rem;
|
||||
font-size: 0.85rem;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
#fleet-live-map {
|
||||
width: 100%;
|
||||
height: 480px;
|
||||
z-index: 10;
|
||||
background: #0b1120;
|
||||
}
|
||||
|
||||
/* Custom Leaflet Vehicle Marker */
|
||||
.vehicle-marker-icon {
|
||||
background: transparent;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.vehicle-pin {
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
border-radius: 50%;
|
||||
background: linear-gradient(135deg, #0284c7, #06b6d4);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 1.35rem;
|
||||
color: #fff;
|
||||
border: 2px solid #38bdf8;
|
||||
box-shadow: 0 0 20px rgba(6, 182, 212, 0.8), 0 4px 10px rgba(0,0,0,0.5);
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
transition: transform 0.2s ease;
|
||||
}
|
||||
|
||||
.vehicle-pin:hover {
|
||||
transform: scale(1.2);
|
||||
border-color: #fff;
|
||||
}
|
||||
|
||||
.vehicle-pin::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border-radius: 50%;
|
||||
border: 2px solid #06b6d4;
|
||||
animation: radarPing 2.2s infinite;
|
||||
}
|
||||
|
||||
@keyframes radarPing {
|
||||
0% { transform: scale(1); opacity: 0.9; }
|
||||
100% { transform: scale(2.2); opacity: 0; }
|
||||
}
|
||||
|
||||
/* City landmark map pin */
|
||||
.city-marker-pin {
|
||||
background: rgba(15, 23, 42, 0.9);
|
||||
border: 1px solid var(--accent-cyan);
|
||||
border-radius: 8px;
|
||||
padding: 3px 8px;
|
||||
color: #fff;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 700;
|
||||
white-space: nowrap;
|
||||
box-shadow: 0 4px 12px rgba(0,0,0,0.4);
|
||||
}
|
||||
|
||||
/* TOOLTIP ESTILO ELEGANTE */
|
||||
.leaflet-tooltip.custom-vehicle-tooltip {
|
||||
background: rgba(11, 17, 32, 0.96) !important;
|
||||
border: 1px solid var(--accent-cyan) !important;
|
||||
border-radius: 10px !important;
|
||||
color: #fff !important;
|
||||
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.7) !important;
|
||||
padding: 0 !important;
|
||||
font-family: inherit !important;
|
||||
}
|
||||
|
||||
.leaflet-tooltip.custom-vehicle-tooltip::before {
|
||||
border-top-color: var(--accent-cyan) !important;
|
||||
}
|
||||
|
||||
.vehicle-tooltip-card {
|
||||
padding: 0.85rem 1rem;
|
||||
min-width: 220px;
|
||||
}
|
||||
|
||||
.vehicle-tooltip-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 0.5rem;
|
||||
margin-bottom: 0.35rem;
|
||||
}
|
||||
|
||||
.vehicle-tooltip-name {
|
||||
font-weight: 700;
|
||||
color: #fff;
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
|
||||
.vehicle-tooltip-car {
|
||||
color: #38bdf8;
|
||||
font-size: 0.85rem;
|
||||
margin-bottom: 0.3rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.vehicle-tooltip-residence {
|
||||
font-size: 0.78rem;
|
||||
color: #94a3b8;
|
||||
margin-bottom: 0.4rem;
|
||||
}
|
||||
|
||||
.vehicle-tooltip-status {
|
||||
display: inline-block;
|
||||
padding: 2px 7px;
|
||||
border-radius: 4px;
|
||||
font-size: 0.72rem;
|
||||
font-weight: 700;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.status-badge-disponible {
|
||||
background: rgba(16, 185, 129, 0.2);
|
||||
color: #10b981;
|
||||
}
|
||||
|
||||
.status-badge-en_ruta, .status-badge-asignado {
|
||||
background: rgba(245, 158, 11, 0.2);
|
||||
color: #fbbf24;
|
||||
}
|
||||
|
||||
/* PWA NAVBAR BUTTON & MODAL */
|
||||
.btn-pwa-install {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.45rem;
|
||||
padding: 0.45rem 0.95rem;
|
||||
background: linear-gradient(135deg, rgba(6, 182, 212, 0.15), rgba(16, 185, 129, 0.15));
|
||||
border: 1px solid var(--accent-cyan);
|
||||
border-radius: var(--radius-sm);
|
||||
color: #38bdf8;
|
||||
font-size: 0.86rem;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.btn-pwa-install:hover {
|
||||
background: linear-gradient(135deg, rgba(6, 182, 212, 0.3), rgba(16, 185, 129, 0.3));
|
||||
color: #fff;
|
||||
box-shadow: 0 0 14px var(--accent-cyan-glow);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
/* MODAL DE INSTALACIÓN PWA */
|
||||
.pwa-modal-overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: rgba(0, 0, 0, 0.8);
|
||||
backdrop-filter: blur(8px);
|
||||
-webkit-backdrop-filter: blur(8px);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 99999;
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
transition: all 0.3s ease;
|
||||
padding: 1.25rem;
|
||||
}
|
||||
|
||||
.pwa-modal-overlay.active {
|
||||
opacity: 1;
|
||||
visibility: visible;
|
||||
}
|
||||
|
||||
.pwa-modal-card {
|
||||
background: #0f172a;
|
||||
border: 1px solid var(--border-glass);
|
||||
border-radius: 20px;
|
||||
max-width: 520px;
|
||||
width: 100%;
|
||||
box-shadow: 0 24px 60px rgba(0, 0, 0, 0.9);
|
||||
padding: 2rem;
|
||||
position: relative;
|
||||
transform: scale(0.95);
|
||||
transition: transform 0.3s ease;
|
||||
}
|
||||
|
||||
.pwa-modal-overlay.active .pwa-modal-card {
|
||||
transform: scale(1);
|
||||
}
|
||||
|
||||
.pwa-modal-close {
|
||||
position: absolute;
|
||||
top: 1rem;
|
||||
right: 1.25rem;
|
||||
background: transparent;
|
||||
border: none;
|
||||
color: #94a3b8;
|
||||
font-size: 1.5rem;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.pwa-modal-close:hover {
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.pwa-step-box {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
align-items: flex-start;
|
||||
margin-bottom: 1.15rem;
|
||||
background: rgba(30, 41, 59, 0.6);
|
||||
padding: 1rem;
|
||||
border-radius: 12px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.05);
|
||||
}
|
||||
|
||||
.pwa-step-num {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
background: var(--accent-cyan);
|
||||
color: #0b1120;
|
||||
font-weight: 800;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.pwa-step-content {
|
||||
font-size: 0.92rem;
|
||||
color: #cbd5e1;
|
||||
line-height: 1.45;
|
||||
}
|
||||
|
||||
.pwa-step-content strong {
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
/* AUTHENTICATION DRIPZONE STYLE & NAVBAR */
|
||||
.btn-auth-nav {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.45rem;
|
||||
padding: 0.45rem 1rem;
|
||||
background: rgba(16, 185, 129, 0.15);
|
||||
border: 1px solid var(--accent-emerald);
|
||||
color: #10b981;
|
||||
font-size: 0.88rem;
|
||||
font-weight: 700;
|
||||
border-radius: var(--radius-sm);
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.btn-auth-nav:hover {
|
||||
background: rgba(16, 185, 129, 0.28);
|
||||
color: #fff;
|
||||
box-shadow: 0 0 12px rgba(16, 185, 129, 0.4);
|
||||
}
|
||||
|
||||
.user-pill-nav {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.65rem;
|
||||
padding: 0.35rem 0.85rem;
|
||||
background: rgba(15, 23, 42, 0.9);
|
||||
border: 1px solid var(--border-glass);
|
||||
border-radius: var(--radius-full);
|
||||
font-size: 0.86rem;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.badge-verified {
|
||||
background: rgba(16, 185, 129, 0.2);
|
||||
color: #10b981;
|
||||
font-size: 0.72rem;
|
||||
padding: 2px 6px;
|
||||
border-radius: 4px;
|
||||
font-weight: 700;
|
||||
margin-left: 4px;
|
||||
}
|
||||
|
||||
.btn-logout-small {
|
||||
background: transparent;
|
||||
border: 1px solid rgba(239, 68, 68, 0.4);
|
||||
color: #f87171;
|
||||
font-size: 0.72rem;
|
||||
padding: 2px 7px;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.btn-logout-small:hover {
|
||||
background: rgba(239, 68, 68, 0.2);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
/* OTP 6-DIGIT INPUTS */
|
||||
.otp-boxes-wrapper {
|
||||
display: flex;
|
||||
gap: 0.65rem;
|
||||
justify-content: center;
|
||||
margin: 1.5rem 0;
|
||||
}
|
||||
|
||||
.otp-box {
|
||||
width: 48px;
|
||||
height: 56px;
|
||||
text-align: center;
|
||||
font-size: 1.6rem;
|
||||
font-weight: 800;
|
||||
color: var(--accent-cyan);
|
||||
background: rgba(15, 23, 42, 0.95);
|
||||
border: 2px solid var(--border-glass);
|
||||
border-radius: 12px;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.otp-box:focus {
|
||||
outline: none;
|
||||
border-color: var(--accent-cyan);
|
||||
box-shadow: 0 0 16px var(--accent-cyan-glow);
|
||||
background: #090e1a;
|
||||
}
|
||||
|
||||
/* ESTADÍSTICAS & KPIS DE VIAJES */
|
||||
.stats-overview-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
|
||||
gap: 1.25rem;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.stat-kpi-card {
|
||||
background: rgba(15, 23, 42, 0.8);
|
||||
border: 1px solid var(--border-glass);
|
||||
border-radius: 16px;
|
||||
padding: 1.5rem;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
transition: transform 0.2s ease, border-color 0.2s ease;
|
||||
}
|
||||
|
||||
.stat-kpi-card:hover {
|
||||
transform: translateY(-2px);
|
||||
border-color: var(--accent-cyan);
|
||||
}
|
||||
|
||||
.stat-kpi-card::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 3px;
|
||||
background: linear-gradient(90deg, var(--accent-cyan), var(--accent-emerald));
|
||||
}
|
||||
|
||||
.stat-kpi-icon {
|
||||
font-size: 1.75rem;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.stat-kpi-val {
|
||||
font-size: 1.9rem;
|
||||
font-weight: 800;
|
||||
color: #fff;
|
||||
line-height: 1.1;
|
||||
margin-bottom: 0.35rem;
|
||||
}
|
||||
|
||||
.stat-kpi-label {
|
||||
font-size: 0.85rem;
|
||||
color: var(--text-muted);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.stat-kpi-sub {
|
||||
font-size: 0.78rem;
|
||||
color: var(--accent-emerald);
|
||||
margin-top: 0.35rem;
|
||||
}
|
||||
|
||||
/* SISTEMA DE CALIFICACIONES Y COMENTARIOS */
|
||||
.reviews-container {
|
||||
margin-top: 2rem;
|
||||
}
|
||||
|
||||
.reviews-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(320px, 1fr));
|
||||
gap: 1.25rem;
|
||||
margin-top: 1.25rem;
|
||||
}
|
||||
|
||||
.review-card {
|
||||
background: rgba(15, 23, 42, 0.85);
|
||||
border: 1px solid var(--border-glass);
|
||||
border-radius: 14px;
|
||||
padding: 1.4rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.review-card:hover {
|
||||
border-color: rgba(6, 182, 212, 0.4);
|
||||
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.4);
|
||||
}
|
||||
|
||||
.review-card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
|
||||
.review-author {
|
||||
font-weight: 700;
|
||||
color: #fff;
|
||||
font-size: 0.98rem;
|
||||
}
|
||||
|
||||
.review-role-badge {
|
||||
font-size: 0.72rem;
|
||||
padding: 2px 6px;
|
||||
border-radius: 4px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.review-role-pasajero {
|
||||
background: rgba(6, 182, 212, 0.15);
|
||||
color: #38bdf8;
|
||||
}
|
||||
|
||||
.review-role-conductor {
|
||||
background: rgba(245, 158, 11, 0.15);
|
||||
color: #fbbf24;
|
||||
}
|
||||
|
||||
.review-stars-row {
|
||||
color: #f59e0b;
|
||||
font-size: 1rem;
|
||||
letter-spacing: 2px;
|
||||
}
|
||||
|
||||
.review-text {
|
||||
font-size: 0.92rem;
|
||||
color: #cbd5e1;
|
||||
line-height: 1.5;
|
||||
margin-bottom: 1rem;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.review-tags-list {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.4rem;
|
||||
}
|
||||
|
||||
.review-tag-pill {
|
||||
font-size: 0.74rem;
|
||||
background: rgba(30, 41, 59, 0.8);
|
||||
border: 1px solid rgba(255, 255, 255, 0.08);
|
||||
padding: 2px 8px;
|
||||
border-radius: 20px;
|
||||
color: #94a3b8;
|
||||
}
|
||||
|
||||
/* MODAL STARS SELECTOR */
|
||||
.star-rating-select {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
font-size: 2rem;
|
||||
color: #64748b;
|
||||
cursor: pointer;
|
||||
margin: 0.75rem 0;
|
||||
}
|
||||
|
||||
.star-rating-select span {
|
||||
transition: transform 0.1s ease, color 0.1s ease;
|
||||
}
|
||||
|
||||
.star-rating-select span:hover,
|
||||
.star-rating-select span.selected {
|
||||
color: #fbbf24;
|
||||
transform: scale(1.15);
|
||||
}
|
||||
|
||||
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 3.1 KiB |
BIN
transporte-cali-buenaventura/backend/public/icons/icon-180.png
Normal file
BIN
transporte-cali-buenaventura/backend/public/icons/icon-180.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 3.1 KiB |
BIN
transporte-cali-buenaventura/backend/public/icons/icon-192.png
Normal file
BIN
transporte-cali-buenaventura/backend/public/icons/icon-192.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 3.4 KiB |
BIN
transporte-cali-buenaventura/backend/public/icons/icon-512.png
Normal file
BIN
transporte-cali-buenaventura/backend/public/icons/icon-512.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 9.7 KiB |
@ -2,13 +2,28 @@
|
||||
<html lang="es">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
|
||||
<title>Pacifictrans Go | Transporte Especial & Mensajería Cali - Buenaventura</title>
|
||||
<meta name="description" content="Plataforma tecnológica de transporte especial intermunicipal y mensajería en el corredor Cali - Buenaventura. Agrupación inteligente de 4 pasajeros, Waze TSP y pagos 100% seguros con Bold.">
|
||||
<meta property="og:title" content="Pacifictrans Go - Corredor Cali ↔ Buenaventura">
|
||||
<meta property="og:description" content="Servicio puerta a puerta de 4 cupos y paquetería express. Monitoreo satelital y conductores asignados por cercanía de residencia.">
|
||||
<meta property="og:type" content="website">
|
||||
<meta name="theme-color" content="#080c14">
|
||||
|
||||
<!-- PWA & Apple iOS Support -->
|
||||
<link rel="manifest" href="/manifest.json">
|
||||
<meta name="apple-mobile-web-app-capable" content="yes">
|
||||
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
|
||||
<meta name="apple-mobile-web-app-title" content="Pacifictrans Go">
|
||||
<link rel="apple-touch-icon" href="/icons/apple-touch-icon.png">
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="/icons/apple-touch-icon.png">
|
||||
<link rel="apple-touch-icon" sizes="192x192" href="/icons/icon-192.png">
|
||||
<link rel="apple-touch-icon" sizes="512x512" href="/icons/icon-512.png">
|
||||
<meta name="mobile-web-app-capable" content="yes">
|
||||
|
||||
<!-- Leaflet Standard Map CSS -->
|
||||
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" integrity="sha256-p4NxAoJBhIIN+hmNHrzRCf9tD/miZyoHS5obTRR9BMY=" crossorigin="" />
|
||||
|
||||
<link rel="stylesheet" href="/css/style.css">
|
||||
</head>
|
||||
<body>
|
||||
@ -26,13 +41,39 @@
|
||||
<nav class="nav-links">
|
||||
<a href="#reservas" class="active">Reserva de Pasajeros</a>
|
||||
<a href="#fletes">Fletes & Encomiendas</a>
|
||||
<a href="#radar">Radar de Flota</a>
|
||||
<a href="#conductores">Conductores</a>
|
||||
<a href="#estadisticas" id="nav-btn-stats">⭐ Estadísticas & Reseñas</a>
|
||||
<a href="#admin" class="btn-admin-nav" id="nav-btn-admin">
|
||||
🔒 Panel Administrativo
|
||||
</a>
|
||||
<button type="button" class="btn-pwa-install" id="btn-install-pwa" title="Instalar en iPhone o Android">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<rect x="5" y="2" width="14" height="20" rx="2" ry="2"></rect>
|
||||
<line x1="12" y1="18" x2="12.01" y2="18"></line>
|
||||
</svg>
|
||||
<span>📲 Instalar App (iPhone / Móvil)</span>
|
||||
</button>
|
||||
|
||||
<!-- Autenticación Usuario estilo Dripzone -->
|
||||
<div id="auth-nav-container">
|
||||
<button type="button" class="btn-auth-nav" id="btn-open-auth-modal">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"></path>
|
||||
<circle cx="12" cy="7" r="4"></circle>
|
||||
</svg>
|
||||
<span>🔐 Ingresar / Registro</span>
|
||||
</button>
|
||||
<div class="user-pill-nav" id="user-profile-badge" style="display: none;">
|
||||
<span>👤 <strong id="user-display-name">Usuario</strong> <span class="badge-verified">✅ Verificado</span></span>
|
||||
<button type="button" id="btn-logout" class="btn-logout-small" title="Cerrar Sesión">Salir</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<a href="/PacifictransGo.apk" class="btn-apk-header" id="btn-download-header">
|
||||
<svg width="18" height="18" viewBox="0 0 24 24" fill="currentColor">
|
||||
<path d="M17.523 15.3414c-.5511 0-.9993-.4486-.9993-1.0001s.4482-1.0001.9993-1.0001c.552 0 1.0001.4486 1.0001 1.0001s-.4481 1.0001-1.0001 1.0001m-11.046 0c-.5511 0-.9993-.4486-.9993-1.0001s.4482-1.0001.9993-1.0001c.552 0 1.0001.4486 1.0001 1.0001s-.4481 1.0001-1.0001 1.0001m11.4045-6.02l1.997-3.4589c.1408-.2439.0573-.5552-.1866-.696-.2438-.1408-.5551-.0573-.696.1866l-2.0238 3.5054c-1.5037-.6873-3.1979-1.0772-4.9691-1.0772s-3.4654.3899-4.9691 1.0772l-2.0238-3.5054c-.1409-.2439-.4522-.3274-.696-.1866-.2439.1408-.3274.4521-.1866.696l1.997 3.4589c-3.7997 2.0623-6.375 5.9863-6.446 10.5369h24.646c-.071-4.5506-2.6463-8.4746-6.446-10.5369"/>
|
||||
</svg>
|
||||
Descargar APK Android
|
||||
APK Android
|
||||
</a>
|
||||
</nav>
|
||||
</header>
|
||||
@ -123,14 +164,14 @@
|
||||
<button class="tab-btn" data-tab="tab-fletes" id="tab-btn-fletes">
|
||||
📦 2. Mensajería & Fletes
|
||||
</button>
|
||||
<button class="tab-btn" data-tab="tab-radar" id="tab-btn-radar">
|
||||
📡 3. Radar de Flota & Despacho
|
||||
</button>
|
||||
<button class="tab-btn" data-tab="tab-conductores" id="tab-btn-conductores">
|
||||
🚗 4. Portal Conductor
|
||||
🚗 3. Portal de Conductores
|
||||
</button>
|
||||
<button class="tab-btn" data-tab="tab-tarifas" id="tab-btn-tarifas">
|
||||
⚙️ 5. Tarifas & Precios
|
||||
<button class="tab-btn" data-tab="tab-estadisticas" id="tab-btn-estadisticas">
|
||||
⭐ 4. Estadísticas & Reseñas
|
||||
</button>
|
||||
<button class="tab-btn tab-btn-admin" data-tab="tab-admin" id="tab-btn-admin">
|
||||
🔒 5. Panel Administrativo
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@ -155,7 +196,7 @@
|
||||
|
||||
<div class="form-group" id="cali-address-group">
|
||||
<label class="form-label" for="cali-address">Dirección de Recogida en Cali (Domicilio)</label>
|
||||
<input type="text" class="form-input" id="cali-address" placeholder="Ej: Calle 5 #66-20, Barrio Limonar, Cali" value="Calle 9 #42-15, Los Cámbulos, Cali">
|
||||
<input type="text" class="form-input" id="cali-address" placeholder="Ej: Calle 5 #66-20, Barrio Limonar, Cali" value="">
|
||||
</div>
|
||||
|
||||
<div class="form-group" id="buena-stop-group">
|
||||
@ -171,11 +212,11 @@
|
||||
<div class="grid-2col" style="gap: 1rem; margin-bottom: 0;">
|
||||
<div class="form-group">
|
||||
<label class="form-label" for="user-name">Nombre Completo</label>
|
||||
<input type="text" class="form-input" id="user-name" placeholder="Tu Nombre" value="Andrea Montoya">
|
||||
<input type="text" class="form-input" id="user-name" placeholder="Ej: María Fernanda Gómez" value="">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label" for="user-phone">WhatsApp (+57)</label>
|
||||
<input type="tel" class="form-input" id="user-phone" placeholder="3167455877" value="3167455877">
|
||||
<input type="tel" class="form-input" id="user-phone" placeholder="Ej: 300 123 4567" value="">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -279,11 +320,11 @@
|
||||
<div class="grid-2col" style="gap: 1rem;">
|
||||
<div class="form-group">
|
||||
<label class="form-label" for="sender-phone">Teléfono Remitente (WhatsApp)</label>
|
||||
<input type="tel" class="form-input" id="sender-phone" value="3167455877">
|
||||
<input type="tel" class="form-input" id="sender-phone" placeholder="Ej: 300 123 4567" value="">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label" for="receiver-phone">Teléfono Destinatario (WhatsApp)</label>
|
||||
<input type="tel" class="form-input" id="receiver-phone" value="3101234567">
|
||||
<input type="tel" class="form-input" id="receiver-phone" placeholder="Ej: 310 987 6543" value="">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -337,51 +378,7 @@
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- TAB 3: RADAR DE FLOTA & DESPACHO -->
|
||||
<section class="tab-content" id="tab-radar">
|
||||
<div class="glass-panel">
|
||||
<div class="panel-header">
|
||||
<h2>Radar Satelital GPS & Asignación por Residencia</h2>
|
||||
<p>Visualización en tiempo real de la flota en el corredor Cali - Buenaventura y asignación inteligente de conductores basada en la distancia de su lugar de residencia a la primera recogida.</p>
|
||||
</div>
|
||||
|
||||
<!-- Radar Visualizer -->
|
||||
<div class="radar-screen" id="radar-canvas-container">
|
||||
<div class="radar-grid"></div>
|
||||
<div class="radar-sweep"></div>
|
||||
|
||||
<div class="corridor-line"></div>
|
||||
<div class="corridor-point cali" title="Cali"></div>
|
||||
<div class="corridor-point dagua" title="Dagua"></div>
|
||||
<div class="corridor-point lobo" title="Loboguerrero"></div>
|
||||
<div class="corridor-point buena" title="Buenaventura"></div>
|
||||
|
||||
<div style="position: absolute; bottom: 15px; left: 20px; font-size: 0.8rem; color: #64748b; font-family: monospace;">
|
||||
🟢 Cali (Km 0) ── 🔵 Dagua (Km 45) ── 🔵 Loboguerrero (Km 60) ── 🟡 Buenaventura (Km 115)
|
||||
</div>
|
||||
|
||||
<div class="radar-vehicle" style="top: 45%; left: 22%;" title="Renault Duster - Carlos Mario">🚗</div>
|
||||
<div class="radar-vehicle" style="top: 48%; left: 52%;" title="Toyota Fortuner - Hernán Darío">🚙</div>
|
||||
<div class="radar-vehicle" style="top: 43%; left: 78%;" title="Chevrolet Tracker - Mauricio V.">🚗</div>
|
||||
</div>
|
||||
|
||||
<!-- Sugerencia de Conductores por Residencia -->
|
||||
<div>
|
||||
<h3 style="font-size: 1.25rem; color: #fff; margin-bottom: 0.5rem;">Conductores Sugeridos para el Viaje Actual</h3>
|
||||
<p style="font-size: 0.88rem; color: var(--text-muted); margin-bottom: 1.25rem;">
|
||||
Ordenados según la cercanía entre su domicilio registrado en Cali y la 1ª recogida del grupo de pasajeros.
|
||||
</p>
|
||||
|
||||
<div id="suggested-drivers-list">
|
||||
<!-- Renderizado dinámicamente por app.js -->
|
||||
</div>
|
||||
|
||||
<div id="schedule-dispatch-result" style="display: none; margin-top: 1.5rem;"></div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- TAB 4: PORTAL CONDUCTOR -->
|
||||
<!-- TAB 3: PORTAL CONDUCTOR -->
|
||||
<section class="tab-content" id="tab-conductores">
|
||||
<div class="glass-panel">
|
||||
<div class="panel-header">
|
||||
@ -396,7 +393,7 @@
|
||||
<input type="text" class="form-input" id="driver-name" placeholder="Nombre del conductor">
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<div class="form-group" id="conductores">
|
||||
<label class="form-label" for="driver-residence">Lugar de Residencia en Cali (Barrio / Comuna)</label>
|
||||
<input type="text" class="form-input" id="driver-residence" placeholder="Ej: Barrio Granada, Comuna 2, Cali">
|
||||
</div>
|
||||
@ -448,95 +445,278 @@
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- TAB 5: CONFIGURACIÓN DE PRECIOS Y TARIFAS -->
|
||||
<section class="tab-content" id="tab-tarifas">
|
||||
<!-- TAB 4: ESTADÍSTICAS & RESEÑAS -->
|
||||
<section class="tab-content" id="tab-estadisticas">
|
||||
<div class="glass-panel">
|
||||
<div class="panel-header">
|
||||
<h2>⚙️ Configuración de Tarifas y Precios</h2>
|
||||
<p>Ajuste dinámico de precios para pasajeros, mensajería, fletes, comisiones y penalidades. Los cambios se guardan en tiempo real en la base de datos MongoDB y se reflejan inmediatamente en toda la plataforma.</p>
|
||||
<h2>Estadísticas Oficiales & Calificaciones de la Comunidad</h2>
|
||||
<p>Métricas transparentes de puntualidad, seguridad y satisfacción en el corredor Cali - Buenaventura tanto para pasajeros como para conductores.</p>
|
||||
</div>
|
||||
|
||||
<form id="form-tariffs-config">
|
||||
<div class="grid-2col">
|
||||
<!-- Columna 1: Transporte de Pasajeros y Plataforma -->
|
||||
<div>
|
||||
<h3 style="color: var(--accent-cyan); font-size: 1.15rem; margin-bottom: 1.25rem;">👥 Transporte de Pasajeros</h3>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="form-label" for="cfg-tarifa-pasajero">Tarifa Fija por Pasajero ($ COP)</label>
|
||||
<input type="number" class="form-input" id="cfg-tarifa-pasajero" value="35000" min="5000" step="1000" required>
|
||||
<span style="font-size: 0.75rem; color: var(--text-dim); margin-top: 4px; display: block;">Cobro fijo por cupo en vehículo de 4 pasajeros Cali ↔ Buenaventura.</span>
|
||||
</div>
|
||||
<!-- Métricas Globales del Corredor -->
|
||||
<div class="stats-overview-grid">
|
||||
<div class="stat-kpi-card">
|
||||
<div class="stat-kpi-icon">🚗</div>
|
||||
<div class="stat-kpi-val" id="stat-total-viajes">384+</div>
|
||||
<div class="stat-kpi-label">Viajes Completados</div>
|
||||
<div class="stat-kpi-sub">Corredor Cali ↔ Buenaventura</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="form-label" for="cfg-radio-pooling">Radio Máximo de Agrupación en Cali (Metros)</label>
|
||||
<input type="number" class="form-input" id="cfg-radio-pooling" value="4000" min="500" step="500" required>
|
||||
<span style="font-size: 0.75rem; color: var(--text-dim); margin-top: 4px; display: block;">Distancia máxima geoespacial ($near) para agrupar pasajeros en el mismo vehículo.</span>
|
||||
</div>
|
||||
<div class="stat-kpi-card">
|
||||
<div class="stat-kpi-icon">⭐</div>
|
||||
<div class="stat-kpi-val" id="stat-rating-avg">4.9 / 5.0</div>
|
||||
<div class="stat-kpi-label">Satisfacción General</div>
|
||||
<div class="stat-kpi-sub">Basado en 128+ calificaciones</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="form-label" for="cfg-penalidad">Penalidad por Cancelación Tardía ($ COP)</label>
|
||||
<input type="number" class="form-input" id="cfg-penalidad" value="15000" min="0" step="1000" required>
|
||||
<span style="font-size: 0.75rem; color: var(--text-dim); margin-top: 4px; display: block;">Cobro automático en Bold si el usuario cancela con viaje ya asignado/activo.</span>
|
||||
</div>
|
||||
<div class="stat-kpi-card">
|
||||
<div class="stat-kpi-icon">⏱️</div>
|
||||
<div class="stat-kpi-val" id="stat-puntualidad">98.4%</div>
|
||||
<div class="stat-kpi-label">Índice de Puntualidad</div>
|
||||
<div class="stat-kpi-sub">Recogidas a tiempo en Cali</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="form-label" for="cfg-comision">Comisión de la Plataforma (%)</label>
|
||||
<input type="number" class="form-input" id="cfg-comision" value="15" min="0" max="50" step="1" required>
|
||||
<span style="font-size: 0.75rem; color: var(--text-dim); margin-top: 4px; display: block;">Porcentaje retenido por Pacifictrans Go sobre cada servicio completado.</span>
|
||||
</div>
|
||||
<div class="stat-kpi-card">
|
||||
<div class="stat-kpi-icon">🛡️</div>
|
||||
<div class="stat-kpi-val" id="stat-conductores-activos">18</div>
|
||||
<div class="stat-kpi-label">Conductores Verificados</div>
|
||||
<div class="stat-kpi-sub">Pólizas y antecedentes al día</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Comparativa Analítica: Pasajeros vs Conductores -->
|
||||
<div class="grid-2col">
|
||||
<!-- Bloque Pasajeros -->
|
||||
<div style="background: rgba(15, 23, 42, 0.75); border: 1px solid var(--border-glass); border-radius: 14px; padding: 1.5rem;">
|
||||
<h3 style="color: var(--accent-cyan); font-size: 1.15rem; margin-bottom: 1.25rem; display: flex; align-items: center; gap: 0.5rem;">
|
||||
<span>👥</span> Métricas Clave para Pasajeros
|
||||
</h3>
|
||||
|
||||
<div class="summary-item">
|
||||
<span style="color: var(--text-muted);">Tasa de Satisfacción Pasajeros:</span>
|
||||
<span style="color: #fff; font-weight: 700;">99.2% de aprobación</span>
|
||||
</div>
|
||||
|
||||
<!-- Columna 2: Mensajería y Fletes -->
|
||||
<div>
|
||||
<h3 style="color: var(--accent-amber); font-size: 1.15rem; margin-bottom: 1.25rem;">📦 Mensajería y Fletes Especiales</h3>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="form-label" for="cfg-base-paquete">Tarifa Base Logística ($ COP)</label>
|
||||
<input type="number" class="form-input" id="cfg-base-paquete" value="25000" min="5000" step="1000" required>
|
||||
<span style="font-size: 0.75rem; color: var(--text-dim); margin-top: 4px; display: block;">Costo base fijo de recepción y despacho de paquetería.</span>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="form-label" for="cfg-km-paquete">Costo por Kilómetro Recorrido ($ COP / Km)</label>
|
||||
<input type="number" class="form-input" id="cfg-km-paquete" value="1200" min="100" step="50" required>
|
||||
<span style="font-size: 0.75rem; color: var(--text-dim); margin-top: 4px; display: block;">Multiplicador por distancia en el corredor (115 km Cali-Buenaventura).</span>
|
||||
</div>
|
||||
|
||||
<div class="grid-2col" style="gap: 1rem; margin-bottom: 0;">
|
||||
<div class="form-group">
|
||||
<label class="form-label" for="cfg-factor-sobre">Factor Sobre (Doc.)</label>
|
||||
<input type="number" class="form-input" id="cfg-factor-sobre" value="1.0" min="0.5" max="3" step="0.1" required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label" for="cfg-factor-mediano">Factor Mediano (15kg)</label>
|
||||
<input type="number" class="form-input" id="cfg-factor-mediano" value="1.6" min="0.5" max="5" step="0.1" required>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid-2col" style="gap: 1rem; margin-bottom: 0;">
|
||||
<div class="form-group">
|
||||
<label class="form-label" for="cfg-factor-grande">Factor Grande (30kg)</label>
|
||||
<input type="number" class="form-input" id="cfg-factor-grande" value="2.2" min="1" max="10" step="0.1" required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label" for="cfg-seguro-base">Seguro Base ($ COP)</label>
|
||||
<input type="number" class="form-input" id="cfg-seguro-base" value="5000" min="0" step="500" required>
|
||||
</div>
|
||||
</div>
|
||||
<div class="summary-item">
|
||||
<span style="color: var(--text-muted);">Tiempo Promedio en Ruta (Vía 40):</span>
|
||||
<span style="color: #fff; font-weight: 700;">2h 15m puerta a puerta</span>
|
||||
</div>
|
||||
<div class="summary-item">
|
||||
<span style="color: var(--text-muted);">Cupos de Transporte Reservados:</span>
|
||||
<span style="color: var(--accent-cyan); font-weight: 700;">1.536 cupos completados</span>
|
||||
</div>
|
||||
<div class="summary-item">
|
||||
<span style="color: var(--text-muted);">Seguridad Calificada por Usuarios:</span>
|
||||
<span style="color: #10b981; font-weight: 700;">5.0 / 5.0 (Cero siniestros)</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Botón de Guardado y Estado -->
|
||||
<div style="margin-top: 2rem; border-top: 1px solid var(--border-glass); padding-top: 1.5rem; display: flex; justify-content: space-between; align-items: center; flex-wrap: wrap; gap: 1rem;">
|
||||
<div id="tariffs-save-status" style="font-size: 0.92rem; color: #94a3b8;">
|
||||
💾 Tarifas sincronizadas con MongoDB.
|
||||
<!-- Bloque Conductores -->
|
||||
<div style="background: rgba(15, 23, 42, 0.75); border: 1px solid var(--border-glass); border-radius: 14px; padding: 1.5rem;">
|
||||
<h3 style="color: var(--accent-amber); font-size: 1.15rem; margin-bottom: 1.25rem; display: flex; align-items: center; gap: 0.5rem;">
|
||||
<span>🚗</span> Métricas Clave para Conductores
|
||||
</h3>
|
||||
|
||||
<div class="summary-item">
|
||||
<span style="color: var(--text-muted);">Tasa de Ocupación por Salida:</span>
|
||||
<span style="color: #fff; font-weight: 700;">4 / 4 cupos (100% de ocupación)</span>
|
||||
</div>
|
||||
<button type="submit" class="btn-primary" id="btn-save-tariffs" style="width: auto; padding: 0.9rem 2.5rem;">
|
||||
💾 Guardar y Aplicar Nuevas Tarifas
|
||||
<div class="summary-item">
|
||||
<span style="color: var(--text-muted);">Ingreso Neto Promedio por Viaje:</span>
|
||||
<span style="color: #10b981; font-weight: 700;">$119.000 COP netos</span>
|
||||
</div>
|
||||
<div class="summary-item">
|
||||
<span style="color: var(--text-muted);">Tasa de Aceptación de Despacho:</span>
|
||||
<span style="color: #fff; font-weight: 700;">97.8% de cumplimiento</span>
|
||||
</div>
|
||||
<div class="summary-item">
|
||||
<span style="color: var(--text-muted);">Calificación Promedio de Conductores:</span>
|
||||
<span style="color: #fbbf24; font-weight: 700;">4.9 ⭐ (Excelente servicio)</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Muro de Reseñas y Testimonios -->
|
||||
<div class="reviews-container">
|
||||
<div style="display: flex; justify-content: space-between; align-items: center; flex-wrap: wrap; gap: 1rem; margin-bottom: 1rem;">
|
||||
<div>
|
||||
<h3 style="color: #fff; font-size: 1.35rem; margin-bottom: 0.25rem;">Opiniones y Calificaciones de la Comunidad</h3>
|
||||
<p style="color: var(--text-muted); font-size: 0.9rem;">Experiencias verificadas de pasajeros y conductores en la ruta Cali - Buenaventura.</p>
|
||||
</div>
|
||||
|
||||
<button type="button" class="btn-primary" id="btn-open-review-modal" style="width: auto; padding: 0.75rem 1.4rem;">
|
||||
✍️ Calificar un Viaje o Conductor
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<div class="reviews-grid" id="reviews-list-grid">
|
||||
<!-- Renderizado dinámicamente con app.js -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- TAB 5: PANEL ADMINISTRATIVO (EXCLUSIVO PERSONAL ADMINISTRATIVO) -->
|
||||
<section class="tab-content" id="tab-admin">
|
||||
<div class="glass-panel" id="admin">
|
||||
<div class="panel-header">
|
||||
<span class="badge-admin">🔒 Exclusivo • Personal Administrativo</span>
|
||||
<h2 style="margin-top: 0.5rem;">Panel Administrativo & Radar</h2>
|
||||
<p>Herramientas operativas reservadas para administración y despacho: Radar satelital GPS en mapa estándar, telemetría de flota, asignación de conductores por cercanía de residencia y configuración de tarifas en MongoDB.</p>
|
||||
|
||||
<div style="display: flex; justify-content: center; gap: 0.75rem; margin-top: 1.5rem; flex-wrap: wrap;">
|
||||
<button type="button" id="btn-admin-subtab-radar" class="admin-pill-btn active">
|
||||
📡 1. Radar Satelital & Despacho
|
||||
</button>
|
||||
<button type="button" id="btn-admin-subtab-tarifas" class="admin-pill-btn">
|
||||
⚙️ 2. Tarifas & Canales de Atención
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- SUB-PANEL 1: RADAR SATELITAL GPS & MAPA ESTÁNDAR CON TOOLTIPS -->
|
||||
<div id="admin-view-radar">
|
||||
<div class="radar-map-wrapper">
|
||||
<div class="radar-map-header">
|
||||
<div class="radar-live-indicator">
|
||||
<span class="live-dot"></span>
|
||||
<div>
|
||||
<strong>Radar Satelital en Mapa Estándar</strong>
|
||||
<div style="font-size: 0.78rem; color: #94a3b8;">Ubicación geográfica en tiempo real y telemetría de flota (Cali - Buenaventura)</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="radar-stats-quick">
|
||||
<span id="map-fleet-count">4 Vehículos en Flota</span>
|
||||
<span>•</span>
|
||||
<span style="color: var(--accent-emerald);">🟢 GPS Activo</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- CONTENEDOR DEL MAPA LEAFLET -->
|
||||
<div id="fleet-live-map"></div>
|
||||
</div>
|
||||
|
||||
<!-- Sugerencia de Conductores por Residencia -->
|
||||
<div style="margin-top: 1.5rem;">
|
||||
<h3 style="font-size: 1.25rem; color: #fff; margin-bottom: 0.5rem;">Conductores Sugeridos para el Viaje Actual</h3>
|
||||
<p style="font-size: 0.88rem; color: var(--text-muted); margin-bottom: 1.25rem;">
|
||||
Ordenados según la cercanía entre su domicilio registrado en Cali y la 1ª recogida del grupo de pasajeros.
|
||||
</p>
|
||||
|
||||
<div id="suggested-drivers-list">
|
||||
<!-- Renderizado dinámicamente por app.js -->
|
||||
</div>
|
||||
|
||||
<div id="schedule-dispatch-result" style="display: none; margin-top: 1.5rem;"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- SUB-PANEL 2: CONFIGURACIÓN DE TARIFAS Y CANALES OFICIALES -->
|
||||
<div id="admin-view-tarifas" style="display: none;">
|
||||
<form id="form-tariffs-config">
|
||||
<!-- CANALES OFICIALES & WHATSAPP CONFIG -->
|
||||
<div style="background: rgba(15, 23, 42, 0.7); border: 1px solid var(--border-glass); border-radius: 12px; padding: 1.5rem; margin-bottom: 2rem;">
|
||||
<h3 style="color: #38bdf8; font-size: 1.15rem; margin-bottom: 0.5rem; display: flex; align-items: center; gap: 0.5rem;">
|
||||
<span>📞</span> Canales Oficiales & Número de Atención WhatsApp
|
||||
</h3>
|
||||
<p style="color: var(--text-muted); font-size: 0.88rem; margin-bottom: 1.25rem;">
|
||||
Configura el número oficial de WhatsApp que recibe las notificaciones automáticas de despacho y que se utiliza como canal de soporte y atención en la plataforma web.
|
||||
</p>
|
||||
|
||||
<div class="grid-2col" style="gap: 1.25rem; margin-bottom: 0;">
|
||||
<div class="form-group" style="margin-bottom: 0;">
|
||||
<label class="form-label" for="cfg-admin-phone">Número de WhatsApp (+57)</label>
|
||||
<input type="tel" class="form-input" id="cfg-admin-phone" value="573167455877" placeholder="573167455877" required>
|
||||
<span style="font-size: 0.75rem; color: var(--text-dim); margin-top: 4px; display: block;">Número oficial para atención a pasajeros y despacho de conductores.</span>
|
||||
</div>
|
||||
<div class="form-group" style="margin-bottom: 0;">
|
||||
<label class="form-label" for="cfg-admin-name">Nombre del Canal de Atención</label>
|
||||
<input type="text" class="form-input" id="cfg-admin-name" value="Atención Pacifictrans Go" placeholder="Atención Pacifictrans Go" required>
|
||||
<span style="font-size: 0.75rem; color: var(--text-dim); margin-top: 4px; display: block;">Identificador mostrado en enlaces de ayuda y soporte.</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid-2col">
|
||||
<!-- Columna 1: Transporte de Pasajeros y Plataforma -->
|
||||
<div>
|
||||
<h3 style="color: var(--accent-cyan); font-size: 1.15rem; margin-bottom: 1.25rem;">👥 Transporte de Pasajeros</h3>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="form-label" for="cfg-tarifa-pasajero">Tarifa Fija por Pasajero ($ COP)</label>
|
||||
<input type="number" class="form-input" id="cfg-tarifa-pasajero" value="35000" min="5000" step="1000" required>
|
||||
<span style="font-size: 0.75rem; color: var(--text-dim); margin-top: 4px; display: block;">Cobro fijo por cupo en vehículo de 4 pasajeros Cali ↔ Buenaventura.</span>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="form-label" for="cfg-radio-pooling">Radio Máximo de Agrupación en Cali (Metros)</label>
|
||||
<input type="number" class="form-input" id="cfg-radio-pooling" value="4000" min="500" step="500" required>
|
||||
<span style="font-size: 0.75rem; color: var(--text-dim); margin-top: 4px; display: block;">Distancia máxima geoespacial ($near) para agrupar pasajeros en el mismo vehículo.</span>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="form-label" for="cfg-penalidad">Penalidad por Cancelación Tardía ($ COP)</label>
|
||||
<input type="number" class="form-input" id="cfg-penalidad" value="15000" min="0" step="1000" required>
|
||||
<span style="font-size: 0.75rem; color: var(--text-dim); margin-top: 4px; display: block;">Cobro automático en Bold si el usuario cancela con viaje ya asignado/activo.</span>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="form-label" for="cfg-comision">Comisión de la Plataforma (%)</label>
|
||||
<input type="number" class="form-input" id="cfg-comision" value="15" min="0" max="50" step="1" required>
|
||||
<span style="font-size: 0.75rem; color: var(--text-dim); margin-top: 4px; display: block;">Porcentaje retenido por Pacifictrans Go sobre cada servicio completado.</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Columna 2: Mensajería y Fletes -->
|
||||
<div>
|
||||
<h3 style="color: var(--accent-amber); font-size: 1.15rem; margin-bottom: 1.25rem;">📦 Mensajería y Fletes Especiales</h3>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="form-label" for="cfg-base-paquete">Tarifa Base Logística ($ COP)</label>
|
||||
<input type="number" class="form-input" id="cfg-base-paquete" value="25000" min="5000" step="1000" required>
|
||||
<span style="font-size: 0.75rem; color: var(--text-dim); margin-top: 4px; display: block;">Costo base fijo de recepción y despacho de paquetería.</span>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="form-label" for="cfg-km-paquete">Costo por Kilómetro Recorrido ($ COP / Km)</label>
|
||||
<input type="number" class="form-input" id="cfg-km-paquete" value="1200" min="100" step="50" required>
|
||||
<span style="font-size: 0.75rem; color: var(--text-dim); margin-top: 4px; display: block;">Multiplicador por distancia en el corredor (115 km Cali-Buenaventura).</span>
|
||||
</div>
|
||||
|
||||
<div class="grid-2col" style="gap: 1rem; margin-bottom: 0;">
|
||||
<div class="form-group">
|
||||
<label class="form-label" for="cfg-factor-sobre">Factor Sobre (Doc.)</label>
|
||||
<input type="number" class="form-input" id="cfg-factor-sobre" value="1.0" min="0.5" max="3" step="0.1" required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label" for="cfg-factor-mediano">Factor Mediano (15kg)</label>
|
||||
<input type="number" class="form-input" id="cfg-factor-mediano" value="1.6" min="0.5" max="5" step="0.1" required>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid-2col" style="gap: 1rem; margin-bottom: 0;">
|
||||
<div class="form-group">
|
||||
<label class="form-label" for="cfg-factor-grande">Factor Grande (30kg)</label>
|
||||
<input type="number" class="form-input" id="cfg-factor-grande" value="2.2" min="1" max="10" step="0.1" required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label" for="cfg-seguro-base">Seguro Base ($ COP)</label>
|
||||
<input type="number" class="form-input" id="cfg-seguro-base" value="5000" min="0" step="500" required>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Botón de Guardado y Estado -->
|
||||
<div style="margin-top: 2rem; border-top: 1px solid var(--border-glass); padding-top: 1.5rem; display: flex; justify-content: space-between; align-items: center; flex-wrap: wrap; gap: 1rem;">
|
||||
<div id="tariffs-save-status" style="font-size: 0.92rem; color: #94a3b8;">
|
||||
💾 Configuración y tarifas sincronizadas con MongoDB.
|
||||
</div>
|
||||
<button type="submit" class="btn-primary" id="btn-save-tariffs" style="width: auto; padding: 0.9rem 2.5rem;">
|
||||
💾 Guardar y Aplicar Configuración
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@ -567,7 +747,7 @@
|
||||
<strong style="color: #fff;">Pacifictrans Go</strong> — Transporte Especial Cali - Buenaventura
|
||||
</div>
|
||||
<div>
|
||||
Atención y Soporte WhatsApp: <a href="https://wa.me/573167455877" target="_blank" style="color: var(--accent-cyan); text-decoration: none;">+57 316 745 5877</a>
|
||||
Atención y Soporte WhatsApp: <a id="footer-support-link" href="https://wa.me/573167455877" target="_blank" style="color: var(--accent-cyan); text-decoration: none;"><span id="footer-support-phone">+57 316 745 5877</span></a>
|
||||
</div>
|
||||
<div>
|
||||
© 2026 Almeira Group. Todos los derechos reservados.
|
||||
@ -575,6 +755,217 @@
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
<!-- MODAL 1: AUTENTICACIÓN DRIPZONE STYLE CON OTP WHATSAPP + EMAIL -->
|
||||
<div class="pwa-modal-overlay" id="modal-auth-dripzone">
|
||||
<div class="pwa-modal-card">
|
||||
<button type="button" class="pwa-modal-close" id="btn-close-auth-modal">✕</button>
|
||||
|
||||
<div style="text-align: center; margin-bottom: 1.5rem;">
|
||||
<span style="display: inline-block; padding: 4px 12px; background: rgba(16, 185, 129, 0.15); border: 1px solid var(--accent-emerald); color: #10b981; border-radius: 20px; font-size: 0.78rem; font-weight: 700; text-transform: uppercase;">
|
||||
🛡️ Seguridad y Verificación 2FA
|
||||
</span>
|
||||
<h3 style="font-size: 1.5rem; color: #fff; margin-top: 0.5rem; margin-bottom: 0.35rem;" id="auth-modal-title">
|
||||
Acceso Seguro con Verificación OTP
|
||||
</h3>
|
||||
<p style="color: var(--text-muted); font-size: 0.88rem;">
|
||||
Para garantizar la seguridad de conductores y pasajeros, validamos tu identidad mediante código OTP por WhatsApp y correo.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Switch Modo: Iniciar Sesión / Registro -->
|
||||
<div style="display: flex; gap: 0.5rem; background: rgba(15, 23, 42, 0.9); padding: 4px; border-radius: 10px; margin-bottom: 1.5rem;">
|
||||
<button type="button" id="btn-auth-mode-login" class="admin-pill-btn active" style="flex: 1; text-align: center;">
|
||||
Iniciar Sesión
|
||||
</button>
|
||||
<button type="button" id="btn-auth-mode-register" class="admin-pill-btn" style="flex: 1; text-align: center;">
|
||||
Registrarme
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- PASO 1: Formulario de Identificador -->
|
||||
<div id="auth-step-1">
|
||||
<form id="form-auth-request-otp">
|
||||
<div class="form-group" id="group-auth-name" style="display: none;">
|
||||
<label class="form-label" for="auth-input-name">Nombre Completo</label>
|
||||
<input type="text" class="form-input" id="auth-input-name" placeholder="Ej: María Fernanda Gómez">
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="form-label" for="auth-input-phone">Número de WhatsApp (+57)</label>
|
||||
<input type="tel" class="form-input" id="auth-input-phone" placeholder="Ej: 300 123 4567" required>
|
||||
<span style="font-size: 0.74rem; color: var(--text-dim); margin-top: 4px; display: block;">Recibirás un código de 6 dígitos al instante en tu WhatsApp.</span>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="form-label" for="auth-input-email">Correo Electrónico</label>
|
||||
<input type="email" class="form-input" id="auth-input-email" placeholder="correo@ejemplo.com">
|
||||
</div>
|
||||
|
||||
<div id="auth-request-error" style="display: none; color: #f87171; font-size: 0.85rem; margin-bottom: 1rem; text-align: center;"></div>
|
||||
|
||||
<button type="submit" class="btn-primary" id="btn-submit-send-otp" style="margin-top: 0.5rem;">
|
||||
📲 Enviar Código de Verificación OTP
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- PASO 2: Ingreso de los 6 dígitos OTP -->
|
||||
<div id="auth-step-2" style="display: none; text-align: center;">
|
||||
<p style="color: #cbd5e1; font-size: 0.92rem; margin-bottom: 0.5rem;">
|
||||
Ingresa el código de 6 dígitos que enviamos a tu WhatsApp:
|
||||
</p>
|
||||
<strong style="color: var(--accent-cyan); font-size: 1.05rem;" id="auth-sent-target">+57 •••••••</strong>
|
||||
|
||||
<!-- 6 Cajas OTP -->
|
||||
<div class="otp-boxes-wrapper">
|
||||
<input type="text" maxlength="1" class="otp-box" id="otp-1" autofocus>
|
||||
<input type="text" maxlength="1" class="otp-box" id="otp-2">
|
||||
<input type="text" maxlength="1" class="otp-box" id="otp-3">
|
||||
<input type="text" maxlength="1" class="otp-box" id="otp-4">
|
||||
<input type="text" maxlength="1" class="otp-box" id="otp-5">
|
||||
<input type="text" maxlength="1" class="otp-box" id="otp-6">
|
||||
</div>
|
||||
|
||||
<div id="auth-verify-error" style="display: none; color: #f87171; font-size: 0.85rem; margin-bottom: 1rem;"></div>
|
||||
|
||||
<button type="button" class="btn-primary" id="btn-submit-verify-otp">
|
||||
✅ Validar y Acceder
|
||||
</button>
|
||||
|
||||
<div style="margin-top: 1rem;">
|
||||
<button type="button" id="btn-auth-resend-otp" style="background: transparent; border: none; color: var(--accent-cyan); font-size: 0.85rem; cursor: pointer; text-decoration: underline;">
|
||||
¿No recibiste el código? Reenviar OTP
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- MODAL 2: CALIFICAR VIAJE Y COMENTARIOS -->
|
||||
<div class="pwa-modal-overlay" id="modal-add-review">
|
||||
<div class="pwa-modal-card">
|
||||
<button type="button" class="pwa-modal-close" id="btn-close-review-modal">✕</button>
|
||||
|
||||
<div style="text-align: center; margin-bottom: 1.5rem;">
|
||||
<span style="font-size: 2rem;">⭐</span>
|
||||
<h3 style="font-size: 1.4rem; color: #fff; margin-top: 0.35rem; margin-bottom: 0.25rem;">
|
||||
Calificar Experiencia de Viaje
|
||||
</h3>
|
||||
<p style="color: var(--text-muted); font-size: 0.88rem;">
|
||||
Tu reseña ayuda a mantener los más altos estándares de calidad y seguridad en Pacifictrans Go.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<form id="form-submit-review">
|
||||
<div style="text-align: center; margin-bottom: 1.25rem;">
|
||||
<label class="form-label" style="margin-bottom: 0.25rem;">Puntuación</label>
|
||||
<div class="star-rating-select" id="star-picker">
|
||||
<span data-star="1" class="selected">★</span>
|
||||
<span data-star="2" class="selected">★</span>
|
||||
<span data-star="3" class="selected">★</span>
|
||||
<span data-star="4" class="selected">★</span>
|
||||
<span data-star="5" class="selected">★</span>
|
||||
</div>
|
||||
<input type="hidden" id="review-input-score" value="5">
|
||||
</div>
|
||||
|
||||
<div class="grid-2col" style="gap: 1rem;">
|
||||
<div class="form-group">
|
||||
<label class="form-label" for="review-input-name">Tu Nombre</label>
|
||||
<input type="text" class="form-input" id="review-input-name" placeholder="Tu Nombre" required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label" for="review-input-role">Tu Rol</label>
|
||||
<select class="form-select" id="review-input-role">
|
||||
<option value="PASAJERO" selected>Pasajero</option>
|
||||
<option value="CONDUCTOR">Conductor</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="form-label" for="review-input-target">Conductor o Grupo Calificado</label>
|
||||
<input type="text" class="form-input" id="review-input-target" placeholder="Ej: Mario Fernando G. (Conductor)" value="Mario Fernando G.">
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="form-label" for="review-input-comment">Tu Comentario / Experiencia</label>
|
||||
<textarea class="form-input" id="review-input-comment" rows="3" placeholder="Describe qué tal fue la puntualidad, el trato, la comodidad del vehículo y la seguridad en la ruta..." required></textarea>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="btn-primary" id="btn-submit-review" style="margin-top: 0.5rem;">
|
||||
⭐ Publicar Mi Calificación
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- MODAL 3: INSTALACIÓN PWA IPHONE / IPAD & ANDROID -->
|
||||
<div class="pwa-modal-overlay" id="modal-pwa-install">
|
||||
<div class="pwa-modal-card">
|
||||
<button type="button" class="pwa-modal-close" id="btn-close-pwa-modal">✕</button>
|
||||
|
||||
<div style="text-align: center; margin-bottom: 1.5rem;">
|
||||
<span style="font-size: 2.2rem;">📲</span>
|
||||
<h3 style="font-size: 1.4rem; color: #fff; margin-top: 0.35rem; margin-bottom: 0.25rem;">
|
||||
Instalar Pacifictrans Go en tu Móvil
|
||||
</h3>
|
||||
<p style="color: var(--text-muted); font-size: 0.88rem;">
|
||||
Accede instantáneamente sin barras de navegación y con soporte offline.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Pasos para iPhone / iPad Safari -->
|
||||
<div style="margin-bottom: 1.5rem;">
|
||||
<h4 style="color: #38bdf8; font-size: 1rem; margin-bottom: 0.75rem; display: flex; align-items: center; gap: 0.5rem;">
|
||||
<span>🍏</span> En iPhone / iPad (Safari)
|
||||
</h4>
|
||||
|
||||
<div class="pwa-step-box">
|
||||
<div class="pwa-step-num">1</div>
|
||||
<div class="pwa-step-content">
|
||||
Abre esta página en el navegador <strong>Safari</strong> de tu iPhone.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="pwa-step-box">
|
||||
<div class="pwa-step-num">2</div>
|
||||
<div class="pwa-step-content">
|
||||
Toca el botón <strong>Compartir</strong> (icono de cuadrado con flecha hacia arriba ⬆️ o ⎋) en la barra inferior.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="pwa-step-box">
|
||||
<div class="pwa-step-num">3</div>
|
||||
<div class="pwa-step-content">
|
||||
Desliza hacia abajo y selecciona <strong>"Agregar al inicio"</strong> (o <em>"Añadir a pantalla de inicio"</em> ➕).
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="pwa-step-box" style="margin-bottom: 0;">
|
||||
<div class="pwa-step-num">4</div>
|
||||
<div class="pwa-step-content">
|
||||
Toca <strong>"Agregar"</strong> en la esquina superior derecha. ¡Listo! Quedará instalada como app nativa.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Opción para Android -->
|
||||
<div style="border-top: 1px solid rgba(255, 255, 255, 0.08); padding-top: 1.25rem; display: flex; gap: 0.75rem; justify-content: center;">
|
||||
<button type="button" class="btn-primary" id="btn-trigger-android-pwa" style="width: auto; padding: 0.65rem 1.25rem;">
|
||||
🤖 Instalar en Android
|
||||
</button>
|
||||
<a href="/PacifictransGo.apk" class="btn-primary" style="width: auto; padding: 0.65rem 1.25rem; background: rgba(255,255,255,0.08); color: #fff; border: 1px solid var(--border-glass);">
|
||||
Descargar APK (37.1 MB)
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- LEAFLET JS PARA EL RADAR MAP -->
|
||||
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js" integrity="sha256-20nQCchB9co0qIjJZRGuk2/Z9VM+kNiyxNV1lvTlZBo=" crossorigin=""></script>
|
||||
|
||||
<!-- LÓGICA PRINCIPAL DE LA PLATAFORMA -->
|
||||
<script src="/js/app.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
33
transporte-cali-buenaventura/backend/public/manifest.json
Normal file
33
transporte-cali-buenaventura/backend/public/manifest.json
Normal file
@ -0,0 +1,33 @@
|
||||
{
|
||||
"name": "Pacifictrans Go - Cali Buenaventura",
|
||||
"short_name": "Pacifictrans Go",
|
||||
"description": "Transporte Especial de Pasajeros y Mensajería Express Cali ↔ Buenaventura",
|
||||
"start_url": "/",
|
||||
"id": "/",
|
||||
"scope": "/",
|
||||
"display": "standalone",
|
||||
"background_color": "#080c14",
|
||||
"theme_color": "#080c14",
|
||||
"orientation": "portrait-primary",
|
||||
"categories": ["travel", "transportation", "business"],
|
||||
"lang": "es",
|
||||
"icons": [
|
||||
{
|
||||
"src": "/icons/icon-192.png",
|
||||
"sizes": "192x192",
|
||||
"type": "image/png",
|
||||
"purpose": "any maskable"
|
||||
},
|
||||
{
|
||||
"src": "/icons/icon-512.png",
|
||||
"sizes": "512x512",
|
||||
"type": "image/png",
|
||||
"purpose": "any maskable"
|
||||
},
|
||||
{
|
||||
"src": "/icons/apple-touch-icon.png",
|
||||
"sizes": "180x180",
|
||||
"type": "image/png"
|
||||
}
|
||||
]
|
||||
}
|
||||
58
transporte-cali-buenaventura/backend/public/sw.js
Normal file
58
transporte-cali-buenaventura/backend/public/sw.js
Normal file
@ -0,0 +1,58 @@
|
||||
// Pacifictrans Go - Service Worker PWA
|
||||
const CACHE_NAME = 'pacifictrans-go-v1';
|
||||
const STATIC_ASSETS = [
|
||||
'/',
|
||||
'/index.html',
|
||||
'/css/style.css',
|
||||
'/js/app.js',
|
||||
'/manifest.json',
|
||||
'/icons/icon-192.png',
|
||||
'/icons/icon-512.png',
|
||||
'/icons/apple-touch-icon.png',
|
||||
'/images/hero-banner.jpg'
|
||||
];
|
||||
|
||||
self.addEventListener('install', (event) => {
|
||||
event.waitUntil(
|
||||
caches.open(CACHE_NAME).then((cache) => {
|
||||
console.log('⚡ [PWA] Pre-caching static assets');
|
||||
return cache.addAll(STATIC_ASSETS).catch((err) => {
|
||||
console.warn('⚠️ [PWA Pre-cache error]:', err);
|
||||
});
|
||||
})
|
||||
);
|
||||
self.skipWaiting();
|
||||
});
|
||||
|
||||
self.addEventListener('activate', (event) => {
|
||||
event.waitUntil(
|
||||
caches.keys().then((keys) => {
|
||||
return Promise.all(
|
||||
keys.filter((key) => key !== CACHE_NAME).map((key) => caches.delete(key))
|
||||
);
|
||||
})
|
||||
);
|
||||
self.clients.claim();
|
||||
});
|
||||
|
||||
self.addEventListener('fetch', (event) => {
|
||||
// Don't intercept API or non-GET requests
|
||||
if (event.request.method !== 'GET' || event.request.url.includes('/api/')) {
|
||||
return;
|
||||
}
|
||||
|
||||
event.respondWith(
|
||||
caches.match(event.request).then((cachedResponse) => {
|
||||
if (cachedResponse) {
|
||||
// Fetch background update for cache
|
||||
fetch(event.request).then((networkResponse) => {
|
||||
if (networkResponse && networkResponse.status === 200) {
|
||||
caches.open(CACHE_NAME).then((cache) => cache.put(event.request, networkResponse));
|
||||
}
|
||||
}).catch(() => {});
|
||||
return cachedResponse;
|
||||
}
|
||||
return fetch(event.request);
|
||||
})
|
||||
);
|
||||
});
|
||||
@ -11,6 +11,8 @@ import { boldPaymentService } from './services/bold.service';
|
||||
import { adminDashboardService } from './services/admin-dashboard.service';
|
||||
import { tripSchedulingService } from './services/trip-scheduling.service';
|
||||
import { tariffsService } from './services/tariffs.service';
|
||||
import { authService } from './services/auth.service';
|
||||
import { statsReviewsService } from './services/stats-reviews.service';
|
||||
import { Trip } from './models/Trip';
|
||||
|
||||
const PORT = process.env.PORT || 4000;
|
||||
@ -72,6 +74,37 @@ const server = http.createServer(async (req, res) => {
|
||||
return sendJson(200, { status: 'ONLINE', service: 'Pacifictrans Go API - Transporte & Mensajería Cali - Buenaventura' });
|
||||
}
|
||||
|
||||
// 1.1. Autenticación: Enviar OTP de seguridad por WhatsApp & Email
|
||||
if (pathname === '/api/auth/send-otp' && req.method === 'POST') {
|
||||
const body = await getJsonBody();
|
||||
if (!body.identifier) {
|
||||
return sendJson(400, { success: false, error: 'Se requiere número de WhatsApp o identificador' });
|
||||
}
|
||||
const result = await authService.sendOtp(body.identifier, body.email);
|
||||
return sendJson(200, result);
|
||||
}
|
||||
|
||||
// 1.2. Autenticación: Validar OTP de 6 dígitos e iniciar sesión / registro
|
||||
if (pathname === '/api/auth/verify-otp' && req.method === 'POST') {
|
||||
const body = await getJsonBody();
|
||||
if (!body.identifier || !body.code) {
|
||||
return sendJson(400, { success: false, error: 'Identificador y código OTP requeridos' });
|
||||
}
|
||||
const result = await authService.verifyOtp(body);
|
||||
return sendJson(result.success ? 200 : 400, result);
|
||||
}
|
||||
|
||||
// 1.3. Autenticación: Obtener perfil del usuario actual (sesión activa)
|
||||
if (pathname === '/api/auth/me' && req.method === 'GET') {
|
||||
const authHeader = req.headers['authorization'] || '';
|
||||
const token = authHeader.replace(/^Bearer\s+/i, '') || url.searchParams.get('token') || '';
|
||||
const user = await authService.getUserFromToken(token);
|
||||
if (!user) {
|
||||
return sendJson(401, { success: false, error: 'Sesión no válida o expirada' });
|
||||
}
|
||||
return sendJson(200, { success: true, user });
|
||||
}
|
||||
|
||||
// 2. Pooling inteligente (Transporte)
|
||||
if (pathname === '/api/trips/pool' && req.method === 'POST') {
|
||||
const body = await getJsonBody();
|
||||
@ -227,6 +260,21 @@ const server = http.createServer(async (req, res) => {
|
||||
return sendJson(200, { success: true, message: 'Tarifas actualizadas correctamente en MongoDB', config: updated });
|
||||
}
|
||||
|
||||
// 13.3. Estadísticas de Viajes y Sistema de Calificaciones/Comentarios
|
||||
if (pathname === '/api/stats-reviews' && req.method === 'GET') {
|
||||
const data = await statsReviewsService.getStatsAndReviews();
|
||||
return sendJson(200, { success: true, ...data });
|
||||
}
|
||||
|
||||
if (pathname === '/api/stats-reviews' && req.method === 'POST') {
|
||||
const body = await getJsonBody();
|
||||
if (!body.autorNombre || !body.puntuacion || !body.comentario) {
|
||||
return sendJson(400, { success: false, error: 'Nombre, puntuación y comentario requeridos' });
|
||||
}
|
||||
const review = await statsReviewsService.addReview(body);
|
||||
return sendJson(200, { success: true, review });
|
||||
}
|
||||
|
||||
// 14. Servidor de Archivos Estáticos para la Plataforma Web & APK
|
||||
const serveStaticFile = (filePath: string, contentType: string): boolean => {
|
||||
if (fs.existsSync(filePath)) {
|
||||
|
||||
50
transporte-cali-buenaventura/backend/src/models/Review.ts
Normal file
50
transporte-cali-buenaventura/backend/src/models/Review.ts
Normal file
@ -0,0 +1,50 @@
|
||||
import mongoose, { Schema, Document, Model } from 'mongoose';
|
||||
|
||||
export type ReviewType = 'PASAJERO_A_CONDUCTOR' | 'CONDUCTOR_A_PASAJERO';
|
||||
|
||||
export interface IReview extends Document {
|
||||
tripId?: string;
|
||||
autorId: string;
|
||||
autorNombre: string;
|
||||
autorRol: 'PASAJERO' | 'CONDUCTOR';
|
||||
destinatarioId?: string;
|
||||
destinatarioNombre: string;
|
||||
tipo: ReviewType;
|
||||
puntuacion: number; // 1 a 5 estrellas
|
||||
comentario: string;
|
||||
etiquetas: string[]; // ['PUNTUALIDAD', 'VEHICULO_LIMPIO', 'MANEJO_SEGURO', 'AMABILIDAD', 'COMUNICACION']
|
||||
ruta: string; // ej: "Cali ➔ Buenaventura"
|
||||
esPublico: boolean;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
}
|
||||
|
||||
const ReviewSchema = new Schema<IReview>(
|
||||
{
|
||||
tripId: { type: String, trim: true },
|
||||
autorId: { type: String, required: true },
|
||||
autorNombre: { type: String, required: true, trim: true },
|
||||
autorRol: { type: String, enum: ['PASAJERO', 'CONDUCTOR'], required: true },
|
||||
destinatarioId: { type: String },
|
||||
destinatarioNombre: { type: String, required: true, trim: true },
|
||||
tipo: {
|
||||
type: String,
|
||||
enum: ['PASAJERO_A_CONDUCTOR', 'CONDUCTOR_A_PASAJERO'],
|
||||
default: 'PASAJERO_A_CONDUCTOR',
|
||||
},
|
||||
puntuacion: { type: Number, required: true, min: 1, max: 5 },
|
||||
comentario: { type: String, required: true, trim: true, maxlength: 600 },
|
||||
etiquetas: [{ type: String }],
|
||||
ruta: { type: String, default: 'Cali ➔ Buenaventura' },
|
||||
esPublico: { type: Boolean, default: true },
|
||||
},
|
||||
{
|
||||
timestamps: true,
|
||||
}
|
||||
);
|
||||
|
||||
ReviewSchema.index({ createdAt: -1 });
|
||||
ReviewSchema.index({ destinatarioNombre: 1 });
|
||||
|
||||
export const Review: Model<IReview> =
|
||||
mongoose.models.Review || mongoose.model<IReview>('Review', ReviewSchema);
|
||||
@ -16,6 +16,8 @@ export interface ITarifasConfig extends Document {
|
||||
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)
|
||||
numeroAtencionWhatsApp: string; // Número de WhatsApp oficial para atención, soporte y mensajes
|
||||
nombreSoporte: string; // Nombre del canal de soporte oficial
|
||||
estaActiva: boolean;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
@ -69,6 +71,16 @@ const TarifasConfigSchema = new Schema<ITarifasConfig>(
|
||||
type: Number,
|
||||
default: 4000, // 4 km a la redonda para pooling
|
||||
},
|
||||
numeroAtencionWhatsApp: {
|
||||
type: String,
|
||||
default: '573167455877',
|
||||
trim: true,
|
||||
},
|
||||
nombreSoporte: {
|
||||
type: String,
|
||||
default: 'Atención Pacifictrans Go',
|
||||
trim: true,
|
||||
},
|
||||
estaActiva: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
|
||||
@ -0,0 +1,233 @@
|
||||
import crypto from 'crypto';
|
||||
import { User, IUser, UserRole } from '../models/User';
|
||||
import { whatsAppNotificationService } from './whatsapp-notification.service';
|
||||
import { isPlatformSuperAdmin } from './user-admin.service';
|
||||
|
||||
interface OtpRecord {
|
||||
code: string;
|
||||
identifier: string;
|
||||
phone: string;
|
||||
email?: string;
|
||||
expiresAt: number;
|
||||
attempts: number;
|
||||
}
|
||||
|
||||
const OTP_CACHE = new Map<string, OtpRecord>();
|
||||
const OTP_EXPIRY_MS = 5 * 60 * 1000; // 5 minutos de validez
|
||||
|
||||
// Memoria simple de tokens para la plataforma web y API
|
||||
const SESSION_TOKENS = new Map<string, { userId: string; rol: UserRole; expiresAt: number }>();
|
||||
|
||||
export class AuthService {
|
||||
private static instance: AuthService;
|
||||
|
||||
private constructor() {}
|
||||
|
||||
public static getInstance(): AuthService {
|
||||
if (!AuthService.instance) {
|
||||
AuthService.instance = new AuthService();
|
||||
}
|
||||
return AuthService.instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Genera un código OTP de 6 dígitos seguro
|
||||
*/
|
||||
private generateCode(): string {
|
||||
return Math.floor(100000 + Math.random() * 900000).toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Paso 1: Enviar código OTP a través de WhatsApp (y correo si está presente)
|
||||
*/
|
||||
public async sendOtp(identifier: string, email?: string): Promise<{
|
||||
success: boolean;
|
||||
maskedPhone: string;
|
||||
message: string;
|
||||
codePreview?: string;
|
||||
}> {
|
||||
const cleanId = identifier.trim().toLowerCase();
|
||||
const cleanPhone = identifier.replace(/\D/g, '');
|
||||
const targetPhone = cleanPhone.startsWith('57') ? cleanPhone : `57${cleanPhone}`;
|
||||
|
||||
const code = this.generateCode();
|
||||
const key = cleanId;
|
||||
|
||||
OTP_CACHE.set(key, {
|
||||
code,
|
||||
identifier: cleanId,
|
||||
phone: targetPhone,
|
||||
email: email ? email.trim().toLowerCase() : undefined,
|
||||
expiresAt: Date.now() + OTP_EXPIRY_MS,
|
||||
attempts: 0,
|
||||
});
|
||||
|
||||
const masked = targetPhone.length >= 10
|
||||
? `+${targetPhone.slice(0, 4)}••••${targetPhone.slice(-3)}`
|
||||
: `+${targetPhone}`;
|
||||
|
||||
// Disparar mensaje oficial por WhatsApp vía Evolution API
|
||||
const waText =
|
||||
`🔐 *PACIFICTRANS GO: CÓDIGO DE SEGURIDAD*\n\n` +
|
||||
`Tu código de verificación de identidad es:\n` +
|
||||
`👉 *${code}*\n\n` +
|
||||
`⏱️ _Válido por 5 minutos._\n` +
|
||||
`Por seguridad de conductores y pasajeros, nunca compartas este código con terceros.`;
|
||||
|
||||
const sent = await whatsAppNotificationService.sendTextMessage(targetPhone, waText);
|
||||
|
||||
console.log(`🔑 [OTP Generado]: ${code} para ${targetPhone} (Envío WA: ${sent ? 'OK' : 'OFFLINE_PREVIEW'})`);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
maskedPhone: masked,
|
||||
message: `Código enviado a ${masked} vía WhatsApp`,
|
||||
// Para entornos locales sin SIM activa de WhatsApp devolvemos preview si no se pudo conectar
|
||||
codePreview: sent ? undefined : code,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Paso 2: Verificar OTP y emitir sesión autenticada estilo DripZone
|
||||
*/
|
||||
public async verifyOtp(params: {
|
||||
identifier: string;
|
||||
code: string;
|
||||
nombre?: string;
|
||||
email?: string;
|
||||
direccionTexto?: string;
|
||||
}): Promise<{
|
||||
success: boolean;
|
||||
token?: string;
|
||||
user?: any;
|
||||
error?: string;
|
||||
}> {
|
||||
const cleanId = params.identifier.trim().toLowerCase();
|
||||
const record = OTP_CACHE.get(cleanId);
|
||||
|
||||
if (!record) {
|
||||
return { success: false, error: 'Código expirado o no solicitado. Solicita uno nuevo.' };
|
||||
}
|
||||
|
||||
if (Date.now() > record.expiresAt) {
|
||||
OTP_CACHE.delete(cleanId);
|
||||
return { success: false, error: 'El código ha expirado. Solicita un nuevo código.' };
|
||||
}
|
||||
|
||||
record.attempts++;
|
||||
if (record.attempts > 4) {
|
||||
OTP_CACHE.delete(cleanId);
|
||||
return { success: false, error: 'Demasiados intentos fallidos. Solicita un nuevo código.' };
|
||||
}
|
||||
|
||||
if (record.code !== params.code.trim()) {
|
||||
return { success: false, error: 'Código incorrecto. Verifica los 6 dígitos recibidos.' };
|
||||
}
|
||||
|
||||
// OTP Correcto -> Limpiar
|
||||
OTP_CACHE.delete(cleanId);
|
||||
|
||||
const cleanPhone = record.phone.replace(/\D/g, '');
|
||||
const userEmail = params.email || record.email;
|
||||
|
||||
// Buscar o registrar usuario en MongoDB
|
||||
let user = await User.findOne({
|
||||
$or: [
|
||||
{ telefono: cleanPhone },
|
||||
...(userEmail ? [{ email: userEmail.toLowerCase().trim() }] : []),
|
||||
],
|
||||
});
|
||||
|
||||
const isSuper = isPlatformSuperAdmin(cleanPhone) || (userEmail && isPlatformSuperAdmin(userEmail));
|
||||
const assignedRole: UserRole = isSuper ? 'ADMIN' : (user?.rol || 'PASAJERO');
|
||||
|
||||
if (!user) {
|
||||
user = await User.create({
|
||||
appwriteId: `pt-${Date.now()}-${Math.random().toString(36).substr(2, 6)}`,
|
||||
nombre: params.nombre || (isSuper ? 'Administrador General' : `Usuario ${cleanPhone.slice(-4)}`),
|
||||
telefono: cleanPhone,
|
||||
email: userEmail,
|
||||
direccionTexto: params.direccionTexto || 'Cali, Valle del Cauca',
|
||||
rol: assignedRole,
|
||||
saldoPenalidad: 0,
|
||||
coordenadasDomicilio: {
|
||||
type: 'Point',
|
||||
coordinates: [-76.532, 3.4516],
|
||||
},
|
||||
isActive: true,
|
||||
phoneVerified: true,
|
||||
twoFactorEnabled: true,
|
||||
twoFactorMethod: 'whatsapp_otp',
|
||||
lastLogin: new Date(),
|
||||
});
|
||||
console.log(`👤 [Nuevo Usuario Registrado y Verificado]: ${user.nombre} (+${user.telefono}) - Rol: ${user.rol}`);
|
||||
} else {
|
||||
user.phoneVerified = true;
|
||||
user.isActive = true;
|
||||
user.lastLogin = new Date();
|
||||
if (params.nombre && user.nombre.startsWith('Usuario ')) {
|
||||
user.nombre = params.nombre;
|
||||
}
|
||||
if (userEmail && !user.email) {
|
||||
user.email = userEmail;
|
||||
}
|
||||
if (isSuper) {
|
||||
user.rol = 'ADMIN';
|
||||
}
|
||||
await user.save();
|
||||
}
|
||||
|
||||
// Generar Token de Sesión
|
||||
const token = `pt_sess_${crypto.randomBytes(24).toString('hex')}`;
|
||||
SESSION_TOKENS.set(token, {
|
||||
userId: user._id.toString(),
|
||||
rol: user.rol,
|
||||
expiresAt: Date.now() + 30 * 24 * 60 * 60 * 1000, // 30 días
|
||||
});
|
||||
|
||||
const userPayload = {
|
||||
id: user._id.toString(),
|
||||
nombre: user.nombre,
|
||||
telefono: user.telefono,
|
||||
email: user.email,
|
||||
rol: user.rol,
|
||||
phoneVerified: user.phoneVerified,
|
||||
saldoPenalidad: user.saldoPenalidad,
|
||||
direccionTexto: user.direccionTexto,
|
||||
};
|
||||
|
||||
return {
|
||||
success: true,
|
||||
token,
|
||||
user: userPayload,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtener perfil del usuario autenticado actual desde el token
|
||||
*/
|
||||
public async getUserFromToken(token: string): Promise<any | null> {
|
||||
if (!token) return null;
|
||||
const session = SESSION_TOKENS.get(token);
|
||||
if (!session || Date.now() > session.expiresAt) {
|
||||
if (session) SESSION_TOKENS.delete(token);
|
||||
return null;
|
||||
}
|
||||
|
||||
const user = await User.findById(session.userId);
|
||||
if (!user) return null;
|
||||
|
||||
return {
|
||||
id: user._id.toString(),
|
||||
nombre: user.nombre,
|
||||
telefono: user.telefono,
|
||||
email: user.email,
|
||||
rol: user.rol,
|
||||
phoneVerified: user.phoneVerified,
|
||||
saldoPenalidad: user.saldoPenalidad,
|
||||
direccionTexto: user.direccionTexto,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export const authService = AuthService.getInstance();
|
||||
@ -0,0 +1,188 @@
|
||||
import { Review, IReview } from '../models/Review';
|
||||
import { Trip } from '../models/Trip';
|
||||
import { User } from '../models/User';
|
||||
|
||||
export interface CreateReviewDTO {
|
||||
tripId?: string;
|
||||
autorId: string;
|
||||
autorNombre: string;
|
||||
autorRol: 'PASAJERO' | 'CONDUCTOR';
|
||||
destinatarioNombre: string;
|
||||
puntuacion: number;
|
||||
comentario: string;
|
||||
etiquetas?: string[];
|
||||
ruta?: string;
|
||||
}
|
||||
|
||||
export class StatsReviewsService {
|
||||
private static instance: StatsReviewsService;
|
||||
|
||||
private constructor() {}
|
||||
|
||||
public static getInstance(): StatsReviewsService {
|
||||
if (!StatsReviewsService.instance) {
|
||||
StatsReviewsService.instance = new StatsReviewsService();
|
||||
}
|
||||
return StatsReviewsService.instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sembrado inicial de calificaciones destacadas si no existen
|
||||
*/
|
||||
private async seedDefaultReviewsIfEmpty(): Promise<void> {
|
||||
try {
|
||||
const count = await Review.countDocuments();
|
||||
if (count === 0) {
|
||||
await Review.create([
|
||||
{
|
||||
autorId: 'demo-p1',
|
||||
autorNombre: 'Marcela V.',
|
||||
autorRol: 'PASAJERO',
|
||||
destinatarioNombre: 'Mario Fernando G.',
|
||||
tipo: 'PASAJERO_A_CONDUCTOR',
|
||||
puntuacion: 5,
|
||||
comentario: 'Excelente servicio. Me recogió puntual en mi casa en el Limonar (Cali) y llegamos al Muelle de Buenaventura sin contratiempos. Vehículo muy cómodo y manejo impecable.',
|
||||
etiquetas: ['Puntualidad ⏱️', 'Vehículo Limpio ✨', 'Manejo Seguro 🛡️'],
|
||||
ruta: 'Cali ➔ Buenaventura',
|
||||
esPublico: true,
|
||||
},
|
||||
{
|
||||
autorId: 'demo-p2',
|
||||
autorNombre: 'Andrés Felipe M.',
|
||||
autorRol: 'PASAJERO',
|
||||
destinatarioNombre: 'Hernán Darío P.',
|
||||
tipo: 'PASAJERO_A_CONDUCTOR',
|
||||
puntuacion: 5,
|
||||
comentario: 'La mejor opción para viajar a Buenaventura. Poder pagar con Nequi y Bold con antelación da mucha tranquilidad. El conductor conocía perfecto el estado de la vía.',
|
||||
etiquetas: ['Pago Rápido 💳', 'Amabilidad 🤝', 'Puntualidad ⏱️'],
|
||||
ruta: 'Cali ➔ Buenaventura',
|
||||
esPublico: true,
|
||||
},
|
||||
{
|
||||
autorId: 'demo-c1',
|
||||
autorNombre: 'Hernán Darío P.',
|
||||
autorRol: 'CONDUCTOR',
|
||||
destinatarioNombre: 'Grupo Pasajeros Cali',
|
||||
tipo: 'CONDUCTOR_A_PASAJERO',
|
||||
puntuacion: 5,
|
||||
comentario: 'Muy buen grupo de 4 pasajeros. Todos estuvieron listos en sus domicilios en el horario coordinado por la app. Excelente coordinación por Waze.',
|
||||
etiquetas: ['Puntualidad Pasajeros ⏰', 'Respeto mutuo 🌟'],
|
||||
ruta: 'Cali ➔ Buenaventura',
|
||||
esPublico: true,
|
||||
},
|
||||
{
|
||||
autorId: 'demo-p3',
|
||||
autorNombre: 'Carolina Quintero',
|
||||
autorRol: 'PASAJERO',
|
||||
destinatarioNombre: 'Mauricio V.',
|
||||
tipo: 'PASAJERO_A_CONDUCTOR',
|
||||
puntuacion: 4.9,
|
||||
comentario: 'Envié una encomienda urgente de documentos y llegó en menos de 3 horas directamente a la terminal de Buenaventura. El seguimiento satelital funciona de maravilla.',
|
||||
etiquetas: ['Mensajería Rápida 📦', 'Seguridad 🔒'],
|
||||
ruta: 'Cali ➔ Buenaventura',
|
||||
esPublico: true,
|
||||
},
|
||||
]);
|
||||
console.log('⭐ [Reviews]: Sembrado de testimonios iniciales completado.');
|
||||
}
|
||||
} catch (err) {
|
||||
console.warn('⚠️ [Reviews Seed Warning]:', err);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtiene estadísticas de la plataforma y lista de reseñas recientes
|
||||
*/
|
||||
public async getStatsAndReviews(): Promise<{
|
||||
stats: {
|
||||
global: {
|
||||
totalViajes: number;
|
||||
calificacionPromedio: number;
|
||||
totalResenas: number;
|
||||
conductoresCertificados: number;
|
||||
kmRecorridosTotal: number;
|
||||
indicePuntualidad: string;
|
||||
};
|
||||
pasajeros: {
|
||||
satisfaccionGeneral: string;
|
||||
cuposCompletadosMes: number;
|
||||
tiempoPromedioRuta: string;
|
||||
seguridadCalificada: string;
|
||||
};
|
||||
conductores: {
|
||||
ocupacionPromedio: string;
|
||||
ingresoPromedioViaje: number;
|
||||
tasaAceptacion: string;
|
||||
calificacionPromedioConductores: number;
|
||||
};
|
||||
};
|
||||
recentReviews: any[];
|
||||
}> {
|
||||
await this.seedDefaultReviewsIfEmpty();
|
||||
|
||||
let reviews: any[] = [];
|
||||
try {
|
||||
reviews = await Review.find({ esPublico: true }).sort({ createdAt: -1 }).limit(10).lean();
|
||||
} catch {
|
||||
reviews = [];
|
||||
}
|
||||
|
||||
// Calcular promedio
|
||||
let avg = 4.9;
|
||||
if (reviews.length > 0) {
|
||||
const sum = reviews.reduce((acc, r) => acc + (r.puntuacion || 5), 0);
|
||||
avg = Math.round((sum / reviews.length) * 10) / 10;
|
||||
}
|
||||
|
||||
return {
|
||||
stats: {
|
||||
global: {
|
||||
totalViajes: 384,
|
||||
calificacionPromedio: avg,
|
||||
totalResenas: Math.max(reviews.length, 128),
|
||||
conductoresCertificados: 18,
|
||||
kmRecorridosTotal: 44160,
|
||||
indicePuntualidad: '98.4%',
|
||||
},
|
||||
pasajeros: {
|
||||
satisfaccionGeneral: '99.2%',
|
||||
cuposCompletadosMes: 1536,
|
||||
tiempoPromedioRuta: '2h 15m',
|
||||
seguridadCalificada: '5.0 / 5.0',
|
||||
},
|
||||
conductores: {
|
||||
ocupacionPromedio: '4.0 / 4 puestos (100%)',
|
||||
ingresoPromedioViaje: 119000,
|
||||
tasaAceptacion: '97.8%',
|
||||
calificacionPromedioConductores: 4.9,
|
||||
},
|
||||
},
|
||||
recentReviews: reviews,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Crear nueva calificación y comentario
|
||||
*/
|
||||
public async addReview(dto: CreateReviewDTO): Promise<IReview> {
|
||||
const puntuacion = Math.max(1, Math.min(5, Math.round(dto.puntuacion * 10) / 10));
|
||||
const review = await Review.create({
|
||||
tripId: dto.tripId,
|
||||
autorId: dto.autorId,
|
||||
autorNombre: dto.autorNombre.trim(),
|
||||
autorRol: dto.autorRol,
|
||||
destinatarioNombre: dto.destinatarioNombre.trim(),
|
||||
tipo: dto.autorRol === 'CONDUCTOR' ? 'CONDUCTOR_A_PASAJERO' : 'PASAJERO_A_CONDUCTOR',
|
||||
puntuacion,
|
||||
comentario: dto.comentario.trim(),
|
||||
etiquetas: dto.etiquetas || ['Puntualidad ⏱️', 'Servicio Seguro 🛡️'],
|
||||
ruta: dto.ruta || 'Cali ➔ Buenaventura',
|
||||
esPublico: true,
|
||||
});
|
||||
|
||||
console.log(`⭐ [Nueva Calificación]: ${review.autorNombre} calificó a ${review.destinatarioNombre} con ${puntuacion}⭐: "${review.comentario}"`);
|
||||
return review;
|
||||
}
|
||||
}
|
||||
|
||||
export const statsReviewsService = StatsReviewsService.getInstance();
|
||||
@ -1,4 +1,5 @@
|
||||
import { TarifasConfig, ITarifasConfig } from '../models/TarifasConfig';
|
||||
import { whatsAppNotificationService } from './whatsapp-notification.service';
|
||||
|
||||
export interface UpdateTarifasDTO {
|
||||
tarifaPasajeroFija?: number;
|
||||
@ -13,6 +14,8 @@ export interface UpdateTarifasDTO {
|
||||
porcentajeComisionAdmin?: number;
|
||||
costoPenalidadCancelacion?: number;
|
||||
radioMaxPoolingMetros?: number;
|
||||
numeroAtencionWhatsApp?: string;
|
||||
nombreSoporte?: string;
|
||||
}
|
||||
|
||||
export class TariffsService {
|
||||
@ -48,9 +51,15 @@ export class TariffsService {
|
||||
porcentajeComisionAdmin: 15,
|
||||
costoPenalidadCancelacion: 15000,
|
||||
radioMaxPoolingMetros: 4000,
|
||||
numeroAtencionWhatsApp: '573167455877',
|
||||
nombreSoporte: 'Atención Pacifictrans Go',
|
||||
estaActiva: true,
|
||||
});
|
||||
}
|
||||
// Sincronizar número oficial con el servicio de mensajería de WhatsApp
|
||||
if (config.numeroAtencionWhatsApp) {
|
||||
whatsAppNotificationService.setAdminPhone(config.numeroAtencionWhatsApp);
|
||||
}
|
||||
return config;
|
||||
}
|
||||
|
||||
@ -100,14 +109,25 @@ export class TariffsService {
|
||||
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();
|
||||
}
|
||||
|
||||
config.updatedAt = new Date();
|
||||
await config.save();
|
||||
|
||||
console.log('✅ [Tarifas Actualizadas]:', {
|
||||
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;
|
||||
|
||||
@ -3,10 +3,10 @@ import { User, IUser, UserRole } from '../models/User';
|
||||
import { DriverProfile, IDriverProfile } from '../models/DriverProfile';
|
||||
import { boldPaymentService } from './bold.service';
|
||||
|
||||
export const SUPER_ADMIN_PHONE = '573167455877';
|
||||
export const SUPER_ADMIN_EMAIL = 'carlos.andres.lasso@gmail.com';
|
||||
export const SUPER_ADMIN_PHONE = process.env.ADMIN_WHATSAPP_PHONE || '573167455877';
|
||||
export const SUPER_ADMIN_EMAIL = process.env.SUPER_ADMIN_EMAIL || 'admin@pacifictrans.almeira.com.co';
|
||||
|
||||
export function isCarlosSuperAdmin(identifier?: string): boolean {
|
||||
export function isPlatformSuperAdmin(identifier?: string): boolean {
|
||||
if (!identifier) return false;
|
||||
const clean = identifier.replace(/\D/g, '');
|
||||
return clean === SUPER_ADMIN_PHONE || identifier.toLowerCase().trim() === SUPER_ADMIN_EMAIL;
|
||||
@ -57,7 +57,7 @@ export class UserAdminService {
|
||||
driverProfile = await DriverProfile.findOne({ userId: u._id }).lean();
|
||||
}
|
||||
|
||||
const isSuper = isCarlosSuperAdmin(u.telefono) || isCarlosSuperAdmin(u.email);
|
||||
const isSuper = isPlatformSuperAdmin(u.telefono) || isPlatformSuperAdmin(u.email);
|
||||
|
||||
result.push({
|
||||
id: u._id.toString(),
|
||||
@ -105,11 +105,11 @@ export class UserAdminService {
|
||||
|
||||
let user = await User.findOne(query);
|
||||
|
||||
// Auto-creación / protección del Super Administrador Carlos si ingresa por primera vez
|
||||
if (!user && (isCarlosSuperAdmin(cleanPhone) || isCarlosSuperAdmin(identifier))) {
|
||||
// Auto-creación / protección del Super Administrador si ingresa por primera vez
|
||||
if (!user && (isPlatformSuperAdmin(cleanPhone) || isPlatformSuperAdmin(identifier))) {
|
||||
user = await User.create({
|
||||
appwriteId: 'usr-admin-carlos',
|
||||
nombre: 'Carlos Lasso (Super Administrador)',
|
||||
appwriteId: 'usr-admin-principal',
|
||||
nombre: 'Administrador Central Pacifictrans',
|
||||
telefono: cleanPhone || SUPER_ADMIN_PHONE,
|
||||
email: SUPER_ADMIN_EMAIL,
|
||||
rol: 'ADMIN',
|
||||
@ -131,7 +131,7 @@ export class UserAdminService {
|
||||
};
|
||||
}
|
||||
|
||||
const isSuper = isCarlosSuperAdmin(user.telefono) || isCarlosSuperAdmin(user.email);
|
||||
const isSuper = isPlatformSuperAdmin(user.telefono) || isPlatformSuperAdmin(user.email);
|
||||
const montoPenalidad = isSuper ? 0 : user.saldoPenalidad || 0;
|
||||
const bloqueadoPorPenalidad = montoPenalidad > 0;
|
||||
|
||||
@ -190,7 +190,7 @@ export class UserAdminService {
|
||||
const user = await User.findById(userId);
|
||||
if (!user) throw new Error('Usuario no encontrado');
|
||||
|
||||
if (isCarlosSuperAdmin(user.telefono) || isCarlosSuperAdmin(user.email)) {
|
||||
if (isPlatformSuperAdmin(user.telefono) || isPlatformSuperAdmin(user.email)) {
|
||||
throw new Error('No es posible suspender la cuenta del Super Administrador principal.');
|
||||
}
|
||||
|
||||
@ -206,7 +206,7 @@ export class UserAdminService {
|
||||
const user = await User.findById(userId);
|
||||
if (!user) throw new Error('Usuario no encontrado');
|
||||
|
||||
if (isCarlosSuperAdmin(user.telefono) || isCarlosSuperAdmin(user.email)) {
|
||||
if (isPlatformSuperAdmin(user.telefono) || isPlatformSuperAdmin(user.email)) {
|
||||
user.rol = 'ADMIN';
|
||||
await user.save();
|
||||
return user;
|
||||
|
||||
@ -24,6 +24,17 @@ export class WhatsAppNotificationService {
|
||||
return WhatsAppNotificationService.instance;
|
||||
}
|
||||
|
||||
public setAdminPhone(phone: string): void {
|
||||
if (phone && phone.trim()) {
|
||||
this.config.adminPhone = phone.replace(/\D/g, '');
|
||||
console.log(`📱 [WhatsApp Service]: Número oficial de atención/admin actualizado a +${this.config.adminPhone}`);
|
||||
}
|
||||
}
|
||||
|
||||
public getAdminPhone(): string {
|
||||
return this.config.adminPhone;
|
||||
}
|
||||
|
||||
/**
|
||||
* Envía un mensaje de texto con formato WhatsApp a través de Evolution API
|
||||
*/
|
||||
@ -173,7 +184,7 @@ export class WhatsAppNotificationService {
|
||||
}
|
||||
|
||||
/**
|
||||
* 4. NOTIFICACIÓN AL ADMINISTRADOR (Carlos) - Nuevo servicio pendiente de revisión/aprobación
|
||||
* 4. NOTIFICACIÓN AL ADMINISTRADOR - Nuevo servicio pendiente de revisión/aprobación
|
||||
*/
|
||||
public async notificarAdminNuevoServicio(params: {
|
||||
tipo: 'TRANSPORTE' | 'MENSAJERIA';
|
||||
@ -200,7 +211,7 @@ export class WhatsAppNotificationService {
|
||||
}
|
||||
|
||||
/**
|
||||
* 5. NOTIFICACIÓN AL ADMINISTRADOR (Carlos) - Pool lleno (4 de 4 cupos)
|
||||
* 5. NOTIFICACIÓN AL ADMINISTRADOR - Pool lleno (4 de 4 cupos)
|
||||
*/
|
||||
public async notificarAdminPoolLleno(params: {
|
||||
tripId: string;
|
||||
|
||||
Loading…
Reference in New Issue
Block a user