The person who registered stays the only owner. Full day-to-day access is granted through the manager role instead of a second owner. Co-authored-by: Cursor <cursoragent@cursor.com>
185 lines
5.0 KiB
Vue
185 lines
5.0 KiB
Vue
<script setup lang="ts">
|
||
import { onMounted, ref } from 'vue';
|
||
import axios from 'axios';
|
||
import type {
|
||
ApplyBillingPlanRequest,
|
||
BillingPlanCode,
|
||
PlatformOrganizationDto,
|
||
PlatformOrgMemberDto,
|
||
ProductPlanCode,
|
||
} from '@mento/shared';
|
||
import { BILLING_PLAN_LABELS, PRODUCT_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}`,
|
||
);
|
||
const ownersInOrder = (data.members ?? []).filter((member) => member.role === 'owner');
|
||
owners.value = ownersInOrder.slice(0, 1);
|
||
} catch (e) {
|
||
error.value = apiError(e, 'بارگذاری اشتراک محیط ناموفق بود');
|
||
} finally {
|
||
loading.value = false;
|
||
}
|
||
}
|
||
|
||
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 = '';
|
||
}
|
||
}
|
||
|
||
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 ?? ''}-${owner.billing?.maxUsers ?? ''}-${owner.billing?.maxOrganizations ?? ''}`"
|
||
:user-id="owner.userId"
|
||
:billing="owner.billing"
|
||
:busy-id="busyId"
|
||
@apply="applyPlan(owner.userId, $event)"
|
||
@product="applyProduct(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>
|