add landing and subscription plans

Signed-off-by: Omar Sánchez Pizarro <omar.sanchez@pistacero.net>
This commit is contained in:
Omar Sánchez Pizarro
2026-01-20 23:49:19 +01:00
parent 05f0455744
commit 6ec8855c00
79 changed files with 8839 additions and 361 deletions

View File

@@ -0,0 +1,303 @@
<template>
<div class="space-y-6">
<!-- Page Header -->
<div class="flex items-center justify-between">
<div>
<h1 class="text-2xl font-bold text-gray-900 dark:text-gray-100">Dashboard</h1>
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400">
<span v-if="isAdmin">Resumen general del sistema (estadísticas de todos los usuarios)</span>
<span v-else>Tu resumen personal</span>
<span v-if="currentUser" class="font-medium text-gray-700 dark:text-gray-300 ml-1">({{ currentUser }})</span>
</p>
</div>
</div>
<!-- Statistics Cards -->
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-6">
<!-- Workers Card -->
<div class="stat-card">
<div class="flex items-center justify-between">
<div class="flex items-center space-x-4">
<div class="flex-shrink-0 w-14 h-14 bg-gradient-to-br from-primary-500 to-primary-600 rounded-xl flex items-center justify-center shadow-lg">
<Cog6ToothIcon class="w-7 h-7 text-white" />
</div>
<div>
<p class="text-sm font-medium text-gray-500 dark:text-gray-400 mb-1">Workers Activos</p>
<p class="text-2xl font-bold text-gray-900 dark:text-gray-100">
{{ stats.activeWorkers }}<span class="text-sm font-normal text-gray-500 dark:text-gray-400">/{{ stats.totalWorkers }}</span>
</p>
</div>
</div>
</div>
<div class="mt-4 pt-4 border-t border-gray-200 dark:border-gray-700">
<div class="flex items-center justify-between">
<span class="text-xs text-gray-500 dark:text-gray-400">Estado del sistema</span>
<span :class="stats.activeWorkers > 0 ? 'badge badge-success' : 'badge badge-danger'">
{{ stats.activeWorkers > 0 ? 'Activo' : 'Inactivo' }}
</span>
</div>
</div>
</div>
<!-- Favorites Card -->
<div class="stat-card">
<div class="flex items-center justify-between">
<div class="flex items-center space-x-4">
<div class="flex-shrink-0 w-14 h-14 bg-gradient-to-br from-green-500 to-green-600 rounded-xl flex items-center justify-center shadow-lg">
<HeartIcon class="w-7 h-7 text-white" />
</div>
<div>
<p class="text-sm font-medium text-gray-500 dark:text-gray-400 mb-1">Favoritos</p>
<p class="text-2xl font-bold text-gray-900 dark:text-gray-100">{{ stats.totalFavorites }}</p>
</div>
</div>
</div>
<div class="mt-4 pt-4 border-t border-gray-200 dark:border-gray-700">
<router-link to="/favorites" class="text-xs text-primary-600 dark:text-primary-400 hover:underline font-medium flex items-center">
Ver todos
</router-link>
</div>
</div>
<!-- Articles Card -->
<div class="stat-card">
<div class="flex items-center justify-between">
<div class="flex items-center space-x-4">
<div class="flex-shrink-0 w-14 h-14 bg-gradient-to-br from-blue-500 to-blue-600 rounded-xl flex items-center justify-center shadow-lg">
<DocumentTextIcon class="w-7 h-7 text-white" />
</div>
<div>
<p class="text-sm font-medium text-gray-500 dark:text-gray-400 mb-1">Artículos Notificados</p>
<p class="text-2xl font-bold text-gray-900 dark:text-gray-100">{{ stats.totalNotified }}</p>
</div>
</div>
</div>
<div class="mt-4 pt-4 border-t border-gray-200 dark:border-gray-700">
<router-link to="/articles" class="text-xs text-primary-600 dark:text-primary-400 hover:underline font-medium flex items-center">
Ver todos
</router-link>
</div>
</div>
<!-- Platforms Card -->
<div class="stat-card">
<div class="flex items-center justify-between">
<div class="flex items-center space-x-4">
<div class="flex-shrink-0 w-14 h-14 bg-gradient-to-br from-purple-500 to-purple-600 rounded-xl flex items-center justify-center shadow-lg">
<ChartBarIcon class="w-7 h-7 text-white" />
</div>
<div>
<p class="text-sm font-medium text-gray-500 dark:text-gray-400 mb-1">Plataformas</p>
<div class="flex items-center space-x-3">
<span class="text-sm font-semibold text-gray-900 dark:text-gray-100">
<span class="text-blue-600 dark:text-blue-400">W:</span> {{ stats.platforms?.wallapop || 0 }}
</span>
<span class="text-gray-300 dark:text-gray-600">|</span>
<span class="text-sm font-semibold text-gray-900 dark:text-gray-100">
<span class="text-green-600 dark:text-green-400">V:</span> {{ stats.platforms?.vinted || 0 }}
</span>
</div>
</div>
</div>
</div>
<div class="mt-4 pt-4 border-t border-gray-200 dark:border-gray-700">
<span class="text-xs text-gray-500 dark:text-gray-400">Total de plataformas activas</span>
</div>
</div>
</div>
<!-- Charts and Quick Actions -->
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
<!-- Platform Distribution -->
<div class="card">
<div class="card-header">
<h3 class="card-title">Distribución por Plataforma</h3>
<p class="card-subtitle">Artículos notificados por plataforma</p>
</div>
<div class="space-y-6">
<!-- Wallapop -->
<div>
<div class="flex items-center justify-between mb-3">
<div class="flex items-center space-x-2">
<span class="w-3 h-3 bg-primary-600 rounded-full"></span>
<span class="text-sm font-semibold text-gray-700 dark:text-gray-300">Wallapop</span>
</div>
<div class="flex items-center space-x-2">
<span class="text-lg font-bold text-gray-900 dark:text-gray-100">{{ stats.platforms?.wallapop || 0 }}</span>
<span class="text-sm text-gray-500 dark:text-gray-400">
({{ getPercentage(stats.platforms?.wallapop || 0, stats.totalNotified) }}%)
</span>
</div>
</div>
<div class="w-full bg-gray-200 dark:bg-gray-700 rounded-full h-3 overflow-hidden">
<div
class="bg-gradient-to-r from-primary-500 to-primary-600 h-3 rounded-full transition-all duration-500 shadow-sm"
:style="{
width: `${getPercentage(stats.platforms?.wallapop || 0, stats.totalNotified)}%`,
}"
></div>
</div>
</div>
<!-- Vinted -->
<div>
<div class="flex items-center justify-between mb-3">
<div class="flex items-center space-x-2">
<span class="w-3 h-3 bg-green-600 rounded-full"></span>
<span class="text-sm font-semibold text-gray-700 dark:text-gray-300">Vinted</span>
</div>
<div class="flex items-center space-x-2">
<span class="text-lg font-bold text-gray-900 dark:text-gray-100">{{ stats.platforms?.vinted || 0 }}</span>
<span class="text-sm text-gray-500 dark:text-gray-400">
({{ getPercentage(stats.platforms?.vinted || 0, stats.totalNotified) }}%)
</span>
</div>
</div>
<div class="w-full bg-gray-200 dark:bg-gray-700 rounded-full h-3 overflow-hidden">
<div
class="bg-gradient-to-r from-green-500 to-green-600 h-3 rounded-full transition-all duration-500 shadow-sm"
:style="{
width: `${getPercentage(stats.platforms?.vinted || 0, stats.totalNotified)}%`,
}"
></div>
</div>
</div>
</div>
</div>
<!-- Quick Actions -->
<div class="card">
<div class="card-header">
<h3 class="card-title">Accesos Rápidos</h3>
<p class="card-subtitle">Navegación rápida a secciones principales</p>
</div>
<div class="space-y-3">
<router-link
to="/articles"
class="flex items-center justify-between p-4 bg-gradient-to-r from-gray-50 to-gray-100 dark:from-gray-700 dark:to-gray-800 rounded-xl hover:from-gray-100 hover:to-gray-200 dark:hover:from-gray-600 dark:hover:to-gray-700 transition-all duration-200 group border border-gray-200 dark:border-gray-700"
>
<div class="flex items-center space-x-3">
<div class="w-10 h-10 bg-blue-100 dark:bg-blue-900/50 rounded-lg flex items-center justify-center group-hover:bg-blue-200 dark:group-hover:bg-blue-900/70 transition-colors">
<DocumentTextIcon class="w-5 h-5 text-blue-600 dark:text-blue-400" />
</div>
<div>
<p class="text-sm font-semibold text-gray-900 dark:text-gray-100">Artículos</p>
<p class="text-xs text-gray-500 dark:text-gray-400">Ver todos los artículos notificados</p>
</div>
</div>
<ArrowRightIcon class="w-5 h-5 text-gray-400 dark:text-gray-500 group-hover:text-primary-600 dark:group-hover:text-primary-400 transition-colors" />
</router-link>
<router-link
to="/favorites"
class="flex items-center justify-between p-4 bg-gradient-to-r from-gray-50 to-gray-100 dark:from-gray-700 dark:to-gray-800 rounded-xl hover:from-gray-100 hover:to-gray-200 dark:hover:from-gray-600 dark:hover:to-gray-700 transition-all duration-200 group border border-gray-200 dark:border-gray-700"
>
<div class="flex items-center space-x-3">
<div class="w-10 h-10 bg-green-100 dark:bg-green-900/50 rounded-lg flex items-center justify-center group-hover:bg-green-200 dark:group-hover:bg-green-900/70 transition-colors">
<HeartIcon class="w-5 h-5 text-green-600 dark:text-green-400" />
</div>
<div>
<p class="text-sm font-semibold text-gray-900 dark:text-gray-100">Favoritos</p>
<p class="text-xs text-gray-500 dark:text-gray-400">Gestionar artículos favoritos</p>
</div>
</div>
<ArrowRightIcon class="w-5 h-5 text-gray-400 dark:text-gray-500 group-hover:text-primary-600 dark:group-hover:text-primary-400 transition-colors" />
</router-link>
<router-link
to="/workers"
class="flex items-center justify-between p-4 bg-gradient-to-r from-gray-50 to-gray-100 dark:from-gray-700 dark:to-gray-800 rounded-xl hover:from-gray-100 hover:to-gray-200 dark:hover:from-gray-600 dark:hover:to-gray-700 transition-all duration-200 group border border-gray-200 dark:border-gray-700"
>
<div class="flex items-center space-x-3">
<div class="w-10 h-10 bg-primary-100 dark:bg-primary-900/50 rounded-lg flex items-center justify-center group-hover:bg-primary-200 dark:group-hover:bg-primary-900/70 transition-colors">
<Cog6ToothIcon class="w-5 h-5 text-primary-600 dark:text-primary-400" />
</div>
<div>
<p class="text-sm font-semibold text-gray-900 dark:text-gray-100">Workers</p>
<p class="text-xs text-gray-500 dark:text-gray-400">Configurar y gestionar workers</p>
</div>
</div>
<ArrowRightIcon class="w-5 h-5 text-gray-400 dark:text-gray-500 group-hover:text-primary-600 dark:group-hover:text-primary-400 transition-colors" />
</router-link>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref, onMounted, onUnmounted } from 'vue';
import api from '../services/api';
import authService from '../services/auth';
import {
Cog6ToothIcon,
HeartIcon,
DocumentTextIcon,
ChartBarIcon,
ArrowRightIcon,
} from '@heroicons/vue/24/outline';
const stats = ref({
totalWorkers: 0,
activeWorkers: 0,
totalFavorites: 0,
totalNotified: 0,
platforms: {},
});
const currentUser = ref(authService.getUsername() || null);
const isAdmin = ref(false);
function getPercentage(value, total) {
if (!total || total === 0) return 0;
return Math.round((value / total) * 100);
}
async function loadStats() {
try {
stats.value = await api.getStats();
// Verificar si el usuario es admin (se puede inferir de si ve todas las estadísticas)
// O podemos añadir un endpoint para verificar el rol
} catch (error) {
console.error('Error cargando estadísticas:', error);
}
}
function handleAuthChange() {
currentUser.value = authService.getUsername() || null;
isAdmin.value = authService.isAdmin();
if (currentUser.value) {
loadStats();
}
}
function handleWSMessage(event) {
const data = event.detail;
if (data.type === 'workers_updated' || data.type === 'favorites_updated') {
loadStats();
}
}
let interval = null;
onMounted(() => {
currentUser.value = authService.getUsername() || null;
isAdmin.value = authService.isAdmin();
loadStats();
window.addEventListener('ws-message', handleWSMessage);
window.addEventListener('auth-logout', handleAuthChange);
window.addEventListener('auth-login', handleAuthChange);
interval = setInterval(loadStats, 10000); // Actualizar cada 10 segundos
});
onUnmounted(() => {
if (interval) {
clearInterval(interval);
}
window.removeEventListener('ws-message', handleWSMessage);
window.removeEventListener('auth-logout', handleAuthChange);
window.removeEventListener('auth-login', handleAuthChange);
});
</script>