feat(billing): cap active users on an owner subscription
Super admin can set a seat limit of 3, 10, 20, or a negotiated number so an owner cannot add users past that cap. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -1,10 +1,11 @@
|
||||
<script setup lang="ts">
|
||||
import { onMounted, ref } from 'vue';
|
||||
import axios from 'axios';
|
||||
import type { BillingAccountDto, BillingPlanCode, StorageUsageDto } from '@mento/shared';
|
||||
import type { BillingAccountDto, BillingPlanCode, OwnerSeatUsageDto, StorageUsageDto } from '@mento/shared';
|
||||
import {
|
||||
BILLING_PLAN_CODES,
|
||||
BILLING_PLAN_LABELS,
|
||||
BILLING_USER_CAPS,
|
||||
isPermanentBillingPlan,
|
||||
} from '@mento/shared';
|
||||
import { toFaDigits } from '@/lib/jalali';
|
||||
@@ -21,6 +22,10 @@ const emit = defineEmits<{
|
||||
}>();
|
||||
|
||||
const usage = ref<StorageUsageDto | null>(null);
|
||||
const seats = ref<OwnerSeatUsageDto | null>(null);
|
||||
const customUsers = ref('');
|
||||
const seatBusy = ref(false);
|
||||
const seatError = ref('');
|
||||
const quotaGb = ref('');
|
||||
const storageBusy = ref(false);
|
||||
const storageError = ref('');
|
||||
@@ -100,8 +105,54 @@ function saveTypedQuota() {
|
||||
void saveQuota(value);
|
||||
}
|
||||
|
||||
async function loadSeats() {
|
||||
seatError.value = '';
|
||||
try {
|
||||
const { data } = await platformApi.get<OwnerSeatUsageDto>(`/platform/users/${props.userId}/seats`);
|
||||
seats.value = data;
|
||||
customUsers.value =
|
||||
data.maxUsers != null && !(BILLING_USER_CAPS as readonly number[]).includes(data.maxUsers)
|
||||
? String(data.maxUsers)
|
||||
: '';
|
||||
} catch (err) {
|
||||
seatError.value = errorText(err, 'تعداد کاربر خوانده نشد');
|
||||
}
|
||||
}
|
||||
|
||||
async function saveUserCap(maxUsers: number) {
|
||||
seatBusy.value = true;
|
||||
seatError.value = '';
|
||||
try {
|
||||
const { data } = await platformApi.patch<OwnerSeatUsageDto>(
|
||||
`/platform/users/${props.userId}/billing/users`,
|
||||
{ maxUsers },
|
||||
);
|
||||
seats.value = data;
|
||||
customUsers.value =
|
||||
data.maxUsers != null && !(BILLING_USER_CAPS as readonly number[]).includes(data.maxUsers)
|
||||
? String(data.maxUsers)
|
||||
: '';
|
||||
} catch (err) {
|
||||
seatError.value = errorText(err, 'سقف کاربر ذخیره نشد');
|
||||
} finally {
|
||||
seatBusy.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
function saveCustomUsers() {
|
||||
const value = Number(customUsers.value.trim());
|
||||
if (!Number.isInteger(value) || value < 21) {
|
||||
seatError.value = 'برای بالای ۲۰ کاربر، عدد توافقی را از ۲۱ به بالا وارد کنید';
|
||||
return;
|
||||
}
|
||||
void saveUserCap(value);
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
if (props.billing) void loadStorage();
|
||||
if (props.billing) {
|
||||
void loadStorage();
|
||||
void loadSeats();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -127,6 +178,41 @@ onMounted(() => {
|
||||
{{ BILLING_PLAN_LABELS[code] }}
|
||||
</button>
|
||||
</div>
|
||||
<div v-if="billing" class="storage">
|
||||
<strong>تعداد کاربر</strong>
|
||||
<p v-if="seats" class="muted">
|
||||
فعال {{ toFaDigits(seats.usedUsers) }}
|
||||
<template v-if="seats.maxUsers == null"> · سقف تعیین نشده</template>
|
||||
<template v-else> از {{ toFaDigits(seats.maxUsers) }}</template>
|
||||
</p>
|
||||
<p v-else-if="!seatError" class="muted">در حال خواندن کاربران…</p>
|
||||
<p v-if="seats?.maxUsers != null && seats.usedUsers > seats.maxUsers" class="error">
|
||||
تعداد فعلی از سقف بیشتر است. تا کم شدن کاربران، افزودن کاربر جدید بسته است.
|
||||
</p>
|
||||
<div class="plan-row">
|
||||
<button
|
||||
v-for="cap in BILLING_USER_CAPS"
|
||||
:key="cap"
|
||||
class="btn"
|
||||
:class="{ 'btn-ghost': seats?.maxUsers !== cap }"
|
||||
type="button"
|
||||
:disabled="seatBusy"
|
||||
@click="saveUserCap(cap)"
|
||||
>
|
||||
تا {{ toFaDigits(cap) }} کاربر
|
||||
</button>
|
||||
</div>
|
||||
<label class="quota">
|
||||
بالای ۲۰، توافقی
|
||||
<input v-model="customUsers" type="number" min="21" step="1" inputmode="numeric" />
|
||||
</label>
|
||||
<div class="plan-row">
|
||||
<button class="btn" type="button" :disabled="seatBusy" @click="saveCustomUsers">
|
||||
ذخیره سقف توافقی
|
||||
</button>
|
||||
</div>
|
||||
<p v-if="seatError" class="error">{{ seatError }}</p>
|
||||
</div>
|
||||
<div v-if="billing" class="storage">
|
||||
<strong>حجم فایل</strong>
|
||||
<p v-if="usage" class="muted">
|
||||
|
||||
Reference in New Issue
Block a user