1 line
15 KiB
JSON
1 line
15 KiB
JSON
{"file":"levels-D2gwg9KO.js","mappings":"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","names":["levels","_unref","_ssrRenderList","_ssrInterpolate","_ssrRenderClass","_ssrRenderAttr","_ssrIncludeBooleanAttr","_ssrLooseContain"],"sources":["../../../../pages/gamification/levels.vue"],"sourcesContent":["<template>\n <div>\n <!-- Page Header -->\n <div class=\"mb-8\">\n <h1 class=\"text-2xl font-bold text-white\">{{ t('gamification.levels.title') }}</h1>\n <p class=\"text-slate-400 mt-1\">{{ t('gamification.levels.subtitle') }}</p>\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.levels.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.levels.load_error') }}</p>\n <button @click=\"fetchLevels\" class=\"mt-3 text-sm text-rose-300 hover:text-rose-200 underline\">{{ t('gamification.levels.retry') }}</button>\n </div>\n\n <template v-else>\n <!-- Create Button -->\n <div class=\"mb-6 flex items-center justify-between\">\n <div></div>\n <button @click=\"openCreateModal\" class=\"px-4 py-2 bg-indigo-600 hover:bg-indigo-500 text-white rounded-lg text-sm font-medium transition flex items-center gap-2\">\n <svg class=\"w-4 h-4\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n <path stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M12 4v16m8-8H4\" />\n </svg>\n {{ t('gamification.levels.create') }}\n </button>\n </div>\n\n <!-- Levels Table -->\n <div class=\"bg-slate-800 rounded-xl border border-slate-700 overflow-hidden mb-8\">\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.levels.col_level') }}</th>\n <th class=\"text-left px-4 py-3 text-xs font-medium text-slate-400 uppercase tracking-wider\">{{ t('gamification.levels.col_rank') }}</th>\n <th class=\"text-left px-4 py-3 text-xs font-medium text-slate-400 uppercase tracking-wider\">{{ t('gamification.levels.col_min_points') }}</th>\n <th class=\"text-left px-4 py-3 text-xs font-medium text-slate-400 uppercase tracking-wider\">{{ t('gamification.levels.col_type') }}</th>\n <th class=\"text-right px-4 py-3 text-xs font-medium text-slate-400 uppercase tracking-wider\">{{ t('gamification.levels.col_actions') }}</th>\n </tr>\n </thead>\n <tbody class=\"divide-y divide-slate-700/50\">\n <tr v-for=\"level in levels\" :key=\"level.id\" class=\"hover:bg-slate-700/30 transition\">\n <td class=\"px-4 py-3\">\n <span class=\"inline-flex items-center justify-center w-8 h-8 rounded-lg bg-indigo-500/10 text-indigo-400 text-sm font-bold\">\n {{ level.level_number }}\n </span>\n </td>\n <td class=\"px-4 py-3\">\n <span class=\"text-sm font-medium text-white\">{{ level.rank_name }}</span>\n </td>\n <td class=\"px-4 py-3 text-sm text-slate-300\">{{ level.min_points.toLocaleString() }}</td>\n <td class=\"px-4 py-3\">\n <span v-if=\"level.is_penalty\" 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 {{ t('gamification.levels.penalty') }}\n </span>\n <span v-else class=\"inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-emerald-500/10 text-emerald-400 border border-emerald-500/20\">\n {{ t('gamification.levels.normal') }}\n </span>\n </td>\n <td class=\"px-4 py-3 text-right\">\n <button @click=\"openEditModal(level)\" 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 <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=\"M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z\" />\n </svg>\n {{ t('gamification.levels.edit') }}\n </button>\n </td>\n </tr>\n <tr v-if=\"levels.length === 0\">\n <td colspan=\"5\" class=\"px-4 py-12 text-center text-slate-500 text-sm\">\n {{ t('gamification.levels.no_levels') }}\n </td>\n </tr>\n </tbody>\n </table>\n </div>\n </div>\n\n <!-- Level Tree Diagram -->\n <div class=\"bg-slate-800 rounded-xl border border-slate-700 p-6\">\n <h2 class=\"text-lg font-semibold text-white mb-4\">{{ t('gamification.levels.tree_diagram') }}</h2>\n <div v-if=\"levels.length === 0\" class=\"text-sm text-slate-500 text-center py-8\">\n {{ t('gamification.levels.no_tree') }}\n </div>\n <div v-else class=\"flex flex-wrap gap-3\">\n <div\n v-for=\"level in sortedLevels\"\n :key=\"level.id\"\n class=\"flex items-center gap-2 px-4 py-2 rounded-lg border transition\"\n :class=\"level.is_penalty ? 'bg-rose-500/5 border-rose-500/20' : 'bg-emerald-500/5 border-emerald-500/20'\"\n >\n <span class=\"text-sm font-bold\" :class=\"level.is_penalty ? 'text-rose-400' : 'text-emerald-400'\">\n Lv.{{ level.level_number }}\n </span>\n <span class=\"text-xs text-slate-400\">{{ level.rank_name }}</span>\n <span v-if=\"!level.is_penalty\" class=\"text-xs text-slate-500\">({{ level.min_points.toLocaleString() }} pts)</span>\n </div>\n </div>\n </div>\n </template>\n\n <!-- Edit/Create Modal -->\n <div v-if=\"showModal\" class=\"fixed inset-0 z-50 flex items-center justify-center bg-black/60\" @click.self=\"closeModal\">\n <div class=\"bg-slate-800 rounded-xl border border-slate-700 w-full max-w-md mx-4 p-6\">\n <h3 class=\"text-lg font-semibold text-white mb-6\">\n {{ editingLevel ? t('gamification.levels.edit_title') : t('gamification.levels.create_title') }}\n </h3>\n <div class=\"space-y-4\">\n <div>\n <label class=\"block text-sm font-medium text-slate-300 mb-1\">{{ t('gamification.levels.form_level_number') }}</label>\n <input v-model.number=\"form.level_number\" type=\"number\" min=\"1\" class=\"w-full px-3 py-2 bg-slate-700 border border-slate-600 rounded-lg text-white focus:outline-none focus:ring-2 focus:ring-indigo-500/50\" />\n </div>\n <div>\n <label class=\"block text-sm font-medium text-slate-300 mb-1\">{{ t('gamification.levels.form_rank_name') }}</label>\n <input v-model=\"form.rank_name\" type=\"text\" class=\"w-full px-3 py-2 bg-slate-700 border border-slate-600 rounded-lg text-white focus:outline-none focus:ring-2 focus:ring-indigo-500/50\" />\n </div>\n <div>\n <label class=\"block text-sm font-medium text-slate-300 mb-1\">{{ t('gamification.levels.form_min_points') }}</label>\n <input v-model.number=\"form.min_points\" type=\"number\" min=\"0\" class=\"w-full px-3 py-2 bg-slate-700 border border-slate-600 rounded-lg text-white focus:outline-none focus:ring-2 focus:ring-indigo-500/50\" />\n </div>\n <div class=\"flex items-center gap-2\">\n <input v-model=\"form.is_penalty\" type=\"checkbox\" id=\"is_penalty\" class=\"rounded bg-slate-700 border-slate-600 text-indigo-600 focus:ring-indigo-500\" />\n <label for=\"is_penalty\" class=\"text-sm text-slate-300\">{{ t('gamification.levels.form_is_penalty') }}</label>\n </div>\n </div>\n <div v-if=\"formError\" class=\"mt-4 bg-rose-500/10 border border-rose-500/30 rounded-lg p-3\">\n <p class=\"text-sm text-rose-400\">{{ formError }}</p>\n </div>\n <div class=\"flex items-center justify-end gap-3 mt-6\">\n <button @click=\"closeModal\" 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.levels.cancel') }}\n </button>\n <button @click=\"saveLevel\" :disabled=\"saving\" class=\"px-4 py-2 bg-indigo-600 hover:bg-indigo-500 disabled:bg-indigo-600/50 text-white text-sm font-medium rounded-lg transition\">\n {{ saving ? t('gamification.levels.saving') : t('gamification.levels.save') }}\n </button>\n </div>\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 LevelConfig {\n id: number\n level_number: number\n rank_name: string\n min_points: number\n is_penalty: boolean\n}\n\nconst loading = ref(true)\nconst error = ref(false)\nconst saving = ref(false)\nconst levels = ref<LevelConfig[]>([])\nconst showModal = ref(false)\nconst editingLevel = ref<LevelConfig | null>(null)\nconst formError = ref('')\n\nconst form = reactive({\n level_number: 1,\n rank_name: '',\n min_points: 0,\n is_penalty: false,\n})\n\nconst sortedLevels = computed(() => {\n return [...levels.value].sort((a, b) => a.level_number - b.level_number)\n})\n\nfunction getHeaders(): Record<string, string> {\n const tokenCookie = useCookie('access_token')\n const token = tokenCookie.value\n return token ? { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' } : { 'Content-Type': 'application/json' }\n}\n\nfunction openCreateModal() {\n editingLevel.value = null\n form.level_number = levels.value.length + 1\n form.rank_name = ''\n form.min_points = 0\n form.is_penalty = false\n formError.value = ''\n showModal.value = true\n}\n\nfunction openEditModal(level: LevelConfig) {\n editingLevel.value = level\n form.level_number = level.level_number\n form.rank_name = level.rank_name\n form.min_points = level.min_points\n form.is_penalty = level.is_penalty\n formError.value = ''\n showModal.value = true\n}\n\nfunction closeModal() {\n showModal.value = false\n editingLevel.value = null\n}\n\nasync function fetchLevels() {\n loading.value = true\n error.value = false\n try {\n const data = await $fetch<LevelConfig[]>('/api/v1/admin/gamification/level-configs', { headers: getHeaders() })\n levels.value = data || []\n } catch (e) {\n console.error('Failed to fetch level configs:', e)\n error.value = true\n } finally {\n loading.value = false\n }\n}\n\nasync function saveLevel() {\n saving.value = true\n formError.value = ''\n try {\n const body = {\n level_number: form.level_number,\n rank_name: form.rank_name,\n min_points: form.min_points,\n is_penalty: form.is_penalty,\n }\n if (editingLevel.value) {\n await $fetch(`/api/v1/admin/gamification/level-configs/${editingLevel.value.id}`, {\n method: 'PUT',\n headers: getHeaders(),\n body,\n })\n showToast(t('gamification.levels.updated'))\n } else {\n await $fetch('/api/v1/admin/gamification/level-configs', {\n method: 'POST',\n headers: getHeaders(),\n body,\n })\n showToast(t('gamification.levels.created'))\n }\n closeModal()\n await fetchLevels()\n } catch (e: any) {\n if (e?.data?.detail?.includes('already exists')) {\n formError.value = t('gamification.levels.duplicate_error')\n } else {\n formError.value = t('gamification.levels.save_error')\n }\n } finally {\n saving.value = false\n }\n}\n\nfunction showToast(msg: string) {\n // Simple toast - could be enhanced\n const toast = document.createElement('div')\n toast.className = 'fixed bottom-6 right-6 z-50 bg-emerald-600 text-white px-4 py-3 rounded-lg shadow-lg text-sm animate-slide-up'\n toast.textContent = msg\n document.body.appendChild(toast)\n setTimeout(() => toast.remove(), 3000)\n}\n\nonMounted(() => {\n fetchLevels()\n})\n</script>\n\n<style scoped>\n.animate-slide-up {\n animation: slideUp 0.3s ease-out;\n}\n@keyframes slideUp {\n from {\n opacity: 0;\n transform: translateY(1rem);\n }\n to {\n opacity: 1;\n transform: translateY(0);\n }\n}\n</style>\n"],"version":3} |