Build Nuxt 4 client cabinet with Apollo and GraphQL flows

This commit is contained in:
Ruslan Bakiev
2026-03-30 21:41:19 +07:00
parent 0220041129
commit 79d6138cca
36 changed files with 14418 additions and 1 deletions

8
app/app.vue Normal file
View File

@@ -0,0 +1,8 @@
<template>
<div>
<AppHeader />
<main class="container mx-auto p-4 md:p-6">
<NuxtPage />
</main>
</div>
</template>

12
app/assets/css/main.css Normal file
View File

@@ -0,0 +1,12 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
:root {
--brand-primary: #0f766e;
--brand-secondary: #1d4ed8;
}
body {
@apply bg-base-200 text-base-content;
}

View File

@@ -0,0 +1,19 @@
import type { Meta, StoryObj } from '@storybook/vue3-vite';
import OrderStatusBadge from './OrderStatusBadge.vue';
const meta: Meta<typeof OrderStatusBadge> = {
title: 'Orders/OrderStatusBadge',
component: OrderStatusBadge,
};
export default meta;
type Story = StoryObj<typeof OrderStatusBadge>;
export const InProgress: Story = {
args: { status: 'IN_PROGRESS' },
};
export const Completed: Story = {
args: { status: 'COMPLETED' },
};

View File

@@ -0,0 +1,16 @@
<script setup lang="ts">
const props = defineProps<{
status: string;
}>();
const className = computed(() => {
if (props.status === 'COMPLETED') return 'badge badge-success';
if (props.status === 'CLIENT_REJECTED' || props.status === 'MANAGER_REJECTED') return 'badge badge-error';
if (props.status === 'MANAGER_BLOCKED') return 'badge badge-warning';
return 'badge badge-info';
});
</script>
<template>
<span :class="className">{{ status }}</span>
</template>

View File

@@ -0,0 +1,13 @@
import type { Meta, StoryObj } from '@storybook/vue3-vite';
import AppHeader from './AppHeader.vue';
const meta: Meta<typeof AppHeader> = {
title: 'UI/AppHeader',
component: AppHeader,
};
export default meta;
type Story = StoryObj<typeof AppHeader>;
export const Default: Story = {};

View File

@@ -0,0 +1,15 @@
<template>
<header class="navbar bg-base-100 border-b border-base-300 sticky top-0 z-10">
<div class="navbar-start">
<NuxtLink to="/" class="btn btn-ghost text-xl">Fregat</NuxtLink>
</div>
<div class="navbar-center hidden md:flex">
<ul class="menu menu-horizontal px-1">
<li><NuxtLink to="/products">Товары</NuxtLink></li>
<li><NuxtLink to="/cart">Корзина</NuxtLink></li>
<li><NuxtLink to="/orders">Заказы</NuxtLink></li>
<li><NuxtLink to="/profile">Профиль</NuxtLink></li>
</ul>
</div>
</header>
</template>

View File

@@ -0,0 +1,713 @@
import gql from 'graphql-tag';
import * as VueApolloComposable from '@vue/apollo-composable';
import type * as VueCompositionApi from '@vue/composition-api';
export type Maybe<T> = T | null;
export type InputMaybe<T> = Maybe<T>;
export type Exact<T extends { [key: string]: unknown }> = { [K in keyof T]: T[K] };
export type MakeOptional<T, K extends keyof T> = Omit<T, K> & { [SubKey in K]?: Maybe<T[SubKey]> };
export type MakeMaybe<T, K extends keyof T> = Omit<T, K> & { [SubKey in K]: Maybe<T[SubKey]> };
export type MakeEmpty<T extends { [key: string]: unknown }, K extends keyof T> = { [_ in K]?: never };
export type Incremental<T> = T | { [P in keyof T]?: P extends ' $fragmentName' | '__typename' ? T[P] : never };
export type ReactiveFunction<TParam> = () => TParam;
/** All built-in and custom scalars, mapped to their actual values */
export type Scalars = {
ID: { input: string; output: string; }
String: { input: string; output: string; }
Boolean: { input: boolean; output: boolean; }
Int: { input: number; output: number; }
Float: { input: number; output: number; }
DateTime: { input: any; output: any; }
JSON: { input: any; output: any; }
};
export type AcceptInvitationInput = {
fullName: Scalars['String']['input'];
token: Scalars['String']['input'];
};
export type AddBonusTransactionInput = {
amount: Scalars['Float']['input'];
orderId?: InputMaybe<Scalars['ID']['input']>;
reason: Scalars['String']['input'];
userId: Scalars['ID']['input'];
};
export type BlockOrderInput = {
orderId: Scalars['ID']['input'];
reason: Scalars['String']['input'];
};
export type BonusTransaction = {
__typename?: 'BonusTransaction';
amount: Scalars['Float']['output'];
createdAt: Scalars['DateTime']['output'];
id: Scalars['ID']['output'];
orderId?: Maybe<Scalars['ID']['output']>;
reason: Scalars['String']['output'];
userId: Scalars['ID']['output'];
};
export type Company = {
__typename?: 'Company';
id: Scalars['ID']['output'];
inn?: Maybe<Scalars['String']['output']>;
name: Scalars['String']['output'];
};
export type ConnectMessengerInput = {
channelId: Scalars['String']['input'];
type: MessengerType;
};
export type CreateInvitationInput = {
companyName: Scalars['String']['input'];
email: Scalars['String']['input'];
expiresInDays?: InputMaybe<Scalars['Int']['input']>;
};
export type CreateReferralInput = {
refereeUserId: Scalars['ID']['input'];
};
export enum Decision {
Approve = 'APPROVE',
Reject = 'REJECT'
}
export type Invitation = {
__typename?: 'Invitation';
acceptedAt?: Maybe<Scalars['DateTime']['output']>;
acceptedById?: Maybe<Scalars['ID']['output']>;
companyName: Scalars['String']['output'];
createdAt: Scalars['DateTime']['output'];
email: Scalars['String']['output'];
expiresAt: Scalars['DateTime']['output'];
id: Scalars['ID']['output'];
managerId: Scalars['ID']['output'];
token: Scalars['String']['output'];
};
export type MessengerConnection = {
__typename?: 'MessengerConnection';
channelId: Scalars['String']['output'];
id: Scalars['ID']['output'];
isActive: Scalars['Boolean']['output'];
type: MessengerType;
userId: Scalars['ID']['output'];
};
export enum MessengerType {
Max = 'MAX',
Telegram = 'TELEGRAM'
}
export type Mutation = {
__typename?: 'Mutation';
acceptInvitation: User;
addBonusTransaction: BonusTransaction;
blockOrder: Order;
clientReviewOrder: Order;
completeOrder: Order;
connectMessenger: MessengerConnection;
createInvitation: Invitation;
createReferral: ReferralLink;
managerFinalizeOrder: Order;
managerSetOrderOffer: Order;
registerSelf: RegistrationRequest;
requestRewardWithdrawal: RewardWithdrawalRequest;
reviewRegistrationRequest: RegistrationRequest;
reviewRewardWithdrawal: RewardWithdrawalRequest;
startOrderWork: Order;
submitCalculationOrder: Order;
submitReadyOrder: Order;
};
export type MutationAcceptInvitationArgs = {
input: AcceptInvitationInput;
};
export type MutationAddBonusTransactionArgs = {
input: AddBonusTransactionInput;
};
export type MutationBlockOrderArgs = {
input: BlockOrderInput;
};
export type MutationClientReviewOrderArgs = {
decision: Decision;
orderId: Scalars['ID']['input'];
};
export type MutationCompleteOrderArgs = {
orderId: Scalars['ID']['input'];
};
export type MutationConnectMessengerArgs = {
input: ConnectMessengerInput;
};
export type MutationCreateInvitationArgs = {
input: CreateInvitationInput;
};
export type MutationCreateReferralArgs = {
input: CreateReferralInput;
};
export type MutationManagerFinalizeOrderArgs = {
decision: Decision;
orderId: Scalars['ID']['input'];
};
export type MutationManagerSetOrderOfferArgs = {
input: SetOrderOfferInput;
};
export type MutationRegisterSelfArgs = {
input: RegisterSelfInput;
};
export type MutationRequestRewardWithdrawalArgs = {
input: RequestRewardWithdrawalInput;
};
export type MutationReviewRegistrationRequestArgs = {
input: ReviewRegistrationRequestInput;
};
export type MutationReviewRewardWithdrawalArgs = {
input: ReviewRewardWithdrawalInput;
};
export type MutationStartOrderWorkArgs = {
orderId: Scalars['ID']['input'];
};
export type MutationSubmitCalculationOrderArgs = {
input: SubmitCalculationOrderInput;
};
export type MutationSubmitReadyOrderArgs = {
input: SubmitReadyOrderInput;
};
export type Order = {
__typename?: 'Order';
blockReason?: Maybe<Scalars['String']['output']>;
calculationPayload?: Maybe<Scalars['JSON']['output']>;
clientApproved?: Maybe<Scalars['Boolean']['output']>;
code: Scalars['String']['output'];
createdAt: Scalars['DateTime']['output'];
customerId: Scalars['ID']['output'];
deliveryFee?: Maybe<Scalars['Float']['output']>;
deliveryTerms?: Maybe<Scalars['String']['output']>;
history: Array<OrderStatusEvent>;
id: Scalars['ID']['output'];
items: Array<OrderItem>;
kind: OrderKind;
managerApproved?: Maybe<Scalars['Boolean']['output']>;
managerId?: Maybe<Scalars['ID']['output']>;
status: OrderStatus;
totalPrice?: Maybe<Scalars['Float']['output']>;
updatedAt: Scalars['DateTime']['output'];
};
export type OrderItem = {
__typename?: 'OrderItem';
id: Scalars['ID']['output'];
productId?: Maybe<Scalars['ID']['output']>;
productName: Scalars['String']['output'];
quantity: Scalars['Float']['output'];
};
export enum OrderKind {
Calculation = 'CALCULATION',
Ready = 'READY'
}
export enum OrderStatus {
ClientRejected = 'CLIENT_REJECTED',
Completed = 'COMPLETED',
Confirmed = 'CONFIRMED',
InProgress = 'IN_PROGRESS',
ManagerBlocked = 'MANAGER_BLOCKED',
ManagerProcessing = 'MANAGER_PROCESSING',
ManagerRejected = 'MANAGER_REJECTED',
New = 'NEW',
WaitingDoubleConfirm = 'WAITING_DOUBLE_CONFIRM'
}
export type OrderStatusEvent = {
__typename?: 'OrderStatusEvent';
actorUserId: Scalars['ID']['output'];
createdAt: Scalars['DateTime']['output'];
id: Scalars['ID']['output'];
note?: Maybe<Scalars['String']['output']>;
status: OrderStatus;
};
export type Product = {
__typename?: 'Product';
availableInWarehouses: Array<ProductWarehouseBalance>;
description?: Maybe<Scalars['String']['output']>;
id: Scalars['ID']['output'];
isActive: Scalars['Boolean']['output'];
isCustomizable: Scalars['Boolean']['output'];
name: Scalars['String']['output'];
sku: Scalars['String']['output'];
};
export type ProductWarehouseBalance = {
__typename?: 'ProductWarehouseBalance';
availableQty: Scalars['Float']['output'];
warehouse: Warehouse;
};
export type Query = {
__typename?: 'Query';
clientProducts: Array<Product>;
healthcheck: Scalars['String']['output'];
managerOrders: Array<Order>;
me?: Maybe<User>;
myCurrentOrders: Array<Order>;
myOrders: Array<Order>;
referralStats: ReferralStats;
registrationRequests: Array<RegistrationRequest>;
};
export type QueryManagerOrdersArgs = {
status?: InputMaybe<OrderStatus>;
};
export type QueryRegistrationRequestsArgs = {
status?: InputMaybe<RegistrationStatus>;
};
export type ReadyOrderItemInput = {
productId: Scalars['ID']['input'];
quantity: Scalars['Float']['input'];
};
export type ReferralLink = {
__typename?: 'ReferralLink';
createdAt: Scalars['DateTime']['output'];
id: Scalars['ID']['output'];
refereeId: Scalars['ID']['output'];
referrerId: Scalars['ID']['output'];
};
export type ReferralStats = {
__typename?: 'ReferralStats';
availableBalance: Scalars['Float']['output'];
pendingWithdrawals: Array<RewardWithdrawalRequest>;
referralsCount: Scalars['Int']['output'];
referrerId: Scalars['ID']['output'];
transactions: Array<BonusTransaction>;
};
export type RegisterSelfInput = {
companyName: Scalars['String']['input'];
contactName: Scalars['String']['input'];
email: Scalars['String']['input'];
inn?: InputMaybe<Scalars['String']['input']>;
};
export type RegistrationRequest = {
__typename?: 'RegistrationRequest';
companyName: Scalars['String']['output'];
contactName: Scalars['String']['output'];
createdAt: Scalars['DateTime']['output'];
email: Scalars['String']['output'];
id: Scalars['ID']['output'];
inn?: Maybe<Scalars['String']['output']>;
rejectionReason?: Maybe<Scalars['String']['output']>;
reviewedById?: Maybe<Scalars['ID']['output']>;
status: RegistrationStatus;
updatedAt: Scalars['DateTime']['output'];
};
export enum RegistrationStatus {
Approved = 'APPROVED',
Pending = 'PENDING',
Rejected = 'REJECTED'
}
export type RequestRewardWithdrawalInput = {
amount: Scalars['Float']['input'];
};
export type ReviewRegistrationRequestInput = {
decision: Decision;
rejectionReason?: InputMaybe<Scalars['String']['input']>;
requestId: Scalars['ID']['input'];
};
export type ReviewRewardWithdrawalInput = {
decision: Decision;
reviewComment?: InputMaybe<Scalars['String']['input']>;
withdrawalId: Scalars['ID']['input'];
};
export type RewardWithdrawalRequest = {
__typename?: 'RewardWithdrawalRequest';
amount: Scalars['Float']['output'];
createdAt: Scalars['DateTime']['output'];
id: Scalars['ID']['output'];
requesterId: Scalars['ID']['output'];
reviewComment?: Maybe<Scalars['String']['output']>;
reviewedById?: Maybe<Scalars['ID']['output']>;
status: WithdrawalStatus;
updatedAt: Scalars['DateTime']['output'];
};
export type SetOrderOfferInput = {
deliveryFee: Scalars['Float']['input'];
deliveryTerms: Scalars['String']['input'];
orderId: Scalars['ID']['input'];
totalPrice: Scalars['Float']['input'];
};
export type SubmitCalculationOrderInput = {
parameters: Scalars['JSON']['input'];
productName: Scalars['String']['input'];
quantity: Scalars['Float']['input'];
};
export type SubmitReadyOrderInput = {
items: Array<ReadyOrderItemInput>;
};
export type User = {
__typename?: 'User';
company?: Maybe<Company>;
email: Scalars['String']['output'];
fullName: Scalars['String']['output'];
id: Scalars['ID']['output'];
role: UserRole;
};
export enum UserRole {
Client = 'CLIENT',
Manager = 'MANAGER'
}
export type Warehouse = {
__typename?: 'Warehouse';
code: Scalars['String']['output'];
id: Scalars['ID']['output'];
name: Scalars['String']['output'];
};
export enum WithdrawalStatus {
Approved = 'APPROVED',
Pending = 'PENDING',
Rejected = 'REJECTED'
}
export type RegisterSelfMutationVariables = Exact<{
input: RegisterSelfInput;
}>;
export type RegisterSelfMutation = { __typename?: 'Mutation', registerSelf: { __typename?: 'RegistrationRequest', id: string, companyName: string, contactName: string, email: string, status: RegistrationStatus, createdAt: any } };
export type ClientProductsQueryVariables = Exact<{ [key: string]: never; }>;
export type ClientProductsQuery = { __typename?: 'Query', clientProducts: Array<{ __typename?: 'Product', id: string, sku: string, name: string, description?: string | null, isCustomizable: boolean, availableInWarehouses: Array<{ __typename?: 'ProductWarehouseBalance', availableQty: number, warehouse: { __typename?: 'Warehouse', id: string, code: string, name: string } }> }> };
export type MyCurrentOrdersQueryVariables = Exact<{ [key: string]: never; }>;
export type MyCurrentOrdersQuery = { __typename?: 'Query', myCurrentOrders: Array<{ __typename?: 'Order', id: string, code: string, kind: OrderKind, status: OrderStatus, createdAt: any, items: Array<{ __typename?: 'OrderItem', id: string, productName: string, quantity: number }> }> };
export type MyOrdersQueryVariables = Exact<{ [key: string]: never; }>;
export type MyOrdersQuery = { __typename?: 'Query', myOrders: Array<{ __typename?: 'Order', id: string, code: string, kind: OrderKind, status: OrderStatus, totalPrice?: number | null, deliveryTerms?: string | null, createdAt: any, items: Array<{ __typename?: 'OrderItem', id: string, productName: string, quantity: number }> }> };
export type SubmitCalculationOrderMutationVariables = Exact<{
input: SubmitCalculationOrderInput;
}>;
export type SubmitCalculationOrderMutation = { __typename?: 'Mutation', submitCalculationOrder: { __typename?: 'Order', id: string, code: string, status: OrderStatus, createdAt: any } };
export type SubmitReadyOrderMutationVariables = Exact<{
input: SubmitReadyOrderInput;
}>;
export type SubmitReadyOrderMutation = { __typename?: 'Mutation', submitReadyOrder: { __typename?: 'Order', id: string, code: string, status: OrderStatus, createdAt: any } };
export type ConnectMessengerMutationVariables = Exact<{
input: ConnectMessengerInput;
}>;
export type ConnectMessengerMutation = { __typename?: 'Mutation', connectMessenger: { __typename?: 'MessengerConnection', id: string, type: MessengerType, channelId: string, isActive: boolean } };
export const RegisterSelfDocument = gql`
mutation RegisterSelf($input: RegisterSelfInput!) {
registerSelf(input: $input) {
id
companyName
contactName
email
status
createdAt
}
}
`;
/**
* __useRegisterSelfMutation__
*
* To run a mutation, you first call `useRegisterSelfMutation` within a Vue component and pass it any options that fit your needs.
* When your component renders, `useRegisterSelfMutation` returns an object that includes:
* - A mutate function that you can call at any time to execute the mutation
* - Several other properties: https://v4.apollo.vuejs.org/api/use-mutation.html#return
*
* @param options that will be passed into the mutation, supported options are listed on: https://v4.apollo.vuejs.org/guide-composable/mutation.html#options;
*
* @example
* const { mutate, loading, error, onDone } = useRegisterSelfMutation({
* variables: {
* input: // value for 'input'
* },
* });
*/
export function useRegisterSelfMutation(options: VueApolloComposable.UseMutationOptions<RegisterSelfMutation, RegisterSelfMutationVariables> | ReactiveFunction<VueApolloComposable.UseMutationOptions<RegisterSelfMutation, RegisterSelfMutationVariables>> = {}) {
return VueApolloComposable.useMutation<RegisterSelfMutation, RegisterSelfMutationVariables>(RegisterSelfDocument, options);
}
export type RegisterSelfMutationCompositionFunctionResult = VueApolloComposable.UseMutationReturn<RegisterSelfMutation, RegisterSelfMutationVariables>;
export const ClientProductsDocument = gql`
query ClientProducts {
clientProducts {
id
sku
name
description
isCustomizable
availableInWarehouses {
availableQty
warehouse {
id
code
name
}
}
}
}
`;
/**
* __useClientProductsQuery__
*
* To run a query within a Vue component, call `useClientProductsQuery` and pass it any options that fit your needs.
* When your component renders, `useClientProductsQuery` returns an object from Apollo Client that contains result, loading and error properties
* you can use to render your UI.
*
* @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 } = useClientProductsQuery();
*/
export function useClientProductsQuery(options: VueApolloComposable.UseQueryOptions<ClientProductsQuery, ClientProductsQueryVariables> | VueCompositionApi.Ref<VueApolloComposable.UseQueryOptions<ClientProductsQuery, ClientProductsQueryVariables>> | ReactiveFunction<VueApolloComposable.UseQueryOptions<ClientProductsQuery, ClientProductsQueryVariables>> = {}) {
return VueApolloComposable.useQuery<ClientProductsQuery, ClientProductsQueryVariables>(ClientProductsDocument, {}, options);
}
export function useClientProductsLazyQuery(options: VueApolloComposable.UseQueryOptions<ClientProductsQuery, ClientProductsQueryVariables> | VueCompositionApi.Ref<VueApolloComposable.UseQueryOptions<ClientProductsQuery, ClientProductsQueryVariables>> | ReactiveFunction<VueApolloComposable.UseQueryOptions<ClientProductsQuery, ClientProductsQueryVariables>> = {}) {
return VueApolloComposable.useLazyQuery<ClientProductsQuery, ClientProductsQueryVariables>(ClientProductsDocument, {}, options);
}
export type ClientProductsQueryCompositionFunctionResult = VueApolloComposable.UseQueryReturn<ClientProductsQuery, ClientProductsQueryVariables>;
export const MyCurrentOrdersDocument = gql`
query MyCurrentOrders {
myCurrentOrders {
id
code
kind
status
createdAt
items {
id
productName
quantity
}
}
}
`;
/**
* __useMyCurrentOrdersQuery__
*
* To run a query within a Vue component, call `useMyCurrentOrdersQuery` and pass it any options that fit your needs.
* When your component renders, `useMyCurrentOrdersQuery` returns an object from Apollo Client that contains result, loading and error properties
* you can use to render your UI.
*
* @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 } = useMyCurrentOrdersQuery();
*/
export function useMyCurrentOrdersQuery(options: VueApolloComposable.UseQueryOptions<MyCurrentOrdersQuery, MyCurrentOrdersQueryVariables> | VueCompositionApi.Ref<VueApolloComposable.UseQueryOptions<MyCurrentOrdersQuery, MyCurrentOrdersQueryVariables>> | ReactiveFunction<VueApolloComposable.UseQueryOptions<MyCurrentOrdersQuery, MyCurrentOrdersQueryVariables>> = {}) {
return VueApolloComposable.useQuery<MyCurrentOrdersQuery, MyCurrentOrdersQueryVariables>(MyCurrentOrdersDocument, {}, options);
}
export function useMyCurrentOrdersLazyQuery(options: VueApolloComposable.UseQueryOptions<MyCurrentOrdersQuery, MyCurrentOrdersQueryVariables> | VueCompositionApi.Ref<VueApolloComposable.UseQueryOptions<MyCurrentOrdersQuery, MyCurrentOrdersQueryVariables>> | ReactiveFunction<VueApolloComposable.UseQueryOptions<MyCurrentOrdersQuery, MyCurrentOrdersQueryVariables>> = {}) {
return VueApolloComposable.useLazyQuery<MyCurrentOrdersQuery, MyCurrentOrdersQueryVariables>(MyCurrentOrdersDocument, {}, options);
}
export type MyCurrentOrdersQueryCompositionFunctionResult = VueApolloComposable.UseQueryReturn<MyCurrentOrdersQuery, MyCurrentOrdersQueryVariables>;
export const MyOrdersDocument = gql`
query MyOrders {
myOrders {
id
code
kind
status
totalPrice
deliveryTerms
createdAt
items {
id
productName
quantity
}
}
}
`;
/**
* __useMyOrdersQuery__
*
* To run a query within a Vue component, call `useMyOrdersQuery` and pass it any options that fit your needs.
* When your component renders, `useMyOrdersQuery` returns an object from Apollo Client that contains result, loading and error properties
* you can use to render your UI.
*
* @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 } = useMyOrdersQuery();
*/
export function useMyOrdersQuery(options: VueApolloComposable.UseQueryOptions<MyOrdersQuery, MyOrdersQueryVariables> | VueCompositionApi.Ref<VueApolloComposable.UseQueryOptions<MyOrdersQuery, MyOrdersQueryVariables>> | ReactiveFunction<VueApolloComposable.UseQueryOptions<MyOrdersQuery, MyOrdersQueryVariables>> = {}) {
return VueApolloComposable.useQuery<MyOrdersQuery, MyOrdersQueryVariables>(MyOrdersDocument, {}, options);
}
export function useMyOrdersLazyQuery(options: VueApolloComposable.UseQueryOptions<MyOrdersQuery, MyOrdersQueryVariables> | VueCompositionApi.Ref<VueApolloComposable.UseQueryOptions<MyOrdersQuery, MyOrdersQueryVariables>> | ReactiveFunction<VueApolloComposable.UseQueryOptions<MyOrdersQuery, MyOrdersQueryVariables>> = {}) {
return VueApolloComposable.useLazyQuery<MyOrdersQuery, MyOrdersQueryVariables>(MyOrdersDocument, {}, options);
}
export type MyOrdersQueryCompositionFunctionResult = VueApolloComposable.UseQueryReturn<MyOrdersQuery, MyOrdersQueryVariables>;
export const SubmitCalculationOrderDocument = gql`
mutation SubmitCalculationOrder($input: SubmitCalculationOrderInput!) {
submitCalculationOrder(input: $input) {
id
code
status
createdAt
}
}
`;
/**
* __useSubmitCalculationOrderMutation__
*
* To run a mutation, you first call `useSubmitCalculationOrderMutation` within a Vue component and pass it any options that fit your needs.
* When your component renders, `useSubmitCalculationOrderMutation` returns an object that includes:
* - A mutate function that you can call at any time to execute the mutation
* - Several other properties: https://v4.apollo.vuejs.org/api/use-mutation.html#return
*
* @param options that will be passed into the mutation, supported options are listed on: https://v4.apollo.vuejs.org/guide-composable/mutation.html#options;
*
* @example
* const { mutate, loading, error, onDone } = useSubmitCalculationOrderMutation({
* variables: {
* input: // value for 'input'
* },
* });
*/
export function useSubmitCalculationOrderMutation(options: VueApolloComposable.UseMutationOptions<SubmitCalculationOrderMutation, SubmitCalculationOrderMutationVariables> | ReactiveFunction<VueApolloComposable.UseMutationOptions<SubmitCalculationOrderMutation, SubmitCalculationOrderMutationVariables>> = {}) {
return VueApolloComposable.useMutation<SubmitCalculationOrderMutation, SubmitCalculationOrderMutationVariables>(SubmitCalculationOrderDocument, options);
}
export type SubmitCalculationOrderMutationCompositionFunctionResult = VueApolloComposable.UseMutationReturn<SubmitCalculationOrderMutation, SubmitCalculationOrderMutationVariables>;
export const SubmitReadyOrderDocument = gql`
mutation SubmitReadyOrder($input: SubmitReadyOrderInput!) {
submitReadyOrder(input: $input) {
id
code
status
createdAt
}
}
`;
/**
* __useSubmitReadyOrderMutation__
*
* To run a mutation, you first call `useSubmitReadyOrderMutation` within a Vue component and pass it any options that fit your needs.
* When your component renders, `useSubmitReadyOrderMutation` returns an object that includes:
* - A mutate function that you can call at any time to execute the mutation
* - Several other properties: https://v4.apollo.vuejs.org/api/use-mutation.html#return
*
* @param options that will be passed into the mutation, supported options are listed on: https://v4.apollo.vuejs.org/guide-composable/mutation.html#options;
*
* @example
* const { mutate, loading, error, onDone } = useSubmitReadyOrderMutation({
* variables: {
* input: // value for 'input'
* },
* });
*/
export function useSubmitReadyOrderMutation(options: VueApolloComposable.UseMutationOptions<SubmitReadyOrderMutation, SubmitReadyOrderMutationVariables> | ReactiveFunction<VueApolloComposable.UseMutationOptions<SubmitReadyOrderMutation, SubmitReadyOrderMutationVariables>> = {}) {
return VueApolloComposable.useMutation<SubmitReadyOrderMutation, SubmitReadyOrderMutationVariables>(SubmitReadyOrderDocument, options);
}
export type SubmitReadyOrderMutationCompositionFunctionResult = VueApolloComposable.UseMutationReturn<SubmitReadyOrderMutation, SubmitReadyOrderMutationVariables>;
export const ConnectMessengerDocument = gql`
mutation ConnectMessenger($input: ConnectMessengerInput!) {
connectMessenger(input: $input) {
id
type
channelId
isActive
}
}
`;
/**
* __useConnectMessengerMutation__
*
* To run a mutation, you first call `useConnectMessengerMutation` within a Vue component and pass it any options that fit your needs.
* When your component renders, `useConnectMessengerMutation` returns an object that includes:
* - A mutate function that you can call at any time to execute the mutation
* - Several other properties: https://v4.apollo.vuejs.org/api/use-mutation.html#return
*
* @param options that will be passed into the mutation, supported options are listed on: https://v4.apollo.vuejs.org/guide-composable/mutation.html#options;
*
* @example
* const { mutate, loading, error, onDone } = useConnectMessengerMutation({
* variables: {
* input: // value for 'input'
* },
* });
*/
export function useConnectMessengerMutation(options: VueApolloComposable.UseMutationOptions<ConnectMessengerMutation, ConnectMessengerMutationVariables> | ReactiveFunction<VueApolloComposable.UseMutationOptions<ConnectMessengerMutation, ConnectMessengerMutationVariables>> = {}) {
return VueApolloComposable.useMutation<ConnectMessengerMutation, ConnectMessengerMutationVariables>(ConnectMessengerDocument, options);
}
export type ConnectMessengerMutationCompositionFunctionResult = VueApolloComposable.UseMutationReturn<ConnectMessengerMutation, ConnectMessengerMutationVariables>;

View File

@@ -0,0 +1,5 @@
import type { ApolloClient, NormalizedCacheObject } from '@apollo/client/core';
export function useGqlClient(): ApolloClient<NormalizedCacheObject> {
return useNuxtApp().$apollo as ApolloClient<NormalizedCacheObject>;
}

73
app/pages/cart.vue Normal file
View 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>

12
app/pages/index.vue Normal file
View File

@@ -0,0 +1,12 @@
<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-4">
<NuxtLink to="/products" class="card bg-base-100 border border-base-300 shadow-sm"><div class="card-body"><h2 class="card-title">Товары</h2></div></NuxtLink>
<NuxtLink to="/cart" 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="/profile" 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>

46
app/pages/orders.vue Normal file
View File

@@ -0,0 +1,46 @@
<script setup lang="ts">
import { useQuery } from '@vue/apollo-composable';
import OrderStatusBadge from '~/components/orders/OrderStatusBadge.vue';
import { MyCurrentOrdersDocument, MyOrdersDocument } from '~/composables/graphql/generated';
const currentOrders = useQuery(MyCurrentOrdersDocument);
const allOrders = useQuery(MyOrdersDocument);
</script>
<template>
<section class="space-y-8">
<h1 class="text-2xl font-bold">Мои заказы</h1>
<div class="space-y-3">
<h2 class="text-xl font-semibold">Текущие</h2>
<div v-if="currentOrders.loading.value" class="alert">Загрузка...</div>
<div v-else class="space-y-3">
<article v-for="order in currentOrders.result.value?.myCurrentOrders ?? []" :key="order.id" class="card bg-base-100 border border-base-300">
<div class="card-body gap-2">
<div class="flex items-center justify-between">
<h3 class="font-semibold">{{ order.code }}</h3>
<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>
</article>
</div>
</div>
<div class="space-y-3">
<h2 class="text-xl font-semibold">Все</h2>
<article v-for="order in allOrders.result.value?.myOrders ?? []" :key="order.id" class="card bg-base-100 border border-base-300">
<div class="card-body gap-2">
<div class="flex items-center justify-between">
<h3 class="font-semibold">{{ order.code }}</h3>
<OrderStatusBadge :status="order.status" />
</div>
<p class="text-sm">Условия доставки: {{ order.deliveryTerms || 'ожидает обработки менеджером' }}</p>
<p class="text-sm">Итого: {{ order.totalPrice ?? 'после обработки менеджером' }}</p>
</div>
</article>
</div>
</section>
</template>

26
app/pages/products.vue Normal file
View 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
View 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>

23
app/plugins/apollo.ts Normal file
View File

@@ -0,0 +1,23 @@
import { ApolloClient, HttpLink, InMemoryCache } from '@apollo/client/core';
import { provideApolloClient } from '@vue/apollo-composable';
export default defineNuxtPlugin(() => {
const config = useRuntimeConfig();
const client = new ApolloClient({
link: new HttpLink({
uri: config.public.graphqlEndpoint,
fetch,
}),
cache: new InMemoryCache(),
connectToDevTools: import.meta.dev,
});
provideApolloClient(client);
return {
provide: {
apollo: client,
},
};
});

View File

@@ -0,0 +1,18 @@
import * as Sentry from '@sentry/vue';
export default defineNuxtPlugin((nuxtApp) => {
const config = useRuntimeConfig();
const dsn = config.public.sentryDsn;
if (!dsn) {
return;
}
Sentry.init({
app: nuxtApp.vueApp,
dsn,
environment: config.public.sentryEnvironment,
release: config.public.sentryRelease,
tracesSampleRate: 0.1,
});
});