refactor(admin): unify AdminTaxis into AdminDrivers - multi-shifts, vehicle_type, is_accessible; remove redundant AdminTaxis.vue + DB migration

This commit is contained in:
2026-03-04 15:09:10 -05:00
parent 927d6549bf
commit c178523c7e
3 changed files with 103 additions and 812 deletions

View File

@ -75,11 +75,19 @@
<p class="phone">📞 {{ taxi.phone_number }}</p>
<div class="badges">
<span class="badge plate">{{ taxi.license_plate }}</span>
<span class="badge">{{ taxi.corregimiento }}</span>
<span class="badge">{{ getShiftLabel(taxi.shift) }}</span>
<span class="badge" v-if="taxi.corregimiento">{{ taxi.corregimiento }}</span>
<span class="badge" v-if="taxi.vehicle_type">{{ taxi.vehicle_type }}</span>
<span v-if="taxi.english_speaking" class="badge english">🌐 Inglés</span>
<span v-if="taxi.is_accessible" class="badge accessible"> Accesible</span>
</div>
<div class="taxi-meta">
<span class="shifts-badges">
<span
v-for="s in (taxi.shifts?.length ? taxi.shifts : (taxi.shift ? [taxi.shift] : []))"
:key="s"
class="shift-pill"
>{{ getShiftLabel(s) }}</span>
</span>
<span class="rating"> {{ taxi.rating || 5.0 }}</span>
<span v-if="taxi.cooperative" class="cooperative">{{ taxi.cooperative }}</span>
<span :class="taxi.is_active ? 'status-active' : 'status-inactive'">
@ -265,6 +273,11 @@
<input v-model="taxiForm.license_plate" type="text" placeholder="CHI-1234" required>
</div>
<div class="form-group">
<label>Tipo de Vehículo</label>
<input v-model="taxiForm.vehicle_type" type="text" placeholder="Toyota Corolla / SUV / Van">
</div>
<div class="form-group">
<label>Zona de Servicio *</label>
<select v-model="taxiForm.corregimiento" required>
@ -276,41 +289,44 @@
</select>
</div>
<div class="form-group">
<label>Horario *</label>
<select v-model="taxiForm.shift" required>
<option value="">Seleccionar...</option>
<option value="dia">Día</option>
<option value="tarde">Tarde</option>
<option value="noche">Noche</option>
</select>
</div>
<div class="form-group">
<label>Cooperativa</label>
<input v-model="taxiForm.cooperative" type="text" placeholder="Cooperativa Boquete">
</div>
<div class="form-group">
<label>Rating (1-5)</label>
<input v-model.number="taxiForm.rating" type="number" min="1" max="5" step="0.1" placeholder="5.0">
</div>
<div class="form-group checkbox-group">
<label>
<input v-model="taxiForm.english_speaking" type="checkbox">
<span>Habla Inglés</span>
</label>
<!-- Horarios (múltiples) -->
<div class="form-group full-col">
<label>Horarios de Servicio</label>
<div class="checkbox-group horizontal">
<label v-for="s in ['dia', 'tarde', 'noche', 'aeropuerto']" :key="s" class="checkbox-item">
<input type="checkbox" v-model="taxiForm.shifts" :value="s">
<span>{{ getShiftLabel(s) }}</span>
</label>
</div>
</div>
<div class="form-group checkbox-group">
<label>
<!-- Flags -->
<div class="form-group full-col flags-row">
<label class="checkbox-item">
<input v-model="taxiForm.english_speaking" type="checkbox">
<span>🌐 Habla Inglés</span>
</label>
<label class="checkbox-item">
<input v-model="taxiForm.is_accessible" type="checkbox">
<span>&#9855; Accesible para discapacitados</span>
</label>
<label class="checkbox-item">
<input v-model="taxiForm.is_active" type="checkbox">
<span>Activo en el directorio</span>
</label>
</div>
<div class="form-group full-width">
<div class="form-group full-col">
<label>Foto del Conductor</label>
<input type="file" @change="handleTaxiFileChange" accept="image/*">
<small>Opcional - Foto para el directorio público</small>
@ -378,11 +394,13 @@ const taxiForm = reactive({
owner_name: '',
phone_number: '',
license_plate: '',
vehicle_type: '',
corregimiento: '',
shift: '',
shifts: [] as string[],
cooperative: '',
rating: 5.0,
english_speaking: false,
is_accessible: false,
is_active: true
})
@ -487,15 +505,21 @@ const handleRegisterDriver = async () => {
function editTaxi(taxi: any) {
editingTaxi.value = taxi
modalMode.value = 'taxi'
// shifts: preferimos el array `shifts`, pero si solo tiene el legacy `shift` lo convertimos
const shiftsArr: string[] = Array.isArray(taxi.shifts) && taxi.shifts.length
? taxi.shifts
: (taxi.shift ? [taxi.shift] : [])
Object.assign(taxiForm, {
owner_name: taxi.owner_name,
phone_number: taxi.phone_number,
license_plate: taxi.license_plate,
vehicle_type: taxi.vehicle_type || '',
corregimiento: taxi.corregimiento,
shift: taxi.shift,
shifts: shiftsArr,
cooperative: taxi.cooperative || '',
rating: taxi.rating || 5.0,
english_speaking: taxi.english_speaking || false,
is_accessible: taxi.is_accessible || false,
is_active: taxi.is_active
})
photoFile.value = null
@ -524,7 +548,12 @@ async function saveTaxi() {
const { data: urlData } = supabase.storage.from('uploads').getPublicUrl(filename)
image_url = urlData.publicUrl
}
const payload = { ...taxiForm, image_url }
// Guardamos shifts (array) y tambien shift (primer valor) para compatibilidad legacy
const payload = {
...taxiForm,
shift: taxiForm.shifts[0] || null, // backward compat
image_url
}
if (editingTaxi.value) {
const { error: e } = await supabase.from('taxis').update(payload).eq('id', editingTaxi.value.id)
if (e) throw e
@ -534,8 +563,9 @@ async function saveTaxi() {
}
closeModal()
Object.assign(taxiForm, {
owner_name: '', phone_number: '', license_plate: '', corregimiento: '',
shift: '', cooperative: '', rating: 5.0, english_speaking: false, is_active: true
owner_name: '', phone_number: '', license_plate: '', vehicle_type: '', corregimiento: '',
shifts: [], cooperative: '', rating: 5.0, english_speaking: false,
is_accessible: false, is_active: true
})
await loadData()
} catch (e: any) {
@ -559,9 +589,7 @@ async function deleteTaxi(taxi: any) {
function getShiftLabel(shift: string) {
const labels: Record<string, string> = {
'dia': 'Día',
'tarde': 'Tarde',
'noche': 'Noche'
'dia': 'Día', 'tarde': 'Tarde', 'noche': 'Noche', 'aeropuerto': 'Aeropuerto'
}
return labels[shift] || shift
}
@ -803,6 +831,29 @@ h1 {
border-color: #4a90e2;
}
.badge.accessible {
background: #0ea5e9;
color: white;
border-color: #0ea5e9;
}
/* Shift pills in taxi card */
.shifts-badges {
display: flex;
gap: 4px;
flex-wrap: wrap;
}
.shift-pill {
background: #fee715;
color: #101820;
padding: 2px 8px;
border-radius: 10px;
font-size: 0.72rem;
font-weight: 800;
text-transform: uppercase;
}
.taxi-meta {
display: flex;
gap: 12px;
@ -972,6 +1023,27 @@ h1 {
grid-column: 1 / -1;
}
.form-group.full-col {
grid-column: 1 / -1;
}
.checkbox-group.horizontal {
flex-direction: row;
flex-wrap: wrap;
gap: 16px;
background: var(--bg-secondary);
border: 1px solid var(--border-color);
border-radius: 8px;
padding: 10px 14px;
}
.flags-row {
flex-direction: row !important;
flex-wrap: wrap;
gap: 20px;
align-items: center;
}
.form-group.checkbox-group {
flex-direction: row;
align-items: center;

View File

@ -1,775 +0,0 @@
<template>
<div class="admin-taxis">
<div class="header">
<button class="back-link" @click="router.push('/admin')"> Volver al Panel</button>
<h1>Directorio de Taxis</h1>
<button class="btn-primary" @click="openModal()">
<span class="material-icons">add</span>
Nuevo Taxi
</button>
</div>
<div v-if="isLoading" class="loading">Cargando directorio...</div>
<div v-else class="taxis-list">
<div v-if="taxis.length > 0" class="taxis-grid">
<div v-for="taxi in taxis" :key="taxi.id" class="taxi-card">
<div class="card-header">
<div class="taxi-info">
<div class="avatar">
<img v-if="taxi.image_url" :src="getImageUrl(taxi.image_url)" alt="Taxi">
<span v-else class="material-icons">local_taxi</span>
</div>
<div>
<h3>{{ taxi.owner_name }}</h3>
<p class="plate">{{ taxi.license_plate }}</p>
<p class="phone">{{ taxi.phone_number }}</p>
</div>
</div>
<div class="card-actions">
<button class="btn-icon" @click="openModal(taxi)" title="Editar">
<span class="material-icons">edit</span>
</button>
<button class="btn-icon delete" @click="deleteTaxi(taxi)" title="Eliminar">
<span class="material-icons">delete</span>
</button>
</div>
</div>
<div class="card-body">
<div class="info-row">
<span class="label">Zona:</span>
<span>{{ taxi.corregimiento }}</span>
</div>
<div class="info-row">
<span class="label">Horarios:</span>
<span>{{ getShiftsLabel(taxi.shifts) }}</span>
</div>
<div class="info-row" v-if="taxi.vehicle_type">
<span class="label">Vehículo:</span>
<span>{{ taxi.vehicle_type }}</span>
</div>
<div class="info-row">
<span class="label">Accesible:</span>
<span class="flex items-center gap-1">
{{ taxi.is_accessible ? 'Sí' : 'No' }}
<span v-if="taxi.is_accessible" class="material-icons text-blue-500 text-sm">accessible</span>
</span>
</div>
<div class="info-row" v-if="taxi.cooperative">
<span class="label">Cooperativa:</span>
<span>{{ taxi.cooperative }}</span>
</div>
<div class="info-row">
<span class="label">Rating:</span>
<span class="rating">{{ taxi.rating || 5.0 }} </span>
</div>
<div class="info-row">
<span class="label">Inglés:</span>
<span>{{ taxi.english_speaking ? 'Sí' : 'No' }}</span>
</div>
<div class="info-row">
<span class="label">Estado:</span>
<span :class="taxi.is_active ? 'status-active' : 'status-inactive'">
{{ taxi.is_active ? 'Activo' : 'Inactivo' }}
</span>
</div>
</div>
</div>
</div>
<div v-else class="empty-state">
<span class="material-icons">local_taxi</span>
<p>No hay taxis registrados en el directorio</p>
</div>
</div>
<!-- Modal for Create/Edit -->
<div v-if="showModal" class="modal-overlay" @click.self="closeModal">
<div class="modal">
<div class="modal-header">
<h2>{{ editingTaxi ? 'Editar Taxi' : 'Nuevo Taxi' }}</h2>
<button @click="closeModal">&times;</button>
</div>
<div class="modal-body">
<form @submit.prevent="saveTaxi">
<div class="form-grid">
<div class="form-group">
<label>Nombre del Conductor *</label>
<input v-model="taxiForm.owner_name" type="text" placeholder="Juan Pérez" required>
</div>
<div class="form-group">
<label>Teléfono *</label>
<input v-model="taxiForm.phone_number" type="tel" placeholder="+507 1234-5678" required>
</div>
<div class="form-group">
<label>Placa del Vehículo *</label>
<input v-model="taxiForm.license_plate" type="text" placeholder="CHI-1234" required>
</div>
<div class="form-group">
<label>Zona de Servicio *</label>
<select v-model="taxiForm.corregimiento" required>
<option value="">Seleccionar...</option>
<option value="Boquete">Boquete</option>
<option value="David - Boquete">David - Boquete</option>
<option value="Boquete - David">Boquete - David</option>
<option value="Aeropuerto - Boquete">Aeropuerto - Boquete</option>
</select>
</div>
<div class="form-group full-width">
<label>Tipo de Vehículo</label>
<input v-model="taxiForm.vehicle_type" type="text" placeholder="Toyota Corolla / SUV / Van">
</div>
<div class="form-group full-width">
<label>Horarios de Servicio *</label>
<div class="checkbox-list">
<label class="checkbox-item">
<input type="checkbox" value="dia" v-model="taxiForm.shifts">
<span>Día</span>
</label>
<label class="checkbox-item">
<input type="checkbox" value="tarde" v-model="taxiForm.shifts">
<span>Tarde</span>
</label>
<label class="checkbox-item">
<input type="checkbox" value="noche" v-model="taxiForm.shifts">
<span>Noche</span>
</label>
</div>
</div>
<div class="form-group">
<label>Cooperativa</label>
<input v-model="taxiForm.cooperative" type="text" placeholder="Cooperativa Boquete">
</div>
<div class="form-group">
<label>Rating (1-5)</label>
<input v-model.number="taxiForm.rating" type="number" min="1" max="5" step="0.1" placeholder="5.0">
</div>
<div class="form-group checkbox-group">
<label>
<input v-model="taxiForm.english_speaking" type="checkbox">
<span>Habla Inglés</span>
</label>
</div>
<div class="form-group checkbox-group">
<label>
<input v-model="taxiForm.is_accessible" type="checkbox">
<span class="flex items-center gap-1">Accesible <span class="material-icons text-sm">accessible</span></span>
</label>
</div>
<div class="form-group checkbox-group">
<label>
<input v-model="taxiForm.is_active" type="checkbox">
<span>Activo en el directorio</span>
</label>
</div>
<div class="form-group full-width">
<label>Foto del Conductor</label>
<input type="file" @change="handleFileChange" accept="image/*">
<small>Opcional - Foto para el directorio público</small>
</div>
</div>
<p v-if="error" class="error-text">{{ error }}</p>
<div class="form-actions">
<button type="button" class="btn-secondary" @click="closeModal">Cancelar</button>
<button type="submit" class="btn-primary" :disabled="isSaving">
{{ isSaving ? 'Guardando...' : 'Guardar' }}
</button>
</div>
</form>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted, reactive } from 'vue'
import { useRouter } from 'vue-router'
import { supabase } from '@/supabase'
const router = useRouter()
const isLoading = ref(false)
const taxis = ref<any[]>([])
const showModal = ref(false)
const editingTaxi = ref<any>(null)
const isSaving = ref(false)
const error = ref('')
const photoFile = ref<File | null>(null)
const taxiForm = reactive({
owner_name: '',
phone_number: '',
license_plate: '',
corregimiento: '',
vehicle_type: '',
shifts: [] as string[],
cooperative: '',
rating: 5.0,
english_speaking: false,
is_accessible: false,
is_active: true
})
onMounted(() => {
loadTaxis()
})
async function loadTaxis() {
isLoading.value = true
try {
const { data, error } = await supabase.from('taxis').select('*').order('owner_name')
if (error) throw error
taxis.value = data || []
} catch (e) {
console.error('Error cargando taxis:', e)
error.value = 'Failed to load taxis'
} finally {
isLoading.value = false
}
}
function openModal(taxi?: any) {
if (taxi) {
editingTaxi.value = taxi
Object.assign(taxiForm, {
owner_name: taxi.owner_name,
phone_number: taxi.phone_number,
license_plate: taxi.license_plate,
corregimiento: taxi.corregimiento,
vehicle_type: taxi.vehicle_type || '',
shifts: taxi.shifts || [],
cooperative: taxi.cooperative || '',
rating: taxi.rating || 5.0,
english_speaking: taxi.english_speaking || false,
is_accessible: taxi.is_accessible || false,
is_active: taxi.is_active
})
} else {
editingTaxi.value = null
Object.assign(taxiForm, {
owner_name: '',
phone_number: '',
license_plate: '',
corregimiento: '',
vehicle_type: '',
shifts: [],
cooperative: '',
rating: 5.0,
english_speaking: false,
is_accessible: false,
is_active: true
})
}
photoFile.value = null
error.value = ''
showModal.value = true
}
function closeModal() {
showModal.value = false
editingTaxi.value = null
photoFile.value = null
}
function handleFileChange(event: Event) {
const target = event.target as HTMLInputElement
if (target.files && target.files[0]) {
photoFile.value = target.files[0]
}
}
async function saveTaxi() {
isSaving.value = true
error.value = ''
try {
let image_url = editingTaxi.value?.image_url || null
// Upload image to Supabase Storage if provided
if (photoFile.value) {
const ext = photoFile.value.name.split('.').pop()
const filename = `taxis/${Date.now()}.${ext}`
const { error: upErr } = await supabase.storage.from('uploads').upload(filename, photoFile.value)
if (upErr) throw upErr
const { data: urlData } = supabase.storage.from('uploads').getPublicUrl(filename)
image_url = urlData.publicUrl
}
const payload = { ...taxiForm, image_url }
if (editingTaxi.value) {
const { error: e } = await supabase.from('taxis').update(payload).eq('id', editingTaxi.value.id)
if (e) throw e
} else {
const { error: e } = await supabase.from('taxis').insert([payload])
if (e) throw e
}
closeModal()
await loadTaxis()
} catch (e: any) {
error.value = e.message || 'Error al guardar el taxi'
console.error('Error saving taxi:', e)
} finally {
isSaving.value = false
}
}
async function deleteTaxi(taxi: any) {
if (!confirm(`¿Eliminar a ${taxi.owner_name} del directorio?`)) return
try {
const { error: e } = await supabase.from('taxis').delete().eq('id', taxi.id)
if (e) throw e
await loadTaxis()
} catch (e) {
alert('Error al eliminar el taxi')
console.error('Error deleting taxi:', e)
}
}
function getShiftsLabel(shifts: string[]) {
if (!shifts || shifts.length === 0) return 'No definido'
const labels: Record<string, string> = {
'dia': 'Día',
'tarde': 'Tarde',
'noche': 'Noche'
}
return shifts.map(s => labels[s] || s).join(', ')
}
function getImageUrl(path: string) {
if (!path) return ''
if (path.startsWith('http')) return path
return path
}
</script>
<style scoped>
.admin-taxis {
padding: 24px;
background: var(--bg-primary);
min-height: 100vh;
}
.header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 32px;
gap: 16px;
}
.back-link {
background: var(--bg-secondary);
border: 1px solid var(--border-color);
color: var(--text-primary);
cursor: pointer;
font-size: 0.95rem;
font-weight: 600;
padding: 10px 16px;
border-radius: 8px;
transition: all 0.2s;
display: inline-flex;
align-items: center;
gap: 4px;
}
.back-link:hover {
background: var(--hover-bg);
border-color: var(--accent-color);
color: var(--accent-color);
transform: translateX(-2px);
}
h1 {
flex: 1;
font-size: 28px;
font-weight: 800;
color: var(--text-primary);
margin: 0;
}
.btn-primary {
background: var(--accent-color);
color: white;
border: none;
padding: 12px 24px;
border-radius: 8px;
font-weight: 600;
cursor: pointer;
display: flex;
align-items: center;
gap: 8px;
transition: background 0.2s;
}
.btn-primary:hover {
background: var(--accent-hover);
}
.btn-primary:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.loading, .empty-state {
text-align: center;
padding: 60px 20px;
color: var(--text-secondary);
}
.empty-state .material-icons {
font-size: 64px;
margin-bottom: 16px;
opacity: 0.3;
}
.taxis-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(350px, 1fr));
gap: 20px;
}
.taxi-card {
background: var(--card-bg);
border-radius: 16px;
padding: 20px;
border: 1px solid var(--border-color);
box-shadow: 0 2px 8px var(--shadow);
transition: transform 0.2s, box-shadow 0.2s;
}
.taxi-card:hover {
transform: translateY(-2px);
box-shadow: 0 4px 16px var(--shadow);
}
.card-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: 16px;
padding-bottom: 16px;
border-bottom: 1px solid var(--border-color);
}
.taxi-info {
display: flex;
gap: 12px;
flex: 1;
}
.avatar {
width: 60px;
height: 60px;
border-radius: 50%;
overflow: hidden;
background: var(--bg-secondary);
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
}
.avatar img {
width: 100%;
height: 100%;
object-fit: cover;
}
.avatar .material-icons {
font-size: 32px;
color: var(--text-secondary);
}
.taxi-info h3 {
margin: 0 0 4px;
font-size: 1.1rem;
font-weight: 700;
color: var(--text-primary);
}
.plate {
font-family: 'Courier New', monospace;
background: var(--bg-secondary);
padding: 2px 8px;
border-radius: 4px;
display: inline-block;
font-size: 0.9rem;
margin: 4px 0;
color: var(--text-primary);
}
.phone {
color: var(--text-secondary);
font-size: 0.9rem;
margin: 4px 0 0;
}
.card-actions {
display: flex;
gap: 8px;
}
.btn-icon {
background: transparent;
border: 1px solid var(--border-color);
width: 36px;
height: 36px;
border-radius: 8px;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.2s;
color: var(--text-secondary);
}
.btn-icon:hover {
background: var(--hover-bg);
color: var(--text-primary);
}
.btn-icon.delete:hover {
background: #fee;
border-color: #f44;
color: #f44;
}
.card-body {
display: flex;
flex-direction: column;
gap: 8px;
}
.info-row {
display: flex;
justify-content: space-between;
font-size: 0.9rem;
}
.info-row .label {
color: var(--text-secondary);
font-weight: 500;
}
.rating {
color: #fee715;
font-weight: 600;
}
.status-active {
color: var(--accent-color);
font-weight: 600;
}
.status-inactive {
color: var(--text-secondary);
}
/* Modal Styles */
.modal-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
padding: 20px;
}
.modal {
background: var(--card-bg);
border-radius: 16px;
max-width: 600px;
width: 100%;
max-height: 90vh;
overflow-y: auto;
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
}
.modal-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 24px;
border-bottom: 1px solid var(--border-color);
}
.modal-header h2 {
margin: 0;
font-size: 1.5rem;
color: var(--text-primary);
}
.modal-header button {
background: transparent;
border: none;
font-size: 2rem;
cursor: pointer;
color: var(--text-secondary);
width: 36px;
height: 36px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 8px;
transition: all 0.2s;
}
.modal-header button:hover {
background: var(--hover-bg);
color: var(--text-primary);
}
.modal-body {
padding: 24px;
}
.form-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 16px;
}
.form-group {
display: flex;
flex-direction: column;
gap: 8px;
}
.form-group.full-width {
grid-column: 1 / -1;
}
.form-group.checkbox-group {
flex-direction: row;
align-items: center;
}
.form-group.checkbox-group label {
display: flex;
align-items: center;
gap: 8px;
cursor: pointer;
}
.form-group label {
font-weight: 600;
color: var(--text-primary);
font-size: 0.9rem;
}
.form-group input[type="text"],
.form-group input[type="tel"],
.form-group input[type="number"],
.form-group select {
padding: 10px 12px;
border: 1px solid var(--border-color);
border-radius: 8px;
background: var(--bg-secondary);
color: var(--text-primary);
font-size: 1rem;
transition: border-color 0.2s;
}
.form-group input:focus,
.form-group select:focus {
outline: none;
border-color: var(--accent-color);
}
.form-group input[type="file"] {
padding: 8px;
border: 1px dashed var(--border-color);
border-radius: 8px;
background: var(--bg-secondary);
color: var(--text-primary);
cursor: pointer;
}
.form-group small {
color: var(--text-secondary);
font-size: 0.85rem;
}
.checkbox-list {
display: flex;
gap: 20px;
background: var(--bg-secondary);
padding: 10px 12px;
border-radius: 8px;
border: 1px solid var(--border-color);
}
.checkbox-item {
display: flex;
align-items: center;
gap: 8px;
cursor: pointer;
font-weight: 600;
font-size: 0.9rem;
}
.error-text {
color: #f44;
margin: 16px 0;
padding: 12px;
background: #fee;
border-radius: 8px;
font-size: 0.9rem;
}
.form-actions {
display: flex;
gap: 12px;
justify-content: flex-end;
margin-top: 24px;
}
.btn-secondary {
background: var(--bg-secondary);
color: var(--text-primary);
border: 1px solid var(--border-color);
padding: 12px 24px;
border-radius: 8px;
font-weight: 600;
cursor: pointer;
transition: all 0.2s;
}
.btn-secondary:hover {
background: var(--hover-bg);
}
@media (max-width: 768px) {
.form-grid {
grid-template-columns: 1fr;
}
.taxis-grid {
grid-template-columns: 1fr;
}
}
</style>