2026-11-02 19:13:57 +03:30
|
|
|
|
<script setup lang="ts">
|
|
|
|
|
|
import { onMounted, ref } from 'vue';
|
|
|
|
|
|
import axios from 'axios';
|
|
|
|
|
|
import type {
|
|
|
|
|
|
ApplyBillingPlanRequest,
|
|
|
|
|
|
BillingPlanCode,
|
|
|
|
|
|
PlatformOrganizationDto,
|
|
|
|
|
|
PlatformOrgMemberDto,
|
2026-11-10 14:08:14 +03:30
|
|
|
|
ProductPlanCode,
|
2026-11-02 19:13:57 +03:30
|
|
|
|
} from '@mento/shared';
|
2026-11-10 14:08:14 +03:30
|
|
|
|
import { BILLING_PLAN_LABELS, PRODUCT_PLAN_LABELS } from '@mento/shared';
|
2026-11-02 19:13:57 +03:30
|
|
|
|
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;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-11-10 14:08:14 +03:30
|
|
|
|
async function applyProduct(userId: string, planCode: ProductPlanCode) {
|
|
|
|
|
|
busyId.value = `product-${userId}-${planCode}`;
|
|
|
|
|
|
error.value = '';
|
|
|
|
|
|
success.value = '';
|
|
|
|
|
|
try {
|
|
|
|
|
|
await platformApi.post(`/platform/organizations/${orgId.value}/product-plan`, { planCode });
|
|
|
|
|
|
await loadOwners();
|
|
|
|
|
|
success.value = `سطح «${PRODUCT_PLAN_LABELS[planCode]}» روی دسترسیها اعمال شد`;
|
|
|
|
|
|
} catch (e) {
|
|
|
|
|
|
error.value = apiError(e, 'اعمال سطح امکانات ناموفق بود');
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
busyId.value = '';
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-11-02 19:13:57 +03:30
|
|
|
|
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"
|
2026-11-10 14:08:14 +03:30
|
|
|
|
:key="`${owner.userId}-${owner.billing?.planCode ?? ''}-${owner.billing?.expiresAt ?? ''}-${owner.billing?.maxUsers ?? ''}-${owner.billing?.maxOrganizations ?? ''}`"
|
2026-11-02 19:13:57 +03:30
|
|
|
|
:user-id="owner.userId"
|
|
|
|
|
|
:billing="owner.billing"
|
|
|
|
|
|
:busy-id="busyId"
|
|
|
|
|
|
@apply="applyPlan(owner.userId, $event)"
|
2026-11-10 14:08:14 +03:30
|
|
|
|
@product="applyProduct(owner.userId, $event)"
|
2026-11-02 19:13:57 +03:30
|
|
|
|
/>
|
|
|
|
|
|
</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>
|