refactor: migrate fully to Supabase, remove Firebase/Render/Python backend
- DELETED: entire backend/ (Python/FastAPI — replaced by Supabase)
- DELETED: old/ directory (obsolete code)
- DELETED: render.yaml, inject_api.py, check_tags.py, PENDING_FOR_TOMORROW.md
- DELETED: frontend/src/firebaseConfig.ts (Firebase Auth replaced by Supabase Auth)
- DELETED: frontend/src/services/apiClient.ts (HTTP client for dead backend)
- MIGRATED services to Supabase native:
schedulesService, favoritesService, usersService,
telemetryService (stub), reportsService, analyticsService (stub)
- MIGRATED stores/favorites.ts to Supabase direct queries
- MIGRATED views: SplashScreen, AdminTaxis, AdminDrivers, StrategicAnalytics
- MIGRATED utils/imageUrl.ts to Supabase Storage URLs
- FIXED router/index.ts: guard now uses supabase.auth.getSession()
instead of old localStorage auth_token (fixes logout + map loading)
- FIXED AuthView.vue: removed aggressive watch({ immediate: true })
that caused wrong redirects on map route
- FIXED SplashScreen.vue: navigate() now reads Supabase session + role
- FIXED RLS: added INSERT policy on public.users for trigger
- CONFIRMED: admin@sibu.com assigned ADMIN role in Supabase
2026-02-25 21:49:26 -05:00
< script setup lang = "ts" >
2026-02-21 09:53:31 -05:00
import { ref , onMounted , watch , computed } from 'vue'
import { useRoute } from 'vue-router'
import { businessService } from '@/services/businessService'
2026-02-25 10:34:38 -05:00
import { couponsService } from '@/services/couponsService'
import { shuttlesService } from '@/services/shuttlesService'
2026-02-21 09:53:31 -05:00
import { useAuthStore } from '@/stores/auth'
2026-02-26 20:58:10 -05:00
import { supabase } from '@/supabase'
2026-02-25 10:34:38 -05:00
import type { Coupon , Business , Shuttle } from '@/types'
2026-02-21 09:53:31 -05:00
const route = useRoute ( )
const authStore = useAuthStore ( )
// State
2026-02-25 10:34:38 -05:00
const activeTab = ref < 'promotions' | 'businesses' | 'shuttles' > ( 'promotions' )
2026-02-21 09:53:31 -05:00
const coupons = ref < Coupon [ ] > ( [ ] )
const businesses = ref < Business [ ] > ( [ ] )
2026-02-25 10:34:38 -05:00
const shuttles = ref < Shuttle [ ] > ( [ ] )
2026-02-21 09:53:31 -05:00
const isLoading = ref ( true )
const searchQuery = ref ( '' )
const categoryFilter = ref ( 'Todas' )
const categories = [ 'Todas' , 'Restaurante' , 'Turismo' , 'Bebidas' , 'Comercio' ]
// Modals
const showModal = ref ( false )
const showBusinessModal = ref ( false )
const isEditing = ref ( false )
const isEditingBusiness = ref ( false )
const businessImageFile = ref < File | null > ( null )
const businessImagePreview = ref < string | null > ( null )
// Current data
const currentCoupon = ref < Partial < Coupon > > ( {
title : '' ,
business _id : null ,
description : '' ,
image _url : '' ,
social _media : '' ,
terms : '' ,
discount _percentage : null ,
discount _amount : null ,
category : 'Restaurante' ,
valid _from : '' ,
valid _until : '' ,
is _active : true
} )
const currentBusiness = ref < Partial < Business > > ( {
name : '' ,
address : '' ,
phone : '' ,
image _url : '' ,
social _media : '' ,
category : 'Restaurante' ,
2026-03-03 11:45:36 -05:00
area : 'Boquete' ,
description : '' ,
feat: business template/mold redesign
- Supabase: Add schedule, whatsapp, instagram, facebook, gallery_images
columns to businesses table
- types/index.ts: Add 5 new fields to Business interface
- businessService.ts: Update all SELECT queries to include new fields,
add _parseFormData helper, handle gallery_images as JSON array
- BusinessDetailsView.vue: Full redesign matching the approved mockup:
* Hero 300px with centered name + yellow category badge
* Horizontal scrollable quick-info pills (area, schedule, phone, web)
* Image carousel (main image + gallery_images) with arrows + dots
* About section with yellow left-accent bar
* 2x2 social grid (WhatsApp, Instagram, Facebook, Maps) with brand colors
* Coupon section preserved
* Sticky CTA bar: Ver en el Mapa + Llamar buttons
- PromoterDashboard.vue: Updated business modal with grouped sections
(Info Basica / Portada / Descripcion / Redes / Galeria) + new fields
2026-03-03 21:31:35 -05:00
website : '' ,
// Template fields
schedule : '' ,
whatsapp : '' ,
instagram : '' ,
facebook : '' ,
gallery _images : [ ]
2026-02-21 09:53:31 -05:00
} )
const userName = localStorage . getItem ( 'user_name' ) || 'Promotor'
onMounted ( async ( ) => {
2026-02-25 10:34:38 -05:00
await Promise . all ( [ loadCoupons ( ) , loadBusinesses ( ) , loadShuttles ( ) ] )
2026-02-21 09:53:31 -05:00
checkHash ( )
} )
watch ( ( ) => route . hash , ( ) => checkHash ( ) )
function checkHash ( ) {
if ( route . hash === '#businesses' ) {
activeTab . value = 'businesses'
2026-02-25 10:34:38 -05:00
} else if ( route . hash === '#shuttles' ) {
activeTab . value = 'shuttles'
2026-02-21 09:53:31 -05:00
} else {
activeTab . value = 'promotions'
}
}
// Search Computed
const filteredCoupons = computed ( ( ) => {
return coupons . value . filter ( c => {
const matchesSearch = c . title . toLowerCase ( ) . includes ( searchQuery . value . toLowerCase ( ) ) ||
2026-02-26 15:36:32 -05:00
( c . business ? . name ? . toLowerCase ( ) . includes ( searchQuery . value . toLowerCase ( ) ) )
2026-02-21 09:53:31 -05:00
const matchesCategory = categoryFilter . value === 'Todas' || c . category === categoryFilter . value
return matchesSearch && matchesCategory
} )
} )
2026-02-25 10:34:38 -05:00
const filteredShuttles = computed ( ( ) => {
return shuttles . value . filter ( s => {
const matchesSearch = s . route _name . toLowerCase ( ) . includes ( searchQuery . value . toLowerCase ( ) ) ||
( s . company _name ? . toLowerCase ( ) . includes ( searchQuery . value . toLowerCase ( ) ) ? ? false ) ||
s . origin . toLowerCase ( ) . includes ( searchQuery . value . toLowerCase ( ) ) ||
s . destination . toLowerCase ( ) . includes ( searchQuery . value . toLowerCase ( ) )
return matchesSearch
} )
} )
2026-02-21 09:53:31 -05:00
const filteredBusinesses = computed ( ( ) => {
return businesses . value . filter ( b => {
const matchesSearch = b . name . toLowerCase ( ) . includes ( searchQuery . value . toLowerCase ( ) ) ||
( b . address ? . toLowerCase ( ) . includes ( searchQuery . value . toLowerCase ( ) ) ? ? false )
const matchesCategory = categoryFilter . value === 'Todas' || b . category === categoryFilter . value
return matchesSearch && matchesCategory
} )
} )
async function loadCoupons ( ) {
isLoading . value = true
try {
coupons . value = await couponsService . getAllCoupons ( { active _only : false } )
} catch ( e ) {
console . error ( 'Failed to load coupons' , e )
} finally {
isLoading . value = false
}
}
async function loadBusinesses ( ) {
try {
businesses . value = await businessService . getAllBusinesses ( )
} catch ( e ) {
console . error ( 'Failed to load businesses' , e )
}
}
2026-02-25 10:34:38 -05:00
async function loadShuttles ( ) {
try {
shuttles . value = await shuttlesService . getAllShuttles ( )
} catch ( e ) {
console . error ( 'Failed to load shuttles' , e )
}
}
// Shuttle Methods
async function toggleShuttleStatus ( shuttle : Shuttle ) {
try {
2026-02-26 14:51:03 -05:00
const { error } = await supabase
. from ( 'shuttles' )
. update ( { is _active : ! shuttle . is _active } )
. eq ( 'id' , shuttle . id ) ;
if ( error ) throw error ;
await loadShuttles ( ) ;
2026-02-25 10:34:38 -05:00
} catch ( e ) {
alert ( 'Error al actualizar estado del shuttle' )
}
}
async function deleteShuttle ( id : string ) {
if ( confirm ( '¿Estás seguro de eliminar este transporte turístico?' ) ) {
try {
2026-02-26 14:51:03 -05:00
const { error } = await supabase
. from ( 'shuttles' )
. delete ( )
. eq ( 'id' , id ) ;
if ( error ) throw error ;
await loadShuttles ( ) ;
2026-02-25 10:34:38 -05:00
} catch ( e ) {
alert ( 'Error al eliminar shuttle' )
}
}
}
2026-02-21 09:53:31 -05:00
// Business Methods
function openCreateBusinessModal ( ) {
isEditingBusiness . value = false
currentBusiness . value = {
name : '' ,
address : '' ,
phone : '' ,
image _url : '' ,
social _media : '' ,
category : 'Restaurante' ,
2026-03-03 11:45:36 -05:00
area : 'Boquete' ,
description : '' ,
feat: business template/mold redesign
- Supabase: Add schedule, whatsapp, instagram, facebook, gallery_images
columns to businesses table
- types/index.ts: Add 5 new fields to Business interface
- businessService.ts: Update all SELECT queries to include new fields,
add _parseFormData helper, handle gallery_images as JSON array
- BusinessDetailsView.vue: Full redesign matching the approved mockup:
* Hero 300px with centered name + yellow category badge
* Horizontal scrollable quick-info pills (area, schedule, phone, web)
* Image carousel (main image + gallery_images) with arrows + dots
* About section with yellow left-accent bar
* 2x2 social grid (WhatsApp, Instagram, Facebook, Maps) with brand colors
* Coupon section preserved
* Sticky CTA bar: Ver en el Mapa + Llamar buttons
- PromoterDashboard.vue: Updated business modal with grouped sections
(Info Basica / Portada / Descripcion / Redes / Galeria) + new fields
2026-03-03 21:31:35 -05:00
website : '' ,
schedule : '' ,
whatsapp : '' ,
instagram : '' ,
facebook : '' ,
gallery _images : [ ]
2026-02-21 09:53:31 -05:00
}
showBusinessModal . value = true
businessImageFile . value = null
businessImagePreview . value = null
}
function handleBusinessImage ( event : any ) {
const file = event . target . files [ 0 ]
if ( file ) {
businessImageFile . value = file
businessImagePreview . value = URL . createObjectURL ( file )
}
}
function openEditBusinessModal ( biz : Business ) {
isEditingBusiness . value = true
currentBusiness . value = { ... biz }
showBusinessModal . value = true
businessImageFile . value = null
businessImagePreview . value = getImageUrl ( biz . image _url )
}
async function saveBusiness ( ) {
try {
const formData = new FormData ( )
formData . append ( 'name' , currentBusiness . value . name || '' )
formData . append ( 'category' , currentBusiness . value . category || 'Restaurante' )
formData . append ( 'address' , currentBusiness . value . address || '' )
formData . append ( 'phone' , currentBusiness . value . phone || '' )
formData . append ( 'social_media' , currentBusiness . value . social _media || '' )
formData . append ( 'area' , currentBusiness . value . area || 'Boquete' )
2026-03-03 11:45:36 -05:00
formData . append ( 'description' , currentBusiness . value . description || '' )
formData . append ( 'website' , currentBusiness . value . website || '' )
feat: business template/mold redesign
- Supabase: Add schedule, whatsapp, instagram, facebook, gallery_images
columns to businesses table
- types/index.ts: Add 5 new fields to Business interface
- businessService.ts: Update all SELECT queries to include new fields,
add _parseFormData helper, handle gallery_images as JSON array
- BusinessDetailsView.vue: Full redesign matching the approved mockup:
* Hero 300px with centered name + yellow category badge
* Horizontal scrollable quick-info pills (area, schedule, phone, web)
* Image carousel (main image + gallery_images) with arrows + dots
* About section with yellow left-accent bar
* 2x2 social grid (WhatsApp, Instagram, Facebook, Maps) with brand colors
* Coupon section preserved
* Sticky CTA bar: Ver en el Mapa + Llamar buttons
- PromoterDashboard.vue: Updated business modal with grouped sections
(Info Basica / Portada / Descripcion / Redes / Galeria) + new fields
2026-03-03 21:31:35 -05:00
// Template fields
formData . append ( 'schedule' , currentBusiness . value . schedule || '' )
formData . append ( 'whatsapp' , currentBusiness . value . whatsapp || '' )
formData . append ( 'instagram' , currentBusiness . value . instagram || '' )
formData . append ( 'facebook' , currentBusiness . value . facebook || '' )
// gallery_images handled separately via JSON
if ( currentBusiness . value . gallery _images ? . length ) {
formData . append ( 'gallery_images' , JSON . stringify ( currentBusiness . value . gallery _images ) )
}
2026-02-21 09:53:31 -05:00
if ( businessImageFile . value ) {
formData . append ( 'image' , businessImageFile . value )
}
if ( isEditingBusiness . value && currentBusiness . value . id ) {
await businessService . updateBusiness ( currentBusiness . value . id , formData )
} else {
await businessService . createBusiness ( formData )
}
showBusinessModal . value = false
await loadBusinesses ( )
} catch ( e ) {
console . error ( 'Error saving business:' , e )
alert ( 'Error al guardar el negocio' )
}
}
async function deleteBusiness ( id : string ) {
if ( confirm ( '¿Estás seguro de eliminar este negocio? Los cupones asociados podrían verse afectados.' ) ) {
try {
await businessService . deleteBusiness ( id )
await loadBusinesses ( )
} catch ( e ) {
alert ( 'Error al eliminar' )
}
}
}
// Coupon Methods
function openCreateModal ( ) {
isEditing . value = false
currentCoupon . value = {
title : '' ,
business _id : null ,
description : '' ,
image _url : '' ,
social _media : '' ,
terms : '' ,
discount _percentage : null ,
discount _amount : null ,
category : 'Restaurante' ,
valid _from : '' ,
valid _until : '' ,
is _active : true
}
showModal . value = true
}
function handleBusinessChange ( ) {
const selectedBiz = businesses . value . find ( b => b . id === currentCoupon . value . business _id )
if ( selectedBiz ) {
currentCoupon . value . image _url = selectedBiz . image _url
currentCoupon . value . social _media = selectedBiz . social _media
currentCoupon . value . category = selectedBiz . category
}
}
function openEditModal ( coupon : Coupon ) {
isEditing . value = true
currentCoupon . value = { ... coupon }
showModal . value = true
}
function getImageUrl ( path : string | null | undefined ) {
if ( ! path ) return '/default-coupon.png'
2026-02-26 14:51:03 -05:00
return path
2026-02-21 09:53:31 -05:00
}
async function saveCoupon ( ) {
try {
if ( ! currentCoupon . value . title ? . trim ( ) ) {
alert ( 'El título es obligatorio' )
return
}
const data : any = { ... currentCoupon . value }
if ( ! isEditing . value ) delete data . id
const fieldsToClean = [
2026-02-26 15:36:32 -05:00
'description' , 'image_url' , 'social_media' , 'terms' ,
2026-02-21 09:53:31 -05:00
'discount_percentage' , 'discount_amount' , 'valid_from' , 'valid_until'
]
fieldsToClean . forEach ( field => {
if ( data [ field ] === '' || data [ field ] === undefined ) data [ field ] = null
} )
if ( data . discount _percentage !== null ) data . discount _percentage = Number ( data . discount _percentage )
if ( data . discount _amount !== null ) data . discount _amount = Number ( data . discount _amount )
if ( isEditing . value && data . id ) {
await couponsService . updateCoupon ( data . id , data )
} else {
await couponsService . createCoupon ( data )
}
showModal . value = false
await loadCoupons ( )
} catch ( e : any ) {
console . error ( 'Error saving coupon:' , e )
alert ( 'Error al guardar el cupón.' )
}
}
async function deleteCoupon ( id : string ) {
if ( confirm ( '¿Estás seguro de eliminar este cupón?' ) ) {
try {
await couponsService . deleteCoupon ( id )
await loadCoupons ( )
} catch ( e ) {
alert ( 'Error al eliminar' )
}
}
}
async function toggleCouponStatus ( coupon : Coupon ) {
try {
await couponsService . updateCoupon ( coupon . id , { is _active : ! coupon . is _active } )
await loadCoupons ( )
} catch ( e ) {
alert ( 'Error al actualizar estado' )
}
}
< / script >
< template >
< div class = "promoter-dashboard" >
< div class = "dashboard-header" >
< div class = "welcome-section" >
< button v-if = "authStore.isAdmin" class="back-link" @click="$router.push('/admin')" >
& larr ; Volver al Panel
< / button >
< button v-if = "authStore.isAdmin" class="back-analytics" @click="$router.push('/admin/analytics')" >
& larr ; Ver Análisis y Datos
< / button >
< h1 > { { authStore . isAdmin ? 'Gestión de Promociones' : 'Panel de Control' } } < / h1 >
< p > Bienvenido , { { userName } } . Gestiona tus negocios y promociones aquí . < / p >
< / div >
< / div >
<!-- Tabs -- >
< div class = "tabs-container" >
< div class = "tabs-buttons" >
< button : class = "['tab-btn', { active: activeTab === 'promotions' }]" @click ="activeTab = 'promotions'" >
Promociones
< / button >
< button : class = "['tab-btn', { active: activeTab === 'businesses' }]" @click ="activeTab = 'businesses'" >
Mis Negocios
< / button >
2026-02-25 10:34:38 -05:00
< button : class = "['tab-btn', { active: activeTab === 'shuttles' }]" @click ="activeTab = 'shuttles'" >
Viajes Turísticos
< / button >
2026-02-21 09:53:31 -05:00
< / div >
< div class = "tabs-actions" >
< div class = "stats-header" >
2026-02-25 10:34:38 -05:00
< div v-if = "activeTab === 'promotions'" class="stats-group" >
< div class = "stat-mini" >
< div class = "stat-value" > { { coupons . length } } < / div >
< div class = "stat-label" > Total Cupones < / div >
< / div >
< div class = "stat-mini" >
< div class = "stat-value active" > { { coupons . filter ( c => c . is _active ) . length } } < / div >
< div class = "stat-label" > Activos < / div >
< / div >
2026-02-21 09:53:31 -05:00
< / div >
2026-02-25 10:34:38 -05:00
< div v-else-if = "activeTab === 'shuttles'" class="stats-group" >
< div class = "stat-mini" >
< div class = "stat-value" > { { shuttles . length } } < / div >
< div class = "stat-label" > Total Shuttles < / div >
< / div >
< div class = "stat-mini" >
< div class = "stat-value active" > { { shuttles . filter ( s => s . is _active ) . length } } < / div >
< div class = "stat-label" > Activos < / div >
< / div >
2026-02-21 09:53:31 -05:00
< / div >
< / div >
< button v-if = "activeTab === 'promotions'" class="primary-btn" @click="openCreateModal" >
< span class = "material-icons" > add < / span >
Nuevo Cupón
< / button >
< button v-if = "activeTab === 'businesses'" class="primary-btn" @click="openCreateBusinessModal" >
< span class = "material-icons" > add < / span >
Nuevo Negocio
< / button >
2026-02-25 10:34:38 -05:00
< button v-if = "activeTab === 'shuttles'" class="primary-btn" @click="$router.push('/admin/shuttles')" >
< span class = "material-icons" > rocket _launch < / span >
Nuevo Shuttle
< / button >
2026-02-21 09:53:31 -05:00
< / div >
< / div >
<!-- Global Search Bar ( Visible in Promos / Businesses ) -- >
< div class = "search-filter-bar" >
< div class = "search-box" >
< span class = "material-icons" > search < / span >
2026-02-25 10:34:38 -05:00
< input v-model = "searchQuery" type="text" :placeholder="activeTab === 'promotions' ? 'Buscar promoción...' : (activeTab === 'businesses' ? 'Buscar negocio...' : 'Buscar shuttle...')" >
2026-02-21 09:53:31 -05:00
< / div >
< div class = "filter-box" >
< span class = "material-icons" > filter _alt < / span >
< select v-model = "categoryFilter" >
< option v-for = "cat in categories" :key="cat" :value="cat" > {{ cat }} < / option >
< / select >
< / div >
< / div >
< div v-if = "activeTab === 'promotions'" >
< div class = "coupons-section" >
< div v-if = "isLoading" class="loading-state" >
< span class = "material-icons spin" > refresh < / span >
Cargando promociones ...
< / div >
< div v-else-if = "coupons.length === 0" class="empty-state" >
< span class = "material-icons" > sentiment _dissatisfied < / span >
< p > No hay cupones creados aún . < / p >
< / div >
< div v-else class = "table-card" >
< table class = "coupons-table" >
< thead >
< tr >
< th > Promoción / Local < / th >
< th class = "text-center" > Categoría < / th >
< th class = "text-center" > Descuento < / th >
< th class = "text-center" > Estado < / th >
< th class = "text-center" > Acciones < / th >
< / tr >
< / thead >
< tbody >
< tr v-for = "coupon in filteredCoupons" :key="coupon.id" >
< td >
< div class = "title-cell" >
< div class = "coupon-header-cell" >
< img :src = "getImageUrl(coupon.image_url)" class = "coupon-mini-img" / >
< div >
< strong > { { coupon . title } } < / strong >
< div class = "business-tag" >
< span class = "material-icons" > store < / span >
2026-02-26 15:36:32 -05:00
{ { coupon . business ? . name || 'Comercio Local' } }
2026-02-21 09:53:31 -05:00
< / div >
< / div >
< / div >
< / div >
< / td >
< td class = "text-center" > < span class = "badge" > { { coupon . category } } < / span > < / td >
< td class = "text-center" >
< span class = "discount-label" >
{ { coupon . discount _percentage ? ` ${ coupon . discount _percentage } % ` : ` $ ${ coupon . discount _amount } ` } }
< / span >
< / td >
< td class = "text-center" >
< button
: class = "['status-toggle', { active: coupon.is_active }]"
@ click = "toggleCouponStatus(coupon)"
>
{ { coupon . is _active ? 'Activo' : 'Inactivo' } }
< / button >
< / td >
< td class = "text-center" >
< div class = "action-buttons justify-center" >
< button class = "icon-btn edit" @click ="openEditModal(coupon)" >
< span class = "material-icons" > edit < / span >
< / button >
< button class = "icon-btn delete" @click ="deleteCoupon(coupon.id)" >
< span class = "material-icons" > delete < / span >
< / button >
< / div >
< / td >
< / tr >
< / tbody >
< / table >
< / div >
< / div >
< / div >
<!-- Businesses Tab -- >
< div v-if = "activeTab === 'businesses'" >
< div v-if = "businesses.length === 0" class="empty-state" >
< span class = "material-icons" > store _front < / span >
< p > Aún no has registrado ningún negocio o local . < / p >
< / div >
< div v-else class = "table-card" >
< table class = "coupons-table" >
< thead >
< tr >
< th > Negocio / Local < / th >
< th class = "text-center" > Categoría < / th >
< th class = "text-center" > Área < / th >
< th class = "text-center" > Contacto < / th >
< th class = "text-center" > Dirección < / th >
< th class = "text-center" > Acciones < / th >
< / tr >
< / thead >
< tbody >
< tr v-for = "biz in filteredBusinesses" :key="biz.id" >
< td >
< div class = "title-cell" >
< div class = "coupon-header-cell" >
< img :src = "getImageUrl(biz.image_url)" class = "coupon-mini-img" / >
< strong > { { biz . name } } < / strong >
< / div >
< / div >
< / td >
< td class = "text-center" > < span class = "badge" > { { biz . category } } < / span > < / td >
< td class = "text-center" > < span class = "badge area-badge" > { { biz . area } } < / span > < / td >
< td class = "text-center" >
< div class = "contact-info align-center" >
< span v-if = "biz.phone"><span class="material-icons" > phone < / span > {{ biz.phone }} < / span >
< span v-if = "biz.social_media" class="social-tag"><span class="material-icons" > share < / span > {{ biz.social_media }} < / span >
< / div >
< / td >
< td class = "address-cell text-center" > { { biz . address } } < / td >
< td class = "text-center" >
< div class = "action-buttons justify-center" >
< button class = "icon-btn edit" @click ="openEditBusinessModal(biz)" >
< span class = "material-icons" > edit < / span >
< / button >
< button class = "icon-btn delete" @click ="deleteBusiness(biz.id)" >
< span class = "material-icons" > delete < / span >
< / button >
< / div >
< / td >
< / tr >
< / tbody >
< / table >
< / div >
< / div >
2026-02-25 10:34:38 -05:00
<!-- Shuttles Tab -- >
< div v-if = "activeTab === 'shuttles'" >
< div v-if = "shuttles.length === 0" class="empty-state" >
< span class = "material-icons" > directions _bus < / span >
< p > No hay shuttles turísticos registrados . < / p >
< / div >
< div v-else class = "table-card" >
< table class = "coupons-table" >
< thead >
< tr >
< th > Ruta / Empresa < / th >
< th class = "text-center" > Tipo Vehículo < / th >
< th class = "text-center" > Precio ( Persona ) < / th >
< th class = "text-center" > Estado < / th >
< th class = "text-center" > Acciones < / th >
< / tr >
< / thead >
< tbody >
< tr v-for = "shuttle in filteredShuttles" :key="shuttle.id" >
< td >
< div class = "title-cell" >
< div class = "coupon-header-cell" >
< img :src = "getImageUrl(shuttle.image_url)" class = "coupon-mini-img" / >
< div >
< strong > { { shuttle . route _name } } < / strong >
< div class = "business-tag" >
< span class = "material-icons" > business < / span >
{ { shuttle . company _name } }
< / div >
< / div >
< / div >
< / div >
< / td >
< td class = "text-center" > < span class = "badge" > { { shuttle . vehicle _type } } < / span > < / td >
< td class = "text-center" >
< span class = "discount-label" > $ { { shuttle . price _per _person } } < / span >
< / td >
< td class = "text-center" >
< button
: class = "['status-toggle', { active: shuttle.is_active }]"
@ click = "toggleShuttleStatus(shuttle)"
>
{ { shuttle . is _active ? 'Activo' : 'Inactivo' } }
< / button >
< / td >
< td class = "text-center" >
< div class = "action-buttons justify-center" >
< button class = "icon-btn delete" @click ="deleteShuttle(shuttle.id)" >
< span class = "material-icons" > delete < / span >
< / button >
< / div >
< / td >
< / tr >
< / tbody >
< / table >
< / div >
< / div >
2026-02-21 09:53:31 -05:00
<!-- Coupon Modal -- >
< div v-if = "showModal" class="modal-overlay" @click.self="showModal = false" >
< div class = "modal-content" >
< div class = "modal-header" >
< h2 > { { isEditing ? 'Editar Cupón' : 'Nuevo Cupón' } } < / h2 >
< button class = "close-btn" @click ="showModal = false" >
< span class = "material-icons" > close < / span >
< / button >
< / div >
< form @submit.prevent ="saveCoupon" class = "coupon-form" >
< div class = "form-row" >
< div class = "form-group full" >
< label > Título de la promoción < / label >
< input v-model = "currentCoupon.title" type="text" placeholder="Ej: 2x1 en Pizzas" required >
< / div >
< / div >
< div class = "form-group" >
< label > Seleccionar Negocio ( Auto - completa la info ) < / label >
< select v-model = "currentCoupon.business_id" @change="handleBusinessChange" >
< option :value = "null" > -- Ingresar datos manualmente -- < / option >
< option v-for = "biz in businesses" :key="biz.id" :value="biz.id" > {{ biz.name }} < / option >
< / select >
< / div >
< div class = "form-row" >
< div class = "form-group" >
< label > Nombre del Local < / label >
2026-02-26 15:36:32 -05:00
< input : value = "businesses.find(b => b.id === currentCoupon.business_id)?.name || ''" type = "text" readonly disabled class = "readonly-input" >
2026-02-21 09:53:31 -05:00
< / div >
< div class = "form-group" >
< label > Categoría < / label >
< select v-model = "currentCoupon.category" >
< option value = "Restaurante" > Restaurante < / option >
< option value = "Area Turistica" > Área Turística < / option >
< option value = "Bebidas" > Bar / Bebidas < / option >
< option value = "Viajes de Turismo" > Viajes de Turismo < / option >
< / select >
< / div >
< / div >
< div class = "form-group" >
< label > Descripción < / label >
< textarea v-model = "currentCoupon.description" placeholder="¿En qué consiste la promo?" > < / textarea >
< / div >
< div class = "form-row" >
< div class = "form-group" >
< label > URL Imagen < / label >
< input v-model = "currentCoupon.image_url" type="text" >
< / div >
< div class = "form-group" >
< label > Redes Sociales < / label >
< input v-model = "currentCoupon.social_media" type="text" >
< / div >
< / div >
< div class = "form-row" >
< div class = "form-group" >
< label > Vence el ... < / label >
< input v-model = "currentCoupon.valid_until" type="date" >
< / div >
< div class = "form-group" >
< label > Estado < / label >
< select v-model = "currentCoupon.is_active" >
< option :value = "true" > Activo < / option >
< option :value = "false" > Pausado < / option >
< / select >
< / div >
< / div >
< div class = "form-actions" >
< button type = "submit" class = "submit-btn" > { { isEditing ? 'Guardar Cambios' : 'Lanzar Promo' } } < / button >
< / div >
< / form >
< / div >
< / div >
<!-- Business Modal -- >
< div v-if = "showBusinessModal" class="modal-overlay" @click.self="showBusinessModal = false" >
< div class = "modal-content" >
< div class = "modal-header" >
< h2 > { { isEditingBusiness ? 'Editar Negocio' : 'Registrar Negocio' } } < / h2 >
< button class = "close-btn" @click ="showBusinessModal = false" > < span class = "material-icons" > close < / span > < / button >
< / div >
< form @submit.prevent ="saveBusiness" class = "coupon-form" >
feat: business template/mold redesign
- Supabase: Add schedule, whatsapp, instagram, facebook, gallery_images
columns to businesses table
- types/index.ts: Add 5 new fields to Business interface
- businessService.ts: Update all SELECT queries to include new fields,
add _parseFormData helper, handle gallery_images as JSON array
- BusinessDetailsView.vue: Full redesign matching the approved mockup:
* Hero 300px with centered name + yellow category badge
* Horizontal scrollable quick-info pills (area, schedule, phone, web)
* Image carousel (main image + gallery_images) with arrows + dots
* About section with yellow left-accent bar
* 2x2 social grid (WhatsApp, Instagram, Facebook, Maps) with brand colors
* Coupon section preserved
* Sticky CTA bar: Ver en el Mapa + Llamar buttons
- PromoterDashboard.vue: Updated business modal with grouped sections
(Info Basica / Portada / Descripcion / Redes / Galeria) + new fields
2026-03-03 21:31:35 -05:00
<!-- ─ ─ Info básica ─ ─ -- >
< div class = "form-section-label" > 📋 Información Básica < / div >
2026-02-21 09:53:31 -05:00
< div class = "form-group" >
feat: business template/mold redesign
- Supabase: Add schedule, whatsapp, instagram, facebook, gallery_images
columns to businesses table
- types/index.ts: Add 5 new fields to Business interface
- businessService.ts: Update all SELECT queries to include new fields,
add _parseFormData helper, handle gallery_images as JSON array
- BusinessDetailsView.vue: Full redesign matching the approved mockup:
* Hero 300px with centered name + yellow category badge
* Horizontal scrollable quick-info pills (area, schedule, phone, web)
* Image carousel (main image + gallery_images) with arrows + dots
* About section with yellow left-accent bar
* 2x2 social grid (WhatsApp, Instagram, Facebook, Maps) with brand colors
* Coupon section preserved
* Sticky CTA bar: Ver en el Mapa + Llamar buttons
- PromoterDashboard.vue: Updated business modal with grouped sections
(Info Basica / Portada / Descripcion / Redes / Galeria) + new fields
2026-03-03 21:31:35 -05:00
< label > Nombre del Negocio * < / label >
< input v-model = "currentBusiness.name" type="text" required placeholder="Ej: Restaurante La Casona" >
2026-02-21 09:53:31 -05:00
< / div >
< div class = "form-row" >
< div class = "form-group" >
< label > Categoría < / label >
< select v-model = "currentBusiness.category" >
< option value = "Restaurante" > Restaurante < / option >
feat: business template/mold redesign
- Supabase: Add schedule, whatsapp, instagram, facebook, gallery_images
columns to businesses table
- types/index.ts: Add 5 new fields to Business interface
- businessService.ts: Update all SELECT queries to include new fields,
add _parseFormData helper, handle gallery_images as JSON array
- BusinessDetailsView.vue: Full redesign matching the approved mockup:
* Hero 300px with centered name + yellow category badge
* Horizontal scrollable quick-info pills (area, schedule, phone, web)
* Image carousel (main image + gallery_images) with arrows + dots
* About section with yellow left-accent bar
* 2x2 social grid (WhatsApp, Instagram, Facebook, Maps) with brand colors
* Coupon section preserved
* Sticky CTA bar: Ver en el Mapa + Llamar buttons
- PromoterDashboard.vue: Updated business modal with grouped sections
(Info Basica / Portada / Descripcion / Redes / Galeria) + new fields
2026-03-03 21:31:35 -05:00
< option value = "Hotel" > Hotel < / option >
< option value = "Café" > Café < / option >
2026-02-21 09:53:31 -05:00
< option value = "Bebidas" > Bar / Bebidas < / option >
feat: business template/mold redesign
- Supabase: Add schedule, whatsapp, instagram, facebook, gallery_images
columns to businesses table
- types/index.ts: Add 5 new fields to Business interface
- businessService.ts: Update all SELECT queries to include new fields,
add _parseFormData helper, handle gallery_images as JSON array
- BusinessDetailsView.vue: Full redesign matching the approved mockup:
* Hero 300px with centered name + yellow category badge
* Horizontal scrollable quick-info pills (area, schedule, phone, web)
* Image carousel (main image + gallery_images) with arrows + dots
* About section with yellow left-accent bar
* 2x2 social grid (WhatsApp, Instagram, Facebook, Maps) with brand colors
* Coupon section preserved
* Sticky CTA bar: Ver en el Mapa + Llamar buttons
- PromoterDashboard.vue: Updated business modal with grouped sections
(Info Basica / Portada / Descripcion / Redes / Galeria) + new fields
2026-03-03 21:31:35 -05:00
< option value = "Comercio" > Comercio < / option >
< option value = "Turismo" > Turismo < / option >
2026-02-21 09:53:31 -05:00
< / select >
< / div >
< div class = "form-group" >
< label > Área / Región < / label >
< select v-model = "currentBusiness.area" >
< option value = "Boquete" > Boquete < / option >
feat: business template/mold redesign
- Supabase: Add schedule, whatsapp, instagram, facebook, gallery_images
columns to businesses table
- types/index.ts: Add 5 new fields to Business interface
- businessService.ts: Update all SELECT queries to include new fields,
add _parseFormData helper, handle gallery_images as JSON array
- BusinessDetailsView.vue: Full redesign matching the approved mockup:
* Hero 300px with centered name + yellow category badge
* Horizontal scrollable quick-info pills (area, schedule, phone, web)
* Image carousel (main image + gallery_images) with arrows + dots
* About section with yellow left-accent bar
* 2x2 social grid (WhatsApp, Instagram, Facebook, Maps) with brand colors
* Coupon section preserved
* Sticky CTA bar: Ver en el Mapa + Llamar buttons
- PromoterDashboard.vue: Updated business modal with grouped sections
(Info Basica / Portada / Descripcion / Redes / Galeria) + new fields
2026-03-03 21:31:35 -05:00
< option value = "Alto Boquete" > Alto Boquete < / option >
2026-02-21 09:53:31 -05:00
< option value = "Dolega" > Dolega < / option >
< option value = "David" > David < / option >
feat: business template/mold redesign
- Supabase: Add schedule, whatsapp, instagram, facebook, gallery_images
columns to businesses table
- types/index.ts: Add 5 new fields to Business interface
- businessService.ts: Update all SELECT queries to include new fields,
add _parseFormData helper, handle gallery_images as JSON array
- BusinessDetailsView.vue: Full redesign matching the approved mockup:
* Hero 300px with centered name + yellow category badge
* Horizontal scrollable quick-info pills (area, schedule, phone, web)
* Image carousel (main image + gallery_images) with arrows + dots
* About section with yellow left-accent bar
* 2x2 social grid (WhatsApp, Instagram, Facebook, Maps) with brand colors
* Coupon section preserved
* Sticky CTA bar: Ver en el Mapa + Llamar buttons
- PromoterDashboard.vue: Updated business modal with grouped sections
(Info Basica / Portada / Descripcion / Redes / Galeria) + new fields
2026-03-03 21:31:35 -05:00
< option value = "Caldera" > Caldera < / option >
< option value = "Chiriquí" > Chiriquí < / option >
2026-02-21 09:53:31 -05:00
< / select >
< / div >
< / div >
feat: business template/mold redesign
- Supabase: Add schedule, whatsapp, instagram, facebook, gallery_images
columns to businesses table
- types/index.ts: Add 5 new fields to Business interface
- businessService.ts: Update all SELECT queries to include new fields,
add _parseFormData helper, handle gallery_images as JSON array
- BusinessDetailsView.vue: Full redesign matching the approved mockup:
* Hero 300px with centered name + yellow category badge
* Horizontal scrollable quick-info pills (area, schedule, phone, web)
* Image carousel (main image + gallery_images) with arrows + dots
* About section with yellow left-accent bar
* 2x2 social grid (WhatsApp, Instagram, Facebook, Maps) with brand colors
* Coupon section preserved
* Sticky CTA bar: Ver en el Mapa + Llamar buttons
- PromoterDashboard.vue: Updated business modal with grouped sections
(Info Basica / Portada / Descripcion / Redes / Galeria) + new fields
2026-03-03 21:31:35 -05:00
< div class = "form-row" >
< div class = "form-group" >
< label > Teléfono < / label >
< input v-model = "currentBusiness.phone" type="text" placeholder="+507 6000-0000" >
< / div >
< div class = "form-group" >
< label > 🕐 Horario de Atención < / label >
< input v-model = "currentBusiness.schedule" type="text" placeholder="Ej: Lun-Sáb 8am-10pm" >
< / div >
< / div >
2026-02-21 09:53:31 -05:00
< div class = "form-group" >
< label > Dirección Física < / label >
< input v-model = "currentBusiness.address" type="text" placeholder="Ej: Calle Principal #123, Frente al Parque" >
< / div >
feat: business template/mold redesign
- Supabase: Add schedule, whatsapp, instagram, facebook, gallery_images
columns to businesses table
- types/index.ts: Add 5 new fields to Business interface
- businessService.ts: Update all SELECT queries to include new fields,
add _parseFormData helper, handle gallery_images as JSON array
- BusinessDetailsView.vue: Full redesign matching the approved mockup:
* Hero 300px with centered name + yellow category badge
* Horizontal scrollable quick-info pills (area, schedule, phone, web)
* Image carousel (main image + gallery_images) with arrows + dots
* About section with yellow left-accent bar
* 2x2 social grid (WhatsApp, Instagram, Facebook, Maps) with brand colors
* Coupon section preserved
* Sticky CTA bar: Ver en el Mapa + Llamar buttons
- PromoterDashboard.vue: Updated business modal with grouped sections
(Info Basica / Portada / Descripcion / Redes / Galeria) + new fields
2026-03-03 21:31:35 -05:00
<!-- ─ ─ Imagen principal ─ ─ -- >
< div class = "form-section-label" > 🖼 ️ Imagen de Portada < / div >
2026-02-21 09:53:31 -05:00
< div class = "form-group" >
feat: business template/mold redesign
- Supabase: Add schedule, whatsapp, instagram, facebook, gallery_images
columns to businesses table
- types/index.ts: Add 5 new fields to Business interface
- businessService.ts: Update all SELECT queries to include new fields,
add _parseFormData helper, handle gallery_images as JSON array
- BusinessDetailsView.vue: Full redesign matching the approved mockup:
* Hero 300px with centered name + yellow category badge
* Horizontal scrollable quick-info pills (area, schedule, phone, web)
* Image carousel (main image + gallery_images) with arrows + dots
* About section with yellow left-accent bar
* 2x2 social grid (WhatsApp, Instagram, Facebook, Maps) with brand colors
* Coupon section preserved
* Sticky CTA bar: Ver en el Mapa + Llamar buttons
- PromoterDashboard.vue: Updated business modal with grouped sections
(Info Basica / Portada / Descripcion / Redes / Galeria) + new fields
2026-03-03 21:31:35 -05:00
< label > Foto Principal ( Logo o Fachada ) < / label >
2026-02-21 09:53:31 -05:00
< div class = "file-upload-wrapper" >
< input type = "file" @change ="handleBusinessImage" accept = "image/*" class = "file-input" >
< div class = "file-preview" v-if = "businessImagePreview" >
< img :src = "businessImagePreview" alt = "Vista previa" >
< / div >
< / div >
< / div >
feat: business template/mold redesign
- Supabase: Add schedule, whatsapp, instagram, facebook, gallery_images
columns to businesses table
- types/index.ts: Add 5 new fields to Business interface
- businessService.ts: Update all SELECT queries to include new fields,
add _parseFormData helper, handle gallery_images as JSON array
- BusinessDetailsView.vue: Full redesign matching the approved mockup:
* Hero 300px with centered name + yellow category badge
* Horizontal scrollable quick-info pills (area, schedule, phone, web)
* Image carousel (main image + gallery_images) with arrows + dots
* About section with yellow left-accent bar
* 2x2 social grid (WhatsApp, Instagram, Facebook, Maps) with brand colors
* Coupon section preserved
* Sticky CTA bar: Ver en el Mapa + Llamar buttons
- PromoterDashboard.vue: Updated business modal with grouped sections
(Info Basica / Portada / Descripcion / Redes / Galeria) + new fields
2026-03-03 21:31:35 -05:00
<!-- ─ ─ Descripción ─ ─ -- >
< div class = "form-section-label" > 📝 Descripción < / div >
2026-03-03 11:45:36 -05:00
< div class = "form-group" >
feat: business template/mold redesign
- Supabase: Add schedule, whatsapp, instagram, facebook, gallery_images
columns to businesses table
- types/index.ts: Add 5 new fields to Business interface
- businessService.ts: Update all SELECT queries to include new fields,
add _parseFormData helper, handle gallery_images as JSON array
- BusinessDetailsView.vue: Full redesign matching the approved mockup:
* Hero 300px with centered name + yellow category badge
* Horizontal scrollable quick-info pills (area, schedule, phone, web)
* Image carousel (main image + gallery_images) with arrows + dots
* About section with yellow left-accent bar
* 2x2 social grid (WhatsApp, Instagram, Facebook, Maps) with brand colors
* Coupon section preserved
* Sticky CTA bar: Ver en el Mapa + Llamar buttons
- PromoterDashboard.vue: Updated business modal with grouped sections
(Info Basica / Portada / Descripcion / Redes / Galeria) + new fields
2026-03-03 21:31:35 -05:00
< label > Sobre el Negocio ( Texto de marketing ) < / label >
< textarea v-model = "currentBusiness.description" placeholder="Describe la experiencia del lugar, su especialidad y ambiente para atraer clientes..." rows="4" > < / textarea >
2026-03-03 11:45:36 -05:00
< / div >
feat: business template/mold redesign
- Supabase: Add schedule, whatsapp, instagram, facebook, gallery_images
columns to businesses table
- types/index.ts: Add 5 new fields to Business interface
- businessService.ts: Update all SELECT queries to include new fields,
add _parseFormData helper, handle gallery_images as JSON array
- BusinessDetailsView.vue: Full redesign matching the approved mockup:
* Hero 300px with centered name + yellow category badge
* Horizontal scrollable quick-info pills (area, schedule, phone, web)
* Image carousel (main image + gallery_images) with arrows + dots
* About section with yellow left-accent bar
* 2x2 social grid (WhatsApp, Instagram, Facebook, Maps) with brand colors
* Coupon section preserved
* Sticky CTA bar: Ver en el Mapa + Llamar buttons
- PromoterDashboard.vue: Updated business modal with grouped sections
(Info Basica / Portada / Descripcion / Redes / Galeria) + new fields
2026-03-03 21:31:35 -05:00
<!-- ─ ─ Redes Sociales y Contacto ─ ─ -- >
< div class = "form-section-label" > 🌐 Contacto y Redes Sociales < / div >
< div class = "form-row" >
< div class = "form-group" >
< label > 💬 WhatsApp < / label >
< input v-model = "currentBusiness.whatsapp" type="text" placeholder="50760000000 (con código de país)" >
< / div >
< div class = "form-group" >
< label > 📸 Instagram < / label >
< input v-model = "currentBusiness.instagram" type="text" placeholder="@usuario" >
< / div >
2026-03-03 11:45:36 -05:00
< / div >
feat: business template/mold redesign
- Supabase: Add schedule, whatsapp, instagram, facebook, gallery_images
columns to businesses table
- types/index.ts: Add 5 new fields to Business interface
- businessService.ts: Update all SELECT queries to include new fields,
add _parseFormData helper, handle gallery_images as JSON array
- BusinessDetailsView.vue: Full redesign matching the approved mockup:
* Hero 300px with centered name + yellow category badge
* Horizontal scrollable quick-info pills (area, schedule, phone, web)
* Image carousel (main image + gallery_images) with arrows + dots
* About section with yellow left-accent bar
* 2x2 social grid (WhatsApp, Instagram, Facebook, Maps) with brand colors
* Coupon section preserved
* Sticky CTA bar: Ver en el Mapa + Llamar buttons
- PromoterDashboard.vue: Updated business modal with grouped sections
(Info Basica / Portada / Descripcion / Redes / Galeria) + new fields
2026-03-03 21:31:35 -05:00
< div class = "form-row" >
< div class = "form-group" >
< label > 👍 Facebook < / label >
< input v-model = "currentBusiness.facebook" type="text" placeholder="/nombre-de-pagina" >
< / div >
< div class = "form-group" >
< label > 🌐 Página Web < / label >
< input v-model = "currentBusiness.website" type="url" placeholder="https://www.ejemplo.com" >
< / div >
< / div >
<!-- ─ ─ Galería ─ ─ -- >
< div class = "form-section-label" > 📸 Galería de Imágenes ( Carrusel ) < / div >
2026-02-21 09:53:31 -05:00
< div class = "form-group" >
feat: business template/mold redesign
- Supabase: Add schedule, whatsapp, instagram, facebook, gallery_images
columns to businesses table
- types/index.ts: Add 5 new fields to Business interface
- businessService.ts: Update all SELECT queries to include new fields,
add _parseFormData helper, handle gallery_images as JSON array
- BusinessDetailsView.vue: Full redesign matching the approved mockup:
* Hero 300px with centered name + yellow category badge
* Horizontal scrollable quick-info pills (area, schedule, phone, web)
* Image carousel (main image + gallery_images) with arrows + dots
* About section with yellow left-accent bar
* 2x2 social grid (WhatsApp, Instagram, Facebook, Maps) with brand colors
* Coupon section preserved
* Sticky CTA bar: Ver en el Mapa + Llamar buttons
- PromoterDashboard.vue: Updated business modal with grouped sections
(Info Basica / Portada / Descripcion / Redes / Galeria) + new fields
2026-03-03 21:31:35 -05:00
< label > URLs de fotos adicionales ( una por línea ) < / label >
< textarea
: value = "currentBusiness.gallery_images?.join('\n') || ''"
@ input = "(e: any) => currentBusiness.gallery_images = e.target.value.split('\n').map((s: string) => s.trim()).filter((s: string) => s.length > 0)"
placeholder = "https://url-foto1.jpg https://url-foto2.jpg https://url-foto3.jpg"
rows = "3"
> < / textarea >
< small class = "form-hint" > Agrega URLs de imágenes para el carrusel ( menú , ambiente , experiencias ) . Una URL por línea . < / small >
2026-02-21 09:53:31 -05:00
< / div >
feat: business template/mold redesign
- Supabase: Add schedule, whatsapp, instagram, facebook, gallery_images
columns to businesses table
- types/index.ts: Add 5 new fields to Business interface
- businessService.ts: Update all SELECT queries to include new fields,
add _parseFormData helper, handle gallery_images as JSON array
- BusinessDetailsView.vue: Full redesign matching the approved mockup:
* Hero 300px with centered name + yellow category badge
* Horizontal scrollable quick-info pills (area, schedule, phone, web)
* Image carousel (main image + gallery_images) with arrows + dots
* About section with yellow left-accent bar
* 2x2 social grid (WhatsApp, Instagram, Facebook, Maps) with brand colors
* Coupon section preserved
* Sticky CTA bar: Ver en el Mapa + Llamar buttons
- PromoterDashboard.vue: Updated business modal with grouped sections
(Info Basica / Portada / Descripcion / Redes / Galeria) + new fields
2026-03-03 21:31:35 -05:00
2026-02-21 09:53:31 -05:00
< div class = "form-actions" >
feat: business template/mold redesign
- Supabase: Add schedule, whatsapp, instagram, facebook, gallery_images
columns to businesses table
- types/index.ts: Add 5 new fields to Business interface
- businessService.ts: Update all SELECT queries to include new fields,
add _parseFormData helper, handle gallery_images as JSON array
- BusinessDetailsView.vue: Full redesign matching the approved mockup:
* Hero 300px with centered name + yellow category badge
* Horizontal scrollable quick-info pills (area, schedule, phone, web)
* Image carousel (main image + gallery_images) with arrows + dots
* About section with yellow left-accent bar
* 2x2 social grid (WhatsApp, Instagram, Facebook, Maps) with brand colors
* Coupon section preserved
* Sticky CTA bar: Ver en el Mapa + Llamar buttons
- PromoterDashboard.vue: Updated business modal with grouped sections
(Info Basica / Portada / Descripcion / Redes / Galeria) + new fields
2026-03-03 21:31:35 -05:00
< button type = "submit" class = "submit-btn" >
< span class = "material-icons" > { { isEditingBusiness ? 'save' : 'store' } } < / span >
{ { isEditingBusiness ? 'Guardar Cambios' : 'Publicar Negocio' } }
< / button >
2026-02-21 09:53:31 -05:00
< / div >
< / form >
< / div >
< / div >
< / div >
< / template >
< style scoped >
/* Tabs */
. dashboard - header {
display : flex ;
justify - content : center ;
align - items : center ;
text - align : center ;
margin - bottom : 2.5 rem ;
padding - bottom : 1 rem ;
}
. welcome - section {
max - width : 800 px ;
}
. welcome - section h1 {
font - size : 2.5 rem ;
font - weight : 800 ;
margin : 0 ;
color : var ( -- text - primary ) ;
}
. welcome - section p {
color : var ( -- text - secondary ) ;
margin - top : 0.5 rem ;
font - size : 1.1 rem ;
}
. back - analytics {
background : transparent ;
border : 1 px solid var ( -- border - color ) ;
color : var ( -- text - secondary ) ;
padding : 6 px 12 px ;
border - radius : 6 px ;
cursor : pointer ;
font - size : 0.9 rem ;
margin - bottom : 12 px ;
}
. back - analytics : hover {
background : var ( -- bg - secondary ) ;
color : var ( -- active - color ) ;
}
. logout - btn {
background : # fdeaea ;
color : # e74c3c ;
border : 1 px solid # fabebb ;
padding : 0.6 rem 1.2 rem ;
border - radius : 8 px ;
display : flex ;
align - items : center ;
gap : 8 px ;
font - weight : 600 ;
cursor : pointer ;
transition : all 0.2 s ;
}
. logout - btn : hover {
background : # fcd5d5 ;
transform : translateY ( - 2 px ) ;
}
/* Header Stats */
. tabs - actions {
display : flex ;
align - items : center ;
gap : 2 rem ;
}
. stats - header {
display : flex ;
gap : 1.5 rem ;
}
. stat - mini {
text - align : center ;
display : flex ;
flex - direction : column ;
align - items : center ;
}
. stat - value {
font - size : 1.8 rem ;
font - weight : 800 ;
color : var ( -- text - primary ) ;
line - height : 1 ;
}
. stat - value . active {
color : # 4 caf50 ;
}
. stat - label {
font - size : 0.8 rem ;
font - weight : 600 ;
color : var ( -- text - secondary ) ;
text - transform : uppercase ;
letter - spacing : 0.05 em ;
margin - top : 0.25 rem ;
}
/* Tabs */
. tabs - container {
display : flex ;
justify - content : space - between ;
align - items : center ;
margin - bottom : 2 rem ;
border - bottom : 1 px solid var ( -- border - color ) ;
}
. tabs - buttons {
display : flex ;
gap : 0.5 rem ;
}
. primary - btn {
display : flex ;
align - items : center ;
gap : 8 px ;
background : var ( -- text - primary ) ;
color : var ( -- bg - primary ) ;
padding : 0.6 rem 1.2 rem ;
border - radius : 8 px ;
border : none ;
font - weight : 600 ;
cursor : pointer ;
transition : all 0.2 s ease ;
}
. primary - btn : hover {
transform : translateY ( - 2 px ) ;
box - shadow : 0 4 px 12 px var ( -- shadow ) ;
}
. tab - btn {
background : none ;
border : none ;
padding : 0.75 rem 1.5 rem ;
font - weight : 600 ;
color : var ( -- text - secondary ) ;
cursor : pointer ;
border - bottom : 3 px solid transparent ;
transition : all 0.3 s cubic - bezier ( 0.4 , 0 , 0.2 , 1 ) ;
}
. tab - btn . active {
color : var ( -- active - color ) ;
border - bottom - color : var ( -- active - color ) ;
}
/* Table and Cards */
. table - card {
background : var ( -- card - bg ) ;
border - radius : 12 px ;
overflow : hidden ;
border : 1 px solid var ( -- border - color ) ;
box - shadow : 0 4 px 15 px var ( -- shadow ) ;
}
. coupons - table {
width : 100 % ;
border - collapse : collapse ;
font - size : 0.9 rem ;
}
. coupons - table th {
text - align : left ;
padding : 1 rem ;
background : var ( -- bg - secondary ) ;
color : var ( -- text - secondary ) ;
font - weight : 700 ;
text - transform : uppercase ;
font - size : 0.75 rem ;
letter - spacing : 0.05 em ;
}
. coupons - table th . text - center {
text - align : center ;
}
. coupons - table td {
padding : 1 rem ;
border - bottom : 1 px solid var ( -- border - color ) ;
vertical - align : middle ;
}
. text - center {
text - align : center ;
}
. justify - center {
justify - content : center ;
}
. align - center {
align - items : center ;
}
. coupons - table tr : last - child td {
border - bottom : none ;
}
. coupon - header - cell {
display : flex ;
align - items : center ;
gap : 12 px ;
}
. coupon - mini - img {
width : 40 px ;
height : 40 px ;
border - radius : 8 px ;
object - fit : cover ;
border : 1 px solid var ( -- border - color ) ;
}
. business - tag {
display : flex ;
align - items : center ;
gap : 4 px ;
font - size : 0.75 rem ;
color : var ( -- active - color ) ;
font - weight : 600 ;
margin - top : 2 px ;
}
. business - tag . material - icons {
font - size : 0.9 rem ;
}
. badge {
background : var ( -- bg - secondary ) ;
color : var ( -- text - secondary ) ;
padding : 4 px 8 px ;
border - radius : 6 px ;
font - size : 0.75 rem ;
font - weight : 700 ;
}
. discount - label {
font - weight : 800 ;
color : # c62828 ;
background : # ffebee ;
padding : 4 px 8 px ;
border - radius : 6 px ;
}
. form - group . full { grid - column : 1 / - 1 ; }
. status - toggle {
padding : 0.25 rem 0.75 rem ;
border - radius : 4 px ;
border : none ;
font - size : 0.75 rem ;
cursor : pointer ;
background : # ffcdd2 ;
color : # c62828 ;
}
. status - toggle . active {
background : # c8e6c9 ;
color : # 2 e7d32 ;
}
. action - buttons {
display : flex ;
gap : 0.5 rem ;
}
. icon - btn {
background : none ;
border : none ;
cursor : pointer ;
padding : 4 px ;
border - radius : 4 px ;
}
. icon - btn . edit { color : var ( -- active - color ) ; }
. icon - btn . delete { color : # d32f2f ; }
. icon - btn : hover { background : var ( -- bg - secondary ) ; }
. contact - info {
display : flex ;
flex - direction : column ;
gap : 4 px ;
font - size : 0.8 rem ;
color : var ( -- text - secondary ) ;
}
. contact - info span {
display : flex ;
align - items : center ;
gap : 4 px ;
}
. contact - info . material - icons {
font - size : 0.9 rem ;
}
. social - tag {
color : var ( -- active - color ) ;
font - weight : 500 ;
}
. address - cell {
max - width : 200 px ;
font - size : 0.8 rem ;
color : var ( -- text - secondary ) ;
margin : 0 auto ;
}
/* File Upload Styles */
. file - upload - wrapper {
display : flex ;
flex - direction : column ;
gap : 1 rem ;
}
. file - input {
padding : 0.5 rem ;
border : 2 px dashed var ( -- border - color ) ;
border - radius : 8 px ;
cursor : pointer ;
width : 100 % ;
}
. file - preview {
width : 100 px ;
height : 100 px ;
border - radius : 12 px ;
overflow : hidden ;
border : 1 px solid var ( -- border - color ) ;
background : var ( -- bg - secondary ) ;
}
. file - preview img {
width : 100 % ;
height : 100 % ;
object - fit : cover ;
}
/* 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 : 2000 ;
}
. modal - content {
background : var ( -- card - bg ) ;
width : 95 % ;
max - width : 600 px ;
border - radius : 16 px ;
padding : 2 rem ;
max - height : 90 vh ;
overflow - y : auto ;
}
. modal - header {
display : flex ;
justify - content : space - between ;
align - items : center ;
margin - bottom : 1.5 rem ;
}
. close - btn { background : none ; border : none ; cursor : pointer ; color : var ( -- text - primary ) ; }
. coupon - form {
display : flex ;
flex - direction : column ;
gap : 1 rem ;
}
. form - group {
display : flex ;
flex - direction : column ;
gap : 0.4 rem ;
}
. form - group label {
font - size : 0.8 rem ;
font - weight : 600 ;
color : var ( -- text - secondary ) ;
}
. form - group input , . form - group textarea , . form - group select {
padding : 0.75 rem ;
border : 1 px solid var ( -- border - color ) ;
border - radius : 8 px ;
background : var ( -- bg - primary ) ;
color : var ( -- text - primary ) ;
font - size : 0.9 rem ;
}
. form - row {
display : grid ;
grid - template - columns : 1 fr 1 fr ;
gap : 1 rem ;
}
. form - actions {
display : flex ;
gap : 1 rem ;
margin - top : 1 rem ;
}
. submit - btn {
flex : 1 ;
padding : 0.8 rem ;
border - radius : 8 px ;
background : var ( -- text - primary ) ;
color : var ( -- bg - primary ) ;
border : none ;
font - weight : 600 ;
cursor : pointer ;
transition : opacity 0.2 s ;
}
. submit - btn : hover { opacity : 0.9 ; }
feat: business template/mold redesign
- Supabase: Add schedule, whatsapp, instagram, facebook, gallery_images
columns to businesses table
- types/index.ts: Add 5 new fields to Business interface
- businessService.ts: Update all SELECT queries to include new fields,
add _parseFormData helper, handle gallery_images as JSON array
- BusinessDetailsView.vue: Full redesign matching the approved mockup:
* Hero 300px with centered name + yellow category badge
* Horizontal scrollable quick-info pills (area, schedule, phone, web)
* Image carousel (main image + gallery_images) with arrows + dots
* About section with yellow left-accent bar
* 2x2 social grid (WhatsApp, Instagram, Facebook, Maps) with brand colors
* Coupon section preserved
* Sticky CTA bar: Ver en el Mapa + Llamar buttons
- PromoterDashboard.vue: Updated business modal with grouped sections
(Info Basica / Portada / Descripcion / Redes / Galeria) + new fields
2026-03-03 21:31:35 -05:00
/* Form section labels (organize modal into visual sections) */
. form - section - label {
font - size : 0.72 rem ;
font - weight : 800 ;
letter - spacing : 0.1 em ;
text - transform : uppercase ;
color : var ( -- active - color ) ;
padding : 8 px 0 2 px ;
border - bottom : 1 px solid var ( -- border - color ) ;
margin - top : 4 px ;
}
. form - hint {
font - size : 0.75 rem ;
color : var ( -- text - secondary ) ;
line - height : 1.5 ;
margin - top : 4 px ;
}
/* Submit btn icon alignment */
. submit - btn {
display : flex ;
align - items : center ;
justify - content : center ;
gap : 8 px ;
}
. submit - btn . material - icons { font - size : 1.1 rem ; }
2026-02-21 09:53:31 -05:00
. spin {
animation : spin 1 s linear infinite ;
}
@ keyframes spin { 100 % { transform : rotate ( 360 deg ) ; } }
. loading - state , . empty - state {
display : flex ;
flex - direction : column ;
align - items : center ;
justify - content : center ;
padding : 3 rem ;
color : var ( -- text - secondary ) ;
text - align : center ;
}
. empty - state . material - icons { font - size : 3 rem ; margin - bottom : 1 rem ; }
. map - picker - btn - container {
margin - bottom : 15 px ;
}
. map - btn {
width : 100 % ;
justify - content : center ;
gap : 8 px ;
background : var ( -- bg - secondary ) ;
}
. picker - map - wrapper {
margin - bottom : 20 px ;
}
. map - picker - div {
width : 100 % ;
height : 250 px ;
border - radius : 12 px ;
border : 2 px solid var ( -- border - color ) ;
}
. picker - hint {
font - size : 0.8 rem ;
color : var ( -- text - secondary ) ;
text - align : center ;
margin - top : 5 px ;
}
. secondary - btn {
display : flex ;
align - items : center ;
padding : 10 px 16 px ;
border - radius : 8 px ;
border : 1 px solid var ( -- border - color ) ;
background : white ;
cursor : pointer ;
font - weight : 600 ;
}
. badge . area - badge {
background : var ( -- bg - secondary ) ;
color : var ( -- active - color ) ;
border : 1 px solid var ( -- border - color ) ;
}
/* Search and Filter Bar */
. search - filter - bar {
display : flex ;
gap : 15 px ;
margin - bottom : 20 px ;
background : var ( -- card - bg ) ;
padding : 15 px ;
border - radius : 12 px ;
border : 1 px solid var ( -- border - color ) ;
}
. search - box {
flex : 1 ;
display : flex ;
align - items : center ;
gap : 10 px ;
background : var ( -- bg - primary ) ;
padding : 0 15 px ;
border - radius : 8 px ;
border : 1 px solid var ( -- border - color ) ;
}
. search - box input {
flex : 1 ;
border : none ;
background : transparent ;
padding : 10 px 0 ;
color : var ( -- text - primary ) ;
font - size : 0.95 rem ;
}
. search - box input : focus { outline : none ; }
. filter - box {
display : flex ;
align - items : center ;
gap : 8 px ;
background : var ( -- bg - primary ) ;
padding : 0 15 px ;
border - radius : 8 px ;
border : 1 px solid var ( -- border - color ) ;
}
. filter - box select {
border : none ;
background : transparent ;
color : var ( -- text - primary ) ;
padding : 10 px 0 ;
cursor : pointer ;
}
. back - link , . back - analytics {
background : var ( -- bg - secondary ) ;
border : 1 px solid var ( -- border - color ) ;
color : var ( -- text - primary ) ;
cursor : pointer ;
font - size : 0.95 rem ;
font - weight : 600 ;
padding : 10 px 16 px ;
border - radius : 8 px ;
transition : all 0.2 s ;
margin - bottom : 8 px ;
display : inline - flex ;
align - items : center ;
gap : 4 px ;
margin - right : 8 px ;
}
. back - link : hover , . back - analytics : hover {
background : var ( -- hover - bg ) ;
border - color : var ( -- accent - color ) ;
color : var ( -- accent - color ) ;
transform : translateX ( - 2 px ) ;
}
< / style >
refactor: migrate fully to Supabase, remove Firebase/Render/Python backend
- DELETED: entire backend/ (Python/FastAPI — replaced by Supabase)
- DELETED: old/ directory (obsolete code)
- DELETED: render.yaml, inject_api.py, check_tags.py, PENDING_FOR_TOMORROW.md
- DELETED: frontend/src/firebaseConfig.ts (Firebase Auth replaced by Supabase Auth)
- DELETED: frontend/src/services/apiClient.ts (HTTP client for dead backend)
- MIGRATED services to Supabase native:
schedulesService, favoritesService, usersService,
telemetryService (stub), reportsService, analyticsService (stub)
- MIGRATED stores/favorites.ts to Supabase direct queries
- MIGRATED views: SplashScreen, AdminTaxis, AdminDrivers, StrategicAnalytics
- MIGRATED utils/imageUrl.ts to Supabase Storage URLs
- FIXED router/index.ts: guard now uses supabase.auth.getSession()
instead of old localStorage auth_token (fixes logout + map loading)
- FIXED AuthView.vue: removed aggressive watch({ immediate: true })
that caused wrong redirects on map route
- FIXED SplashScreen.vue: navigate() now reads Supabase session + role
- FIXED RLS: added INSERT policy on public.users for trigger
- CONFIRMED: admin@sibu.com assigned ADMIN role in Supabase
2026-02-25 21:49:26 -05:00