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 <cursoragent@cursor.com>
This commit is contained in:
@@ -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<TenantRow[]>([])
|
||||
const q = ref('')
|
||||
const toneFilter = ref<'healthy' | 'warning' | 'critical' | 'unknown' | null>(null)
|
||||
const mainTab = ref<'khanegi' | 'hamkar' | 'requests'>('khanegi')
|
||||
const requests = ref<RequestRow[]>([])
|
||||
const loadingRequests = ref(false)
|
||||
const actingId = ref<string | null>(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<RequestRow['status'], string> = {
|
||||
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()
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -86,7 +159,13 @@ onMounted(loadTenants)
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="toolbar">
|
||||
<div class="main-tabs">
|
||||
<button type="button" :class="{ active: mainTab === 'khanegi' }" @click="setTab('khanegi')">خانگی</button>
|
||||
<button type="button" :class="{ active: mainTab === 'hamkar' }" @click="setTab('hamkar')">همکار</button>
|
||||
<button type="button" :class="{ active: mainTab === 'requests' }" @click="setTab('requests')">درخواستها</button>
|
||||
</div>
|
||||
|
||||
<div v-if="mainTab !== 'requests'" class="toolbar">
|
||||
<a-input
|
||||
v-model:value="q"
|
||||
allow-clear
|
||||
@@ -95,9 +174,12 @@ onMounted(loadTenants)
|
||||
>
|
||||
<template #prefix><SearchOutlined /></template>
|
||||
</a-input>
|
||||
<a-button v-if="mainTab === 'khanegi'" type="primary" @click="router.push({ name: 'plugin-catalog' })">
|
||||
قیمت فروشگاه
|
||||
</a-button>
|
||||
</div>
|
||||
|
||||
<div class="legend" role="group" aria-label="فیلتر وضعیت افزونه">
|
||||
<div v-if="mainTab !== 'requests'" class="legend" role="group" aria-label="فیلتر وضعیت افزونه">
|
||||
<button
|
||||
type="button"
|
||||
class="chip all"
|
||||
@@ -140,11 +222,55 @@ onMounted(loadTenants)
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<section class="sms-selector">
|
||||
<section v-if="mainTab === 'requests'" class="sms-selector">
|
||||
<div class="sms-side-head">درخواستهای خانگی</div>
|
||||
<div class="req-toolbar">
|
||||
<a-checkbox v-model:checked="autoCut">قطع خودکار بعد از انقضا (موقع تأیید)</a-checkbox>
|
||||
</div>
|
||||
<a-spin :spinning="loadingRequests">
|
||||
<div class="req-table-wrap">
|
||||
<table class="req-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>نسخه</th>
|
||||
<th>افزونه</th>
|
||||
<th>مدت</th>
|
||||
<th>مبلغ</th>
|
||||
<th>وضعیت</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="row in requests" :key="row.id">
|
||||
<td>
|
||||
<strong>{{ row.tenantName }}</strong>
|
||||
<small>{{ row.domain }}</small>
|
||||
</td>
|
||||
<td>{{ row.pluginTitle }}</td>
|
||||
<td>{{ row.durationDays }} روز</td>
|
||||
<td>{{ Number(row.finalPriceRials || 0).toLocaleString('fa-IR') }}</td>
|
||||
<td>{{ statusLabel[row.status] }}</td>
|
||||
<td>
|
||||
<template v-if="row.status === 'pending'">
|
||||
<a-button size="small" type="primary" :loading="actingId === row.id" @click="approve(row)">تأیید</a-button>
|
||||
<a-button size="small" danger :loading="actingId === row.id" @click="reject(row)">رد</a-button>
|
||||
</template>
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-if="!requests.length">
|
||||
<td colspan="6" class="empty-hint">درخواستی نیست</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</a-spin>
|
||||
</section>
|
||||
|
||||
<section v-else class="sms-selector">
|
||||
<div class="sms-side-head"><AppstoreAddOutlined /> نسخهها</div>
|
||||
<a-spin :spinning="loadingList">
|
||||
<div class="tenant-groups">
|
||||
<section class="tenant-group">
|
||||
<section v-if="mainTab === 'hamkar'" class="tenant-group">
|
||||
<div class="tenant-group-head">
|
||||
<h3>همکار</h3>
|
||||
<span>{{ groupedTenants.hamkar.length }} نسخه</span>
|
||||
@@ -166,7 +292,7 @@ onMounted(loadTenants)
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="tenant-group">
|
||||
<section v-if="mainTab === 'khanegi'" class="tenant-group">
|
||||
<div class="tenant-group-head">
|
||||
<h3>خانگی</h3>
|
||||
<span>{{ groupedTenants.khanegi.length }} نسخه</span>
|
||||
@@ -194,6 +320,33 @@ onMounted(loadTenants)
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.main-tabs {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
.main-tabs button {
|
||||
appearance: none;
|
||||
border: 1px solid #d7e0ea;
|
||||
background: #fff;
|
||||
border-radius: 999px;
|
||||
padding: 8px 16px;
|
||||
font: inherit;
|
||||
font-weight: 800;
|
||||
cursor: pointer;
|
||||
}
|
||||
.main-tabs button.active {
|
||||
background: #17324d;
|
||||
color: #fff;
|
||||
border-color: #17324d;
|
||||
}
|
||||
.req-toolbar { margin-bottom: 12px; }
|
||||
.req-table-wrap { overflow-x: auto; }
|
||||
.req-table { width: 100%; border-collapse: collapse; font-size: 0.88rem; }
|
||||
.req-table th, .req-table td { padding: 8px 6px; text-align: right; border-bottom: 1px solid #eef2f7; }
|
||||
.req-table small { display: block; color: #94a3b8; }
|
||||
.req-table .ant-btn + .ant-btn { margin-right: 6px; }
|
||||
.legend {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
|
||||
Reference in New Issue
Block a user