1 line
17 KiB
JSON
1 line
17 KiB
JSON
{"file":"index-B6MDQHdV.js","mappings":"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","names":["_unref","_ssrIncludeBooleanAttr","_ssrLooseContain","_ssrLooseEqual","_ssrRenderList","_ssrRenderAttr","_ssrInterpolate","_push","_parent","_createTextVNode","_toDisplayString","_createBlock","_createVNode"],"sources":["../../../../pages/gamification/users/index.vue"],"sourcesContent":["<template>\n <div>\n <!-- Page Header -->\n <div class=\"mb-8\">\n <h1 class=\"text-2xl font-bold text-white\">{{ t('gamification.users.title') }}</h1>\n <p class=\"text-slate-400 mt-1\">{{ t('gamification.users.subtitle') }}</p>\n </div>\n\n <!-- Filters -->\n <div class=\"bg-slate-800 rounded-xl border border-slate-700 p-4 mb-6\">\n <div class=\"grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4\">\n <!-- Level Filter -->\n <div>\n <label class=\"block text-xs font-medium text-slate-400 mb-1.5 uppercase tracking-wider\">{{ t('gamification.users.filter_level') }}</label>\n <select v-model=\"filters.level\" class=\"w-full bg-slate-700 border border-slate-600 rounded-lg px-3 py-2 text-sm text-white focus:ring-2 focus:ring-indigo-500 focus:border-transparent\">\n <option value=\"\">{{ t('gamification.users.all_levels') }}</option>\n <option v-for=\"lvl in 20\" :key=\"lvl\" :value=\"lvl\">{{ lvl }}. {{ t('gamification.users.level_suffix') }}</option>\n </select>\n </div>\n\n <!-- Min XP -->\n <div>\n <label class=\"block text-xs font-medium text-slate-400 mb-1.5 uppercase tracking-wider\">{{ t('gamification.users.filter_min_xp') }}</label>\n <input v-model.number=\"filters.min_xp\" type=\"number\" min=\"0\" placeholder=\"0\" class=\"w-full bg-slate-700 border border-slate-600 rounded-lg px-3 py-2 text-sm text-white focus:ring-2 focus:ring-indigo-500 focus:border-transparent\" />\n </div>\n\n <!-- Max XP -->\n <div>\n <label class=\"block text-xs font-medium text-slate-400 mb-1.5 uppercase tracking-wider\">{{ t('gamification.users.filter_max_xp') }}</label>\n <input v-model.number=\"filters.max_xp\" type=\"number\" min=\"0\" placeholder=\"100000\" class=\"w-full bg-slate-700 border border-slate-600 rounded-lg px-3 py-2 text-sm text-white focus:ring-2 focus:ring-indigo-500 focus:border-transparent\" />\n </div>\n\n <!-- Has Penalty -->\n <div>\n <label class=\"block text-xs font-medium text-slate-400 mb-1.5 uppercase tracking-wider\">{{ t('gamification.users.filter_penalty') }}</label>\n <select v-model=\"filters.has_penalty\" class=\"w-full bg-slate-700 border border-slate-600 rounded-lg px-3 py-2 text-sm text-white focus:ring-2 focus:ring-indigo-500 focus:border-transparent\">\n <option value=\"\">{{ t('gamification.users.penalty_any') }}</option>\n <option :value=\"true\">{{ t('gamification.users.penalty_only') }}</option>\n <option :value=\"false\">{{ t('gamification.users.penalty_none') }}</option>\n </select>\n </div>\n </div>\n\n <!-- Action Buttons -->\n <div class=\"flex items-center gap-3 mt-4 pt-4 border-t border-slate-700/50\">\n <button @click=\"applyFilters\" class=\"px-4 py-2 bg-indigo-600 hover:bg-indigo-500 text-white text-sm font-medium rounded-lg transition\">\n {{ t('gamification.users.filter_apply') }}\n </button>\n <button @click=\"resetFilters\" class=\"px-4 py-2 bg-slate-700 hover:bg-slate-600 text-slate-300 text-sm font-medium rounded-lg transition\">\n {{ t('gamification.users.filter_reset') }}\n </button>\n <span class=\"text-xs text-slate-500 ml-auto\">{{ t('gamification.users.total_count', { count: totalCount }) }}</span>\n </div>\n </div>\n\n <!-- Loading State -->\n <div v-if=\"loading\" class=\"flex items-center justify-center py-20\">\n <div class=\"text-slate-400 flex items-center gap-3\">\n <svg class=\"w-5 h-5 animate-spin\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n <path stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15\" />\n </svg>\n <span>{{ t('gamification.users.loading') }}</span>\n </div>\n </div>\n\n <!-- Error State -->\n <div v-else-if=\"error\" class=\"bg-rose-500/10 border border-rose-500/30 rounded-xl p-6 mb-8\">\n <p class=\"text-rose-400\">{{ t('gamification.users.load_error') }}</p>\n <button @click=\"fetchUsers\" class=\"mt-3 text-sm text-rose-300 hover:text-rose-200 underline\">{{ t('gamification.users.retry') }}</button>\n </div>\n\n <!-- Empty State -->\n <div v-else-if=\"users.length === 0\" class=\"bg-slate-800 rounded-xl border border-slate-700 p-6\">\n <p class=\"text-slate-400 text-center py-12\">{{ t('gamification.users.empty') }}</p>\n </div>\n\n <!-- Users Table -->\n <div v-else class=\"bg-slate-800 rounded-xl border border-slate-700 overflow-hidden\">\n <div class=\"overflow-x-auto\">\n <table class=\"w-full\">\n <thead>\n <tr class=\"border-b border-slate-700\">\n <th class=\"text-left px-4 py-3 text-xs font-medium text-slate-400 uppercase tracking-wider\">{{ t('gamification.users.col_user_id') }}</th>\n <th class=\"text-left px-4 py-3 text-xs font-medium text-slate-400 uppercase tracking-wider\">{{ t('gamification.users.col_xp') }}</th>\n <th class=\"text-left px-4 py-3 text-xs font-medium text-slate-400 uppercase tracking-wider\">{{ t('gamification.users.col_level') }}</th>\n <th class=\"text-left px-4 py-3 text-xs font-medium text-slate-400 uppercase tracking-wider\">{{ t('gamification.users.col_points') }}</th>\n <th class=\"text-left px-4 py-3 text-xs font-medium text-slate-400 uppercase tracking-wider\">{{ t('gamification.users.col_penalty') }}</th>\n <th class=\"text-left px-4 py-3 text-xs font-medium text-slate-400 uppercase tracking-wider\">{{ t('gamification.users.col_restriction') }}</th>\n <th class=\"text-left px-4 py-3 text-xs font-medium text-slate-400 uppercase tracking-wider\">{{ t('gamification.users.col_services') }}</th>\n <th class=\"text-left px-4 py-3 text-xs font-medium text-slate-400 uppercase tracking-wider\">{{ t('gamification.users.col_discoveries') }}</th>\n <th class=\"text-left px-4 py-3 text-xs font-medium text-slate-400 uppercase tracking-wider\">{{ t('gamification.users.col_updated') }}</th>\n <th class=\"text-right px-4 py-3 text-xs font-medium text-slate-400 uppercase tracking-wider\">{{ t('gamification.users.col_actions') }}</th>\n </tr>\n </thead>\n <tbody class=\"divide-y divide-slate-700/50\">\n <tr v-for=\"user in users\" :key=\"user.user_id\" class=\"hover:bg-slate-700/30 transition\">\n <td class=\"px-4 py-3 text-sm text-white font-mono\">#{{ user.user_id }}</td>\n <td class=\"px-4 py-3 text-sm text-white\">{{ formatNumber(user.total_xp) }}</td>\n <td class=\"px-4 py-3\">\n <span class=\"inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-indigo-500/10 text-indigo-400 border border-indigo-500/20\">\n {{ user.current_level }}\n </span>\n </td>\n <td class=\"px-4 py-3 text-sm text-slate-300\">{{ formatNumber(user.total_points) }}</td>\n <td class=\"px-4 py-3\">\n <span v-if=\"user.penalty_points > 0\" class=\"inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-rose-500/10 text-rose-400 border border-rose-500/20\">\n -{{ user.penalty_points }}\n </span>\n <span v-else class=\"text-sm text-slate-500\">—</span>\n </td>\n <td class=\"px-4 py-3\">\n <span v-if=\"user.restriction_level > 0\" class=\"inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-amber-500/10 text-amber-400 border border-amber-500/20\">\n {{ restrictionLabel(user.restriction_level) }}\n </span>\n <span v-else class=\"text-sm text-slate-500\">{{ t('gamification.users.none') }}</span>\n </td>\n <td class=\"px-4 py-3 text-sm text-slate-300\">{{ user.services_submitted }}</td>\n <td class=\"px-4 py-3 text-sm text-slate-300\">{{ user.places_discovered }}</td>\n <td class=\"px-4 py-3 text-sm text-slate-500\">{{ user.updated_at ? formatDate(user.updated_at) : '—' }}</td>\n <td class=\"px-4 py-3 text-right\">\n <NuxtLink :to=\"`/gamification/users/${user.user_id}`\" class=\"inline-flex items-center gap-1.5 px-3 py-1.5 bg-indigo-600/10 hover:bg-indigo-600/20 text-indigo-400 text-xs font-medium rounded-lg transition\">\n {{ t('gamification.users.details') }}\n <svg class=\"w-3.5 h-3.5\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n <path stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M9 5l7 7-7 7\" />\n </svg>\n </NuxtLink>\n </td>\n </tr>\n </tbody>\n </table>\n </div>\n\n <!-- Pagination -->\n <div class=\"flex items-center justify-between px-4 py-3 border-t border-slate-700\">\n <button @click=\"prevPage\" :disabled=\"offset === 0\" class=\"px-3 py-1.5 text-sm rounded-lg transition\" :class=\"offset === 0 ? 'text-slate-600 cursor-not-allowed' : 'text-slate-300 hover:bg-slate-700'\">\n {{ t('gamification.users.prev') }}\n </button>\n <span class=\"text-sm text-slate-500\">{{ offset + 1 }}–{{ Math.min(offset + limit, totalCount) }} / {{ totalCount }}</span>\n <button @click=\"nextPage\" :disabled=\"offset + limit >= totalCount\" class=\"px-3 py-1.5 text-sm rounded-lg transition\" :class=\"offset + limit >= totalCount ? 'text-slate-600 cursor-not-allowed' : 'text-slate-300 hover:bg-slate-700'\">\n {{ t('gamification.users.next') }}\n </button>\n </div>\n </div>\n </div>\n</template>\n\n<script setup lang=\"ts\">\ndefinePageMeta({\n middleware: 'auth',\n})\n\nconst { t } = useI18n()\n\ninterface UserStats {\n user_id: number\n total_xp: number\n total_points: number\n social_points: number\n current_level: number\n penalty_points: number\n restriction_level: number\n penalty_quota_remaining: number\n places_discovered: number\n places_validated: number\n providers_added_count: number\n services_submitted: number\n banned_until: string | null\n updated_at: string | null\n}\n\nconst { formatNumber, formatDate } = useFormatter()\n\nconst loading = ref(false)\nconst error = ref(false)\nconst users = ref<UserStats[]>([])\nconst totalCount = ref(0)\nconst offset = ref(0)\nconst limit = ref(50)\n\nconst filters = ref({\n level: '' as number | string,\n min_xp: null as number | null,\n max_xp: null as number | null,\n has_penalty: '' as boolean | string,\n})\n\nfunction restrictionLabel(level: number): string {\n switch (level) {\n case 1: return t('gamification.users.restriction_mild')\n case 2: return t('gamification.users.restriction_moderate')\n case 3: return t('gamification.users.restriction_severe')\n default: return `${level}. ${t('gamification.users.restriction_level')}`\n }\n}\n\nfunction buildQueryParams(): string {\n const params = new URLSearchParams()\n params.set('limit', String(limit.value))\n params.set('offset', String(offset.value))\n if (filters.value.level !== '') params.set('level', String(filters.value.level))\n if (filters.value.min_xp !== null) params.set('min_xp', String(filters.value.min_xp))\n if (filters.value.max_xp !== null) params.set('max_xp', String(filters.value.max_xp))\n if (filters.value.has_penalty !== '') params.set('has_penalty', String(filters.value.has_penalty))\n return params.toString()\n}\n\nasync function fetchUsers() {\n loading.value = true\n error.value = false\n try {\n const tokenCookie = useCookie('access_token')\n const token = tokenCookie.value\n const headers = token ? { Authorization: `Bearer ${token}` } : {}\n\n const data = await $fetch<UserStats[]>(`/api/v1/admin/gamification/user-stats?${buildQueryParams()}`, { headers })\n users.value = data\n totalCount.value = data.length < limit.value ? offset.value + data.length : offset.value + limit.value + 1\n } catch (e) {\n console.error('Failed to load user stats:', e)\n error.value = true\n } finally {\n loading.value = false\n }\n}\n\nfunction applyFilters() {\n offset.value = 0\n fetchUsers()\n}\n\nfunction resetFilters() {\n filters.value = { level: '', min_xp: null, max_xp: null, has_penalty: '' }\n offset.value = 0\n fetchUsers()\n}\n\nfunction prevPage() {\n if (offset.value > 0) {\n offset.value = Math.max(0, offset.value - limit.value)\n fetchUsers()\n }\n}\n\nfunction nextPage() {\n offset.value += limit.value\n fetchUsers()\n}\n\nonMounted(() => {\n fetchUsers()\n})\n</script>\n"],"version":3} |