From 7eacb513cdafeffdfd479964256ec7499e917274 Mon Sep 17 00:00:00 2001 From: adonis Date: Mon, 7 Sep 2026 16:56:52 +0330 Subject: [PATCH] feat(plugins): SuperAdmin store tabs, pricing, and request approval Khanegi plugin prices and purchase requests sit beside existing version cards. Also rewrite leftover nerkh upload URLs on tenant forms. Co-authored-by: Cursor --- src/layouts/AppLayout.vue | 36 +++++ src/pages/PluginCatalogPage.vue | 237 ++++++++++++++++++++++++++++ src/pages/PluginPanelPage.vue | 165 ++++++++++++++++++- src/pages/PluginTenantPage.vue | 47 ++++++ src/pages/ProductVersionsPage.vue | 3 +- src/pages/TenantFormPage.vue | 5 +- src/router/index.ts | 6 + src/utils/rewriteLegacyUploadUrl.ts | 40 +++++ 8 files changed, 530 insertions(+), 9 deletions(-) create mode 100644 src/pages/PluginCatalogPage.vue create mode 100644 src/utils/rewriteLegacyUploadUrl.ts diff --git a/src/layouts/AppLayout.vue b/src/layouts/AppLayout.vue index 2484b3e..ef40781 100644 --- a/src/layouts/AppLayout.vue +++ b/src/layouts/AppLayout.vue @@ -9,7 +9,9 @@ import api from '@/api/http' const auth = useAuthStore() const router = useRouter() const adminSmsEnabled = ref(true) +const pluginStoreEnabled = ref(false) const smsToggleLoading = ref(false) +const storeToggleLoading = ref(false) const canSettingsWrite = computed(() => auth.can('settings:write')) function goHome() { @@ -26,6 +28,7 @@ async function loadSmsToggle() { try { const { data } = await api.get('/settings/admin-sms') adminSmsEnabled.value = data.adminSmsEnabled !== false + pluginStoreEnabled.value = data.pluginStoreEnabled === true } catch { // keep default on; non-blocking for layout } @@ -50,6 +53,25 @@ async function onSmsToggle(checked: boolean) { } } +async function onStoreToggle(checked: boolean) { + if (!canSettingsWrite.value) return + storeToggleLoading.value = true + const previous = pluginStoreEnabled.value + pluginStoreEnabled.value = checked + try { + const { data } = await api.patch('/settings/plugin-store', { enabled: checked }) + pluginStoreEnabled.value = data.pluginStoreEnabled === true + message.success( + pluginStoreEnabled.value ? 'فروشگاه افزونه روشن شد' : 'فروشگاه افزونه خاموش شد', + ) + } catch (e: any) { + pluginStoreEnabled.value = previous + message.error(e?.response?.data?.message || 'تغییر فروشگاه افزونه ناموفق') + } finally { + storeToggleLoading.value = false + } +} + onMounted(loadSmsToggle) @@ -79,6 +101,20 @@ onMounted(loadSmsToggle) @change="onSmsToggle" /> +
+ فروشگاه افزونه + +
diff --git a/src/pages/PluginCatalogPage.vue b/src/pages/PluginCatalogPage.vue new file mode 100644 index 0000000..2c70f92 --- /dev/null +++ b/src/pages/PluginCatalogPage.vue @@ -0,0 +1,237 @@ + + + + + diff --git a/src/pages/PluginPanelPage.vue b/src/pages/PluginPanelPage.vue index c89da7e..647706a 100644 --- a/src/pages/PluginPanelPage.vue +++ b/src/pages/PluginPanelPage.vue @@ -20,11 +20,27 @@ type TenantRow = { } } +type RequestRow = { + id: string + tenantName: string + domain: string + pluginTitle: string + durationDays: number + finalPriceRials: number + status: 'pending' | 'approved' | 'rejected' | 'cancelled' + createdAt: string +} + const router = useRouter() const loadingList = ref(false) const tenants = ref([]) const q = ref('') const toneFilter = ref<'healthy' | 'warning' | 'critical' | 'unknown' | null>(null) +const mainTab = ref<'khanegi' | 'hamkar' | 'requests'>('khanegi') +const requests = ref([]) +const loadingRequests = ref(false) +const actingId = ref(null) +const autoCut = ref(true) const filteredTenants = computed(() => { const term = q.value.trim().toLowerCase() @@ -68,7 +84,64 @@ async function loadTenants() { } } -onMounted(loadTenants) +const statusLabel: Record = { + pending: 'در انتظار', + approved: 'تأیید شده', + rejected: 'رد شده', + cancelled: 'لغو شده', +} + +async function loadRequests() { + loadingRequests.value = true + try { + const { data } = await api.get('/plugins/store/requests') + requests.value = data + } catch (e: any) { + message.error(e?.response?.data?.message || 'خطا در دریافت درخواست‌ها') + } finally { + loadingRequests.value = false + } +} + +async function approve(row: RequestRow) { + actingId.value = row.id + try { + await api.post(`/plugins/store/requests/${row.id}/approve`, { + autoCutOnExpiry: autoCut.value, + }) + message.success('تأیید شد و سوییچ روشن شد') + await loadRequests() + await loadTenants() + } catch (e: any) { + const msg = e?.response?.data?.message + message.error(Array.isArray(msg) ? msg.join('، ') : msg || 'تأیید ناموفق') + } finally { + actingId.value = null + } +} + +async function reject(row: RequestRow) { + actingId.value = row.id + try { + await api.post(`/plugins/store/requests/${row.id}/reject`) + message.success('رد شد') + await loadRequests() + } catch (e: any) { + const msg = e?.response?.data?.message + message.error(Array.isArray(msg) ? msg.join('، ') : msg || 'رد ناموفق') + } finally { + actingId.value = null + } +} + +function setTab(tab: 'khanegi' | 'hamkar' | 'requests') { + mainTab.value = tab + if (tab === 'requests') void loadRequests() +} + +onMounted(() => { + loadTenants() +})