310 lines
14 KiB
Vue
310 lines
14 KiB
Vue
<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>
|
|
|
|
<!-- Active Users (Solo para admin) -->
|
|
<div v-if="isAdmin">
|
|
<ActiveUsers />
|
|
</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 ActiveUsers from '../components/ActiveUsers.vue';
|
|
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>
|
|
|