Files
service-finder/frontend_admin/.nuxt/dist/server/_nuxt/point-rules-tyImSuJC.js.map.json
2026-07-01 02:27:38 +00:00

1 line
26 KiB
JSON

{"file":"point-rules-tyImSuJC.js","mappings":";;;;;;;;;;;;;;;;;;;;;;;AA2RA,UAAM,EAAE,EAAA,IAAM,QAAA;AAed,UAAM,UAAU,IAAI,IAAI;AACxB,UAAM,QAAQ,IAAI,KAAK;AACvB,UAAM,QAAQ,IAAiB,EAAE;AACjC,UAAM,YAAY,IAAI,KAAK;AAC3B,UAAM,kBAAkB,IAAI,KAAK;AACjC,UAAM,cAAc,IAAsB,IAAI;AAC9C,UAAM,eAAe,IAAsB,IAAI;AAC/C,UAAM,SAAS,IAAI,KAAK;AACxB,UAAM,YAAY,IAAI,EAAE;AACxB,UAAM,QAAQ,IAAI,EAAE;AAGpB,UAAM,cAAc,IAAwB,IAAI;AAChD,UAAM,YAAY,IAA+B,EAAE;AAC5B,QAA6B,IAAI;AACnC,QAAI,KAAK;AAE9B,UAAM,OAAO,SAAS;AAAA,MACpB,YAAY;AAAA,MACZ,QAAQ;AAAA,MACR,aAAa;AAAA,MACb,WAAW;AAAA,IAAA,CACZ;AA6HD,aAAS,UAAU,QAAgB,OAAwB;AACzD,aAAO,YAAY,OAAO,OAAO,UAAU,YAAY,OAAO,UAAU;AAAA,IAC1E;;wKA3bmDA,MAAA,CAAA,EAAC,gCAAA,CAAA,uEACZA,MAAA,CAAA,EAAC,mCAAA,CAAA;UAI1B,QAAA,OAAO;yfAKLA,MAAA,CAAA,EAAC,kCAAA,CAAA,CAAA,qBAAA;AAAA,iBAKE,MAAA,OAAK;mKACSA,MAAA,CAAA,EAAC,gCAAA,CAAA,gHACmEA,MAAA,CAAA,EAAC,gCAAA,CAAA;;wcAU1FA,MAAA,CAAA,EAAC,iCAAA,CAAA,8VAS+FA,MAAA,CAAA,EAAC,6BAAA,CAAA,oIACDA,MAAA,CAAA,EAAC,qCAAA,CAAA,oIACDA,MAAA,CAAA,EAAC,iCAAA,CAAA,oIACDA,MAAA,CAAA,EAAC,sCAAA,CAAA,oIACDA,MAAA,CAAA,EAAC,iCAAA,CAAA,qIACAA,MAAA,CAAA,EAAC,kCAAA,CAAA;AAIhFC,sBAAA,MAAA,QAAR,SAAI;AACmC,gBAAA,6HAAAC,eAAA,KAAK,EAAE,CAAA,6CAAA;cAIrC,UAAU,KAAK,IAAE,YAAA,GAAA;kDAGpB,UAAA,KAAS,CAAA,gMAAA;AAAA;AAchB,kBAAA,sJAAAA,eAAA,KAAK,UAAU,CAAA,SAAA;AAAA;;cAML,UAAU,KAAK,IAAE,QAAA,GAAA;kDAGb,UAAA,KAAS,CAAA,yMAAA;AAAA;kCAcjBC,eAAA,CAAA,KAAK,UAAM,IAAA,qBAAA,iBAAA,kEAAA,CAAA,sBAGhBD,eAAA,KAAK,UAAM,IAAA,MAAA,EAAA,IAAsBA,eAAA,KAAK,MAAM;;;cAOnC,UAAU,KAAK,IAAE,aAAA,GAAA;kDAGpB,UAAA,KAAS,CAAA,gLAAA;AAAA;AAahB,kBAAA,2HAAAA,eAAA,KAAK,eAAW,GAAA,CAAA,SAAA;AAAA;;cAMN,UAAU,KAAK,IAAE,WAAA,GAAA;yGAKnB,UAAA,QAAS,kEAAA,kDAAA,kDAAA,CAAA,qCACfF,MAAA,CAAA,EAAC,iCAAA,CAAA,6CAIM,UAAA,QAAS,yDAAA,kDAAA,kDAAA,CAAA,qCAChBA,MAAA,CAAA,EAAC,mCAAA,CAAA;;kCAMGG,eAAA,CAAA,KAAK,YAAS,uCAAA,kCAAA,wIAAA,CAAA,sBAGnBD,eAAA,KAAK,YAAYF,MAAA,CAAA,uCAAuCA,MAAA,CAAA,EAAC,mCAAA,CAAA;;wOAO+DI,cAAA,SAAOJ,MAAA,CAAA,EAAC,+BAAA,CAAA,0bAKHI,cAAA,SAAOJ,MAAA,CAAA,EAAC,uCAAA,CAAA;;;AAQtI,YAAA,MAAA,MAAM,WAAM,GAAA;4IAEfA,MAAA,CAAA,EAAC,mCAAA,CAAA,CAAA,YAAA;AAAA;;;;;UASL,UAAA,OAAS;iUAEqC,YAAA,QAAcA,MAAA,CAAA,EAAC,qCAAA,IAA0CA,MAAA,CAAA,EAAC,uCAAA,CAAA,kKAK3CA,MAAA,CAAA,EAAC,2CAAA,CAAA,kBAEtDI,cAAA,SAAA,KAAK,UAAU,yCAIvBA,cAAA,eAAaJ,MAAA,CAAA,EAAC,iDAAA,CAAA,6UAO+CA,MAAA,CAAA,EAAC,uCAAA,CAAA,kBAE/CI,cAAA,SAAA,KAAK,MAAM,6YAYmCJ,MAAA,CAAA,EAAC,4CAAA,CAAA,8BAI9DI,cAAA,eAAaJ,MAAA,CAAA,EAAC,kDAAA,CAAA,kOAFNE,eAAA,KAAK,WAAW,gFAUhBG,sBAAA,MAAA,QAAA,KAAK,SAAS,IAAdC,gBAAA,KAAK,WAAS,IAAA,IAAd,KAAK,SAAS,IAAA,aAAA,uOAKgCN,MAAA,CAAA,EAAC,oCAAA,CAAA;YAIjD,UAAA,OAAS;wKACkB,UAAA,KAAS,CAAA,YAAA;AAAA;;;oMAKqEA,MAAA,CAAA,EAAC,iCAAA,CAAA,wDAClF,OAAA,KAAM,IAAA,cAAA;YAC1B,OAAA,OAAM;;;;;iCAGd,OAAA,QAASA,MAAA,CAAA,EAAC,iCAAA,IAAsCA,MAAA,CAAA,EAAC,+BAAA,CAAA,CAAA,oCAAA;AAAA;;;UAQnD,gBAAA,OAAe;iUAE+BA,MAAA,CAAA,EAAC,uCAAA,CAAA,+EAEjDA,MAAA,CAAA,EAAC,2CAAA,EAAA,MAAoD,aAAA,OAAc,WAAA,CAAU,CAAA,wKAGiCA,MAAA,CAAA,EAAC,iCAAA,CAAA,0CAC3E,OAAA,KAAM,IAAA,cAAA,kKACxC,OAAA,QAASA,MAAA,CAAA,EAAC,mCAAA,IAAwCA,MAAA,CAAA,EAAC,iCAAA,CAAA;;;;UAOnD,MAAA,OAAK;kcAIX,MAAA,KAAK,CAAA,QAAA;AAAA;;;;;;;;;;;;;;","names":["_unref","_ssrRenderList","_ssrInterpolate","_ssrRenderClass","_ssrRenderAttr","_ssrIncludeBooleanAttr","_ssrLooseContain"],"sources":["../../../../pages/gamification/point-rules.vue"],"sourcesContent":["<template>\n <div>\n <!-- Page Header -->\n <div class=\"mb-8\">\n <h1 class=\"text-2xl font-bold text-white\">{{ t('gamification.point_rules.title') }}</h1>\n <p class=\"text-slate-400 mt-1\">{{ t('gamification.point_rules.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.point_rules.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.point_rules.error') }}</p>\n <button @click=\"fetchRules\" class=\"mt-3 text-sm text-rose-300 hover:text-rose-200 underline\">{{ t('gamification.point_rules.retry') }}</button>\n </div>\n\n <template v-else>\n <!-- Create Button -->\n <div class=\"mb-6 flex justify-end\">\n <button @click=\"openCreateModal\" class=\"px-4 py-2 bg-emerald-600 hover:bg-emerald-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.point_rules.create') }}\n </button>\n </div>\n\n <!-- Rules Table -->\n <div class=\"bg-slate-800 rounded-xl border border-slate-700 overflow-hidden\">\n <table class=\"w-full\">\n <thead>\n <tr class=\"border-b border-slate-700\">\n <th class=\"text-left px-6 py-4 text-xs font-medium text-slate-400 uppercase tracking-wider\">{{ t('gamification.point_rules.id') }}</th>\n <th class=\"text-left px-6 py-4 text-xs font-medium text-slate-400 uppercase tracking-wider\">{{ t('gamification.point_rules.action_key') }}</th>\n <th class=\"text-left px-6 py-4 text-xs font-medium text-slate-400 uppercase tracking-wider\">{{ t('gamification.point_rules.points') }}</th>\n <th class=\"text-left px-6 py-4 text-xs font-medium text-slate-400 uppercase tracking-wider\">{{ t('gamification.point_rules.description') }}</th>\n <th class=\"text-left px-6 py-4 text-xs font-medium text-slate-400 uppercase tracking-wider\">{{ t('gamification.point_rules.status') }}</th>\n <th class=\"text-right px-6 py-4 text-xs font-medium text-slate-400 uppercase tracking-wider\">{{ t('gamification.point_rules.actions') }}</th>\n </tr>\n </thead>\n <tbody class=\"divide-y divide-slate-700/50\">\n <tr v-for=\"rule in rules\" :key=\"rule.id\" class=\"hover:bg-slate-700/30 transition\">\n <td class=\"px-6 py-4 text-sm text-slate-400\">{{ rule.id }}</td>\n\n <!-- Action Key (inline editable) -->\n <td class=\"px-6 py-4\">\n <template v-if=\"isEditing(rule.id, 'action_key')\">\n <input\n ref=\"inlineInputRef\"\n v-model=\"editValue\"\n type=\"text\"\n maxlength=\"100\"\n class=\"w-full px-2 py-1 bg-slate-700 border border-amber-500 rounded text-sm text-white focus:outline-none focus:ring-2 focus:ring-amber-500/50\"\n @keyup.enter=\"saveEdit(rule, 'action_key')\"\n @keyup.escape=\"cancelEdit()\"\n @blur=\"saveEdit(rule, 'action_key')\"\n @click.stop\n />\n </template>\n <template v-else>\n <code\n class=\"text-sm font-mono text-amber-300 bg-amber-500/10 px-2 py-0.5 rounded cursor-pointer hover:bg-amber-500/20 transition\"\n @click=\"startEdit(rule, 'action_key')\"\n >{{ rule.action_key }}</code>\n </template>\n </td>\n\n <!-- Points (inline editable) -->\n <td class=\"px-6 py-4\">\n <template v-if=\"isEditing(rule.id, 'points')\">\n <input\n ref=\"inlineInputRef\"\n v-model.number=\"editValue\"\n type=\"number\"\n min=\"-10000\"\n max=\"10000\"\n class=\"w-24 px-2 py-1 bg-slate-700 border border-amber-500 rounded text-sm text-white focus:outline-none focus:ring-2 focus:ring-amber-500/50\"\n @keyup.enter=\"saveEdit(rule, 'points')\"\n @keyup.escape=\"cancelEdit()\"\n @blur=\"saveEdit(rule, 'points')\"\n @click.stop\n />\n </template>\n <template v-else>\n <span\n class=\"text-sm font-semibold cursor-pointer hover:opacity-80 transition\"\n :class=\"rule.points >= 0 ? 'text-emerald-400' : 'text-rose-400'\"\n @click=\"startEdit(rule, 'points')\"\n >\n {{ rule.points >= 0 ? '+' : '' }}{{ rule.points }}\n </span>\n </template>\n </td>\n\n <!-- Description (inline editable) -->\n <td class=\"px-6 py-4\">\n <template v-if=\"isEditing(rule.id, 'description')\">\n <input\n ref=\"inlineInputRef\"\n v-model=\"editValue\"\n type=\"text\"\n class=\"w-full px-2 py-1 bg-slate-700 border border-amber-500 rounded text-sm text-white focus:outline-none focus:ring-2 focus:ring-amber-500/50\"\n @keyup.enter=\"saveEdit(rule, 'description')\"\n @keyup.escape=\"cancelEdit()\"\n @blur=\"saveEdit(rule, 'description')\"\n @click.stop\n />\n </template>\n <template v-else>\n <span\n class=\"text-sm text-slate-300 max-w-xs truncate block cursor-pointer hover:text-white transition\"\n @click=\"startEdit(rule, 'description')\"\n >{{ rule.description || '—' }}</span>\n </template>\n </td>\n\n <!-- Status (inline toggle) -->\n <td class=\"px-6 py-4\">\n <template v-if=\"isEditing(rule.id, 'is_active')\">\n <div class=\"flex items-center gap-2\" @click.stop>\n <button\n @click=\"editValue = true; saveEdit(rule, 'is_active')\"\n class=\"px-2 py-1 text-xs font-medium rounded transition\"\n :class=\"editValue ? 'bg-emerald-500/20 text-emerald-400 ring-1 ring-emerald-500/50' : 'bg-slate-700 text-slate-400 hover:bg-slate-600'\"\n >{{ t('gamification.point_rules.active') }}</button>\n <button\n @click=\"editValue = false; saveEdit(rule, 'is_active')\"\n class=\"px-2 py-1 text-xs font-medium rounded transition\"\n :class=\"!editValue ? 'bg-rose-500/20 text-rose-400 ring-1 ring-rose-500/50' : 'bg-slate-700 text-slate-400 hover:bg-slate-600'\"\n >{{ t('gamification.point_rules.inactive') }}</button>\n </div>\n </template>\n <template v-else>\n <span\n class=\"inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium cursor-pointer hover:ring-2 hover:ring-amber-500/50 transition\"\n :class=\"rule.is_active ? 'bg-emerald-500/10 text-emerald-400' : 'bg-slate-500/10 text-slate-400'\"\n @click=\"startEdit(rule, 'is_active')\"\n >\n {{ rule.is_active ? t('gamification.point_rules.active') : t('gamification.point_rules.inactive') }}\n </span>\n </template>\n </td>\n\n <td class=\"px-6 py-4 text-right\">\n <div class=\"flex items-center justify-end gap-2\">\n <button @click=\"openEditModal(rule)\" class=\"p-1.5 rounded-lg text-slate-400 hover:text-white hover:bg-slate-700 transition\" :title=\"t('gamification.point_rules.edit')\">\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=\"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 </button>\n <button @click=\"confirmDelete(rule)\" class=\"p-1.5 rounded-lg text-slate-400 hover:text-rose-400 hover:bg-rose-500/10 transition\" :title=\"t('gamification.point_rules.delete_title')\">\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=\"M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16\" />\n </svg>\n </button>\n </div>\n </td>\n </tr>\n <tr v-if=\"rules.length === 0\">\n <td colspan=\"6\" class=\"px-6 py-12 text-center text-sm text-slate-500\">\n {{ t('gamification.point_rules.no_items') }}\n </td>\n </tr>\n </tbody>\n </table>\n </div>\n </template>\n\n <!-- Create/Edit Modal -->\n <div v-if=\"showModal\" class=\"fixed inset-0 z-50 flex items-center justify-center bg-black/60 backdrop-blur-sm\" @click.self=\"closeModal\">\n <div class=\"bg-slate-800 rounded-xl border border-slate-700 p-6 w-full max-w-lg mx-4 shadow-2xl\">\n <h2 class=\"text-lg font-semibold text-white mb-6\">{{ editingRule ? t('gamification.point_rules.edit_title') : t('gamification.point_rules.create_title') }}</h2>\n\n <form @submit.prevent=\"saveRule\" class=\"space-y-4\">\n <!-- Action Key -->\n <div>\n <label class=\"block text-sm font-medium text-slate-300 mb-1\">{{ t('gamification.point_rules.action_key_label') }}</label>\n <input\n v-model=\"form.action_key\"\n type=\"text\"\n required\n maxlength=\"100\"\n :placeholder=\"t('gamification.point_rules.action_key_placeholder')\"\n class=\"w-full px-3 py-2 bg-slate-700 border border-slate-600 rounded-lg text-white placeholder-slate-500 focus:outline-none focus:ring-2 focus:ring-amber-500/50 focus:border-amber-500 text-sm\"\n />\n </div>\n\n <!-- Points -->\n <div>\n <label class=\"block text-sm font-medium text-slate-300 mb-1\">{{ t('gamification.point_rules.points_label') }}</label>\n <input\n v-model.number=\"form.points\"\n type=\"number\"\n required\n min=\"-10000\"\n max=\"10000\"\n placeholder=\"0\"\n class=\"w-full px-3 py-2 bg-slate-700 border border-slate-600 rounded-lg text-white placeholder-slate-500 focus:outline-none focus:ring-2 focus:ring-amber-500/50 focus:border-amber-500 text-sm\"\n />\n </div>\n\n <!-- Description -->\n <div>\n <label class=\"block text-sm font-medium text-slate-300 mb-1\">{{ t('gamification.point_rules.description_label') }}</label>\n <textarea\n v-model=\"form.description\"\n rows=\"3\"\n :placeholder=\"t('gamification.point_rules.description_placeholder')\"\n class=\"w-full px-3 py-2 bg-slate-700 border border-slate-600 rounded-lg text-white placeholder-slate-500 focus:outline-none focus:ring-2 focus:ring-amber-500/50 focus:border-amber-500 text-sm resize-none\"\n ></textarea>\n </div>\n\n <!-- Is Active -->\n <div class=\"flex items-center gap-3\">\n <input\n v-model=\"form.is_active\"\n type=\"checkbox\"\n id=\"is_active\"\n class=\"w-4 h-4 rounded bg-slate-700 border-slate-600 text-amber-500 focus:ring-amber-500/50\"\n />\n <label for=\"is_active\" class=\"text-sm text-slate-300\">{{ t('gamification.point_rules.is_active') }}</label>\n </div>\n\n <!-- Error Message -->\n <div v-if=\"formError\" class=\"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\n <!-- Actions -->\n <div class=\"flex justify-end gap-3 pt-2\">\n <button type=\"button\" @click=\"closeModal\" class=\"px-4 py-2 text-sm text-slate-400 hover:text-white transition\">{{ t('gamification.point_rules.cancel') }}</button>\n <button type=\"submit\" :disabled=\"saving\" class=\"px-4 py-2 bg-amber-600 hover:bg-amber-500 disabled:bg-amber-600/50 text-white rounded-lg text-sm font-medium transition flex items-center gap-2\">\n <svg v-if=\"saving\" class=\"w-4 h-4 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 {{ saving ? t('gamification.point_rules.saving') : t('gamification.point_rules.save') }}\n </button>\n </div>\n </form>\n </div>\n </div>\n\n <!-- Delete Confirmation Modal -->\n <div v-if=\"showDeleteModal\" class=\"fixed inset-0 z-50 flex items-center justify-center bg-black/60 backdrop-blur-sm\" @click.self=\"showDeleteModal = false\">\n <div class=\"bg-slate-800 rounded-xl border border-slate-700 p-6 w-full max-w-md mx-4 shadow-2xl\">\n <h2 class=\"text-lg font-semibold text-white mb-2\">{{ t('gamification.point_rules.delete_title') }}</h2>\n <p class=\"text-sm text-slate-400 mb-6\">\n {{ t('gamification.point_rules.delete_confirm', { name: deletingRule?.action_key }) }}\n </p>\n <div class=\"flex justify-end gap-3\">\n <button @click=\"showDeleteModal = false\" class=\"px-4 py-2 text-sm text-slate-400 hover:text-white transition\">{{ t('gamification.point_rules.cancel') }}</button>\n <button @click=\"deleteRule\" :disabled=\"saving\" class=\"px-4 py-2 bg-rose-600 hover:bg-rose-500 disabled:bg-rose-600/50 text-white rounded-lg text-sm font-medium transition\">\n {{ saving ? t('gamification.point_rules.deleting') : t('gamification.point_rules.delete') }}\n </button>\n </div>\n </div>\n </div>\n\n <!-- Success Toast -->\n <div v-if=\"toast\" class=\"fixed bottom-6 right-6 z-50 bg-emerald-600 text-white px-4 py-3 rounded-lg shadow-lg text-sm flex items-center gap-2 animate-slide-up\">\n <svg class=\"w-4 h-4 flex-shrink-0\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n <path stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z\" />\n </svg>\n {{ toast }}\n </div>\n </div>\n</template>\n\n<script setup lang=\"ts\">\nimport { nextTick, ref, reactive, onMounted } from 'vue'\n\ndefinePageMeta({\n middleware: 'auth',\n})\n\nconst { t } = useI18n()\n\ninterface PointRule {\n id: number\n action_key: string\n points: number\n description: string | null\n is_active: boolean\n}\n\ninterface EditingCell {\n id: number\n field: string\n}\n\nconst loading = ref(true)\nconst error = ref(false)\nconst rules = ref<PointRule[]>([])\nconst showModal = ref(false)\nconst showDeleteModal = ref(false)\nconst editingRule = ref<PointRule | null>(null)\nconst deletingRule = ref<PointRule | null>(null)\nconst saving = ref(false)\nconst formError = ref('')\nconst toast = ref('')\n\n// Inline editing state\nconst editingCell = ref<EditingCell | null>(null)\nconst editValue = ref<string | number | boolean>('')\nconst inlineInputRef = ref<HTMLInputElement | null>(null)\nconst inlineSaving = ref(false)\n\nconst form = reactive({\n action_key: '',\n points: 0,\n description: '',\n is_active: true,\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 showToast(msg: string) {\n toast.value = msg\n setTimeout(() => { toast.value = '' }, 3000)\n}\n\nfunction resetForm() {\n form.action_key = ''\n form.points = 0\n form.description = ''\n form.is_active = true\n formError.value = ''\n}\n\nfunction openCreateModal() {\n editingRule.value = null\n resetForm()\n showModal.value = true\n}\n\nfunction openEditModal(rule: PointRule) {\n editingRule.value = rule\n form.action_key = rule.action_key\n form.points = rule.points\n form.description = rule.description || ''\n form.is_active = rule.is_active\n formError.value = ''\n showModal.value = true\n}\n\nfunction closeModal() {\n showModal.value = false\n editingRule.value = null\n resetForm()\n}\n\nfunction confirmDelete(rule: PointRule) {\n deletingRule.value = rule\n showDeleteModal.value = true\n}\n\nasync function fetchRules() {\n loading.value = true\n error.value = false\n try {\n const data = await $fetch<PointRule[]>('/api/v1/admin/gamification/point-rules', { headers: getHeaders() })\n rules.value = data\n } catch (e) {\n console.error('Failed to fetch point rules:', e)\n error.value = true\n } finally {\n loading.value = false\n }\n}\n\nasync function saveRule() {\n saving.value = true\n formError.value = ''\n try {\n const payload = {\n action_key: form.action_key,\n points: form.points,\n description: form.description || null,\n is_active: form.is_active,\n }\n\n if (editingRule.value) {\n // Update existing rule\n await $fetch(`/api/v1/admin/gamification/point-rules/${editingRule.value.id}`, {\n method: 'PUT',\n headers: getHeaders(),\n body: payload,\n })\n showToast(t('gamification.point_rules.updated'))\n } else {\n // Create new rule\n await $fetch('/api/v1/admin/gamification/point-rules', {\n method: 'POST',\n headers: getHeaders(),\n body: payload,\n })\n showToast(t('gamification.point_rules.created'))\n }\n closeModal()\n await fetchRules()\n } catch (e: any) {\n if (e?.response?.status === 409) {\n formError.value = t('gamification.point_rules.duplicate_key')\n } else {\n formError.value = t('gamification.point_rules.save_error')\n }\n console.error('Failed to save point rule:', e)\n } finally {\n saving.value = false\n }\n}\n\nasync function deleteRule() {\n if (!deletingRule.value) return\n saving.value = true\n try {\n await $fetch(`/api/v1/admin/gamification/point-rules/${deletingRule.value.id}`, {\n method: 'DELETE',\n headers: getHeaders(),\n })\n showToast(t('gamification.point_rules.deleted'))\n showDeleteModal.value = false\n deletingRule.value = null\n await fetchRules()\n } catch (e) {\n console.error('Failed to delete point rule:', e)\n } finally {\n saving.value = false\n }\n}\n\n// --- Inline Editing Methods ---\n\nfunction isEditing(ruleId: number, field: string): boolean {\n return editingCell.value?.id === ruleId && editingCell.value?.field === field\n}\n\nasync function startEdit(rule: PointRule, field: string) {\n if (inlineSaving.value) return\n editingCell.value = { id: rule.id, field }\n\n // Set the initial edit value based on field type\n if (field === 'is_active') {\n editValue.value = rule.is_active\n } else if (field === 'points') {\n editValue.value = rule.points\n } else if (field === 'description') {\n editValue.value = rule.description || ''\n } else {\n editValue.value = rule.action_key\n }\n\n // Focus the input after DOM update\n await nextTick()\n if (inlineInputRef.value) {\n inlineInputRef.value.focus()\n }\n}\n\nfunction cancelEdit() {\n editingCell.value = null\n editValue.value = ''\n}\n\nasync function saveEdit(rule: PointRule, field: string) {\n if (!editingCell.value || inlineSaving.value) return\n\n // Build the partial payload\n const payload: Record<string, any> = {}\n let newValue = editValue.value\n\n // Type coercion\n if (field === 'points') {\n newValue = Number(newValue)\n if (isNaN(newValue as number)) return\n } else if (field === 'is_active') {\n newValue = Boolean(newValue)\n } else if (field === 'description') {\n newValue = (newValue as string) || null\n } else {\n newValue = String(newValue).trim()\n if (!newValue) return // don't save empty action_key\n }\n\n // Check if value actually changed\n const oldValue = (rule as any)[field]\n if (newValue === oldValue) {\n cancelEdit()\n return\n }\n\n payload[field] = newValue\n\n inlineSaving.value = true\n try {\n await $fetch(`/api/v1/admin/gamification/point-rules/${rule.id}`, {\n method: 'PUT',\n headers: getHeaders(),\n body: payload,\n })\n // Update local state optimistically\n ;(rule as any)[field] = newValue\n showToast(t('gamification.point_rules.updated'))\n cancelEdit()\n } catch (e: any) {\n console.error('Failed to save inline edit:', e)\n if (e?.response?.status === 409) {\n formError.value = t('gamification.point_rules.duplicate_key')\n // Show error briefly\n setTimeout(() => { formError.value = '' }, 3000)\n }\n cancelEdit()\n } finally {\n inlineSaving.value = false\n }\n}\n\nonMounted(() => {\n fetchRules()\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}