From f5a4eb577ba6f6417d4b7c4a6569ccc85e423b73 Mon Sep 17 00:00:00 2001 From: Mahdi Date: Mon, 21 Sep 2026 16:38:24 +0330 Subject: [PATCH] Editing permissions in the CRM Super Admin --- apps/api/src/platform/platform.controller.ts | 4 +- apps/api/src/platform/platform.service.ts | 54 +++--- apps/web/src/views/adm/AdmBillingPanel.vue | 64 +++++++ .../src/views/adm/AdmOrganizationsView.vue | 107 ++++++++---- apps/web/src/views/adm/AdmUsersView.vue | 159 +++++++++++++----- packages/shared/src/index.ts | 2 + 6 files changed, 292 insertions(+), 98 deletions(-) create mode 100644 apps/web/src/views/adm/AdmBillingPanel.vue diff --git a/apps/api/src/platform/platform.controller.ts b/apps/api/src/platform/platform.controller.ts index 87c39a5..ffe5d57 100644 --- a/apps/api/src/platform/platform.controller.ts +++ b/apps/api/src/platform/platform.controller.ts @@ -85,8 +85,8 @@ export class PlatformController { } @Get('users') - listUsers(@Query('q') q?: string) { - return this.platform.listUsers(q); + listUsers(@Query('q') q?: string, @Query('owners') owners?: string) { + return this.platform.listUsers(q, owners === '1' || owners === 'true'); } @Get('users/:id') diff --git a/apps/api/src/platform/platform.service.ts b/apps/api/src/platform/platform.service.ts index b034e12..0f106fc 100644 --- a/apps/api/src/platform/platform.service.ts +++ b/apps/api/src/platform/platform.service.ts @@ -139,6 +139,7 @@ export class PlatformService { email: true, mobile: true, isActive: true, + billingAccount: true, }, }, }, @@ -149,16 +150,24 @@ export class PlatformService { if (!row) throw new NotFoundException('محیط پیدا نشد'); return { ...this.orgToDto(row), - members: row.memberships.map((m) => ({ - membershipId: m.id, - userId: m.user.id, - fullName: m.user.fullName, - email: m.user.email, - mobile: m.user.mobile, - role: m.role, - isActive: m.isActive, - userIsActive: m.user.isActive, - })), + members: row.memberships.map((m) => { + const isBillingOwner = m.role === 'owner'; + return { + membershipId: m.id, + userId: m.user.id, + fullName: m.user.fullName, + email: m.user.email, + mobile: m.user.mobile, + role: m.role, + isActive: m.isActive, + userIsActive: m.user.isActive, + isBillingOwner, + billing: + isBillingOwner && m.user.billingAccount + ? this.billing.toDto(m.user.billingAccount) + : null, + }; + }), }; } @@ -330,18 +339,23 @@ export class PlatformService { return this.updateOrganization(id, { isActive }); } - async listUsers(query?: string): Promise { + async listUsers(query?: string, ownersOnly = false): Promise { const q = query?.trim(); const rows = await this.prisma.user.findMany({ - where: q - ? { - OR: [ - { fullName: { contains: q, mode: 'insensitive' } }, - { email: { contains: q, mode: 'insensitive' } }, - { mobile: { contains: q } }, - ], - } - : undefined, + where: { + ...(q + ? { + OR: [ + { fullName: { contains: q, mode: 'insensitive' } }, + { email: { contains: q, mode: 'insensitive' } }, + { mobile: { contains: q } }, + ], + } + : {}), + ...(ownersOnly + ? { memberships: { some: { role: 'owner', isActive: true } } } + : {}), + }, orderBy: { createdAt: 'desc' }, take: 300, include: { diff --git a/apps/web/src/views/adm/AdmBillingPanel.vue b/apps/web/src/views/adm/AdmBillingPanel.vue new file mode 100644 index 0000000..8caa5f5 --- /dev/null +++ b/apps/web/src/views/adm/AdmBillingPanel.vue @@ -0,0 +1,64 @@ + + + + + diff --git a/apps/web/src/views/adm/AdmOrganizationsView.vue b/apps/web/src/views/adm/AdmOrganizationsView.vue index e611e40..a907354 100644 --- a/apps/web/src/views/adm/AdmOrganizationsView.vue +++ b/apps/web/src/views/adm/AdmOrganizationsView.vue @@ -2,13 +2,17 @@ import { onMounted, reactive, ref } from 'vue'; import axios from 'axios'; import type { + ApplyBillingPlanRequest, + BillingPlanCode, CreatePlatformOrganizationRequest, OrgModuleKey, PlatformOrganizationDto, + PlatformUserDto, UpdatePlatformOrganizationRequest, UserRole, } from '@mento/shared'; import { + BILLING_PLAN_LABELS, ORG_MODULE_GROUPS, ORG_MODULE_LABELS, USER_ROLE_LABELS, @@ -18,6 +22,7 @@ import { import { platformApi } from '@/lib/platform-api'; import { BUSINESS_FIELDS, BUSINESS_FIELD_LABELS } from '@/lib/business-fields'; import { formatJalaliNumeric, toFaDigits } from '@/lib/jalali'; +import AdmBillingPanel from './AdmBillingPanel.vue'; const query = ref(''); const loading = ref(false); @@ -277,6 +282,30 @@ async function saveModules(orgId: string) { } } +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, + ); + if (openId.value) { + const { data } = await platformApi.get( + `/platform/organizations/${openId.value}`, + ); + detail.value = data; + } + success.value = `پلن «${BILLING_PLAN_LABELS[planCode]}» اعمال شد`; + } catch (e) { + error.value = apiError(e, 'اعمال پلن ناموفق بود'); + } finally { + busyId.value = ''; + } +} + onMounted(() => { void load(); }); @@ -443,37 +472,46 @@ onMounted(() => {

عضوی ثبت نشده.

  • -
    - {{ member.fullName }} - - {{ member.email }} - · حساب {{ member.userIsActive ? 'فعال' : 'غیرفعال' }} - -
    -
    - - +
    +
    + {{ member.fullName }} + + {{ member.email }} + · حساب {{ member.userIsActive ? 'فعال' : 'غیرفعال' }} + +
    +
    + + +
    +
@@ -707,6 +745,11 @@ label span { } .detail li { + display: grid; + gap: 0.75rem; +} + +.member-head { display: flex; justify-content: space-between; gap: 0.8rem; @@ -726,6 +769,10 @@ label span { align-items: center; } +:deep(.billing) { + margin-bottom: 0; +} + @media (max-width: 720px) { .grid, .edit-panel { diff --git a/apps/web/src/views/adm/AdmUsersView.vue b/apps/web/src/views/adm/AdmUsersView.vue index 9ddc44a..7c942af 100644 --- a/apps/web/src/views/adm/AdmUsersView.vue +++ b/apps/web/src/views/adm/AdmUsersView.vue @@ -8,15 +8,16 @@ import type { UserRole, } from '@mento/shared'; import { - BILLING_PLAN_CODES, BILLING_PLAN_LABELS, USER_ROLE_LABELS, USER_ROLES, } from '@mento/shared'; import { platformApi } from '@/lib/platform-api'; import { formatJalaliNumeric, toFaDigits } from '@/lib/jalali'; +import AdmBillingPanel from './AdmBillingPanel.vue'; const query = ref(''); +const ownersOnly = ref(false); const loading = ref(false); const error = ref(''); const success = ref(''); @@ -40,7 +41,10 @@ async function load() { error.value = ''; try { const { data } = await platformApi.get('/platform/users', { - params: query.value.trim() ? { q: query.value.trim() } : undefined, + params: { + ...(query.value.trim() ? { q: query.value.trim() } : {}), + ...(ownersOnly.value ? { owners: '1' } : {}), + }, }); items.value = data; } catch (e) { @@ -150,6 +154,12 @@ async function applyPlan(userId: string, planCode: BillingPlanCode) { } } +function setOwnersOnly(value: boolean) { + if (ownersOnly.value === value) return; + ownersOnly.value = value; + void load(); +} + onMounted(() => { void load(); }); @@ -159,22 +169,49 @@ onMounted(() => {

اشتراک مالک، نقش عضویت و ریست رمز.

- +
+
+ + +
+ +

{{ error }}

{{ success }}

در حال بارگذاری…

-

کاربری پیدا نشد.

+

+ {{ ownersOnly ? 'مالکی پیدا نشد.' : 'کاربری پیدا نشد.' }} +

-
-
+

این کاربر عضو زیرمجموعه است و اشتراک جدا ندارد؛ اشتراک روی مالک محیط است.

@@ -310,6 +332,52 @@ onMounted(() => { align-items: center; } +.head-actions { + display: flex; + gap: 0.55rem; + flex-wrap: wrap; + align-items: center; +} + +.seg { + display: flex; + padding: 0.25rem; + border-radius: 14px; + background: #fff; + border: 1px solid var(--line-light); + box-shadow: 0 6px 18px rgba(15, 23, 42, 0.04); +} + +.seg button { + border: 0; + background: transparent; + font: inherit; + font-size: 0.82rem; + font-weight: 700; + color: var(--muted); + padding: 0.5rem 0.75rem; + border-radius: 10px; + cursor: pointer; +} + +.seg button.on { + background: #eef2ff; + color: #312e81; +} + +.owner-tag { + display: inline-block; + margin-inline-start: 0.4rem; + padding: 0.08rem 0.42rem; + border-radius: 999px; + background: #eef2ff; + color: #312e81; + font-size: 0.72rem; + font-style: normal; + font-weight: 800; + vertical-align: middle; +} + .lead, .head p, .muted { @@ -471,22 +539,6 @@ onMounted(() => { font-size: 0.8rem; } -.billing { - margin-bottom: 0.85rem; - padding: 0.75rem; - border-radius: 12px; - background: #f8fafc; - border: 1px solid var(--line); - display: grid; - gap: 0.45rem; -} - -.plan-row { - display: flex; - flex-wrap: wrap; - gap: 0.4rem; -} - .billing-skip { margin: 0 0 0.75rem; } @@ -496,4 +548,19 @@ onMounted(() => { gap: 0.45rem; align-items: center; } + +@media (max-width: 720px) { + .search { + width: 100%; + } + + .search input { + flex: 1; + min-width: 0; + } + + .row { + grid-template-columns: 1fr; + } +} diff --git a/packages/shared/src/index.ts b/packages/shared/src/index.ts index 699d9d3..9f4b56d 100644 --- a/packages/shared/src/index.ts +++ b/packages/shared/src/index.ts @@ -2783,6 +2783,8 @@ export interface PlatformOrgMemberDto { role: UserRole; isActive: boolean; userIsActive: boolean; + isBillingOwner?: boolean; + billing?: BillingAccountDto | null; } export interface PlatformOrganizationDto {