Support super manager access

This commit is contained in:
Ruslan Bakiev
2026-04-04 09:41:36 +07:00
parent ecd92ef7e4
commit ad0bae79e8
8 changed files with 136 additions and 17 deletions

View File

@@ -1,6 +1,7 @@
<script setup lang="ts">
import { useQuery } from '@vue/apollo-composable';
import { MeDocument } from '~/composables/graphql/generated';
import { hasManagerAccess } from '~/utils/roles';
type NavItem = {
to: string;
@@ -17,7 +18,7 @@ const centerCapsule = computed<NavItem[]>(() => {
{ to: '/orders', label: 'Мои заказы' },
];
if (meQuery.result.value?.me?.role === 'MANAGER') {
if (hasManagerAccess(meQuery.result.value?.me?.role)) {
items.push(
{ to: '/clients', label: 'Пользователи' },
{ to: '/client-orders', label: 'Заказы' },

View File

@@ -198,6 +198,21 @@ export type ManagerUser = {
role: UserRole;
};
export type ManagerWithdrawalRequest = {
__typename?: 'ManagerWithdrawalRequest';
amount: Scalars['Float']['output'];
companyName?: Maybe<Scalars['String']['output']>;
createdAt: Scalars['DateTime']['output'];
id: Scalars['ID']['output'];
requesterEmail: Scalars['String']['output'];
requesterFullName: Scalars['String']['output'];
requesterId: Scalars['ID']['output'];
reviewComment?: Maybe<Scalars['String']['output']>;
reviewedById?: Maybe<Scalars['ID']['output']>;
status: WithdrawalStatus;
updatedAt: Scalars['DateTime']['output'];
};
export type MessengerConnection = {
__typename?: 'MessengerConnection';
avatarAvailable: Scalars['Boolean']['output'];
@@ -504,6 +519,7 @@ export type Query = {
managerNotificationHistory: Array<NotificationHistoryItem>;
managerOrders: Array<Order>;
managerUsers: Array<ManagerUser>;
managerWithdrawalRequests: Array<ManagerWithdrawalRequest>;
me?: Maybe<User>;
myCart: Cart;
myCounterpartyProfile?: Maybe<CounterpartyProfile>;
@@ -529,6 +545,11 @@ export type QueryManagerOrdersArgs = {
};
export type QueryManagerWithdrawalRequestsArgs = {
status?: InputMaybe<WithdrawalStatus>;
};
export type QueryMyNotificationHistoryArgs = {
channel: MessengerType;
limit?: InputMaybe<Scalars['Int']['input']>;
@@ -672,7 +693,8 @@ export type User = {
export enum UserRole {
Client = 'CLIENT',
Manager = 'MANAGER'
Manager = 'MANAGER',
SuperManager = 'SUPER_MANAGER'
}
export type VerifyLoginCodeInput = {
@@ -829,6 +851,13 @@ export type ManagerUsersQueryVariables = Exact<{ [key: string]: never; }>;
export type ManagerUsersQuery = { __typename?: 'Query', managerUsers: Array<{ __typename?: 'ManagerUser', id: string, email: string, fullName: string, role: UserRole, companyName?: string | null, inn?: string | null, createdAt: any, orderCount: number, lastOrderAt?: any | null }> };
export type ManagerWithdrawalRequestsQueryVariables = Exact<{
status?: InputMaybe<WithdrawalStatus>;
}>;
export type ManagerWithdrawalRequestsQuery = { __typename?: 'Query', managerWithdrawalRequests: Array<{ __typename?: 'ManagerWithdrawalRequest', id: string, requesterId: string, requesterEmail: string, requesterFullName: string, companyName?: string | null, amount: number, status: WithdrawalStatus, reviewedById?: string | null, reviewComment?: string | null, createdAt: any, updatedAt: any }> };
export type ReferralStatsQueryVariables = Exact<{ [key: string]: never; }>;
@@ -1738,6 +1767,46 @@ export function useManagerUsersLazyQuery(options: VueApolloComposable.UseQueryOp
return VueApolloComposable.useLazyQuery<ManagerUsersQuery, ManagerUsersQueryVariables>(ManagerUsersDocument, {}, options);
}
export type ManagerUsersQueryCompositionFunctionResult = VueApolloComposable.UseQueryReturn<ManagerUsersQuery, ManagerUsersQueryVariables>;
export const ManagerWithdrawalRequestsDocument = gql`
query ManagerWithdrawalRequests($status: WithdrawalStatus) {
managerWithdrawalRequests(status: $status) {
id
requesterId
requesterEmail
requesterFullName
companyName
amount
status
reviewedById
reviewComment
createdAt
updatedAt
}
}
`;
/**
* __useManagerWithdrawalRequestsQuery__
*
* To run a query within a Vue component, call `useManagerWithdrawalRequestsQuery` and pass it any options that fit your needs.
* When your component renders, `useManagerWithdrawalRequestsQuery` returns an object from Apollo Client that contains result, loading and error properties
* you can use to render your UI.
*
* @param variables that will be passed into the query
* @param options that will be passed into the query, supported options are listed on: https://v4.apollo.vuejs.org/guide-composable/query.html#options;
*
* @example
* const { result, loading, error } = useManagerWithdrawalRequestsQuery({
* status: // value for 'status'
* });
*/
export function useManagerWithdrawalRequestsQuery(variables: ManagerWithdrawalRequestsQueryVariables | VueCompositionApi.Ref<ManagerWithdrawalRequestsQueryVariables> | ReactiveFunction<ManagerWithdrawalRequestsQueryVariables> = {}, options: VueApolloComposable.UseQueryOptions<ManagerWithdrawalRequestsQuery, ManagerWithdrawalRequestsQueryVariables> | VueCompositionApi.Ref<VueApolloComposable.UseQueryOptions<ManagerWithdrawalRequestsQuery, ManagerWithdrawalRequestsQueryVariables>> | ReactiveFunction<VueApolloComposable.UseQueryOptions<ManagerWithdrawalRequestsQuery, ManagerWithdrawalRequestsQueryVariables>> = {}) {
return VueApolloComposable.useQuery<ManagerWithdrawalRequestsQuery, ManagerWithdrawalRequestsQueryVariables>(ManagerWithdrawalRequestsDocument, variables, options);
}
export function useManagerWithdrawalRequestsLazyQuery(variables: ManagerWithdrawalRequestsQueryVariables | VueCompositionApi.Ref<ManagerWithdrawalRequestsQueryVariables> | ReactiveFunction<ManagerWithdrawalRequestsQueryVariables> = {}, options: VueApolloComposable.UseQueryOptions<ManagerWithdrawalRequestsQuery, ManagerWithdrawalRequestsQueryVariables> | VueCompositionApi.Ref<VueApolloComposable.UseQueryOptions<ManagerWithdrawalRequestsQuery, ManagerWithdrawalRequestsQueryVariables>> | ReactiveFunction<VueApolloComposable.UseQueryOptions<ManagerWithdrawalRequestsQuery, ManagerWithdrawalRequestsQueryVariables>> = {}) {
return VueApolloComposable.useLazyQuery<ManagerWithdrawalRequestsQuery, ManagerWithdrawalRequestsQueryVariables>(ManagerWithdrawalRequestsDocument, variables, options);
}
export type ManagerWithdrawalRequestsQueryCompositionFunctionResult = VueApolloComposable.UseQueryReturn<ManagerWithdrawalRequestsQuery, ManagerWithdrawalRequestsQueryVariables>;
export const ReferralStatsDocument = gql`
query ReferralStats {
referralStats {

View File

@@ -1,4 +1,5 @@
import { MeDocument } from '~/composables/graphql/generated';
import { hasManagerAccess } from '~/utils/roles';
export default defineNuxtRouteMiddleware(async () => {
const { client } = useApolloClient('default');
@@ -7,7 +8,7 @@ export default defineNuxtRouteMiddleware(async () => {
fetchPolicy: 'cache-first',
});
if (response.data.me?.role !== 'MANAGER') {
if (!hasManagerAccess(response.data.me?.role)) {
return navigateTo('/');
}
});

View File

@@ -2,9 +2,9 @@
import { useQuery } from '@vue/apollo-composable';
import {
ManagerBonusBalancesDocument,
ReferralStatsDocument,
ManagerWithdrawalRequestsDocument,
type ManagerBonusBalancesQuery,
type ReferralStatsQuery,
type ManagerWithdrawalRequestsQuery,
} from '~/composables/graphql/generated';
definePageMeta({
@@ -12,14 +12,16 @@ definePageMeta({
});
type BalanceItem = ManagerBonusBalancesQuery['managerBonusBalances'][number];
type WithdrawalItem = ReferralStatsQuery['referralStats']['pendingWithdrawals'][number];
type WithdrawalItem = ManagerWithdrawalRequestsQuery['managerWithdrawalRequests'][number];
const route = useRoute();
const router = useRouter();
const search = ref('');
const balancesQuery = useQuery(ManagerBonusBalancesDocument);
const bonusQuery = useQuery(ReferralStatsDocument);
const withdrawalsQuery = useQuery(ManagerWithdrawalRequestsDocument, {
status: 'PENDING',
});
const activeTab = computed<'balances' | 'withdrawals'>(() => (
route.query.tab === 'withdrawals' ? 'withdrawals' : 'balances'
@@ -35,7 +37,7 @@ function setTab(tab: 'balances' | 'withdrawals') {
}
const balances = computed<BalanceItem[]>(() => balancesQuery.result.value?.managerBonusBalances ?? []);
const withdrawals = computed<WithdrawalItem[]>(() => bonusQuery.result.value?.referralStats.pendingWithdrawals ?? []);
const withdrawals = computed<WithdrawalItem[]>(() => withdrawalsQuery.result.value?.managerWithdrawalRequests ?? []);
const filteredBalances = computed(() => {
const query = search.value.trim().toLowerCase();
@@ -67,7 +69,9 @@ const filteredWithdrawals = computed(() => {
}
return [
item.requesterId,
item.requesterFullName,
item.requesterEmail,
item.companyName || '',
String(item.amount),
item.status,
item.reviewComment || '',
@@ -133,7 +137,7 @@ const filteredWithdrawals = computed(() => {
</template>
<template v-else>
<div v-if="bonusQuery.loading.value" class="manager-empty-state">
<div v-if="withdrawalsQuery.loading.value" class="manager-empty-state">
Загружаем заявки...
</div>
<div v-else-if="filteredWithdrawals.length === 0" class="manager-empty-state">
@@ -147,7 +151,9 @@ const filteredWithdrawals = computed(() => {
>
<div class="flex flex-col gap-3 md:flex-row md:items-start md:justify-between">
<div class="space-y-1">
<p class="text-sm font-semibold text-[#123824]">Пользователь: {{ withdrawal.requesterId }}</p>
<p class="text-sm font-semibold text-[#123824]">{{ withdrawal.requesterFullName }}</p>
<p class="text-sm text-[#355947]">{{ withdrawal.requesterEmail }}</p>
<p v-if="withdrawal.companyName" class="text-sm text-[#355947]">{{ withdrawal.companyName }}</p>
<p class="text-sm text-[#355947]">Сумма: {{ withdrawal.amount }}</p>
<p class="text-xs text-[#5c7b69]">{{ new Date(withdrawal.createdAt).toLocaleString() }}</p>
</div>

View File

@@ -1,8 +1,9 @@
<script setup lang="ts">
import { useMutation, useQuery } from '@vue/apollo-composable';
import {
ReferralStatsDocument,
ManagerWithdrawalRequestsDocument,
ReviewRewardWithdrawalDocument,
type ManagerWithdrawalRequestsQuery,
} from '~/composables/graphql/generated';
definePageMeta({
@@ -11,8 +12,11 @@ definePageMeta({
const route = useRoute();
const withdrawalId = computed(() => String(route.params.id || ''));
type WithdrawalItem = ManagerWithdrawalRequestsQuery['managerWithdrawalRequests'][number];
const bonusQuery = useQuery(ReferralStatsDocument);
const withdrawalsQuery = useQuery(ManagerWithdrawalRequestsDocument, {
status: null,
});
const reviewMutation = useMutation(ReviewRewardWithdrawalDocument);
const decision = ref<'APPROVE' | 'REJECT'>('APPROVE');
@@ -20,7 +24,7 @@ const reviewComment = ref('');
const reviewResult = ref('');
const currentWithdrawal = computed(() =>
(bonusQuery.result.value?.referralStats.pendingWithdrawals ?? []).find((item) => item.id === withdrawalId.value),
(withdrawalsQuery.result.value?.managerWithdrawalRequests ?? []).find((item: WithdrawalItem) => item.id === withdrawalId.value),
);
async function reviewWithdrawal() {
@@ -37,7 +41,7 @@ async function reviewWithdrawal() {
});
reviewResult.value = response?.data?.reviewRewardWithdrawal.status ?? '';
await bonusQuery.refetch();
await withdrawalsQuery.refetch({ status: null });
}
</script>
@@ -45,7 +49,7 @@ async function reviewWithdrawal() {
<section class="space-y-6 max-w-3xl">
<NuxtLink to="/bonus-system" class="text-sm font-semibold text-[#0d854a]"> Назад к бонусам</NuxtLink>
<div v-if="bonusQuery.loading.value" class="manager-empty-state">
<div v-if="withdrawalsQuery.loading.value" class="manager-empty-state">
Загружаем заявку на вывод...
</div>
@@ -57,7 +61,9 @@ async function reviewWithdrawal() {
<div class="manager-hero">
<p class="manager-eyebrow">Вывод</p>
<h1 class="manager-title">Проверка заявки на вывод</h1>
<p class="manager-copy">Пользователь: {{ currentWithdrawal.requesterId }} · Сумма: {{ currentWithdrawal.amount }}</p>
<p class="manager-copy">
{{ currentWithdrawal.requesterFullName }} · {{ currentWithdrawal.requesterEmail }} · Сумма: {{ currentWithdrawal.amount }}
</p>
</div>
<div class="surface-card rounded-3xl p-5 space-y-3">

5
app/utils/roles.ts Normal file
View File

@@ -0,0 +1,5 @@
import { UserRole } from '~/composables/graphql/generated';
export function hasManagerAccess(role?: UserRole | null) {
return role === UserRole.Manager || role === UserRole.SuperManager;
}