Files
wallabicher/web/dashboard/src/views/Users.vue
Omar Sánchez Pizarro 7289ad6c26 activity
Signed-off-by: Omar Sánchez Pizarro <omar.sanchez@pistacero.net>
2026-01-21 10:31:06 +01:00

916 lines
33 KiB
Vue
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<div>
<div class="flex flex-col sm:flex-row sm:justify-between sm:items-center gap-3 sm:gap-4 mb-4 sm:mb-6">
<h1 class="text-2xl sm:text-3xl font-bold text-gray-900 dark:text-gray-100">Gestión de Usuarios</h1>
<div class="flex flex-wrap gap-2">
<button
v-if="isAdmin"
@click="showAddModal = true"
class="btn btn-primary text-xs sm:text-sm whitespace-nowrap"
>
+ Crear Usuario
</button>
</div>
</div>
<div v-if="!isAdmin" class="card text-center py-12">
<p class="text-gray-600 dark:text-gray-400 text-lg font-semibold mb-2">Acceso Denegado</p>
<p class="text-gray-500 dark:text-gray-500 text-sm">
Solo los administradores pueden acceder a esta sección.
</p>
<router-link to="/settings" class="btn btn-primary mt-4 inline-block">
Ir a Configuración
</router-link>
</div>
<div v-else-if="loading" class="text-center py-12">
<div class="inline-block animate-spin rounded-full h-8 w-8 border-b-2 border-primary-600"></div>
<p class="mt-2 text-gray-600 dark:text-gray-400">Cargando usuarios...</p>
</div>
<div v-else class="space-y-4">
<!-- Lista de usuarios -->
<div v-if="users.length > 0" class="grid grid-cols-1 gap-4">
<div
v-for="user in users"
:key="user.username"
class="card hover:shadow-lg transition-shadow overflow-hidden"
>
<!-- Header con nombre y badges -->
<div class="flex items-center justify-between mb-4 pb-4 border-b border-gray-200 dark:border-gray-700">
<div class="flex items-center gap-3">
<!-- Avatar -->
<div class="w-12 h-12 rounded-full bg-gradient-to-br from-primary-500 to-primary-600 flex items-center justify-center text-white font-bold text-lg">
{{ user.username.charAt(0).toUpperCase() }}
</div>
<div>
<div class="flex items-center gap-2 mb-1">
<h3 class="text-lg font-bold text-gray-900 dark:text-gray-100">{{ user.username }}</h3>
<span
v-if="user.username === currentUser"
class="px-2 py-0.5 text-xs font-semibold rounded-full bg-blue-100 dark:bg-blue-900 text-blue-800 dark:text-blue-200"
>
</span>
<span
v-if="user.role === 'admin'"
class="px-2 py-0.5 text-xs font-semibold rounded-full bg-purple-100 dark:bg-purple-900 text-purple-800 dark:text-purple-200"
>
Admin
</span>
</div>
<!-- Estado de conexión inline -->
<div v-if="isAdmin" class="flex items-center gap-2">
<span
:class="{
'px-2 py-0.5 text-xs font-semibold rounded-full bg-green-100 dark:bg-green-900 text-green-800 dark:text-green-200': getUserConnectionStatus(user.username) === 'active',
'px-2 py-0.5 text-xs font-semibold rounded-full bg-yellow-100 dark:bg-yellow-900 text-yellow-800 dark:text-yellow-200': getUserConnectionStatus(user.username) === 'inactive',
'px-2 py-0.5 text-xs font-semibold rounded-full bg-gray-100 dark:bg-gray-700 text-gray-700 dark:text-gray-300': getUserConnectionStatus(user.username) === 'offline'
}"
>
{{ getUserConnectionStatus(user.username) === 'active' ? '🟢 Conectado' : getUserConnectionStatus(user.username) === 'inactive' ? '🟡 Inactivo' : '⚫ Desconectado' }}
</span>
<span v-if="getUserLastActivity(user.username)" class="text-xs text-gray-500 dark:text-gray-400">
{{ formatRelativeTime(getUserLastActivity(user.username)) }}
</span>
</div>
</div>
</div>
<!-- Botones de acción -->
<div class="flex gap-2">
<button
v-if="isAdmin"
@click="openSubscriptionModal(user.username)"
class="btn btn-primary text-xs sm:text-sm"
title="Gestionar suscripción"
>
💳 Plan
</button>
<button
v-if="user.username !== currentUser && isAdmin"
@click="confirmDeleteUser(user.username)"
class="btn btn-danger text-xs sm:text-sm"
title="Eliminar usuario"
>
🗑 Eliminar
</button>
</div>
</div>
<!-- Grid de información -->
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<!-- Columna izquierda: Información general -->
<div class="space-y-3">
<div>
<h4 class="text-xs font-semibold text-gray-500 dark:text-gray-400 uppercase mb-2">Información</h4>
<div class="space-y-2 text-sm">
<div v-if="user.createdAt" class="flex justify-between">
<span class="text-gray-600 dark:text-gray-400">Creado:</span>
<span class="font-medium text-gray-900 dark:text-gray-100">{{ formatDate(user.createdAt) }}</span>
</div>
<div v-if="user.createdBy" class="flex justify-between">
<span class="text-gray-600 dark:text-gray-400">Por:</span>
<span class="font-medium text-gray-900 dark:text-gray-100">{{ user.createdBy }}</span>
</div>
<div v-if="user.updatedAt" class="flex justify-between">
<span class="text-gray-600 dark:text-gray-400">Actualizado:</span>
<span class="font-medium text-gray-900 dark:text-gray-100">{{ formatDate(user.updatedAt) }}</span>
</div>
<div v-if="isAdmin && getUserSessionCount(user.username) > 0" class="flex justify-between">
<span class="text-gray-600 dark:text-gray-400">Sesiones activas:</span>
<span class="font-semibold text-primary-600 dark:text-primary-400">{{ getUserSessionCount(user.username) }}</span>
</div>
</div>
</div>
</div>
<!-- Columna derecha: Suscripción -->
<div>
<h4 class="text-xs font-semibold text-gray-500 dark:text-gray-400 uppercase mb-2">Suscripción</h4>
<div v-if="userSubscriptions[user.username]" class="p-3 bg-gradient-to-br from-primary-50 to-teal-50 dark:from-primary-900/20 dark:to-teal-900/20 rounded-lg border border-primary-200 dark:border-primary-700">
<div class="flex items-center justify-between mb-2">
<div>
<p class="text-xs text-gray-600 dark:text-gray-400">Plan</p>
<p class="text-lg font-bold text-primary-700 dark:text-primary-300">
{{ userSubscriptions[user.username].subscription?.plan?.name || 'Gratis' }}
</p>
</div>
<span
:class="userSubscriptions[user.username].subscription?.status === 'active' ? 'badge badge-success' : 'badge badge-warning'"
class="text-xs"
>
{{ userSubscriptions[user.username].subscription?.status === 'active' ? 'Activo' : 'Inactivo' }}
</span>
</div>
<div v-if="userSubscriptions[user.username].usage" class="pt-2 border-t border-primary-200 dark:border-primary-800">
<div class="flex justify-between items-center">
<span class="text-xs text-gray-600 dark:text-gray-400">Uso de búsquedas</span>
<span class="text-sm font-semibold text-gray-900 dark:text-gray-100">
{{ userSubscriptions[user.username].usage.workers }} / {{ userSubscriptions[user.username].usage.maxWorkers === 'Ilimitado' ? '∞' : userSubscriptions[user.username].usage.maxWorkers }}
</span>
</div>
<!-- Barra de progreso -->
<div v-if="userSubscriptions[user.username].usage.maxWorkers !== 'Ilimitado'" class="mt-2 w-full bg-gray-200 dark:bg-gray-700 rounded-full h-2">
<div
class="bg-gradient-to-r from-primary-500 to-primary-600 h-2 rounded-full transition-all"
:style="{ width: `${Math.min(100, (userSubscriptions[user.username].usage.workers / userSubscriptions[user.username].usage.maxWorkers) * 100)}%` }"
></div>
</div>
</div>
</div>
<div v-else class="p-3 bg-gray-50 dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 text-center text-sm text-gray-500 dark:text-gray-400">
Sin información de suscripción
</div>
</div>
</div>
</div>
</div>
<div v-else class="card text-center py-12">
<p class="text-gray-600 dark:text-gray-400 mb-4">No hay usuarios configurados</p>
<button @click="showAddModal = true" class="btn btn-primary">
+ Crear primer usuario
</button>
</div>
</div>
<!-- Modal para crear/editar usuario -->
<div
v-if="showAddModal"
class="fixed inset-0 bg-black bg-opacity-50 dark:bg-opacity-70 flex items-center justify-center z-50 p-4"
@click.self="closeAddModal"
>
<div class="card max-w-md w-full max-h-[90vh] overflow-y-auto">
<div class="flex items-center justify-between mb-4">
<h2 class="text-xl font-bold text-gray-900 dark:text-gray-100">Crear Usuario</h2>
<button
@click="closeAddModal"
class="text-gray-400 hover:text-gray-600 dark:hover:text-gray-300"
title="Cerrar"
>
</button>
</div>
<form @submit.prevent="handleCreateUser" class="space-y-4">
<div v-if="addError" class="bg-red-100 dark:bg-red-900/30 border border-red-400 dark:border-red-700 text-red-700 dark:text-red-300 px-4 py-3 rounded">
{{ addError }}
</div>
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
Nombre de usuario <span class="text-red-500">*</span>
</label>
<input
v-model="userForm.username"
type="text"
class="input"
placeholder="nuevo_usuario"
required
minlength="3"
autocomplete="username"
/>
<p class="text-xs text-gray-500 dark:text-gray-400 mt-1">
Mínimo 3 caracteres
</p>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
Contraseña <span class="text-red-500">*</span>
</label>
<input
v-model="userForm.password"
type="password"
class="input"
placeholder="••••••••"
required
minlength="6"
autocomplete="new-password"
/>
<p class="text-xs text-gray-500 dark:text-gray-400 mt-1">
Mínimo 6 caracteres
</p>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
Confirmar contraseña <span class="text-red-500">*</span>
</label>
<input
v-model="userForm.passwordConfirm"
type="password"
class="input"
placeholder="••••••••"
required
minlength="6"
autocomplete="new-password"
/>
</div>
<div class="flex flex-col-reverse sm:flex-row justify-end gap-2 pt-4 border-t border-gray-200 dark:border-gray-700">
<button
type="button"
@click="closeAddModal"
class="btn btn-secondary text-sm sm:text-base"
>
Cancelar
</button>
<button
type="submit"
class="btn btn-primary text-sm sm:text-base"
:disabled="loadingAction"
>
{{ loadingAction ? 'Creando...' : 'Crear Usuario' }}
</button>
</div>
</form>
</div>
</div>
<!-- Modal para gestionar suscripción (solo admin) -->
<div
v-if="showSubscriptionModal && selectedUserForSubscription"
class="fixed inset-0 bg-black bg-opacity-50 dark:bg-opacity-70 flex items-center justify-center z-50 p-4"
@click.self="closeSubscriptionModal"
>
<div class="card max-w-2xl w-full max-h-[90vh] overflow-y-auto">
<div class="flex items-center justify-between mb-4">
<h2 class="text-xl font-bold text-gray-900 dark:text-gray-100">
Gestionar Suscripción: {{ selectedUserForSubscription }}
</h2>
<button
@click="closeSubscriptionModal"
class="text-gray-400 hover:text-gray-600 dark:hover:text-gray-300"
title="Cerrar"
>
</button>
</div>
<div v-if="loadingSubscription" class="text-center py-8">
<div class="inline-block animate-spin rounded-full h-8 w-8 border-b-2 border-primary-600"></div>
<p class="mt-2 text-sm text-gray-500 dark:text-gray-400">Cargando información...</p>
</div>
<div v-else class="space-y-6">
<!-- Información actual -->
<div v-if="selectedUserSubscription && selectedUserSubscription.subscription" class="p-4 bg-gradient-to-br from-primary-50 to-teal-50 dark:from-primary-900/20 dark:to-teal-900/20 rounded-xl border border-primary-200 dark:border-primary-800">
<h3 class="text-sm font-semibold text-gray-700 dark:text-gray-300 mb-3">Plan Actual</h3>
<div class="grid grid-cols-2 gap-4">
<div>
<p class="text-xs text-gray-600 dark:text-gray-400">Plan</p>
<p class="text-lg font-bold text-gray-900 dark:text-gray-100">
{{ selectedUserSubscription.subscription?.plan?.name || 'Gratis' }}
</p>
</div>
<div>
<p class="text-xs text-gray-600 dark:text-gray-400">Estado</p>
<span
:class="selectedUserSubscription.subscription?.status === 'active' ? 'badge badge-success' : 'badge badge-warning'"
>
{{ selectedUserSubscription.subscription?.status === 'active' ? 'Activo' : 'Inactivo' }}
</span>
</div>
<div v-if="selectedUserSubscription.usage">
<p class="text-xs text-gray-600 dark:text-gray-400">Búsquedas</p>
<p class="text-sm font-semibold text-gray-900 dark:text-gray-100">
{{ selectedUserSubscription.usage.workers }} / {{ selectedUserSubscription.usage.maxWorkers === 'Ilimitado' ? '∞' : selectedUserSubscription.usage.maxWorkers }}
</p>
</div>
</div>
</div>
<!-- Cambiar plan -->
<div>
<h3 class="text-sm font-semibold text-gray-700 dark:text-gray-300 mb-3">Cambiar Plan</h3>
<div class="grid grid-cols-1 sm:grid-cols-2 gap-3">
<button
v-for="plan in availablePlans"
:key="plan.id"
@click="subscriptionForm.planId = plan.id"
class="p-4 rounded-lg border-2 text-left transition-all"
:class="
subscriptionForm.planId === plan.id
? 'border-primary-500 dark:border-primary-400 bg-primary-50 dark:bg-primary-900/20'
: 'border-gray-200 dark:border-gray-700 hover:border-primary-300 dark:hover:border-primary-700'
"
>
<div class="flex items-center justify-between">
<div>
<p class="font-bold text-gray-900 dark:text-gray-100">{{ plan.name }}</p>
<p class="text-xs text-gray-600 dark:text-gray-400 mt-1">{{ plan.description }}</p>
</div>
<div v-if="subscriptionForm.planId === plan.id" class="text-primary-600 dark:text-primary-400">
</div>
</div>
</button>
</div>
</div>
<!-- Formulario de actualización -->
<form @submit.prevent="handleUpdateUserSubscription" class="space-y-4">
<div v-if="subscriptionError" class="bg-red-100 dark:bg-red-900/30 border border-red-400 dark:border-red-700 text-red-700 dark:text-red-300 px-4 py-3 rounded">
{{ subscriptionError }}
</div>
<div v-if="subscriptionSuccess" class="bg-green-100 dark:bg-green-900/30 border border-green-400 dark:border-green-700 text-green-700 dark:text-green-300 px-4 py-3 rounded">
{{ subscriptionSuccess }}
</div>
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
Estado de la suscripción
</label>
<select
v-model="subscriptionForm.status"
class="input"
>
<option value="active">Activo</option>
<option value="inactive">Inactivo</option>
<option value="cancelled">Cancelado</option>
</select>
</div>
<div class="flex items-center">
<input
v-model="subscriptionForm.cancelAtPeriodEnd"
type="checkbox"
id="cancelAtPeriodEnd"
class="w-4 h-4 text-primary-600 rounded focus:ring-primary-500"
/>
<label for="cancelAtPeriodEnd" class="ml-2 text-sm text-gray-700 dark:text-gray-300">
Cancelar al final del período
</label>
</div>
<div class="flex flex-col-reverse sm:flex-row justify-end gap-2 pt-4 border-t border-gray-200 dark:border-gray-700">
<button
type="button"
@click="closeSubscriptionModal"
class="btn btn-secondary text-sm sm:text-base"
>
Cancelar
</button>
<button
type="submit"
class="btn btn-primary text-sm sm:text-base"
:disabled="loadingAction"
>
{{ loadingAction ? 'Actualizando...' : 'Actualizar Suscripción' }}
</button>
</div>
</form>
</div>
</div>
</div>
<!-- Modal de confirmación para eliminar -->
<div
v-if="userToDelete"
class="fixed inset-0 bg-black bg-opacity-50 dark:bg-opacity-70 flex items-center justify-center z-50 p-4"
@click.self="userToDelete = null"
>
<div class="card max-w-md w-full">
<div class="flex items-center justify-between mb-4">
<h2 class="text-xl font-bold text-gray-900 dark:text-gray-100">Confirmar Eliminación</h2>
<button
@click="userToDelete = null"
class="text-gray-400 hover:text-gray-600 dark:hover:text-gray-300"
title="Cerrar"
>
</button>
</div>
<p class="text-gray-700 dark:text-gray-300 mb-4">
¿Estás seguro de que deseas eliminar al usuario <strong>{{ userToDelete }}</strong>?
</p>
<p class="text-sm text-red-600 dark:text-red-400 mb-4">
Esta acción no se puede deshacer.
</p>
<div class="flex flex-col-reverse sm:flex-row justify-end gap-2 pt-4 border-t border-gray-200 dark:border-gray-700">
<button
type="button"
@click="userToDelete = null"
class="btn btn-secondary text-sm sm:text-base"
>
Cancelar
</button>
<button
@click="handleDeleteUser"
class="btn btn-danger text-sm sm:text-base"
:disabled="loadingAction"
>
{{ loadingAction ? 'Eliminando...' : 'Eliminar Usuario' }}
</button>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref, computed, onMounted, onUnmounted, watch } from 'vue';
import api from '../services/api';
import authService from '../services/auth';
const users = ref([]);
const loading = ref(true);
const loadingAction = ref(false);
const showAddModal = ref(false);
const showSubscriptionModal = ref(false);
const userToDelete = ref(null);
const selectedUserForSubscription = ref(null);
const loadingSubscription = ref(false);
const userSubscriptions = ref({});
const availablePlans = ref([]);
const subscriptionError = ref('');
const subscriptionSuccess = ref('');
const addError = ref('');
const activeUsers = ref([]);
const userSessions = ref({});
const userForm = ref({
username: '',
password: '',
passwordConfirm: '',
});
const subscriptionForm = ref({
planId: 'free',
status: 'active',
cancelAtPeriodEnd: false,
});
const selectedUserSubscription = ref(null);
const isAuthenticated = computed(() => authService.hasCredentials());
const currentUser = computed(() => {
return authService.getUsername() || '';
});
const isAdmin = computed(() => {
return authService.isAdmin();
});
function formatDate(dateString) {
if (!dateString) return 'N/A';
try {
const date = new Date(dateString);
return date.toLocaleString('es-ES', {
year: 'numeric',
month: 'short',
day: 'numeric',
hour: '2-digit',
minute: '2-digit',
});
} catch (e) {
return dateString;
}
}
function formatRelativeTime(timestamp) {
if (!timestamp) return 'Nunca';
const date = new Date(timestamp);
const now = new Date();
const diffInSeconds = Math.floor((now - date) / 1000);
if (diffInSeconds < 60) {
return 'Hace unos segundos';
} else if (diffInSeconds < 3600) {
const minutes = Math.floor(diffInSeconds / 60);
return `Hace ${minutes} ${minutes === 1 ? 'minuto' : 'minutos'}`;
} else if (diffInSeconds < 86400) {
const hours = Math.floor(diffInSeconds / 3600);
return `Hace ${hours} ${hours === 1 ? 'hora' : 'horas'}`;
} else {
const days = Math.floor(diffInSeconds / 86400);
return `Hace ${days} ${days === 1 ? 'día' : 'días'}`;
}
}
// Obtener el estado de conexión de un usuario
function getUserConnectionStatus(username) {
// Buscar si el usuario tiene alguna sesión activa
const activeSessions = userSessions.value[username] || [];
if (activeSessions.length === 0) {
return 'offline';
}
// Verificar si alguna sesión está activamente conectada
const hasActiveSession = activeSessions.some(session => session.isActive);
if (hasActiveSession) {
return 'active';
}
// Si tiene sesiones válidas pero ninguna activa
const hasValidSession = activeSessions.some(session => !session.isExpired);
if (hasValidSession) {
return 'inactive';
}
return 'offline';
}
// Obtener la última actividad de un usuario (de todas sus sesiones)
function getUserLastActivity(username) {
const sessions = userSessions.value[username] || [];
if (sessions.length === 0) {
return null;
}
// Encontrar la sesión con la actividad más reciente
let latestActivity = null;
for (const session of sessions) {
if (session.lastActivity) {
const activityDate = new Date(session.lastActivity);
if (!latestActivity || activityDate > latestActivity) {
latestActivity = activityDate;
}
}
}
return latestActivity ? latestActivity.toISOString() : null;
}
// Obtener el número de sesiones activas de un usuario
function getUserSessionCount(username) {
const sessions = userSessions.value[username] || [];
return sessions.filter(s => !s.isExpired).length;
}
async function loadUsers() {
loading.value = true;
try {
const data = await api.getUsers();
users.value = data.users || [];
// Cargar información de suscripción para todos los usuarios (solo si es admin)
if (isAdmin.value) {
await Promise.all([
loadAllUserSubscriptions(),
loadUserSessions(),
]);
}
} catch (error) {
console.error('Error cargando usuarios:', error);
// El modal de login se manejará automáticamente desde App.vue
} finally {
loading.value = false;
}
}
async function loadUserSessions() {
if (!isAdmin.value) return;
try {
// Obtener todas las sesiones
const data = await api.getSessions();
const sessions = data.sessions || [];
// Agrupar sesiones por usuario
const sessionsByUser = {};
for (const session of sessions) {
if (!sessionsByUser[session.username]) {
sessionsByUser[session.username] = [];
}
sessionsByUser[session.username].push(session);
}
userSessions.value = sessionsByUser;
} catch (error) {
console.error('Error cargando sesiones de usuarios:', error);
}
}
async function loadAllUserSubscriptions() {
// Cargar suscripciones de todos los usuarios en paralelo
const subscriptionPromises = users.value.map(async (user) => {
try {
// Usar el endpoint de admin para obtener suscripción de cualquier usuario
const response = await fetch(`/api/subscription/${user.username}`, {
method: 'GET',
headers: {
'Authorization': authService.getAuthHeader(),
},
});
if (response.ok) {
const data = await response.json();
console.log(`Suscripción de ${user.username}:`, data);
userSubscriptions.value[user.username] = data;
} else {
console.warn(`No se pudo cargar suscripción de ${user.username}, usando valores por defecto`);
// Valores por defecto si no se puede cargar
userSubscriptions.value[user.username] = {
subscription: {
planId: 'free',
status: 'active',
plan: { name: 'Gratis', description: 'Plan gratuito' }
},
usage: { workers: 0, maxWorkers: 2 }
};
}
} catch (error) {
console.error(`Error cargando suscripción de ${user.username}:`, error);
// Valores por defecto en caso de error
userSubscriptions.value[user.username] = {
subscription: {
planId: 'free',
status: 'active',
plan: { name: 'Gratis', description: 'Plan gratuito' }
},
usage: { workers: 0, maxWorkers: 2 }
};
}
});
await Promise.all(subscriptionPromises);
}
async function loadAvailablePlans() {
try {
const data = await api.getSubscriptionPlans();
availablePlans.value = data.plans || [];
} catch (error) {
console.error('Error cargando planes:', error);
}
}
async function openSubscriptionModal(username) {
selectedUserForSubscription.value = username;
showSubscriptionModal.value = true;
loadingSubscription.value = true;
subscriptionError.value = '';
subscriptionSuccess.value = '';
try {
// Cargar planes disponibles
await loadAvailablePlans();
// Cargar suscripción del usuario seleccionado
const response = await fetch(`/api/subscription/${username}`, {
method: 'GET',
headers: {
'Authorization': authService.getAuthHeader(),
},
});
if (response.ok) {
const data = await response.json();
selectedUserSubscription.value = data;
subscriptionForm.value = {
planId: data.subscription?.planId || 'free',
status: data.subscription?.status || 'active',
cancelAtPeriodEnd: data.subscription?.cancelAtPeriodEnd || false,
};
} else {
// Si no hay suscripción, usar valores por defecto
selectedUserSubscription.value = {
subscription: {
planId: 'free',
status: 'active',
plan: { name: 'Gratis', description: 'Plan gratuito' }
},
usage: { workers: 0, maxWorkers: 2 },
};
subscriptionForm.value = {
planId: 'free',
status: 'active',
cancelAtPeriodEnd: false,
};
}
} catch (error) {
console.error('Error cargando suscripción:', error);
subscriptionError.value = 'Error al cargar información de suscripción';
} finally {
loadingSubscription.value = false;
}
}
function closeSubscriptionModal() {
showSubscriptionModal.value = false;
selectedUserForSubscription.value = null;
selectedUserSubscription.value = null;
subscriptionError.value = '';
subscriptionSuccess.value = '';
subscriptionForm.value = {
planId: 'free',
status: 'active',
cancelAtPeriodEnd: false,
};
}
async function handleUpdateUserSubscription() {
if (!selectedUserForSubscription.value) return;
subscriptionError.value = '';
subscriptionSuccess.value = '';
loadingAction.value = true;
try {
await api.updateUserSubscription(selectedUserForSubscription.value, {
planId: subscriptionForm.value.planId,
status: subscriptionForm.value.status,
cancelAtPeriodEnd: subscriptionForm.value.cancelAtPeriodEnd,
});
subscriptionSuccess.value = 'Suscripción actualizada correctamente';
// Actualizar la información en la lista
await loadAllUserSubscriptions();
// Recargar la información del modal después de un momento
setTimeout(async () => {
await openSubscriptionModal(selectedUserForSubscription.value);
}, 1000);
} catch (error) {
console.error('Error actualizando suscripción:', error);
subscriptionError.value = error.response?.data?.error || 'Error al actualizar la suscripción';
} finally {
loadingAction.value = false;
}
}
async function handleCreateUser() {
addError.value = '';
loadingAction.value = true;
if (!userForm.value.username || !userForm.value.password || !userForm.value.passwordConfirm) {
addError.value = 'Todos los campos son requeridos';
loadingAction.value = false;
return;
}
if (userForm.value.username.length < 3) {
addError.value = 'El nombre de usuario debe tener al menos 3 caracteres';
loadingAction.value = false;
return;
}
if (userForm.value.password.length < 6) {
addError.value = 'La contraseña debe tener al menos 6 caracteres';
loadingAction.value = false;
return;
}
if (userForm.value.password !== userForm.value.passwordConfirm) {
addError.value = 'Las contraseñas no coinciden';
loadingAction.value = false;
return;
}
try {
await api.createUser({
username: userForm.value.username,
password: userForm.value.password,
});
closeAddModal();
await loadUsers();
} catch (error) {
console.error('Error creando usuario:', error);
if (error.response?.data?.error) {
addError.value = error.response.data.error;
} else {
addError.value = 'Error creando usuario. Intenta de nuevo.';
}
} finally {
loadingAction.value = false;
}
}
async function handleDeleteUser() {
if (!userToDelete.value) return;
loadingAction.value = true;
try {
await api.deleteUser(userToDelete.value);
userToDelete.value = null;
await loadUsers();
} catch (error) {
console.error('Error eliminando usuario:', error);
alert(error.response?.data?.error || 'Error eliminando usuario. Intenta de nuevo.');
} finally {
loadingAction.value = false;
}
}
function confirmDeleteUser(username) {
userToDelete.value = username;
}
function closeAddModal() {
showAddModal.value = false;
addError.value = '';
userForm.value = {
username: '',
password: '',
passwordConfirm: '',
};
}
function handleAuthLogout() {
// Cuando el usuario se desconecta globalmente, limpiar datos
users.value = [];
showAddModal.value = false;
userToDelete.value = null;
addError.value = '';
activeUsers.value = [];
userSessions.value = {};
}
// Manejar cambios de estado de usuarios vía WebSocket
function handleUserStatusChange(event) {
const { username, status } = event.detail;
// Recargar sesiones para actualizar el estado
if (isAdmin.value) {
loadUserSessions();
}
}
let refreshInterval = null;
onMounted(() => {
loadUsers();
window.addEventListener('auth-logout', handleAuthLogout);
// Escuchar evento de login exitoso para recargar usuarios
window.addEventListener('auth-login', () => {
loadUsers();
});
// Escuchar cambios de estado de usuarios
window.addEventListener('user-status-change', handleUserStatusChange);
// Actualizar sesiones periódicamente (cada 30 segundos)
if (isAdmin.value) {
refreshInterval = setInterval(() => {
loadUserSessions();
}, 30000);
}
});
onUnmounted(() => {
window.removeEventListener('auth-logout', handleAuthLogout);
window.removeEventListener('auth-login', loadUsers);
window.removeEventListener('user-status-change', handleUserStatusChange);
if (refreshInterval) {
clearInterval(refreshInterval);
}
});
</script>