Files
SIB/frontend/src/views/PromoterDashboard.vue
2026-02-26 20:58:10 -05:00

1309 lines
35 KiB
Vue

<script setup lang="ts">
import { ref, onMounted, watch, computed } from 'vue'
import { useRoute } from 'vue-router'
import { businessService } from '@/services/businessService'
import { couponsService } from '@/services/couponsService'
import { shuttlesService } from '@/services/shuttlesService'
import { useAuthStore } from '@/stores/auth'
import { supabase } from '@/supabase'
import type { Coupon, Business, Shuttle } from '@/types'
const route = useRoute()
const authStore = useAuthStore()
// State
const activeTab = ref<'promotions' | 'businesses' | 'shuttles'>('promotions')
const coupons = ref<Coupon[]>([])
const businesses = ref<Business[]>([])
const shuttles = ref<Shuttle[]>([])
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',
area: 'Boquete'
})
const userName = localStorage.getItem('user_name') || 'Promotor'
onMounted(async () => {
await Promise.all([loadCoupons(), loadBusinesses(), loadShuttles()])
checkHash()
})
watch(() => route.hash, () => checkHash())
function checkHash() {
if (route.hash === '#businesses') {
activeTab.value = 'businesses'
} else if (route.hash === '#shuttles') {
activeTab.value = 'shuttles'
} else {
activeTab.value = 'promotions'
}
}
// Search Computed
const filteredCoupons = computed(() => {
return coupons.value.filter(c => {
const matchesSearch = c.title.toLowerCase().includes(searchQuery.value.toLowerCase()) ||
(c.business?.name?.toLowerCase().includes(searchQuery.value.toLowerCase()))
const matchesCategory = categoryFilter.value === 'Todas' || c.category === categoryFilter.value
return matchesSearch && matchesCategory
})
})
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
})
})
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)
}
}
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 {
const { error } = await supabase
.from('shuttles')
.update({ is_active: !shuttle.is_active })
.eq('id', shuttle.id);
if (error) throw error;
await loadShuttles();
} 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 {
const { error } = await supabase
.from('shuttles')
.delete()
.eq('id', id);
if (error) throw error;
await loadShuttles();
} catch (e) {
alert('Error al eliminar shuttle')
}
}
}
// Business Methods
function openCreateBusinessModal() {
isEditingBusiness.value = false
currentBusiness.value = {
name: '',
address: '',
phone: '',
image_url: '',
social_media: '',
category: 'Restaurante',
area: 'Boquete'
}
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')
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'
return path
}
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 = [
'description', 'image_url', 'social_media', 'terms',
'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>
<button :class="['tab-btn', { active: activeTab === 'shuttles' }]" @click="activeTab = 'shuttles'">
Viajes Turísticos
</button>
</div>
<div class="tabs-actions">
<div class="stats-header">
<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>
</div>
<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>
</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>
<button v-if="activeTab === 'shuttles'" class="primary-btn" @click="$router.push('/admin/shuttles')">
<span class="material-icons">rocket_launch</span>
Nuevo Shuttle
</button>
</div>
</div>
<!-- Global Search Bar (Visible in Promos/Businesses) -->
<div class="search-filter-bar">
<div class="search-box">
<span class="material-icons">search</span>
<input v-model="searchQuery" type="text" :placeholder="activeTab === 'promotions' ? 'Buscar promoción...' : (activeTab === 'businesses' ? 'Buscar negocio...' : 'Buscar shuttle...')">
</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>
{{ coupon.business?.name || 'Comercio Local' }}
</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>
<!-- 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>
<!-- 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>
<input :value="businesses.find(b => b.id === currentCoupon.business_id)?.name || ''" type="text" readonly disabled class="readonly-input">
</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">
<div class="form-group">
<label>Nombre del Negocio</label>
<input v-model="currentBusiness.name" type="text" required>
</div>
<div class="form-row">
<div class="form-group">
<label>Categoría</label>
<select v-model="currentBusiness.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 class="form-group">
<label>Teléfono</label>
<input v-model="currentBusiness.phone" type="text">
</div>
<div class="form-group">
<label>Área / Región</label>
<select v-model="currentBusiness.area">
<option value="Boquete">Boquete</option>
<option value="Dolega">Dolega</option>
<option value="David">David</option>
</select>
</div>
</div>
<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>
<div class="form-group">
<label>Imagen del Negocio (Logo o Fachada)</label>
<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>
<div class="form-group">
<label>Redes Sociales</label>
<input v-model="currentBusiness.social_media" type="text" placeholder="Ej: @pizzeria_centro">
</div>
<div class="form-actions">
<button type="submit" class="submit-btn">Guardar Negocio</button>
</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.5rem;
padding-bottom: 1rem;
}
.welcome-section {
max-width: 800px;
}
.welcome-section h1 {
font-size: 2.5rem;
font-weight: 800;
margin: 0;
color: var(--text-primary);
}
.welcome-section p {
color: var(--text-secondary);
margin-top: 0.5rem;
font-size: 1.1rem;
}
.back-analytics {
background: transparent;
border: 1px solid var(--border-color);
color: var(--text-secondary);
padding: 6px 12px;
border-radius: 6px;
cursor: pointer;
font-size: 0.9rem;
margin-bottom: 12px;
}
.back-analytics:hover {
background: var(--bg-secondary);
color: var(--active-color);
}
.logout-btn {
background: #fdeaea;
color: #e74c3c;
border: 1px solid #fabebb;
padding: 0.6rem 1.2rem;
border-radius: 8px;
display: flex;
align-items: center;
gap: 8px;
font-weight: 600;
cursor: pointer;
transition: all 0.2s;
}
.logout-btn:hover {
background: #fcd5d5;
transform: translateY(-2px);
}
/* Header Stats */
.tabs-actions {
display: flex;
align-items: center;
gap: 2rem;
}
.stats-header {
display: flex;
gap: 1.5rem;
}
.stat-mini {
text-align: center;
display: flex;
flex-direction: column;
align-items: center;
}
.stat-value {
font-size: 1.8rem;
font-weight: 800;
color: var(--text-primary);
line-height: 1;
}
.stat-value.active {
color: #4caf50;
}
.stat-label {
font-size: 0.8rem;
font-weight: 600;
color: var(--text-secondary);
text-transform: uppercase;
letter-spacing: 0.05em;
margin-top: 0.25rem;
}
/* Tabs */
.tabs-container {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 2rem;
border-bottom: 1px solid var(--border-color);
}
.tabs-buttons {
display: flex;
gap: 0.5rem;
}
.primary-btn {
display: flex;
align-items: center;
gap: 8px;
background: var(--text-primary);
color: var(--bg-primary);
padding: 0.6rem 1.2rem;
border-radius: 8px;
border: none;
font-weight: 600;
cursor: pointer;
transition: all 0.2s ease;
}
.primary-btn:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px var(--shadow);
}
.tab-btn {
background: none;
border: none;
padding: 0.75rem 1.5rem;
font-weight: 600;
color: var(--text-secondary);
cursor: pointer;
border-bottom: 3px solid transparent;
transition: all 0.3s 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: 12px;
overflow: hidden;
border: 1px solid var(--border-color);
box-shadow: 0 4px 15px var(--shadow);
}
.coupons-table {
width: 100%;
border-collapse: collapse;
font-size: 0.9rem;
}
.coupons-table th {
text-align: left;
padding: 1rem;
background: var(--bg-secondary);
color: var(--text-secondary);
font-weight: 700;
text-transform: uppercase;
font-size: 0.75rem;
letter-spacing: 0.05em;
}
.coupons-table th.text-center {
text-align: center;
}
.coupons-table td {
padding: 1rem;
border-bottom: 1px 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: 12px;
}
.coupon-mini-img {
width: 40px;
height: 40px;
border-radius: 8px;
object-fit: cover;
border: 1px solid var(--border-color);
}
.business-tag {
display: flex;
align-items: center;
gap: 4px;
font-size: 0.75rem;
color: var(--active-color);
font-weight: 600;
margin-top: 2px;
}
.business-tag .material-icons {
font-size: 0.9rem;
}
.badge {
background: var(--bg-secondary);
color: var(--text-secondary);
padding: 4px 8px;
border-radius: 6px;
font-size: 0.75rem;
font-weight: 700;
}
.discount-label {
font-weight: 800;
color: #c62828;
background: #ffebee;
padding: 4px 8px;
border-radius: 6px;
}
.form-group.full { grid-column: 1 / -1; }
.status-toggle {
padding: 0.25rem 0.75rem;
border-radius: 4px;
border: none;
font-size: 0.75rem;
cursor: pointer;
background: #ffcdd2;
color: #c62828;
}
.status-toggle.active {
background: #c8e6c9;
color: #2e7d32;
}
.action-buttons {
display: flex;
gap: 0.5rem;
}
.icon-btn {
background: none;
border: none;
cursor: pointer;
padding: 4px;
border-radius: 4px;
}
.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: 4px;
font-size: 0.8rem;
color: var(--text-secondary);
}
.contact-info span {
display: flex;
align-items: center;
gap: 4px;
}
.contact-info .material-icons {
font-size: 0.9rem;
}
.social-tag {
color: var(--active-color);
font-weight: 500;
}
.address-cell {
max-width: 200px;
font-size: 0.8rem;
color: var(--text-secondary);
margin: 0 auto;
}
/* File Upload Styles */
.file-upload-wrapper {
display: flex;
flex-direction: column;
gap: 1rem;
}
.file-input {
padding: 0.5rem;
border: 2px dashed var(--border-color);
border-radius: 8px;
cursor: pointer;
width: 100%;
}
.file-preview {
width: 100px;
height: 100px;
border-radius: 12px;
overflow: hidden;
border: 1px 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: 600px;
border-radius: 16px;
padding: 2rem;
max-height: 90vh;
overflow-y: auto;
}
.modal-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 1.5rem;
}
.close-btn { background: none; border: none; cursor: pointer; color: var(--text-primary); }
.coupon-form {
display: flex;
flex-direction: column;
gap: 1rem;
}
.form-group {
display: flex;
flex-direction: column;
gap: 0.4rem;
}
.form-group label {
font-size: 0.8rem;
font-weight: 600;
color: var(--text-secondary);
}
.form-group input, .form-group textarea, .form-group select {
padding: 0.75rem;
border: 1px solid var(--border-color);
border-radius: 8px;
background: var(--bg-primary);
color: var(--text-primary);
font-size: 0.9rem;
}
.form-row {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 1rem;
}
.form-actions {
display: flex;
gap: 1rem;
margin-top: 1rem;
}
.submit-btn {
flex: 1;
padding: 0.8rem;
border-radius: 8px;
background: var(--text-primary);
color: var(--bg-primary);
border: none;
font-weight: 600;
cursor: pointer;
transition: opacity 0.2s;
}
.submit-btn:hover { opacity: 0.9; }
.spin {
animation: spin 1s linear infinite;
}
@keyframes spin { 100% { transform: rotate(360deg); } }
.loading-state, .empty-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 3rem;
color: var(--text-secondary);
text-align: center;
}
.empty-state .material-icons { font-size: 3rem; margin-bottom: 1rem; }
.map-picker-btn-container {
margin-bottom: 15px;
}
.map-btn {
width: 100%;
justify-content: center;
gap: 8px;
background: var(--bg-secondary);
}
.picker-map-wrapper {
margin-bottom: 20px;
}
.map-picker-div {
width: 100%;
height: 250px;
border-radius: 12px;
border: 2px solid var(--border-color);
}
.picker-hint {
font-size: 0.8rem;
color: var(--text-secondary);
text-align: center;
margin-top: 5px;
}
.secondary-btn {
display: flex;
align-items: center;
padding: 10px 16px;
border-radius: 8px;
border: 1px solid var(--border-color);
background: white;
cursor: pointer;
font-weight: 600;
}
.badge.area-badge {
background: var(--bg-secondary);
color: var(--active-color);
border: 1px solid var(--border-color);
}
/* Search and Filter Bar */
.search-filter-bar {
display: flex;
gap: 15px;
margin-bottom: 20px;
background: var(--card-bg);
padding: 15px;
border-radius: 12px;
border: 1px solid var(--border-color);
}
.search-box {
flex: 1;
display: flex;
align-items: center;
gap: 10px;
background: var(--bg-primary);
padding: 0 15px;
border-radius: 8px;
border: 1px solid var(--border-color);
}
.search-box input {
flex: 1;
border: none;
background: transparent;
padding: 10px 0;
color: var(--text-primary);
font-size: 0.95rem;
}
.search-box input:focus { outline: none; }
.filter-box {
display: flex;
align-items: center;
gap: 8px;
background: var(--bg-primary);
padding: 0 15px;
border-radius: 8px;
border: 1px solid var(--border-color);
}
.filter-box select {
border: none;
background: transparent;
color: var(--text-primary);
padding: 10px 0;
cursor: pointer;
}
.back-link, .back-analytics {
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;
margin-bottom: 8px;
display: inline-flex;
align-items: center;
gap: 4px;
margin-right: 8px;
}
.back-link:hover, .back-analytics:hover {
background: var(--hover-bg);
border-color: var(--accent-color);
color: var(--accent-color);
transform: translateX(-2px);
}
</style>