2026-02-21 09:53:31 -05:00
< template >
2026-02-24 10:46:17 -05:00
< div class = "admin-routes" : class = "{ 'with-map': selectedRoute }" >
2026-02-21 09:53:31 -05:00
< div class = "header" >
2026-02-24 10:46:17 -05:00
< button class = "back-link" @click ="handleBack" > & larr ; Volver < / button >
< h1 > Gestión de Rutas < / h1 >
< button v-if = "!selectedRoute" class="add-button" @click="createRoute" >
2026-02-21 09:53:31 -05:00
< span class = "material-icons" > add < / span > Nueva Ruta
< / button >
< / div >
2026-02-24 10:46:17 -05:00
< div class = "main-layout" >
<!-- MAP SIDE ( Visible when editing ) -- >
< div v-if = "selectedRoute" class="map-container" >
< div id = "route-map" class = "route-map" > < / div >
< div class = "map-hint" >
< span class = "material-icons" > info < / span >
Haz clic en el mapa para crear una nueva parada o en una existente para añadirla a la ruta .
2026-02-21 09:53:31 -05:00
< / div >
< / div >
2026-02-24 10:46:17 -05:00
<!-- CONTENT SIDE -- >
< div class = "content-side" >
2026-02-25 10:34:38 -05:00
<!-- Route List & Create Form -- >
2026-02-24 10:46:17 -05:00
< div v-if = "!selectedRoute" class="route-list" >
2026-02-25 10:34:38 -05:00
<!-- Create Form ( Simplified ) -- >
< div v-if = "isCreating" class="create-route-form nexus-glass" >
< div class = "form-header" >
< span class = "material-icons" > route < / span >
< h3 > Nueva Ruta < / h3 >
< / div >
< div class = "form-body" >
< div class = "form-group" >
< label > Punto de Salida < / label >
< input v-model = "newRouteForm.origin" type="text" placeholder="Ej: David" >
< / div >
< div class = "form-group" >
< label > Punto de Llegada < / label >
< input v-model = "newRouteForm.destination" type="text" placeholder="Ej: Boquete" >
< / div >
< div class = "form-group" >
< label > Nombre de Ruta ( Auto ) < / label >
< input :value = "computedRouteName" disabled type = "text" placeholder = "Salida - Llegada" >
< / div >
< / div >
< div class = "form-footer" >
< button class = "cancel-btn" @click ="isCreating = false" > Cancelar < / button >
< button class = "save-btn" :disabled = "!isFormValid" @click ="confirmCreateRoute" >
< span class = "material-icons" > save < / span > Guardar Ruta
< / button >
< / div >
< / div >
<!-- Existing List -- >
2026-02-24 10:46:17 -05:00
< div v-for = "route in routes" :key="route.id" class="route-card" @click="selectRoute(route)" >
< div class = "route-info" >
< h3 > { { route . name } } < / h3 >
< p > { { route . origin _city } } & rarr ; { { route . destination _city } } < / p >
< div class = "status" :class = "route.status" > { { translateStatus ( route . status ) } } < / div >
2026-02-21 09:53:31 -05:00
< / div >
2026-02-24 10:46:17 -05:00
< span class = "material-icons" > chevron _right < / span >
< / div >
2026-02-25 10:34:38 -05:00
< div v-if = "routes.length === 0 && !isCreating" class="empty-state" > No hay rutas registradas. < / div >
2026-02-24 10:46:17 -05:00
< / div >
2026-02-21 09:53:31 -05:00
2026-02-24 10:46:17 -05:00
<!-- Single Route Editor -- >
< div v-else class = "route-editor" >
< div class = "editor-header" >
< h2 > Editar : { { selectedRoute . name } } < / h2 >
< div class = "editor-actions" >
< button class = "delete-route-btn" @click ="deleteRoute" > Eliminar Ruta < / button >
< button class = "close-btn" @click ="selectedRoute = null" > Cerrar < / button >
2026-02-21 09:53:31 -05:00
< / div >
< / div >
2026-02-24 10:46:17 -05:00
< div class = "editor-scroll" >
< section class = "form-section" >
< h3 > Información Básica < / h3 >
< div class = "route-details-form" >
< div class = "form-group" >
< label > Nombre de la Ruta < / label >
< input v-model = "selectedRoute.name" @change="updateRouteDetails" type="text" placeholder="Ej: Boquete - David" >
< / div >
< div class = "form-group" >
< label > Origen < / label >
< input v-model = "selectedRoute.origin_city" @change="updateRouteDetails" type="text" placeholder="David" >
< / div >
< div class = "form-group" >
< label > Destino < / label >
< input v-model = "selectedRoute.destination_city" @change="updateRouteDetails" type="text" placeholder="Boquete" >
< / div >
< div class = "form-group" >
< label > Velocidad Promedio ( km / h ) < / label >
< input v -model .number = " selectedRoute.average_speed_kmh " @change ="updateRouteDetails" type = "number" placeholder = "30" >
< / div >
< div class = "form-group" >
< label > Estado < / label >
< select v-model = "selectedRoute.status" @change="updateRouteDetails" >
< option value = "active" > Activa < / option >
< option value = "inactive" > Inactiva < / option >
< option value = "maintenance" > Mantenimiento < / option >
< / select >
< / div >
< div class = "form-group" >
< label > Color de Línea < / label >
< input v-model = "selectedRoute.color" @change="updateRouteDetails" type="color" >
< / div >
< / div >
< / section >
< section class = "stops-section" >
< div class = "section-header" >
< h3 > Paradas en la Ruta ( { { routeStops . length } } ) < / h3 >
< button class = "save-changes-btn" @click ="selectedRoute = null" > Guardar Todo < / button >
< / div >
< div class = "add-stop-inline" >
< select v-model = "newStopId" >
< option value = "" > Selecciona una parada para añadir ... < / option >
< option v-for = "stop in availableStops" :key="stop.id" :value="stop.id" >
{ { stop . name } }
< / option >
< / select >
< button @click ="addStop" :disabled = "!newStopId" class = "add-btn" > Añadir < / button >
< / div >
< div class = "stops-list-editor" >
< div v-for = "(stop, index) in routeStops" :key="stop.id" class="stop-item" >
< div class = "stop-main" >
< div class = "stop-rank" > { { index + 1 } } < / div >
< div class = "stop-details" >
< div class = "name" > { { stop . name } } < / div >
< div class = "stats" > + { { Math . round ( arrivalTimes [ index ] || 0 ) } } min < / div >
< / div >
< / div >
< div class = "stop-edit-fields" >
< div class = "input-with-label" >
< label > Espera ( min ) < / label >
< input
v - model . number = "stop.stop_delay_minutes"
type = "number"
min = "0"
@ change = "updateStop(stop)"
>
< / div >
< / div >
< div class = "stop-controls" >
< button class = "move-btn" @click ="moveStop(stop, index, -1)" : disabled = "index === 0" >
< span class = "material-icons" > expand _less < / span >
< / button >
< button class = "move-btn" @click ="moveStop(stop, index, 1)" : disabled = "index === routeStops.length - 1" >
< span class = "material-icons" > expand _more < / span >
< / button >
< button class = "remove-btn" @click ="removeStop(stop)" >
< span class = "material-icons" > close < / span >
< / button >
< / div >
< / div >
< div v-if = "routeStops.length === 0" class="no-stops" >
No hay paradas en esta ruta . Usa el mapa o el buscador superior .
< / div >
< / div >
< / section >
< / div >
2026-02-21 09:53:31 -05:00
< / div >
< / div >
< / div >
< / div >
< / template >
< script setup lang = "ts" >
2026-02-24 10:46:17 -05:00
import { ref , onMounted , computed , watch , nextTick } from 'vue'
2026-02-21 09:53:31 -05:00
import { useRouter } from 'vue-router'
import { routesService } from '@/services/routesService'
import { busStopsService } from '@/services/busStopsService'
2026-02-24 10:46:17 -05:00
import { useGoogleMaps } from '@/composables/useGoogleMaps'
2026-02-21 09:53:31 -05:00
import type { Route , BusStop } from '@/types'
2026-02-24 10:46:17 -05:00
const router = useRouter ( )
2026-02-21 09:53:31 -05:00
const routes = ref < Route [ ] > ( [ ] )
const allStops = ref < BusStop [ ] > ( [ ] )
const selectedRoute = ref < Route | null > ( null )
const routeStops = ref < BusStop [ ] > ( [ ] )
const newStopId = ref ( '' )
2026-02-25 10:34:38 -05:00
const isCreating = ref ( false )
const newRouteForm = ref ( {
origin : '' ,
destination : ''
} )
const computedRouteName = computed ( ( ) => {
if ( ! newRouteForm . value . origin || ! newRouteForm . value . destination ) return ''
return ` ${ newRouteForm . value . origin } - ${ newRouteForm . value . destination } `
} )
const isFormValid = computed ( ( ) => {
return newRouteForm . value . origin . trim ( ) !== '' && newRouteForm . value . destination . trim ( ) !== ''
} )
2026-02-21 09:53:31 -05:00
2026-02-24 10:46:17 -05:00
// Map integration
const { initMap , addNumberedMarker , addMarker , addPolyline , clearAllOverlays , isLoaded : mapsLoaded , map : gmap } = useGoogleMaps ( )
2026-02-21 09:53:31 -05:00
onMounted ( async ( ) => {
2026-02-24 10:46:17 -05:00
await loadInitialData ( )
} )
async function loadInitialData ( ) {
2026-02-21 09:53:31 -05:00
routes . value = await routesService . getAllRoutes ( )
allStops . value = await busStopsService . getAllBusStops ( )
2026-02-24 10:46:17 -05:00
}
2026-02-21 09:53:31 -05:00
const availableStops = computed ( ( ) => {
const currentIds = new Set ( routeStops . value . map ( ( s : BusStop ) => s . id ) )
return allStops . value . filter ( ( s : BusStop ) => ! currentIds . has ( s . id ) )
} )
const arrivalTimes = computed ( ( ) => {
if ( ! selectedRoute . value || ! routeStops . value . length ) return [ ]
2026-02-24 10:46:17 -05:00
const speed = selectedRoute . value . average _speed _kmh || 30
2026-02-21 09:53:31 -05:00
const speedKmPerMin = speed / 60
const times : number [ ] = [ ]
2026-02-24 10:46:17 -05:00
let currentTime = 0
2026-02-21 09:53:31 -05:00
times . push ( 0 )
for ( let i = 1 ; i < routeStops . value . length ; i ++ ) {
const prev = routeStops . value [ i - 1 ]
const curr = routeStops . value [ i ]
2026-02-24 10:46:17 -05:00
if ( ! prev || ! curr ) continue ;
2026-02-21 09:53:31 -05:00
currentTime += ( prev . stop _delay _minutes || 0 )
2026-02-24 10:46:17 -05:00
const dist = calculateDistance ( prev . latitude , prev . longitude , curr . latitude , curr . longitude )
currentTime += ( dist / speedKmPerMin )
2026-02-21 09:53:31 -05:00
times . push ( currentTime )
}
return times
} )
2026-02-24 10:46:17 -05:00
// Watchers for Map Rendering
watch ( [ selectedRoute , mapsLoaded ] , ( [ route , loaded ] ) => {
if ( route && loaded ) {
nextTick ( ( ) => {
initRouteMap ( )
} )
}
} )
2026-02-21 09:53:31 -05:00
2026-02-24 10:46:17 -05:00
// Update map when stops change
watch ( routeStops , ( ) => {
if ( selectedRoute . value ) updateMapOverlays ( )
} , { deep : true } )
async function initRouteMap ( ) {
initMap ( 'route-map' , { lat : 8.4284 , lng : - 82.4309 } , 13 )
// Add Click listener to Create New Stops
if ( gmap . value ) {
gmap . value . addListener ( 'click' , async ( e : any ) => {
const lat = e . latLng . lat ( )
const lng = e . latLng . lng ( )
const name = prompt ( 'Crear nueva parada en este punto? Introduce el nombre:' )
if ( name ) {
try {
const newStop = await busStopsService . createBusStop ( {
name ,
latitude : lat ,
longitude : lng ,
stop _type : 'regular' ,
has _shelter : false ,
has _seating : false ,
is _accessible : true ,
city : selectedRoute . value ? . origin _city || 'David'
} )
// Refetch all stops
allStops . value = await busStopsService . getAllBusStops ( )
// Add to current route
await addExistingStop ( newStop . id )
} catch ( err ) {
alert ( 'Error creando parada' )
}
}
} )
}
updateMapOverlays ( )
2026-02-21 09:53:31 -05:00
}
2026-02-24 10:46:17 -05:00
function updateMapOverlays ( ) {
clearAllOverlays ( )
// 1. Draw Polyline
if ( routeStops . value . length > 1 ) {
const path = routeStops . value . map ( s => ( { lat : s . latitude , lng : s . longitude } ) )
addPolyline ( path )
}
// 2. Add Route Markers (Yellow Numbered)
routeStops . value . forEach ( ( stop , index ) => {
addNumberedMarker (
{ lat : stop . latitude , lng : stop . longitude } ,
index + 1 ,
stop . name
)
} )
// 3. Add Available Markers (Small Gray Dots)
availableStops . value . forEach ( stop => {
const marker = addMarker (
{ lat : stop . latitude , lng : stop . longitude } ,
{
title : stop . name ,
icon : {
path : google . maps . SymbolPath . CIRCLE ,
scale : 6 ,
fillColor : '#666' ,
fillOpacity : 0.8 ,
strokeWeight : 1 ,
strokeColor : '#fff'
}
}
)
if ( marker ) {
marker . addListener ( 'click' , ( ) => {
addExistingStop ( stop . id )
} )
}
} )
}
// Actions
function handleBack ( ) {
if ( selectedRoute . value ) {
selectedRoute . value = null
} else {
router . push ( '/admin' )
2026-02-21 09:53:31 -05:00
}
}
async function createRoute ( ) {
2026-02-25 10:34:38 -05:00
isCreating . value = true
newRouteForm . value = { origin : '' , destination : '' }
}
async function confirmCreateRoute ( ) {
2026-02-24 16:39:38 -05:00
try {
2026-02-25 10:34:38 -05:00
await routesService . createRoute ( {
name : computedRouteName . value ,
origin _city : newRouteForm . value . origin ,
destination _city : newRouteForm . value . destination ,
2026-02-25 20:09:51 -05:00
status : 'ACTIVE' ,
2026-02-24 16:39:38 -05:00
color : '#FEE715' ,
direction : 'outbound'
} )
routes . value = await routesService . getAllRoutes ( )
2026-02-25 10:34:38 -05:00
isCreating . value = false
2026-02-24 16:39:38 -05:00
} catch ( err : any ) {
console . error ( 'Error creating route:' , err )
2026-02-25 19:49:29 -05:00
if ( err . response ? . status === 401 ) {
// El interceptor ya redirige al login, pero mostramos aviso
alert ( 'Tu sesión ha expirado. Serás redirigido al inicio de sesión.' )
return
} else if ( err . response ? . status === 403 ) {
alert ( 'No tienes permisos de administrador para crear rutas.' )
return
} else if ( ! err . response && err . request ) {
// Network Error - servidor no respondió
alert ( 'No se pudo conectar al servidor. Si es la primera solicitud del día, el servidor puede tardar ~30 segundos en iniciar. Por favor, intenta de nuevo en un momento.' )
return
}
2026-02-25 11:49:42 -05:00
const errorMsg = err . response ? . data ? . detail
|| err . response ? . data ? . message
|| err . message
|| 'Error desconocido'
2026-02-25 19:49:29 -05:00
alert ( ` No se pudo crear la ruta: ${ errorMsg } ` )
2026-02-24 16:39:38 -05:00
}
2026-02-21 09:53:31 -05:00
}
async function selectRoute ( route : Route ) {
selectedRoute . value = route
routeStops . value = await routesService . getRouteStops ( route . id )
}
async function updateRouteDetails ( ) {
if ( ! selectedRoute . value ) return
2026-02-24 16:39:38 -05:00
try {
await routesService . updateRoute ( selectedRoute . value . id , {
name : selectedRoute . value . name ,
origin _city : selectedRoute . value . origin _city ,
destination _city : selectedRoute . value . destination _city ,
average _speed _kmh : selectedRoute . value . average _speed _kmh ,
status : selectedRoute . value . status ,
color : selectedRoute . value . color
} )
} catch ( err : any ) {
console . error ( 'Error updating route:' , err )
// Opcional: mostrar notificación sutil en lugar de alert recurrente
}
2026-02-24 10:46:17 -05:00
}
async function addStop ( ) {
if ( newStopId . value ) {
await addExistingStop ( newStopId . value )
newStopId . value = ''
2026-02-21 09:53:31 -05:00
}
}
2026-02-24 10:46:17 -05:00
async function addExistingStop ( stopId : string ) {
if ( ! selectedRoute . value ) return
2026-02-24 16:39:38 -05:00
try {
await routesService . addStopToRoute ( selectedRoute . value . id , {
stop _id : stopId ,
stop _order : routeStops . value . length + 1
} )
routeStops . value = await routesService . getRouteStops ( selectedRoute . value . id )
} catch ( err : any ) {
console . error ( 'Error adding stop:' , err )
alert ( 'Error al añadir parada: ' + ( err . response ? . data ? . detail || err . message ) )
}
2026-02-24 10:46:17 -05:00
}
2026-02-21 09:53:31 -05:00
async function updateStop ( stop : BusStop ) {
if ( ! selectedRoute . value ) return
2026-02-24 10:46:17 -05:00
await routesService . updateRouteStop ( selectedRoute . value . id , stop . id , {
stop _delay _minutes : stop . stop _delay _minutes || 0
} )
2026-02-21 09:53:31 -05:00
}
2026-02-24 10:46:17 -05:00
async function moveStop ( stop : BusStop , index : number , direction : number ) {
if ( ! selectedRoute . value ) return
await routesService . updateRouteStop ( selectedRoute . value . id , stop . id , {
stop _order : index + 1 + direction
} )
routeStops . value = await routesService . getRouteStops ( selectedRoute . value . id )
}
async function removeStop ( stop : BusStop ) {
if ( ! selectedRoute . value ) return
if ( confirm ( ` Borrar ${ stop . name } de la ruta? ` ) ) {
await routesService . removeStopFromRoute ( selectedRoute . value . id , stop . id )
2026-02-21 09:53:31 -05:00
routeStops . value = await routesService . getRouteStops ( selectedRoute . value . id )
}
}
2026-02-24 10:46:17 -05:00
async function deleteRoute ( ) {
2026-02-21 09:53:31 -05:00
if ( ! selectedRoute . value ) return
2026-02-24 10:46:17 -05:00
if ( confirm ( ` Estás SEGURO de que quieres eliminar la ruta ${ selectedRoute . value . name } ? Esta acción es permanente. ` ) ) {
2026-02-24 16:39:38 -05:00
try {
await routesService . deleteRoute ( selectedRoute . value . id )
selectedRoute . value = null
routes . value = await routesService . getAllRoutes ( )
} catch ( err : any ) {
console . error ( 'Error deleting route:' , err )
alert ( 'No se pudo eliminar la ruta: ' + ( err . response ? . data ? . detail || err . message ) )
}
2026-02-21 09:53:31 -05:00
}
}
2026-02-24 10:46:17 -05:00
function calculateDistance ( lat1 : number , lon1 : number , lat2 : number , lon2 : number ) {
const R = 6371
const dLat = ( lat2 - lat1 ) * Math . PI / 180
const dLon = ( lon2 - lon1 ) * Math . PI / 180
const a = Math . sin ( dLat / 2 ) * Math . sin ( dLat / 2 ) + Math . cos ( lat1 * Math . PI / 180 ) * Math . cos ( lat2 * Math . PI / 180 ) * Math . sin ( dLon / 2 ) * Math . sin ( dLon / 2 )
return R * 2 * Math . atan2 ( Math . sqrt ( a ) , Math . sqrt ( 1 - a ) )
}
function translateStatus ( s : string ) {
return { active : 'Activa' , inactive : 'Inactiva' , maintenance : 'Mantenimiento' } [ s ] || s
2026-02-21 09:53:31 -05:00
}
< / script >
< style scoped >
. admin - routes {
2026-02-24 10:46:17 -05:00
padding : 24 px ;
background : # 0 f172a ;
2026-02-21 09:53:31 -05:00
min - height : 100 vh ;
2026-02-24 10:46:17 -05:00
color : white ;
2026-02-21 09:53:31 -05:00
}
. header {
display : flex ;
justify - content : space - between ;
align - items : center ;
2026-02-24 10:46:17 -05:00
margin - bottom : 24 px ;
2026-02-21 09:53:31 -05:00
}
2026-02-24 10:46:17 -05:00
h1 { font - size : 1.5 rem ; font - weight : 800 ; color : # FEE715 ; margin : 0 ; }
2026-02-21 09:53:31 -05:00
2026-02-24 10:46:17 -05:00
. back - link {
background : rgba ( 255 , 255 , 255 , 0.05 ) ; border : 1 px solid rgba ( 255 , 255 , 255 , 0.1 ) ;
color : white ; padding : 8 px 16 px ; border - radius : 8 px ; cursor : pointer ;
2026-02-21 09:53:31 -05:00
}
. add - button {
2026-02-24 10:46:17 -05:00
background : # FEE715 ; color : # 101820 ; border : none ; padding : 10 px 20 px ;
border - radius : 10 px ; font - weight : 800 ; cursor : pointer ; display : flex ; align - items : center ; gap : 8 px ;
2026-02-21 09:53:31 -05:00
}
2026-02-24 10:46:17 -05:00
. main - layout {
2026-02-21 09:53:31 -05:00
display : flex ;
gap : 24 px ;
2026-02-24 10:46:17 -05:00
height : calc ( 100 vh - 100 px ) ;
2026-02-21 09:53:31 -05:00
}
2026-02-24 10:46:17 -05:00
. map - container {
flex : 1 ;
position : relative ;
border - radius : 20 px ;
overflow : hidden ;
border : 1 px solid rgba ( 255 , 255 , 255 , 0.1 ) ;
2026-02-21 09:53:31 -05:00
}
2026-02-24 10:46:17 -05:00
. route - map { width : 100 % ; height : 100 % ; }
2026-02-21 09:53:31 -05:00
2026-02-24 10:46:17 -05:00
. map - hint {
position : absolute ; bottom : 16 px ; left : 16 px ; right : 16 px ;
background : rgba ( 0 , 0 , 0 , 0.8 ) ; backdrop - filter : blur ( 8 px ) ;
padding : 12 px ; border - radius : 12 px ; color : # 94 a3b8 ; font - size : 0.8 rem ;
display : flex ; align - items : center ; gap : 8 px ; border : 1 px solid rgba ( 255 , 255 , 255 , 0.1 ) ;
2026-02-21 09:53:31 -05:00
}
2026-02-24 10:46:17 -05:00
. content - side {
width : 400 px ;
2026-02-21 09:53:31 -05:00
display : flex ;
flex - direction : column ;
}
2026-02-24 10:46:17 -05:00
. with - map . content - side { width : 45 % ; }
@ media ( max - width : 1000 px ) {
. main - layout { flex - direction : column ; height : auto ; }
. content - side { width : 100 % ! important ; }
. map - container { height : 400 px ; order : - 1 ; }
2026-02-21 09:53:31 -05:00
}
2026-02-24 10:46:17 -05:00
/* Route List */
. route - list { display : flex ; flex - direction : column ; gap : 12 px ; height : 100 % ; }
. route - card {
background : rgba ( 255 , 255 , 255 , 0.03 ) ; border : 1 px solid rgba ( 255 , 255 , 255 , 0.1 ) ;
padding : 16 px ; border - radius : 16 px ; cursor : pointer ; display : flex ;
justify - content : space - between ; align - items : center ; transition : all 0.2 s ;
}
. route - card : hover { border - color : # FEE715 ; background : rgba ( 254 , 231 , 21 , 0.05 ) ; }
. route - info h3 { margin : 0 ; font - size : 1.1 rem ; }
. route - info p { margin : 4 px 0 10 px ; color : # 94 a3b8 ; font - size : 0.85 rem ; }
. status { font - size : 0.7 rem ; font - weight : 800 ; padding : 4 px 8 px ; border - radius : 6 px ; width : fit - content ; text - transform : uppercase ; }
. status . active { background : rgba ( 34 , 197 , 94 , 0.1 ) ; color : # 4 ade80 ; }
. status . inactive { background : rgba ( 239 , 68 , 68 , 0.1 ) ; color : # f87171 ; }
2026-02-25 10:34:38 -05:00
/* Nexus Create Form */
. create - route - form {
margin - bottom : 24 px ;
padding : 24 px ;
border - radius : 20 px ;
background : rgba ( 30 , 41 , 59 , 0.5 ) ;
}
. nexus - glass {
backdrop - filter : blur ( 20 px ) ;
border : 1 px solid rgba ( 255 , 255 , 255 , 0.1 ) ;
}
. form - header {
display : flex ;
align - items : center ;
gap : 12 px ;
margin - bottom : 20 px ;
color : # FEE715 ;
}
. form - header h3 {
margin : 0 ;
font - size : 1.1 rem ;
font - weight : 800 ;
}
. form - body {
display : flex ;
flex - direction : column ;
gap : 16 px ;
margin - bottom : 24 px ;
}
. form - footer {
display : flex ;
justify - content : flex - end ;
gap : 12 px ;
}
. cancel - btn {
background : transparent ;
border : 1 px solid rgba ( 255 , 255 , 255 , 0.2 ) ;
color : white ;
padding : 10 px 20 px ;
border - radius : 10 px ;
font - weight : 700 ;
cursor : pointer ;
}
. save - btn {
background : # FEE715 ;
color : # 101820 ;
border : none ;
padding : 10 px 20 px ;
border - radius : 10 px ;
font - weight : 800 ;
cursor : pointer ;
display : flex ;
align - items : center ;
gap : 8 px ;
}
. save - btn : disabled {
opacity : 0.5 ;
cursor : not - allowed ;
}
2026-02-24 10:46:17 -05:00
/* Editor View */
. route - editor {
background : rgba ( 255 , 255 , 255 , 0.03 ) ; border : 1 px solid rgba ( 255 , 255 , 255 , 0.1 ) ;
border - radius : 20 px ; height : 100 % ; display : flex ; flex - direction : column ; overflow : hidden ;
2026-02-21 09:53:31 -05:00
}
2026-02-24 10:46:17 -05:00
. editor - header { padding : 20 px ; border - bottom : 1 px solid rgba ( 255 , 255 , 255 , 0.1 ) ; }
. editor - header h2 { margin : 0 0 12 px ; font - size : 1.25 rem ; font - weight : 800 ; }
. editor - actions { display : flex ; gap : 10 px ; }
. close - btn { background : # 334155 ; color : white ; border : none ; padding : 6 px 16 px ; border - radius : 8 px ; cursor : pointer ; }
. delete - route - btn { background : rgba ( 239 , 68 , 68 , 0.1 ) ; color : # f87171 ; border : 1 px solid rgba ( 239 , 68 , 68 , 0.2 ) ; padding : 6 px 16 px ; border - radius : 8 px ; cursor : pointer ; }
2026-02-21 09:53:31 -05:00
2026-02-24 10:46:17 -05:00
. editor - scroll { flex : 1 ; overflow - y : auto ; padding : 20 px ; }
2026-02-21 09:53:31 -05:00
2026-02-24 10:46:17 -05:00
. form - section { margin - bottom : 32 px ; }
. form - section h3 { font - size : 0.9 rem ; text - transform : uppercase ; color : # FEE715 ; margin - bottom : 16 px ; letter - spacing : 0.05 em ; }
2026-02-21 09:53:31 -05:00
2026-02-24 10:46:17 -05:00
. route - details - form { display : grid ; grid - template - columns : 1 fr 1 fr ; gap : 16 px ; }
. form - group { display : flex ; flex - direction : column ; gap : 6 px ; }
. form - group label { font - size : 0.75 rem ; color : # 94 a3b8 ; font - weight : 700 ; }
. form - group input , . form - group select {
background : # 1 e293b ; border : 1 px solid # 334155 ; border - radius : 8 px ; padding : 10 px ; color : white ; font - size : 0.9 rem ;
2026-02-21 09:53:31 -05:00
}
2026-02-24 10:46:17 -05:00
. section - header { display : flex ; justify - content : space - between ; align - items : center ; margin - bottom : 16 px ; }
. save - changes - btn { background : # FEE715 ; color : # 101820 ; border : none ; padding : 8 px 16 px ; border - radius : 8 px ; font - weight : 800 ; cursor : pointer ; }
2026-02-21 09:53:31 -05:00
2026-02-24 10:46:17 -05:00
. add - stop - inline { display : flex ; gap : 8 px ; margin - bottom : 20 px ; }
. add - stop - inline select { flex : 1 ; background : # 1 e293b ; border : 1 px solid # 334155 ; border - radius : 8 px ; padding : 10 px ; color : white ; }
. add - btn { background : # 334155 ; color : white ; border : none ; padding : 0 16 px ; border - radius : 8 px ; cursor : pointer ; }
2026-02-21 09:53:31 -05:00
2026-02-24 10:46:17 -05:00
. stops - list - editor { display : flex ; flex - direction : column ; gap : 8 px ; }
. stop - item {
background : # 1 e293b ; border : 1 px solid # 334155 ; border - radius : 12 px ; padding : 12 px ;
display : flex ; align - items : center ; justify - content : space - between ; gap : 12 px ;
}
. stop - main { display : flex ; align - items : center ; gap : 12 px ; flex : 1 ; }
. stop - rank { width : 28 px ; height : 28 px ; background : # FEE715 ; color : # 101820 ; border - radius : 50 % ; display : flex ; align - items : center ; justify - content : center ; font - weight : 900 ; font - size : 0.8 rem ; }
. stop - details . name { font - weight : 700 ; font - size : 0.9 rem ; }
. stop - details . stats { font - size : 0.75 rem ; color : # 94 a3b8 ; margin - top : 2 px ; }
. stop - edit - fields { display : flex ; gap : 12 px ; }
. input - with - label { display : flex ; flex - direction : column ; gap : 2 px ; }
. input - with - label label { font - size : 0.6 rem ; color : # 64748 b ; text - transform : uppercase ; font - weight : 800 ; }
. input - with - label input { width : 60 px ; background : # 0 f172a ; border : 1 px solid # 334155 ; padding : 4 px 6 px ; border - radius : 4 px ; color : white ; text - align : center ; }
. stop - controls { display : flex ; gap : 4 px ; }
. move - btn , . remove - btn {
width : 32 px ; height : 32 px ; display : flex ; align - items : center ; justify - content : center ;
border - radius : 6 px ; cursor : pointer ; border : none ; color : # 94 a3b8 ; background : transparent ; transition : all 0.2 s ;
}
. move - btn : hover : not ( : disabled ) { background : rgba ( 255 , 255 , 255 , 0.05 ) ; color : # FEE715 ; }
. remove - btn : hover { background : rgba ( 239 , 68 , 68 , 0.1 ) ; color : # f87171 ; }
. move - btn : disabled { opacity : 0.3 ; cursor : not - allowed ; }
. no - stops { text - align : center ; padding : 40 px ; color : # 64748 b ; font - style : italic ; border : 1 px dashed # 334155 ; border - radius : 12 px ; }
. empty - state { text - align : center ; padding : 40 px ; color : # 64748 b ; }
2026-02-21 09:53:31 -05:00
< / style >