Build Nuxt 4 manager cabinet workflows
This commit is contained in:
73
app/pages/cart.vue
Normal file
73
app/pages/cart.vue
Normal file
@@ -0,0 +1,73 @@
|
||||
<script setup lang="ts">
|
||||
import { useMutation } from '@vue/apollo-composable';
|
||||
import { SubmitCalculationOrderDocument } from '~/composables/graphql/generated';
|
||||
|
||||
const productName = ref('');
|
||||
const quantity = ref(1);
|
||||
const width = ref(100);
|
||||
const thickness = ref(50);
|
||||
const color = ref('прозрачный');
|
||||
|
||||
const { mutate, loading, onDone, onError } = useMutation(SubmitCalculationOrderDocument);
|
||||
const success = ref('');
|
||||
const errorMessage = ref('');
|
||||
|
||||
onDone((result) => {
|
||||
success.value = `Заявка ${result.data?.submitCalculationOrder.code} отправлена`;
|
||||
errorMessage.value = '';
|
||||
});
|
||||
|
||||
onError((error) => {
|
||||
errorMessage.value = error.message;
|
||||
success.value = '';
|
||||
});
|
||||
|
||||
function submit() {
|
||||
mutate({
|
||||
input: {
|
||||
productName: productName.value,
|
||||
quantity: Number(quantity.value),
|
||||
parameters: {
|
||||
width: Number(width.value),
|
||||
thickness: Number(thickness.value),
|
||||
color: color.value,
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section class="space-y-4 max-w-2xl">
|
||||
<h1 class="text-2xl font-bold">Корзина / заявка на расчет</h1>
|
||||
<div class="card bg-base-100 border border-base-300">
|
||||
<div class="card-body space-y-3">
|
||||
<label class="form-control">
|
||||
<span class="label-text">Название позиции</span>
|
||||
<input v-model="productName" type="text" class="input input-bordered" />
|
||||
</label>
|
||||
<label class="form-control">
|
||||
<span class="label-text">Количество</span>
|
||||
<input v-model="quantity" type="number" min="1" class="input input-bordered" />
|
||||
</label>
|
||||
<label class="form-control">
|
||||
<span class="label-text">Ширина</span>
|
||||
<input v-model="width" type="number" min="1" class="input input-bordered" />
|
||||
</label>
|
||||
<label class="form-control">
|
||||
<span class="label-text">Толщина</span>
|
||||
<input v-model="thickness" type="number" min="1" class="input input-bordered" />
|
||||
</label>
|
||||
<label class="form-control">
|
||||
<span class="label-text">Цвет</span>
|
||||
<input v-model="color" type="text" class="input input-bordered" />
|
||||
</label>
|
||||
|
||||
<button class="btn btn-primary" :disabled="loading" @click="submit">Отправить менеджеру</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="success" class="alert alert-success">{{ success }}</div>
|
||||
<div v-if="errorMessage" class="alert alert-error">{{ errorMessage }}</div>
|
||||
</section>
|
||||
</template>
|
||||
13
app/pages/index.vue
Normal file
13
app/pages/index.vue
Normal file
@@ -0,0 +1,13 @@
|
||||
<template>
|
||||
<section class="space-y-6">
|
||||
<h1 class="text-3xl font-bold">Кабинет менеджера</h1>
|
||||
<p class="text-base-content/80">Рабочие разделы: заявки, согласование заказов, инвайты, рефералка и выводы.</p>
|
||||
<div class="grid gap-4 md:grid-cols-2 xl:grid-cols-5">
|
||||
<NuxtLink to="/requests" class="card bg-base-100 border border-base-300 shadow-sm"><div class="card-body"><h2 class="card-title">Заявки</h2></div></NuxtLink>
|
||||
<NuxtLink to="/orders" class="card bg-base-100 border border-base-300 shadow-sm"><div class="card-body"><h2 class="card-title">Заказы</h2></div></NuxtLink>
|
||||
<NuxtLink to="/invitations" class="card bg-base-100 border border-base-300 shadow-sm"><div class="card-body"><h2 class="card-title">Инвайты</h2></div></NuxtLink>
|
||||
<NuxtLink to="/referrals" class="card bg-base-100 border border-base-300 shadow-sm"><div class="card-body"><h2 class="card-title">Рефералка</h2></div></NuxtLink>
|
||||
<NuxtLink to="/withdrawals" class="card bg-base-100 border border-base-300 shadow-sm"><div class="card-body"><h2 class="card-title">Выводы</h2></div></NuxtLink>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
29
app/pages/invitations.vue
Normal file
29
app/pages/invitations.vue
Normal file
@@ -0,0 +1,29 @@
|
||||
<script setup lang="ts">
|
||||
import { useMutation } from '@vue/apollo-composable';
|
||||
import { CreateInvitationDocument } from '~/composables/graphql/generated';
|
||||
|
||||
const email = ref('');
|
||||
const companyName = ref('');
|
||||
const token = ref('');
|
||||
|
||||
const createInvitation = useMutation(CreateInvitationDocument);
|
||||
|
||||
async function submit() {
|
||||
const result = await createInvitation.mutate({ input: { email: email.value, companyName: companyName.value, expiresInDays: 7 } });
|
||||
token.value = result?.data?.createInvitation.token ?? '';
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section class="space-y-4 max-w-2xl">
|
||||
<h1 class="text-2xl font-bold">Инвайты клиентам</h1>
|
||||
<div class="card bg-base-100 border border-base-300">
|
||||
<div class="card-body space-y-3">
|
||||
<input v-model="email" type="email" class="input input-bordered" placeholder="Email клиента" />
|
||||
<input v-model="companyName" type="text" class="input input-bordered" placeholder="Компания" />
|
||||
<button class="btn btn-primary" @click="submit">Создать инвайт</button>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="token" class="alert alert-success">Токен: {{ token }}</div>
|
||||
</section>
|
||||
</template>
|
||||
66
app/pages/orders.vue
Normal file
66
app/pages/orders.vue
Normal file
@@ -0,0 +1,66 @@
|
||||
<script setup lang="ts">
|
||||
import { useMutation, useQuery } from '@vue/apollo-composable';
|
||||
import OrderStatusBadge from '~/components/orders/OrderStatusBadge.vue';
|
||||
import {
|
||||
BlockOrderDocument,
|
||||
ManagerFinalizeOrderDocument,
|
||||
ManagerOrdersDocument,
|
||||
ManagerSetOrderOfferDocument,
|
||||
} from '~/composables/graphql/generated';
|
||||
|
||||
const { result, refetch } = useQuery(ManagerOrdersDocument, { status: null });
|
||||
|
||||
const setOffer = useMutation(ManagerSetOrderOfferDocument);
|
||||
const finalize = useMutation(ManagerFinalizeOrderDocument);
|
||||
const block = useMutation(BlockOrderDocument);
|
||||
|
||||
async function publishOffer(orderId: string) {
|
||||
await setOffer.mutate({
|
||||
input: {
|
||||
orderId,
|
||||
deliveryTerms: 'Доставка 3-5 дней',
|
||||
deliveryFee: 1000,
|
||||
totalPrice: 12500,
|
||||
},
|
||||
});
|
||||
await refetch();
|
||||
}
|
||||
|
||||
async function approve(orderId: string) {
|
||||
await finalize.mutate({ orderId, decision: 'APPROVE' });
|
||||
await refetch();
|
||||
}
|
||||
|
||||
async function reject(orderId: string) {
|
||||
await finalize.mutate({ orderId, decision: 'REJECT' });
|
||||
await refetch();
|
||||
}
|
||||
|
||||
async function blockOrder(orderId: string) {
|
||||
await block.mutate({ input: { orderId, reason: 'Нужно уточнение параметров' } });
|
||||
await refetch();
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section class="space-y-4">
|
||||
<h1 class="text-2xl font-bold">Заявки и согласования заказов</h1>
|
||||
<article v-for="order in result?.managerOrders ?? []" :key="order.id" class="card bg-base-100 border border-base-300">
|
||||
<div class="card-body space-y-3">
|
||||
<div class="flex items-center justify-between">
|
||||
<h2 class="card-title">{{ order.code }}</h2>
|
||||
<OrderStatusBadge :status="order.status" />
|
||||
</div>
|
||||
<ul class="text-sm">
|
||||
<li v-for="item in order.items" :key="item.id">{{ item.productName }} × {{ item.quantity }}</li>
|
||||
</ul>
|
||||
<div class="flex gap-2 flex-wrap">
|
||||
<button class="btn btn-primary" @click="publishOffer(order.id)">Публиковать оффер</button>
|
||||
<button class="btn btn-success" @click="approve(order.id)">Approve</button>
|
||||
<button class="btn btn-error" @click="reject(order.id)">Reject</button>
|
||||
<button class="btn btn-warning" @click="blockOrder(order.id)">Блокировать</button>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
</section>
|
||||
</template>
|
||||
26
app/pages/products.vue
Normal file
26
app/pages/products.vue
Normal file
@@ -0,0 +1,26 @@
|
||||
<script setup lang="ts">
|
||||
import { useQuery } from '@vue/apollo-composable';
|
||||
import { ClientProductsDocument } from '~/composables/graphql/generated';
|
||||
|
||||
const { result, loading, error } = useQuery(ClientProductsDocument);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section class="space-y-4">
|
||||
<h1 class="text-2xl font-bold">Витрина товаров</h1>
|
||||
<div v-if="loading" class="alert">Загрузка...</div>
|
||||
<div v-else-if="error" class="alert alert-error">{{ error.message }}</div>
|
||||
<div v-else class="grid gap-4 lg:grid-cols-2">
|
||||
<article v-for="product in result?.clientProducts ?? []" :key="product.id" class="card bg-base-100 border border-base-300">
|
||||
<div class="card-body gap-2">
|
||||
<h2 class="card-title">{{ product.name }}</h2>
|
||||
<p class="text-sm opacity-80">{{ product.description }}</p>
|
||||
<p class="text-xs">SKU: {{ product.sku }}</p>
|
||||
<ul class="text-sm space-y-1">
|
||||
<li v-for="stock in product.availableInWarehouses" :key="stock.warehouse.id">{{ stock.warehouse.name }}: {{ stock.availableQty }}</li>
|
||||
</ul>
|
||||
</div>
|
||||
</article>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
71
app/pages/profile.vue
Normal file
71
app/pages/profile.vue
Normal file
@@ -0,0 +1,71 @@
|
||||
<script setup lang="ts">
|
||||
import { useMutation } from '@vue/apollo-composable';
|
||||
import { ConnectMessengerDocument, RegisterSelfDocument } from '~/composables/graphql/generated';
|
||||
|
||||
const companyName = ref('');
|
||||
const inn = ref('');
|
||||
const contactName = ref('');
|
||||
const email = ref('');
|
||||
const channelId = ref('');
|
||||
const channelType = ref<'TELEGRAM' | 'MAX'>('TELEGRAM');
|
||||
|
||||
const registerMutation = useMutation(RegisterSelfDocument);
|
||||
const messengerMutation = useMutation(ConnectMessengerDocument);
|
||||
|
||||
const message = ref('');
|
||||
|
||||
function register() {
|
||||
registerMutation.mutate({
|
||||
input: {
|
||||
companyName: companyName.value,
|
||||
inn: inn.value || null,
|
||||
contactName: contactName.value,
|
||||
email: email.value,
|
||||
},
|
||||
}).then(() => {
|
||||
message.value = 'Заявка на регистрацию отправлена менеджеру';
|
||||
});
|
||||
}
|
||||
|
||||
function connectMessenger() {
|
||||
messengerMutation.mutate({
|
||||
input: {
|
||||
type: channelType.value,
|
||||
channelId: channelId.value,
|
||||
},
|
||||
}).then(() => {
|
||||
message.value = 'Канал уведомлений подключен';
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section class="space-y-6 max-w-2xl">
|
||||
<h1 class="text-2xl font-bold">Профиль и каналы уведомлений</h1>
|
||||
|
||||
<div class="card bg-base-100 border border-base-300">
|
||||
<div class="card-body space-y-3">
|
||||
<h2 class="card-title">Самостоятельная регистрация</h2>
|
||||
<input v-model="companyName" type="text" placeholder="Компания" class="input input-bordered" />
|
||||
<input v-model="inn" type="text" placeholder="ИНН" class="input input-bordered" />
|
||||
<input v-model="contactName" type="text" placeholder="Контактное лицо" class="input input-bordered" />
|
||||
<input v-model="email" type="email" placeholder="Email" class="input input-bordered" />
|
||||
<button class="btn btn-primary" @click="register">Отправить заявку</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card bg-base-100 border border-base-300">
|
||||
<div class="card-body space-y-3">
|
||||
<h2 class="card-title">Подключение мессенджера</h2>
|
||||
<select v-model="channelType" class="select select-bordered">
|
||||
<option value="TELEGRAM">Telegram</option>
|
||||
<option value="MAX">Max</option>
|
||||
</select>
|
||||
<input v-model="channelId" type="text" placeholder="ID канала" class="input input-bordered" />
|
||||
<button class="btn btn-secondary" @click="connectMessenger">Подключить канал</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="message" class="alert alert-success">{{ message }}</div>
|
||||
</section>
|
||||
</template>
|
||||
50
app/pages/referrals.vue
Normal file
50
app/pages/referrals.vue
Normal file
@@ -0,0 +1,50 @@
|
||||
<script setup lang="ts">
|
||||
import { useMutation } from '@vue/apollo-composable';
|
||||
import { AddBonusTransactionDocument, CreateReferralDocument } from '~/composables/graphql/generated';
|
||||
|
||||
const refereeUserId = ref('');
|
||||
const referralCreated = ref('');
|
||||
const bonusUserId = ref('');
|
||||
const bonusAmount = ref(100);
|
||||
const bonusReason = ref('Реферальный бонус');
|
||||
const bonusResult = ref('');
|
||||
|
||||
const createReferral = useMutation(CreateReferralDocument);
|
||||
const addBonus = useMutation(AddBonusTransactionDocument);
|
||||
|
||||
async function submitReferral() {
|
||||
const result = await createReferral.mutate({ input: { refereeUserId: refereeUserId.value } });
|
||||
referralCreated.value = result?.data?.createReferral.id ?? '';
|
||||
}
|
||||
|
||||
async function submitBonus() {
|
||||
const result = await addBonus.mutate({ input: { userId: bonusUserId.value, amount: Number(bonusAmount.value), reason: bonusReason.value } });
|
||||
bonusResult.value = result?.data?.addBonusTransaction.id ?? '';
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section class="space-y-6 max-w-2xl">
|
||||
<h1 class="text-2xl font-bold">Реферальная программа</h1>
|
||||
|
||||
<div class="card bg-base-100 border border-base-300">
|
||||
<div class="card-body space-y-3">
|
||||
<h2 class="card-title">Создать реферальную связь</h2>
|
||||
<input v-model="refereeUserId" class="input input-bordered" placeholder="ID приглашенного пользователя" />
|
||||
<button class="btn btn-primary" @click="submitReferral">Создать</button>
|
||||
<p v-if="referralCreated" class="text-sm">Создано: {{ referralCreated }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card bg-base-100 border border-base-300">
|
||||
<div class="card-body space-y-3">
|
||||
<h2 class="card-title">Начислить бонус</h2>
|
||||
<input v-model="bonusUserId" class="input input-bordered" placeholder="ID пользователя" />
|
||||
<input v-model="bonusAmount" type="number" class="input input-bordered" placeholder="Сумма" />
|
||||
<input v-model="bonusReason" class="input input-bordered" placeholder="Причина" />
|
||||
<button class="btn btn-secondary" @click="submitBonus">Начислить</button>
|
||||
<p v-if="bonusResult" class="text-sm">Транзакция: {{ bonusResult }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
33
app/pages/requests.vue
Normal file
33
app/pages/requests.vue
Normal file
@@ -0,0 +1,33 @@
|
||||
<script setup lang="ts">
|
||||
import { useMutation, useQuery } from '@vue/apollo-composable';
|
||||
import { RegistrationRequestsDocument, ReviewRegistrationRequestDocument } from '~/composables/graphql/generated';
|
||||
|
||||
const { result, refetch } = useQuery(RegistrationRequestsDocument, { status: 'PENDING' });
|
||||
const review = useMutation(ReviewRegistrationRequestDocument);
|
||||
|
||||
async function approve(requestId: string) {
|
||||
await review.mutate({ input: { requestId, decision: 'APPROVE' } });
|
||||
await refetch();
|
||||
}
|
||||
|
||||
async function reject(requestId: string) {
|
||||
await review.mutate({ input: { requestId, decision: 'REJECT', rejectionReason: 'Не хватает данных' } });
|
||||
await refetch();
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section class="space-y-4">
|
||||
<h1 class="text-2xl font-bold">Заявки на регистрацию</h1>
|
||||
<article v-for="request in result?.registrationRequests ?? []" :key="request.id" class="card bg-base-100 border border-base-300">
|
||||
<div class="card-body">
|
||||
<h2 class="card-title">{{ request.companyName }}</h2>
|
||||
<p>{{ request.contactName }} • {{ request.email }}</p>
|
||||
<div class="flex gap-2">
|
||||
<button class="btn btn-success" @click="approve(request.id)">Approve</button>
|
||||
<button class="btn btn-error" @click="reject(request.id)">Reject</button>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
</section>
|
||||
</template>
|
||||
34
app/pages/withdrawals.vue
Normal file
34
app/pages/withdrawals.vue
Normal file
@@ -0,0 +1,34 @@
|
||||
<script setup lang="ts">
|
||||
import { useMutation } from '@vue/apollo-composable';
|
||||
import { ReviewRewardWithdrawalDocument } from '~/composables/graphql/generated';
|
||||
|
||||
const withdrawalId = ref('');
|
||||
const decision = ref<'APPROVE' | 'REJECT'>('APPROVE');
|
||||
const reviewComment = ref('');
|
||||
const resultStatus = ref('');
|
||||
|
||||
const review = useMutation(ReviewRewardWithdrawalDocument);
|
||||
|
||||
async function submit() {
|
||||
const result = await review.mutate({ input: { withdrawalId: withdrawalId.value, decision: decision.value, reviewComment: reviewComment.value } });
|
||||
resultStatus.value = result?.data?.reviewRewardWithdrawal.status ?? '';
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section class="space-y-4 max-w-2xl">
|
||||
<h1 class="text-2xl font-bold">Заявки на вывод</h1>
|
||||
<div class="card bg-base-100 border border-base-300">
|
||||
<div class="card-body space-y-3">
|
||||
<input v-model="withdrawalId" class="input input-bordered" placeholder="ID заявки" />
|
||||
<select v-model="decision" class="select select-bordered">
|
||||
<option value="APPROVE">Approve</option>
|
||||
<option value="REJECT">Reject</option>
|
||||
</select>
|
||||
<input v-model="reviewComment" class="input input-bordered" placeholder="Комментарий" />
|
||||
<button class="btn btn-primary" @click="submit">Подтвердить</button>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="resultStatus" class="alert alert-success">Статус: {{ resultStatus }}</div>
|
||||
</section>
|
||||
</template>
|
||||
Reference in New Issue
Block a user