Files
CRM_Montzar/apps/web/src/views/adm/AdmBillingSettingsView.vue
T
MahdiandCursor 25a24772fc feat(access): keep a single owner per organization
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>
2026-11-10 14:59:32 +03:30

185 lines
5.0 KiB
Vue
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<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>