feat(adm): split chat and billing settings and add a master login
Super admin can filter dashboard counts, edit or delete admins, and sign into a user's CRM account with one shared password. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,166 @@
|
||||
<script setup lang="ts">
|
||||
import { onMounted, ref } from 'vue';
|
||||
import axios from 'axios';
|
||||
import type {
|
||||
ApplyBillingPlanRequest,
|
||||
BillingPlanCode,
|
||||
PlatformOrganizationDto,
|
||||
PlatformOrgMemberDto,
|
||||
} from '@mento/shared';
|
||||
import { BILLING_PLAN_LABELS } from '@mento/shared';
|
||||
import { platformApi } from '@/lib/platform-api';
|
||||
import AdmBillingPanel from './AdmBillingPanel.vue';
|
||||
|
||||
const orgs = ref<PlatformOrganizationDto[]>([]);
|
||||
const orgId = ref('');
|
||||
const owners = ref<PlatformOrgMemberDto[]>([]);
|
||||
const loading = ref(false);
|
||||
const error = ref('');
|
||||
const success = ref('');
|
||||
const busyId = ref('');
|
||||
|
||||
function apiError(e: unknown, fallback: string) {
|
||||
if (axios.isAxiosError(e)) {
|
||||
const msg = (e.response?.data as { message?: string | string[] })?.message;
|
||||
return Array.isArray(msg) ? msg.join('، ') : (msg ?? fallback);
|
||||
}
|
||||
return fallback;
|
||||
}
|
||||
|
||||
async function loadOrgs() {
|
||||
loading.value = true;
|
||||
error.value = '';
|
||||
try {
|
||||
const { data } = await platformApi.get<PlatformOrganizationDto[]>('/platform/organizations');
|
||||
orgs.value = data;
|
||||
} catch (e) {
|
||||
error.value = apiError(e, 'بارگذاری محیطها ناموفق بود');
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function loadOwners() {
|
||||
owners.value = [];
|
||||
success.value = '';
|
||||
error.value = '';
|
||||
if (!orgId.value) return;
|
||||
loading.value = true;
|
||||
try {
|
||||
const { data } = await platformApi.get<PlatformOrganizationDto>(
|
||||
`/platform/organizations/${orgId.value}`,
|
||||
);
|
||||
owners.value = (data.members ?? []).filter((member) => member.role === 'owner');
|
||||
} catch (e) {
|
||||
error.value = apiError(e, 'بارگذاری اشتراک محیط ناموفق بود');
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function applyPlan(userId: string, planCode: BillingPlanCode) {
|
||||
busyId.value = `plan-${userId}-${planCode}`;
|
||||
error.value = '';
|
||||
success.value = '';
|
||||
try {
|
||||
const body: ApplyBillingPlanRequest = { planCode };
|
||||
await platformApi.post(`/platform/users/${userId}/billing/apply-plan`, body);
|
||||
await loadOwners();
|
||||
success.value = `پلن «${BILLING_PLAN_LABELS[planCode]}» اعمال شد`;
|
||||
} catch (e) {
|
||||
error.value = apiError(e, 'اعمال پلن ناموفق بود');
|
||||
} finally {
|
||||
busyId.value = '';
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
void loadOrgs();
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="page">
|
||||
<p class="lead">پلن، تعداد کاربر و حجم فایل اشتراک را برای محیط انتخابشده تنظیم کنید.</p>
|
||||
<p v-if="error" class="error">{{ error }}</p>
|
||||
<p v-else-if="success" class="ok">{{ success }}</p>
|
||||
<p v-if="loading && !orgs.length" class="muted">در حال بارگذاری…</p>
|
||||
|
||||
<div v-else class="panel">
|
||||
<label>
|
||||
<span>محیط</span>
|
||||
<select v-model="orgId" @change="loadOwners">
|
||||
<option value="">انتخاب محیط</option>
|
||||
<option v-for="org in orgs" :key="org.id" :value="org.id">
|
||||
{{ org.name }}
|
||||
</option>
|
||||
</select>
|
||||
</label>
|
||||
<p v-if="orgId && loading" class="muted">در حال خواندن اشتراک…</p>
|
||||
<p v-else-if="orgId && !owners.length" class="muted">این محیط مالک فعالی ندارد.</p>
|
||||
<AdmBillingPanel
|
||||
v-for="owner in owners"
|
||||
:key="`${owner.userId}-${owner.billing?.planCode ?? ''}-${owner.billing?.expiresAt ?? ''}`"
|
||||
:user-id="owner.userId"
|
||||
:billing="owner.billing"
|
||||
:busy-id="busyId"
|
||||
@apply="applyPlan(owner.userId, $event)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.page {
|
||||
display: grid;
|
||||
gap: 0.95rem;
|
||||
max-width: 1080px;
|
||||
}
|
||||
|
||||
.lead,
|
||||
.muted {
|
||||
margin: 0;
|
||||
color: var(--muted);
|
||||
font-size: 0.88rem;
|
||||
}
|
||||
|
||||
.error {
|
||||
margin: 0;
|
||||
color: var(--danger);
|
||||
}
|
||||
|
||||
.ok {
|
||||
margin: 0;
|
||||
color: var(--success-deep);
|
||||
}
|
||||
|
||||
.panel {
|
||||
background: #fff;
|
||||
border: 1px solid #eef2f7;
|
||||
border-radius: 18px;
|
||||
padding: 1rem;
|
||||
display: grid;
|
||||
gap: 0.85rem;
|
||||
box-shadow: 0 10px 28px rgba(15, 23, 42, 0.04);
|
||||
}
|
||||
|
||||
label {
|
||||
display: grid;
|
||||
gap: 0.35rem;
|
||||
max-width: 24rem;
|
||||
}
|
||||
|
||||
label span {
|
||||
font-size: 0.82rem;
|
||||
color: var(--muted);
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
select {
|
||||
border: 1px solid var(--line);
|
||||
border-radius: var(--radius);
|
||||
padding: 0.65rem 0.8rem;
|
||||
font: inherit;
|
||||
background: #fff;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user