Initial commit: SIBU 2.0 MISSION
This commit is contained in:
267
frontend/src/components/BusStopEditor.vue
Normal file
267
frontend/src/components/BusStopEditor.vue
Normal file
@ -0,0 +1,267 @@
|
||||
<template>
|
||||
<div class="editor-container">
|
||||
<div class="editor-content">
|
||||
<h2>{{ isEditing ? 'Editar Parada' : 'Crear Parada' }}</h2>
|
||||
|
||||
<div class="form-grid">
|
||||
<div class="form-group">
|
||||
<label>Nombre</label>
|
||||
<input v-model="formData.name" type="text" placeholder="Nombre de la Parada" />
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>Tipo</label>
|
||||
<select v-model="formData.stop_type">
|
||||
<option value="regular">Regular</option>
|
||||
<option value="terminal">Terminal</option>
|
||||
<option value="express_only">Solo Expreso</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="checkbox-group">
|
||||
<label>
|
||||
<input type="checkbox" v-model="formData.has_shelter"> Techo
|
||||
</label>
|
||||
<label>
|
||||
<input type="checkbox" v-model="formData.has_seating"> Asientos
|
||||
</label>
|
||||
<label>
|
||||
<input type="checkbox" v-model="formData.is_accessible"> Accesible
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="gps-section">
|
||||
<div class="coordinates">
|
||||
Lat: {{ formData.latitude.toFixed(6) }}, Lon: {{ formData.longitude.toFixed(6) }}
|
||||
</div>
|
||||
<button @click="getCurrentLocation" class="gps-button" :disabled="isLoadingGps">
|
||||
<span class="material-icons">my_location</span>
|
||||
{{ isLoadingGps ? 'Localizando...' : 'Usar GPS Preciso' }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="map-wrapper">
|
||||
<div id="editor-map" class="editor-map"></div>
|
||||
<div v-if="!isMapLoaded" class="map-loading">Cargando Mapa...</div>
|
||||
</div>
|
||||
|
||||
<div class="actions">
|
||||
<button @click="$emit('cancel')" class="cancel-button">Cancelar</button>
|
||||
<button @click="handleSave" class="save-button" :disabled="isSaving">
|
||||
{{ isSaving ? 'Guardando...' : 'Guardar Parada' }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div v-if="error" class="error-message">{{ error }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, watch } from 'vue'
|
||||
import { Geolocation } from '@capacitor/geolocation'
|
||||
import { useGoogleMaps } from '@/composables/useGoogleMaps'
|
||||
import type { BusStop } from '@/types'
|
||||
|
||||
const props = defineProps<{
|
||||
initialStop?: BusStop | null
|
||||
}>()
|
||||
|
||||
const emit = defineEmits(['save', 'cancel'])
|
||||
|
||||
const isEditing = ref(!!props.initialStop)
|
||||
const isSaving = ref(false)
|
||||
const isLoadingGps = ref(false)
|
||||
const error = ref<string | null>(null)
|
||||
|
||||
const formData = ref({
|
||||
name: '',
|
||||
latitude: 8.4284, // Default (David)
|
||||
longitude: -82.4309,
|
||||
stop_type: 'regular',
|
||||
has_shelter: false,
|
||||
has_seating: false,
|
||||
is_accessible: false,
|
||||
city: 'David', // Default
|
||||
})
|
||||
|
||||
const { initMap, addMarker, setCenter, isLoaded: isMapLoaded } = useGoogleMaps()
|
||||
let marker: google.maps.Marker | null = null
|
||||
|
||||
onMounted(async () => {
|
||||
if (props.initialStop) {
|
||||
formData.value = {
|
||||
name: props.initialStop.name,
|
||||
latitude: props.initialStop.latitude,
|
||||
longitude: props.initialStop.longitude,
|
||||
stop_type: props.initialStop.stop_type,
|
||||
has_shelter: props.initialStop.has_shelter,
|
||||
has_seating: props.initialStop.has_seating,
|
||||
is_accessible: props.initialStop.is_accessible,
|
||||
city: props.initialStop.city || 'David',
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize map
|
||||
initMap('editor-map', { lat: formData.value.latitude, lng: formData.value.longitude }, 16)
|
||||
updateMarker()
|
||||
})
|
||||
|
||||
// Watch for map load to add marker if missed
|
||||
watch(isMapLoaded, (loaded) => {
|
||||
if (loaded) {
|
||||
updateMarker()
|
||||
}
|
||||
})
|
||||
|
||||
function updateMarker() {
|
||||
if (!isMapLoaded.value) return
|
||||
|
||||
if (marker) {
|
||||
marker.setPosition({ lat: formData.value.latitude, lng: formData.value.longitude })
|
||||
} else {
|
||||
marker = addMarker(
|
||||
{ lat: formData.value.latitude, lng: formData.value.longitude },
|
||||
{
|
||||
draggable: true,
|
||||
onDragEnd: (pos) => {
|
||||
formData.value.latitude = pos.lat
|
||||
formData.value.longitude = pos.lng
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
setCenter(formData.value.latitude, formData.value.longitude)
|
||||
}
|
||||
|
||||
async function getCurrentLocation() {
|
||||
isLoadingGps.value = true
|
||||
try {
|
||||
const coordinates = await Geolocation.getCurrentPosition({
|
||||
enableHighAccuracy: true,
|
||||
timeout: 10000
|
||||
})
|
||||
formData.value.latitude = coordinates.coords.latitude
|
||||
formData.value.longitude = coordinates.coords.longitude
|
||||
updateMarker()
|
||||
} catch (e) {
|
||||
console.error('GPS Error', e)
|
||||
error.value = 'Error al obtener la ubicación GPS. Asegúrate de dar los permisos.'
|
||||
} finally {
|
||||
isLoadingGps.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function handleSave() {
|
||||
if (!formData.value.name) {
|
||||
error.value = 'El nombre es obligatorio'
|
||||
return
|
||||
}
|
||||
|
||||
emit('save', {
|
||||
...formData.value,
|
||||
id: props.initialStop?.id
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.editor-container {
|
||||
background: white;
|
||||
padding: 20px;
|
||||
border-radius: 8px;
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
box-shadow: 0 4px 6px rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
.form-grid {
|
||||
display: grid;
|
||||
gap: 16px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
input, select {
|
||||
padding: 8px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.checkbox-group {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.gps-section {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
background: #f8f9fa;
|
||||
padding: 10px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.gps-button {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
background: #2ecc71;
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 8px 12px;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.gps-button:disabled {
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.map-wrapper {
|
||||
height: 300px;
|
||||
background: #eee;
|
||||
margin-bottom: 20px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.editor-map {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.save-button {
|
||||
background: #3498db;
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 10px 20px;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.cancel-button {
|
||||
background: #95a5a6;
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 10px 20px;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.error-message {
|
||||
color: red;
|
||||
margin-top: 10px;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user