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:
2026-11-02 19:13:57 +03:30
co-authored by Cursor
parent acb5d51f24
commit c273dd6c58
16 changed files with 1031 additions and 253 deletions
@@ -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>