feat(profile): add counterparty card with dadata search and cart gating
This commit is contained in:
@@ -74,6 +74,28 @@ export type ConnectMessengerInput = {
|
|||||||
type: MessengerType;
|
type: MessengerType;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type CounterpartyProfile = {
|
||||||
|
__typename?: 'CounterpartyProfile';
|
||||||
|
bankName: Scalars['String']['output'];
|
||||||
|
bik: Scalars['String']['output'];
|
||||||
|
checkingAccount: Scalars['String']['output'];
|
||||||
|
companyFullName: Scalars['String']['output'];
|
||||||
|
companyName: Scalars['String']['output'];
|
||||||
|
correspondentAccount: Scalars['String']['output'];
|
||||||
|
createdAt: Scalars['DateTime']['output'];
|
||||||
|
id: Scalars['ID']['output'];
|
||||||
|
inn: Scalars['String']['output'];
|
||||||
|
isComplete: Scalars['Boolean']['output'];
|
||||||
|
kpp?: Maybe<Scalars['String']['output']>;
|
||||||
|
legalAddress: Scalars['String']['output'];
|
||||||
|
ogrn?: Maybe<Scalars['String']['output']>;
|
||||||
|
signerBasis: Scalars['String']['output'];
|
||||||
|
signerFullName: Scalars['String']['output'];
|
||||||
|
signerPosition: Scalars['String']['output'];
|
||||||
|
updatedAt: Scalars['DateTime']['output'];
|
||||||
|
userId: Scalars['ID']['output'];
|
||||||
|
};
|
||||||
|
|
||||||
export type CreateInvitationInput = {
|
export type CreateInvitationInput = {
|
||||||
companyName: Scalars['String']['input'];
|
companyName: Scalars['String']['input'];
|
||||||
email: Scalars['String']['input'];
|
email: Scalars['String']['input'];
|
||||||
@@ -153,6 +175,7 @@ export type Mutation = {
|
|||||||
startOrderWork: Order;
|
startOrderWork: Order;
|
||||||
submitCalculationOrder: Order;
|
submitCalculationOrder: Order;
|
||||||
submitReadyOrder: Order;
|
submitReadyOrder: Order;
|
||||||
|
upsertMyCounterpartyProfile: CounterpartyProfile;
|
||||||
verifyLoginCode: AuthSession;
|
verifyLoginCode: AuthSession;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -261,6 +284,11 @@ export type MutationSubmitReadyOrderArgs = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
export type MutationUpsertMyCounterpartyProfileArgs = {
|
||||||
|
input: UpsertMyCounterpartyProfileInput;
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
export type MutationVerifyLoginCodeArgs = {
|
export type MutationVerifyLoginCodeArgs = {
|
||||||
input: VerifyLoginCodeInput;
|
input: VerifyLoginCodeInput;
|
||||||
};
|
};
|
||||||
@@ -354,6 +382,7 @@ export type Query = {
|
|||||||
managerNotificationHistory: Array<NotificationHistoryItem>;
|
managerNotificationHistory: Array<NotificationHistoryItem>;
|
||||||
managerOrders: Array<Order>;
|
managerOrders: Array<Order>;
|
||||||
me?: Maybe<User>;
|
me?: Maybe<User>;
|
||||||
|
myCounterpartyProfile?: Maybe<CounterpartyProfile>;
|
||||||
myCurrentOrders: Array<Order>;
|
myCurrentOrders: Array<Order>;
|
||||||
myMessengerConnections: Array<MessengerConnection>;
|
myMessengerConnections: Array<MessengerConnection>;
|
||||||
myNotificationHistory: Array<NotificationHistoryItem>;
|
myNotificationHistory: Array<NotificationHistoryItem>;
|
||||||
@@ -484,6 +513,22 @@ export type SubmitReadyOrderInput = {
|
|||||||
items: Array<ReadyOrderItemInput>;
|
items: Array<ReadyOrderItemInput>;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type UpsertMyCounterpartyProfileInput = {
|
||||||
|
bankName: Scalars['String']['input'];
|
||||||
|
bik: Scalars['String']['input'];
|
||||||
|
checkingAccount: Scalars['String']['input'];
|
||||||
|
companyFullName: Scalars['String']['input'];
|
||||||
|
companyName: Scalars['String']['input'];
|
||||||
|
correspondentAccount: Scalars['String']['input'];
|
||||||
|
inn: Scalars['String']['input'];
|
||||||
|
kpp?: InputMaybe<Scalars['String']['input']>;
|
||||||
|
legalAddress: Scalars['String']['input'];
|
||||||
|
ogrn?: InputMaybe<Scalars['String']['input']>;
|
||||||
|
signerBasis: Scalars['String']['input'];
|
||||||
|
signerFullName: Scalars['String']['input'];
|
||||||
|
signerPosition: Scalars['String']['input'];
|
||||||
|
};
|
||||||
|
|
||||||
export type User = {
|
export type User = {
|
||||||
__typename?: 'User';
|
__typename?: 'User';
|
||||||
company?: Maybe<Company>;
|
company?: Maybe<Company>;
|
||||||
@@ -615,6 +660,18 @@ export type ConnectMessengerMutationVariables = Exact<{
|
|||||||
|
|
||||||
export type ConnectMessengerMutation = { __typename?: 'Mutation', connectMessenger: { __typename?: 'MessengerConnection', id: string, type: MessengerType, channelId: string, isActive: boolean } };
|
export type ConnectMessengerMutation = { __typename?: 'Mutation', connectMessenger: { __typename?: 'MessengerConnection', id: string, type: MessengerType, channelId: string, isActive: boolean } };
|
||||||
|
|
||||||
|
export type MyCounterpartyProfileQueryVariables = Exact<{ [key: string]: never; }>;
|
||||||
|
|
||||||
|
|
||||||
|
export type MyCounterpartyProfileQuery = { __typename?: 'Query', myCounterpartyProfile?: { __typename?: 'CounterpartyProfile', id: string, companyName: string, companyFullName: string, inn: string, kpp?: string | null, ogrn?: string | null, legalAddress: string, bankName: string, bik: string, correspondentAccount: string, checkingAccount: string, signerFullName: string, signerPosition: string, signerBasis: string, isComplete: boolean, updatedAt: any } | null };
|
||||||
|
|
||||||
|
export type UpsertMyCounterpartyProfileMutationVariables = Exact<{
|
||||||
|
input: UpsertMyCounterpartyProfileInput;
|
||||||
|
}>;
|
||||||
|
|
||||||
|
|
||||||
|
export type UpsertMyCounterpartyProfileMutation = { __typename?: 'Mutation', upsertMyCounterpartyProfile: { __typename?: 'CounterpartyProfile', id: string, companyName: string, companyFullName: string, inn: string, kpp?: string | null, ogrn?: string | null, legalAddress: string, bankName: string, bik: string, correspondentAccount: string, checkingAccount: string, signerFullName: string, signerPosition: string, signerBasis: string, isComplete: boolean, updatedAt: any } };
|
||||||
|
|
||||||
|
|
||||||
export const ConsumeLoginTokenDocument = gql`
|
export const ConsumeLoginTokenDocument = gql`
|
||||||
mutation ConsumeLoginToken($token: String!) {
|
mutation ConsumeLoginToken($token: String!) {
|
||||||
@@ -1130,4 +1187,90 @@ export const ConnectMessengerDocument = gql`
|
|||||||
export function useConnectMessengerMutation(options: VueApolloComposable.UseMutationOptions<ConnectMessengerMutation, ConnectMessengerMutationVariables> | ReactiveFunction<VueApolloComposable.UseMutationOptions<ConnectMessengerMutation, ConnectMessengerMutationVariables>> = {}) {
|
export function useConnectMessengerMutation(options: VueApolloComposable.UseMutationOptions<ConnectMessengerMutation, ConnectMessengerMutationVariables> | ReactiveFunction<VueApolloComposable.UseMutationOptions<ConnectMessengerMutation, ConnectMessengerMutationVariables>> = {}) {
|
||||||
return VueApolloComposable.useMutation<ConnectMessengerMutation, ConnectMessengerMutationVariables>(ConnectMessengerDocument, options);
|
return VueApolloComposable.useMutation<ConnectMessengerMutation, ConnectMessengerMutationVariables>(ConnectMessengerDocument, options);
|
||||||
}
|
}
|
||||||
export type ConnectMessengerMutationCompositionFunctionResult = VueApolloComposable.UseMutationReturn<ConnectMessengerMutation, ConnectMessengerMutationVariables>;
|
export type ConnectMessengerMutationCompositionFunctionResult = VueApolloComposable.UseMutationReturn<ConnectMessengerMutation, ConnectMessengerMutationVariables>;
|
||||||
|
export const MyCounterpartyProfileDocument = gql`
|
||||||
|
query MyCounterpartyProfile {
|
||||||
|
myCounterpartyProfile {
|
||||||
|
id
|
||||||
|
companyName
|
||||||
|
companyFullName
|
||||||
|
inn
|
||||||
|
kpp
|
||||||
|
ogrn
|
||||||
|
legalAddress
|
||||||
|
bankName
|
||||||
|
bik
|
||||||
|
correspondentAccount
|
||||||
|
checkingAccount
|
||||||
|
signerFullName
|
||||||
|
signerPosition
|
||||||
|
signerBasis
|
||||||
|
isComplete
|
||||||
|
updatedAt
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* __useMyCounterpartyProfileQuery__
|
||||||
|
*
|
||||||
|
* To run a query within a Vue component, call `useMyCounterpartyProfileQuery` and pass it any options that fit your needs.
|
||||||
|
* When your component renders, `useMyCounterpartyProfileQuery` 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 } = useMyCounterpartyProfileQuery();
|
||||||
|
*/
|
||||||
|
export function useMyCounterpartyProfileQuery(options: VueApolloComposable.UseQueryOptions<MyCounterpartyProfileQuery, MyCounterpartyProfileQueryVariables> | VueCompositionApi.Ref<VueApolloComposable.UseQueryOptions<MyCounterpartyProfileQuery, MyCounterpartyProfileQueryVariables>> | ReactiveFunction<VueApolloComposable.UseQueryOptions<MyCounterpartyProfileQuery, MyCounterpartyProfileQueryVariables>> = {}) {
|
||||||
|
return VueApolloComposable.useQuery<MyCounterpartyProfileQuery, MyCounterpartyProfileQueryVariables>(MyCounterpartyProfileDocument, {}, options);
|
||||||
|
}
|
||||||
|
export function useMyCounterpartyProfileLazyQuery(options: VueApolloComposable.UseQueryOptions<MyCounterpartyProfileQuery, MyCounterpartyProfileQueryVariables> | VueCompositionApi.Ref<VueApolloComposable.UseQueryOptions<MyCounterpartyProfileQuery, MyCounterpartyProfileQueryVariables>> | ReactiveFunction<VueApolloComposable.UseQueryOptions<MyCounterpartyProfileQuery, MyCounterpartyProfileQueryVariables>> = {}) {
|
||||||
|
return VueApolloComposable.useLazyQuery<MyCounterpartyProfileQuery, MyCounterpartyProfileQueryVariables>(MyCounterpartyProfileDocument, {}, options);
|
||||||
|
}
|
||||||
|
export type MyCounterpartyProfileQueryCompositionFunctionResult = VueApolloComposable.UseQueryReturn<MyCounterpartyProfileQuery, MyCounterpartyProfileQueryVariables>;
|
||||||
|
export const UpsertMyCounterpartyProfileDocument = gql`
|
||||||
|
mutation UpsertMyCounterpartyProfile($input: UpsertMyCounterpartyProfileInput!) {
|
||||||
|
upsertMyCounterpartyProfile(input: $input) {
|
||||||
|
id
|
||||||
|
companyName
|
||||||
|
companyFullName
|
||||||
|
inn
|
||||||
|
kpp
|
||||||
|
ogrn
|
||||||
|
legalAddress
|
||||||
|
bankName
|
||||||
|
bik
|
||||||
|
correspondentAccount
|
||||||
|
checkingAccount
|
||||||
|
signerFullName
|
||||||
|
signerPosition
|
||||||
|
signerBasis
|
||||||
|
isComplete
|
||||||
|
updatedAt
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* __useUpsertMyCounterpartyProfileMutation__
|
||||||
|
*
|
||||||
|
* To run a mutation, you first call `useUpsertMyCounterpartyProfileMutation` within a Vue component and pass it any options that fit your needs.
|
||||||
|
* When your component renders, `useUpsertMyCounterpartyProfileMutation` 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 } = useUpsertMyCounterpartyProfileMutation({
|
||||||
|
* variables: {
|
||||||
|
* input: // value for 'input'
|
||||||
|
* },
|
||||||
|
* });
|
||||||
|
*/
|
||||||
|
export function useUpsertMyCounterpartyProfileMutation(options: VueApolloComposable.UseMutationOptions<UpsertMyCounterpartyProfileMutation, UpsertMyCounterpartyProfileMutationVariables> | ReactiveFunction<VueApolloComposable.UseMutationOptions<UpsertMyCounterpartyProfileMutation, UpsertMyCounterpartyProfileMutationVariables>> = {}) {
|
||||||
|
return VueApolloComposable.useMutation<UpsertMyCounterpartyProfileMutation, UpsertMyCounterpartyProfileMutationVariables>(UpsertMyCounterpartyProfileDocument, options);
|
||||||
|
}
|
||||||
|
export type UpsertMyCounterpartyProfileMutationCompositionFunctionResult = VueApolloComposable.UseMutationReturn<UpsertMyCounterpartyProfileMutation, UpsertMyCounterpartyProfileMutationVariables>;
|
||||||
53
app/composables/useCounterpartyProfile.ts
Normal file
53
app/composables/useCounterpartyProfile.ts
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
import { useQuery } from '@vue/apollo-composable';
|
||||||
|
import { MyCounterpartyProfileDocument } from '~/composables/graphql/generated';
|
||||||
|
|
||||||
|
function hasText(value: string | null | undefined) {
|
||||||
|
return Boolean(value && value.trim().length > 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isCounterpartyProfileComplete(profile: {
|
||||||
|
companyName?: string | null;
|
||||||
|
companyFullName?: string | null;
|
||||||
|
inn?: string | null;
|
||||||
|
legalAddress?: string | null;
|
||||||
|
bankName?: string | null;
|
||||||
|
bik?: string | null;
|
||||||
|
correspondentAccount?: string | null;
|
||||||
|
checkingAccount?: string | null;
|
||||||
|
signerFullName?: string | null;
|
||||||
|
signerPosition?: string | null;
|
||||||
|
signerBasis?: string | null;
|
||||||
|
} | null | undefined) {
|
||||||
|
if (!profile) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
profile.companyName,
|
||||||
|
profile.companyFullName,
|
||||||
|
profile.inn,
|
||||||
|
profile.legalAddress,
|
||||||
|
profile.bankName,
|
||||||
|
profile.bik,
|
||||||
|
profile.correspondentAccount,
|
||||||
|
profile.checkingAccount,
|
||||||
|
profile.signerFullName,
|
||||||
|
profile.signerPosition,
|
||||||
|
profile.signerBasis,
|
||||||
|
].every(hasText);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useCounterpartyProfile() {
|
||||||
|
const query = useQuery(MyCounterpartyProfileDocument);
|
||||||
|
|
||||||
|
const profile = computed(() => query.result.value?.myCounterpartyProfile ?? null);
|
||||||
|
const isComplete = computed(() => isCounterpartyProfileComplete(profile.value));
|
||||||
|
|
||||||
|
return {
|
||||||
|
profile,
|
||||||
|
isComplete,
|
||||||
|
loading: query.loading,
|
||||||
|
error: query.error,
|
||||||
|
refetch: query.refetch,
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { useMutation } from '@vue/apollo-composable';
|
import { useMutation } from '@vue/apollo-composable';
|
||||||
import { SubmitCalculationOrderDocument } from '~/composables/graphql/generated';
|
import { SubmitCalculationOrderDocument } from '~/composables/graphql/generated';
|
||||||
|
import { useCounterpartyProfile } from '~/composables/useCounterpartyProfile';
|
||||||
|
|
||||||
const productName = ref('');
|
const productName = ref('');
|
||||||
const quantity = ref(1);
|
const quantity = ref(1);
|
||||||
@@ -12,6 +13,7 @@ const { mutate, loading, onDone, onError } = useMutation(SubmitCalculationOrderD
|
|||||||
const success = ref('');
|
const success = ref('');
|
||||||
const errorMessage = ref('');
|
const errorMessage = ref('');
|
||||||
const calculatedVolume = computed(() => Number(quantity.value) * Number(width.value) * Number(thickness.value));
|
const calculatedVolume = computed(() => Number(quantity.value) * Number(width.value) * Number(thickness.value));
|
||||||
|
const { isComplete: isCounterpartyComplete, loading: counterpartyLoading } = useCounterpartyProfile();
|
||||||
|
|
||||||
onDone((result) => {
|
onDone((result) => {
|
||||||
success.value = `Заявка ${result.data?.submitCalculationOrder.code} отправлена`;
|
success.value = `Заявка ${result.data?.submitCalculationOrder.code} отправлена`;
|
||||||
@@ -24,6 +26,12 @@ onError((error) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
function submit() {
|
function submit() {
|
||||||
|
if (!isCounterpartyComplete.value) {
|
||||||
|
errorMessage.value = 'Сначала заполните карточку контрагента в профиле.';
|
||||||
|
success.value = '';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
mutate({
|
mutate({
|
||||||
input: {
|
input: {
|
||||||
productName: productName.value,
|
productName: productName.value,
|
||||||
@@ -48,6 +56,14 @@ function submit() {
|
|||||||
<div class="mx-auto max-w-4xl rounded-[30px] p-1 shadow-[0_26px_60px_rgba(13,133,74,0.18)]">
|
<div class="mx-auto max-w-4xl rounded-[30px] p-1 shadow-[0_26px_60px_rgba(13,133,74,0.18)]">
|
||||||
<div class="surface-card grid gap-6 rounded-[26px] p-5 md:grid-cols-[1.45fr_1fr] md:p-6">
|
<div class="surface-card grid gap-6 rounded-[26px] p-5 md:grid-cols-[1.45fr_1fr] md:p-6">
|
||||||
<div class="space-y-3">
|
<div class="space-y-3">
|
||||||
|
<div v-if="counterpartyLoading.value" class="alert">
|
||||||
|
Проверяем карточку контрагента...
|
||||||
|
</div>
|
||||||
|
<div v-else-if="!isCounterpartyComplete" class="alert alert-warning">
|
||||||
|
Для оформления заявки заполните карточку контрагента в
|
||||||
|
<NuxtLink to="/profile" class="link link-hover font-semibold">профиле</NuxtLink>.
|
||||||
|
</div>
|
||||||
|
|
||||||
<label class="form-control">
|
<label class="form-control">
|
||||||
<span class="label-text font-semibold text-[#194631]">Название позиции</span>
|
<span class="label-text font-semibold text-[#194631]">Название позиции</span>
|
||||||
<input v-model="productName" type="text" class="input input-bordered border-[#d0e8d8] bg-white/80">
|
<input v-model="productName" type="text" class="input input-bordered border-[#d0e8d8] bg-white/80">
|
||||||
@@ -70,7 +86,7 @@ function submit() {
|
|||||||
<input v-model="color" type="text" class="input input-bordered border-[#d0e8d8] bg-white/80">
|
<input v-model="color" type="text" class="input input-bordered border-[#d0e8d8] bg-white/80">
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<button class="btn border-0 bg-[#139957] text-white hover:bg-[#0d854a]" :disabled="loading" @click="submit">
|
<button class="btn border-0 bg-[#139957] text-white hover:bg-[#0d854a]" :disabled="loading || counterpartyLoading.value || !isCounterpartyComplete" @click="submit">
|
||||||
{{ loading ? 'Отправляем…' : 'Отправить менеджеру' }}
|
{{ loading ? 'Отправляем…' : 'Отправить менеджеру' }}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,42 +1,136 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { useMutation, useQuery } from '@vue/apollo-composable';
|
import { useMutation, useQuery } from '@vue/apollo-composable';
|
||||||
import { MeDocument, MyMessengerConnectionsDocument, RegisterSelfDocument } from '~/composables/graphql/generated';
|
import {
|
||||||
|
MeDocument,
|
||||||
|
MyCounterpartyProfileDocument,
|
||||||
|
MyMessengerConnectionsDocument,
|
||||||
|
RegisterSelfDocument,
|
||||||
|
UpsertMyCounterpartyProfileDocument,
|
||||||
|
} from '~/composables/graphql/generated';
|
||||||
import { buildMessengerBotStartUrl } from '~/composables/useMessengerBotLink';
|
import { buildMessengerBotStartUrl } from '~/composables/useMessengerBotLink';
|
||||||
|
import { isCounterpartyProfileComplete } from '~/composables/useCounterpartyProfile';
|
||||||
|
|
||||||
|
type MessengerItem = {
|
||||||
|
type: 'TELEGRAM' | 'MAX';
|
||||||
|
isActive: boolean;
|
||||||
|
channelId: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
type PartySuggestion = {
|
||||||
|
value: string;
|
||||||
|
unrestricted_value?: string;
|
||||||
|
data?: {
|
||||||
|
inn?: string;
|
||||||
|
kpp?: string;
|
||||||
|
ogrn?: string;
|
||||||
|
address?: { value?: string };
|
||||||
|
management?: {
|
||||||
|
name?: string;
|
||||||
|
post?: string;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
type BankSuggestion = {
|
||||||
|
value: string;
|
||||||
|
unrestricted_value?: string;
|
||||||
|
data?: {
|
||||||
|
bic?: string;
|
||||||
|
correspondent_account?: string;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
const companyName = ref('');
|
|
||||||
const inn = ref('');
|
|
||||||
const contactName = ref('');
|
|
||||||
const email = ref('');
|
|
||||||
const config = useRuntimeConfig();
|
const config = useRuntimeConfig();
|
||||||
|
|
||||||
const registerMutation = useMutation(RegisterSelfDocument);
|
const registerForm = reactive({
|
||||||
|
companyName: '',
|
||||||
|
inn: '',
|
||||||
|
contactName: '',
|
||||||
|
email: '',
|
||||||
|
});
|
||||||
|
|
||||||
|
const counterpartyForm = reactive({
|
||||||
|
companyName: '',
|
||||||
|
companyFullName: '',
|
||||||
|
inn: '',
|
||||||
|
kpp: '',
|
||||||
|
ogrn: '',
|
||||||
|
legalAddress: '',
|
||||||
|
bankName: '',
|
||||||
|
bik: '',
|
||||||
|
correspondentAccount: '',
|
||||||
|
checkingAccount: '',
|
||||||
|
signerFullName: '',
|
||||||
|
signerPosition: '',
|
||||||
|
signerBasis: '',
|
||||||
|
});
|
||||||
|
|
||||||
|
const registerFeedback = ref('');
|
||||||
|
const registerFeedbackTone = ref<'success' | 'error'>('success');
|
||||||
|
const profileFeedback = ref('');
|
||||||
|
const profileFeedbackTone = ref<'success' | 'error'>('success');
|
||||||
|
|
||||||
const meQuery = useQuery(MeDocument);
|
const meQuery = useQuery(MeDocument);
|
||||||
|
const profileQuery = useQuery(MyCounterpartyProfileDocument);
|
||||||
const connectionsQuery = useQuery(MyMessengerConnectionsDocument);
|
const connectionsQuery = useQuery(MyMessengerConnectionsDocument);
|
||||||
|
|
||||||
const message = ref('');
|
const registerMutation = useMutation(RegisterSelfDocument, { throws: 'never' });
|
||||||
const messageTone = ref<'success' | 'error'>('success');
|
const saveCounterpartyMutation = useMutation(UpsertMyCounterpartyProfileDocument, { throws: 'never' });
|
||||||
|
|
||||||
registerMutation.onDone(() => {
|
const companySearch = ref('');
|
||||||
message.value = 'Заявка на регистрацию отправлена менеджеру';
|
const partySuggestions = ref<PartySuggestion[]>([]);
|
||||||
messageTone.value = 'success';
|
const partyLoading = ref(false);
|
||||||
});
|
const partyOpen = ref(false);
|
||||||
|
const partySearchTimer = ref<ReturnType<typeof setTimeout> | null>(null);
|
||||||
|
|
||||||
registerMutation.onError((error) => {
|
const bankSearch = ref('');
|
||||||
message.value = error.message;
|
const bankSuggestions = ref<BankSuggestion[]>([]);
|
||||||
messageTone.value = 'error';
|
const bankLoading = ref(false);
|
||||||
});
|
const bankOpen = ref(false);
|
||||||
|
const bankSearchTimer = ref<ReturnType<typeof setTimeout> | null>(null);
|
||||||
|
|
||||||
|
const partyDropdownRef = ref<HTMLElement | null>(null);
|
||||||
|
const bankDropdownRef = ref<HTMLElement | null>(null);
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => profileQuery.result.value?.myCounterpartyProfile,
|
||||||
|
(profile) => {
|
||||||
|
if (!profile) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
counterpartyForm.companyName = profile.companyName;
|
||||||
|
counterpartyForm.companyFullName = profile.companyFullName;
|
||||||
|
counterpartyForm.inn = profile.inn;
|
||||||
|
counterpartyForm.kpp = profile.kpp ?? '';
|
||||||
|
counterpartyForm.ogrn = profile.ogrn ?? '';
|
||||||
|
counterpartyForm.legalAddress = profile.legalAddress;
|
||||||
|
counterpartyForm.bankName = profile.bankName;
|
||||||
|
counterpartyForm.bik = profile.bik;
|
||||||
|
counterpartyForm.correspondentAccount = profile.correspondentAccount;
|
||||||
|
counterpartyForm.checkingAccount = profile.checkingAccount;
|
||||||
|
counterpartyForm.signerFullName = profile.signerFullName;
|
||||||
|
counterpartyForm.signerPosition = profile.signerPosition;
|
||||||
|
counterpartyForm.signerBasis = profile.signerBasis;
|
||||||
|
|
||||||
|
companySearch.value = profile.companyName;
|
||||||
|
bankSearch.value = profile.bankName;
|
||||||
|
},
|
||||||
|
{ immediate: true },
|
||||||
|
);
|
||||||
|
|
||||||
|
const profileUpdatedAt = computed(() => profileQuery.result.value?.myCounterpartyProfile?.updatedAt ?? null);
|
||||||
|
const profileIsComplete = computed(() => isCounterpartyProfileComplete(counterpartyForm));
|
||||||
|
|
||||||
const telegramConnection = computed(() =>
|
const telegramConnection = computed(() =>
|
||||||
connectionsQuery.result.value?.myMessengerConnections?.find(
|
connectionsQuery.result.value?.myMessengerConnections?.find(
|
||||||
(item: { type: 'TELEGRAM' | 'MAX'; isActive: boolean; channelId: string }) =>
|
(item: MessengerItem) => item.type === 'TELEGRAM' && item.isActive,
|
||||||
item.type === 'TELEGRAM' && item.isActive,
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
const maxConnection = computed(() =>
|
const maxConnection = computed(() =>
|
||||||
connectionsQuery.result.value?.myMessengerConnections?.find(
|
connectionsQuery.result.value?.myMessengerConnections?.find(
|
||||||
(item: { type: 'TELEGRAM' | 'MAX'; isActive: boolean; channelId: string }) =>
|
(item: MessengerItem) => item.type === 'MAX' && item.isActive,
|
||||||
item.type === 'MAX' && item.isActive,
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -52,37 +146,221 @@ function buildBotConnectUrl(baseUrl: string) {
|
|||||||
const telegramConnectUrl = computed(() => buildBotConnectUrl(config.public.telegramBotUrl || ''));
|
const telegramConnectUrl = computed(() => buildBotConnectUrl(config.public.telegramBotUrl || ''));
|
||||||
const maxConnectUrl = computed(() => buildBotConnectUrl(config.public.maxBotUrl || ''));
|
const maxConnectUrl = computed(() => buildBotConnectUrl(config.public.maxBotUrl || ''));
|
||||||
|
|
||||||
function register() {
|
function clearPartyTimer() {
|
||||||
message.value = '';
|
if (!partySearchTimer.value) {
|
||||||
registerMutation.mutate({
|
return;
|
||||||
|
}
|
||||||
|
clearTimeout(partySearchTimer.value);
|
||||||
|
partySearchTimer.value = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearBankTimer() {
|
||||||
|
if (!bankSearchTimer.value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
clearTimeout(bankSearchTimer.value);
|
||||||
|
bankSearchTimer.value = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function fetchPartySuggestions() {
|
||||||
|
const query = companySearch.value.trim();
|
||||||
|
if (query.length < 2) {
|
||||||
|
partySuggestions.value = [];
|
||||||
|
partyOpen.value = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
partyLoading.value = true;
|
||||||
|
await $fetch<{ suggestions: PartySuggestion[] }>('/api/dadata/party', {
|
||||||
|
method: 'POST',
|
||||||
|
body: { query },
|
||||||
|
})
|
||||||
|
.then((response) => {
|
||||||
|
partySuggestions.value = response.suggestions || [];
|
||||||
|
partyOpen.value = partySuggestions.value.length > 0;
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
partyLoading.value = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function fetchBankSuggestions() {
|
||||||
|
const query = bankSearch.value.trim();
|
||||||
|
if (query.length < 2) {
|
||||||
|
bankSuggestions.value = [];
|
||||||
|
bankOpen.value = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
bankLoading.value = true;
|
||||||
|
await $fetch<{ suggestions: BankSuggestion[] }>('/api/dadata/bank', {
|
||||||
|
method: 'POST',
|
||||||
|
body: { query },
|
||||||
|
})
|
||||||
|
.then((response) => {
|
||||||
|
bankSuggestions.value = response.suggestions || [];
|
||||||
|
bankOpen.value = bankSuggestions.value.length > 0;
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
bankLoading.value = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function schedulePartySuggest() {
|
||||||
|
clearPartyTimer();
|
||||||
|
partySearchTimer.value = setTimeout(() => {
|
||||||
|
void fetchPartySuggestions();
|
||||||
|
}, 250);
|
||||||
|
}
|
||||||
|
|
||||||
|
function scheduleBankSuggest() {
|
||||||
|
clearBankTimer();
|
||||||
|
bankSearchTimer.value = setTimeout(() => {
|
||||||
|
void fetchBankSuggestions();
|
||||||
|
}, 250);
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyPartySuggestion(item: PartySuggestion) {
|
||||||
|
partyOpen.value = false;
|
||||||
|
companySearch.value = item.value;
|
||||||
|
|
||||||
|
counterpartyForm.companyName = item.value;
|
||||||
|
counterpartyForm.companyFullName = item.unrestricted_value || item.value;
|
||||||
|
counterpartyForm.inn = item.data?.inn || '';
|
||||||
|
counterpartyForm.kpp = item.data?.kpp || '';
|
||||||
|
counterpartyForm.ogrn = item.data?.ogrn || '';
|
||||||
|
counterpartyForm.legalAddress = item.data?.address?.value || '';
|
||||||
|
|
||||||
|
if (!counterpartyForm.signerFullName && item.data?.management?.name) {
|
||||||
|
counterpartyForm.signerFullName = item.data.management.name;
|
||||||
|
}
|
||||||
|
if (!counterpartyForm.signerPosition && item.data?.management?.post) {
|
||||||
|
counterpartyForm.signerPosition = item.data.management.post;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyBankSuggestion(item: BankSuggestion) {
|
||||||
|
bankOpen.value = false;
|
||||||
|
bankSearch.value = item.value;
|
||||||
|
|
||||||
|
counterpartyForm.bankName = item.value;
|
||||||
|
counterpartyForm.bik = item.data?.bic || '';
|
||||||
|
counterpartyForm.correspondentAccount = item.data?.correspondent_account || '';
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeDropdownsFromOutside(event: MouseEvent) {
|
||||||
|
const target = event.target as Node | null;
|
||||||
|
if (partyDropdownRef.value && target && !partyDropdownRef.value.contains(target)) {
|
||||||
|
partyOpen.value = false;
|
||||||
|
}
|
||||||
|
if (bankDropdownRef.value && target && !bankDropdownRef.value.contains(target)) {
|
||||||
|
bankOpen.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function registerSelf() {
|
||||||
|
registerFeedback.value = '';
|
||||||
|
const result = await registerMutation.mutate({
|
||||||
input: {
|
input: {
|
||||||
companyName: companyName.value,
|
companyName: registerForm.companyName,
|
||||||
inn: inn.value || null,
|
inn: registerForm.inn.trim() ? registerForm.inn.trim() : null,
|
||||||
contactName: contactName.value,
|
contactName: registerForm.contactName,
|
||||||
email: email.value,
|
email: registerForm.email,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const payload = result?.data?.registerSelf;
|
||||||
|
if (!payload) {
|
||||||
|
registerFeedbackTone.value = 'error';
|
||||||
|
registerFeedback.value = registerMutation.error.value?.message || 'Не удалось отправить заявку.';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
registerFeedbackTone.value = 'success';
|
||||||
|
registerFeedback.value = 'Заявка на регистрацию отправлена менеджеру.';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function saveCounterpartyProfile() {
|
||||||
|
profileFeedback.value = '';
|
||||||
|
const result = await saveCounterpartyMutation.mutate({
|
||||||
|
input: {
|
||||||
|
companyName: counterpartyForm.companyName,
|
||||||
|
companyFullName: counterpartyForm.companyFullName,
|
||||||
|
inn: counterpartyForm.inn,
|
||||||
|
kpp: counterpartyForm.kpp.trim() ? counterpartyForm.kpp.trim() : null,
|
||||||
|
ogrn: counterpartyForm.ogrn.trim() ? counterpartyForm.ogrn.trim() : null,
|
||||||
|
legalAddress: counterpartyForm.legalAddress,
|
||||||
|
bankName: counterpartyForm.bankName,
|
||||||
|
bik: counterpartyForm.bik,
|
||||||
|
correspondentAccount: counterpartyForm.correspondentAccount,
|
||||||
|
checkingAccount: counterpartyForm.checkingAccount,
|
||||||
|
signerFullName: counterpartyForm.signerFullName,
|
||||||
|
signerPosition: counterpartyForm.signerPosition,
|
||||||
|
signerBasis: counterpartyForm.signerBasis,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const payload = result?.data?.upsertMyCounterpartyProfile;
|
||||||
|
if (!payload) {
|
||||||
|
profileFeedbackTone.value = 'error';
|
||||||
|
profileFeedback.value = saveCounterpartyMutation.error.value?.message || 'Не удалось сохранить карточку контрагента.';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
profileFeedbackTone.value = 'success';
|
||||||
|
profileFeedback.value = 'Карточка контрагента сохранена.';
|
||||||
|
await profileQuery.refetch();
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
document.addEventListener('click', closeDropdownsFromOutside);
|
||||||
|
});
|
||||||
|
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
document.removeEventListener('click', closeDropdownsFromOutside);
|
||||||
|
clearPartyTimer();
|
||||||
|
clearBankTimer();
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<section class="space-y-6">
|
<section class="space-y-6">
|
||||||
<div>
|
<div>
|
||||||
<h1 class="text-3xl font-extrabold text-[#0f2f20]">Профиль</h1>
|
<h1 class="text-3xl font-extrabold text-[#0f2f20]">Профиль</h1>
|
||||||
<p class="mt-1 text-sm text-[#28543f]/80">Регистрация компании и подключение каналов уведомлений.</p>
|
<p class="mt-1 text-sm text-[#28543f]/80">Заполните карточку контрагента, чтобы можно было оформлять заявки из корзины.</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="grid gap-4 lg:grid-cols-2">
|
<div class="grid gap-4 lg:grid-cols-2">
|
||||||
<div class="surface-card rounded-3xl p-5">
|
<div class="surface-card rounded-3xl p-5">
|
||||||
<h2 class="text-xl font-bold text-[#123824]">Самостоятельная регистрация</h2>
|
<h2 class="text-xl font-bold text-[#123824]">Самостоятельная регистрация</h2>
|
||||||
<div class="mt-4 space-y-3">
|
<div class="mt-4 space-y-3">
|
||||||
<input v-model="companyName" type="text" placeholder="Компания" class="input input-bordered w-full border-[#d0e8d8] bg-white/80">
|
<fieldset class="fieldset">
|
||||||
<input v-model="inn" type="text" placeholder="ИНН" class="input input-bordered w-full border-[#d0e8d8] bg-white/80">
|
<legend class="fieldset-legend">Компания</legend>
|
||||||
<input v-model="contactName" type="text" placeholder="Контактное лицо" class="input input-bordered w-full border-[#d0e8d8] bg-white/80">
|
<input v-model="registerForm.companyName" type="text" class="input input-bordered w-full" placeholder="ООО Пример" >
|
||||||
<input v-model="email" type="email" placeholder="Email" class="input input-bordered w-full border-[#d0e8d8] bg-white/80">
|
</fieldset>
|
||||||
<button class="btn w-full border-0 bg-[#139957] text-white hover:bg-[#0d854a]" :disabled="registerMutation.loading.value" @click="register">
|
|
||||||
|
<fieldset class="fieldset">
|
||||||
|
<legend class="fieldset-legend">ИНН</legend>
|
||||||
|
<input v-model="registerForm.inn" type="text" class="input input-bordered w-full" placeholder="7701234567" >
|
||||||
|
</fieldset>
|
||||||
|
|
||||||
|
<fieldset class="fieldset">
|
||||||
|
<legend class="fieldset-legend">Контактное лицо</legend>
|
||||||
|
<input v-model="registerForm.contactName" type="text" class="input input-bordered w-full" placeholder="Иванов Иван" >
|
||||||
|
</fieldset>
|
||||||
|
|
||||||
|
<fieldset class="fieldset">
|
||||||
|
<legend class="fieldset-legend">E-mail</legend>
|
||||||
|
<input v-model="registerForm.email" type="email" class="input input-bordered w-full" placeholder="name@company.com" >
|
||||||
|
</fieldset>
|
||||||
|
|
||||||
|
<button class="btn btn-primary w-full" :disabled="registerMutation.loading.value" @click="registerSelf">
|
||||||
{{ registerMutation.loading.value ? 'Отправляем…' : 'Отправить заявку' }}
|
{{ registerMutation.loading.value ? 'Отправляем…' : 'Отправить заявку' }}
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
|
<div v-if="registerFeedback" class="alert" :class="registerFeedbackTone === 'success' ? 'alert-success' : 'alert-error'">
|
||||||
|
{{ registerFeedback }}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -124,12 +402,182 @@ function register() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div
|
<div class="surface-card rounded-3xl p-5">
|
||||||
v-if="message"
|
<div class="flex flex-wrap items-center justify-between gap-2">
|
||||||
class="alert"
|
<h2 class="text-xl font-bold text-[#123824]">Карточка контрагента</h2>
|
||||||
:class="messageTone === 'success' ? 'alert-success' : 'alert-error'"
|
<span class="badge" :class="profileIsComplete ? 'badge-success' : 'badge-warning'">
|
||||||
>
|
{{ profileIsComplete ? 'Заполнена' : 'Не заполнена' }}
|
||||||
{{ message }}
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-4 grid gap-4 xl:grid-cols-3">
|
||||||
|
<div class="rounded-2xl border border-base-300 bg-base-100 p-4">
|
||||||
|
<h3 class="mb-3 text-base font-bold">1. Контрагент (Dadata)</h3>
|
||||||
|
|
||||||
|
<div ref="partyDropdownRef" class="relative">
|
||||||
|
<fieldset class="fieldset">
|
||||||
|
<legend class="fieldset-legend">Поиск компании</legend>
|
||||||
|
<input
|
||||||
|
v-model="companySearch"
|
||||||
|
type="text"
|
||||||
|
class="input input-bordered w-full"
|
||||||
|
placeholder="Введите название или ИНН"
|
||||||
|
@input="schedulePartySuggest"
|
||||||
|
@focus="partyOpen = partySuggestions.length > 0"
|
||||||
|
>
|
||||||
|
</fieldset>
|
||||||
|
|
||||||
|
<span v-if="partyLoading" class="loading loading-spinner loading-sm absolute right-3 top-1/2 -translate-y-1/2" />
|
||||||
|
|
||||||
|
<div
|
||||||
|
v-if="partyOpen && partySuggestions.length > 0"
|
||||||
|
class="absolute z-30 mt-2 max-h-72 w-full overflow-auto rounded-box border border-base-300 bg-base-100 p-2 shadow-xl"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
v-for="item in partySuggestions"
|
||||||
|
:key="`${item.value}-${item.data?.inn || ''}`"
|
||||||
|
type="button"
|
||||||
|
class="btn btn-ghost mb-1 h-auto min-h-0 w-full justify-start whitespace-normal px-3 py-2 text-left"
|
||||||
|
@click="applyPartySuggestion(item)"
|
||||||
|
>
|
||||||
|
<span>
|
||||||
|
<span class="block text-sm font-semibold">{{ item.value }}</span>
|
||||||
|
<span class="block text-xs opacity-70">ИНН: {{ item.data?.inn || '—' }} <span v-if="item.data?.kpp">• КПП: {{ item.data.kpp }}</span></span>
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<fieldset class="fieldset">
|
||||||
|
<legend class="fieldset-legend">Краткое наименование</legend>
|
||||||
|
<input v-model="counterpartyForm.companyName" type="text" class="input input-bordered w-full" >
|
||||||
|
</fieldset>
|
||||||
|
|
||||||
|
<fieldset class="fieldset">
|
||||||
|
<legend class="fieldset-legend">Полное наименование</legend>
|
||||||
|
<input v-model="counterpartyForm.companyFullName" type="text" class="input input-bordered w-full" >
|
||||||
|
</fieldset>
|
||||||
|
|
||||||
|
<div class="grid gap-3 sm:grid-cols-3">
|
||||||
|
<fieldset class="fieldset">
|
||||||
|
<legend class="fieldset-legend">ИНН</legend>
|
||||||
|
<input v-model="counterpartyForm.inn" type="text" class="input input-bordered w-full" >
|
||||||
|
</fieldset>
|
||||||
|
|
||||||
|
<fieldset class="fieldset">
|
||||||
|
<legend class="fieldset-legend">КПП</legend>
|
||||||
|
<input v-model="counterpartyForm.kpp" type="text" class="input input-bordered w-full" >
|
||||||
|
</fieldset>
|
||||||
|
|
||||||
|
<fieldset class="fieldset">
|
||||||
|
<legend class="fieldset-legend">ОГРН</legend>
|
||||||
|
<input v-model="counterpartyForm.ogrn" type="text" class="input input-bordered w-full" >
|
||||||
|
</fieldset>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<fieldset class="fieldset">
|
||||||
|
<legend class="fieldset-legend">Юридический адрес</legend>
|
||||||
|
<textarea v-model="counterpartyForm.legalAddress" class="textarea textarea-bordered min-h-24 w-full" />
|
||||||
|
</fieldset>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="rounded-2xl border border-base-300 bg-base-100 p-4">
|
||||||
|
<h3 class="mb-3 text-base font-bold">2. Банк (Dadata)</h3>
|
||||||
|
|
||||||
|
<div ref="bankDropdownRef" class="relative">
|
||||||
|
<fieldset class="fieldset">
|
||||||
|
<legend class="fieldset-legend">Поиск банка</legend>
|
||||||
|
<input
|
||||||
|
v-model="bankSearch"
|
||||||
|
type="text"
|
||||||
|
class="input input-bordered w-full"
|
||||||
|
placeholder="Введите название банка"
|
||||||
|
@input="scheduleBankSuggest"
|
||||||
|
@focus="bankOpen = bankSuggestions.length > 0"
|
||||||
|
>
|
||||||
|
</fieldset>
|
||||||
|
|
||||||
|
<span v-if="bankLoading" class="loading loading-spinner loading-sm absolute right-3 top-1/2 -translate-y-1/2" />
|
||||||
|
|
||||||
|
<div
|
||||||
|
v-if="bankOpen && bankSuggestions.length > 0"
|
||||||
|
class="absolute z-30 mt-2 max-h-72 w-full overflow-auto rounded-box border border-base-300 bg-base-100 p-2 shadow-xl"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
v-for="item in bankSuggestions"
|
||||||
|
:key="`${item.value}-${item.data?.bic || ''}`"
|
||||||
|
type="button"
|
||||||
|
class="btn btn-ghost mb-1 h-auto min-h-0 w-full justify-start whitespace-normal px-3 py-2 text-left"
|
||||||
|
@click="applyBankSuggestion(item)"
|
||||||
|
>
|
||||||
|
<span>
|
||||||
|
<span class="block text-sm font-semibold">{{ item.value }}</span>
|
||||||
|
<span class="block text-xs opacity-70">БИК: {{ item.data?.bic || '—' }}</span>
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<fieldset class="fieldset">
|
||||||
|
<legend class="fieldset-legend">Банк</legend>
|
||||||
|
<input v-model="counterpartyForm.bankName" type="text" class="input input-bordered w-full" >
|
||||||
|
</fieldset>
|
||||||
|
|
||||||
|
<div class="grid gap-3 sm:grid-cols-2">
|
||||||
|
<fieldset class="fieldset">
|
||||||
|
<legend class="fieldset-legend">БИК</legend>
|
||||||
|
<input v-model="counterpartyForm.bik" type="text" class="input input-bordered w-full" >
|
||||||
|
</fieldset>
|
||||||
|
|
||||||
|
<fieldset class="fieldset">
|
||||||
|
<legend class="fieldset-legend">Корр. счет</legend>
|
||||||
|
<input v-model="counterpartyForm.correspondentAccount" type="text" class="input input-bordered w-full" >
|
||||||
|
</fieldset>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<fieldset class="fieldset">
|
||||||
|
<legend class="fieldset-legend">Расчетный счет</legend>
|
||||||
|
<input v-model="counterpartyForm.checkingAccount" type="text" class="input input-bordered w-full" >
|
||||||
|
</fieldset>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="rounded-2xl border border-base-300 bg-base-100 p-4">
|
||||||
|
<h3 class="mb-3 text-base font-bold">3. Подписант и основание</h3>
|
||||||
|
|
||||||
|
<fieldset class="fieldset">
|
||||||
|
<legend class="fieldset-legend">ФИО подписанта</legend>
|
||||||
|
<input v-model="counterpartyForm.signerFullName" type="text" class="input input-bordered w-full" placeholder="Иванов Иван Иванович" >
|
||||||
|
</fieldset>
|
||||||
|
|
||||||
|
<fieldset class="fieldset">
|
||||||
|
<legend class="fieldset-legend">Должность</legend>
|
||||||
|
<input v-model="counterpartyForm.signerPosition" type="text" class="input input-bordered w-full" placeholder="Генеральный директор" >
|
||||||
|
</fieldset>
|
||||||
|
|
||||||
|
<fieldset class="fieldset">
|
||||||
|
<legend class="fieldset-legend">Основание полномочий</legend>
|
||||||
|
<textarea v-model="counterpartyForm.signerBasis" class="textarea textarea-bordered min-h-24 w-full" placeholder="Действует на основании Устава" />
|
||||||
|
</fieldset>
|
||||||
|
|
||||||
|
<button class="btn btn-primary mt-4 w-full" :disabled="saveCounterpartyMutation.loading.value || !profileIsComplete" @click="saveCounterpartyProfile">
|
||||||
|
{{ saveCounterpartyMutation.loading.value ? 'Сохраняем…' : 'Сохранить карточку' }}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<p v-if="profileUpdatedAt" class="mt-2 text-xs opacity-70">Обновлено: {{ new Date(profileUpdatedAt).toLocaleString() }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="profileFeedback" class="alert mt-4" :class="profileFeedbackTone === 'success' ? 'alert-success' : 'alert-error'">
|
||||||
|
{{ profileFeedback }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="alert mt-4" :class="profileIsComplete ? 'alert-success' : 'alert-warning'">
|
||||||
|
{{
|
||||||
|
profileIsComplete
|
||||||
|
? 'Карточка контрагента заполнена. Оформление заявки в корзине доступно.'
|
||||||
|
: 'Пока карточка не заполнена полностью, оформление заявки в корзине будет заблокировано.'
|
||||||
|
}}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
20
graphql/operations/profile/my-counterparty-profile.graphql
Normal file
20
graphql/operations/profile/my-counterparty-profile.graphql
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
query MyCounterpartyProfile {
|
||||||
|
myCounterpartyProfile {
|
||||||
|
id
|
||||||
|
companyName
|
||||||
|
companyFullName
|
||||||
|
inn
|
||||||
|
kpp
|
||||||
|
ogrn
|
||||||
|
legalAddress
|
||||||
|
bankName
|
||||||
|
bik
|
||||||
|
correspondentAccount
|
||||||
|
checkingAccount
|
||||||
|
signerFullName
|
||||||
|
signerPosition
|
||||||
|
signerBasis
|
||||||
|
isComplete
|
||||||
|
updatedAt
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
mutation UpsertMyCounterpartyProfile($input: UpsertMyCounterpartyProfileInput!) {
|
||||||
|
upsertMyCounterpartyProfile(input: $input) {
|
||||||
|
id
|
||||||
|
companyName
|
||||||
|
companyFullName
|
||||||
|
inn
|
||||||
|
kpp
|
||||||
|
ogrn
|
||||||
|
legalAddress
|
||||||
|
bankName
|
||||||
|
bik
|
||||||
|
correspondentAccount
|
||||||
|
checkingAccount
|
||||||
|
signerFullName
|
||||||
|
signerPosition
|
||||||
|
signerBasis
|
||||||
|
isComplete
|
||||||
|
updatedAt
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -65,6 +65,27 @@ type User {
|
|||||||
company: Company
|
company: Company
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type CounterpartyProfile {
|
||||||
|
id: ID!
|
||||||
|
userId: ID!
|
||||||
|
companyName: String!
|
||||||
|
companyFullName: String!
|
||||||
|
inn: String!
|
||||||
|
kpp: String
|
||||||
|
ogrn: String
|
||||||
|
legalAddress: String!
|
||||||
|
bankName: String!
|
||||||
|
bik: String!
|
||||||
|
correspondentAccount: String!
|
||||||
|
checkingAccount: String!
|
||||||
|
signerFullName: String!
|
||||||
|
signerPosition: String!
|
||||||
|
signerBasis: String!
|
||||||
|
isComplete: Boolean!
|
||||||
|
createdAt: DateTime!
|
||||||
|
updatedAt: DateTime!
|
||||||
|
}
|
||||||
|
|
||||||
type AuthCodeRequestResult {
|
type AuthCodeRequestResult {
|
||||||
challengeToken: String!
|
challengeToken: String!
|
||||||
channel: LoginChannel!
|
channel: LoginChannel!
|
||||||
@@ -222,6 +243,7 @@ type ReferralStats {
|
|||||||
type Query {
|
type Query {
|
||||||
healthcheck: String!
|
healthcheck: String!
|
||||||
me: User
|
me: User
|
||||||
|
myCounterpartyProfile: CounterpartyProfile
|
||||||
myMessengerConnections: [MessengerConnection!]!
|
myMessengerConnections: [MessengerConnection!]!
|
||||||
myNotificationHistory(channel: MessengerType!, limit: Int = 50): [NotificationHistoryItem!]!
|
myNotificationHistory(channel: MessengerType!, limit: Int = 50): [NotificationHistoryItem!]!
|
||||||
managerNotificationHistory(userId: ID!, channel: MessengerType!, limit: Int = 50): [NotificationHistoryItem!]!
|
managerNotificationHistory(userId: ID!, channel: MessengerType!, limit: Int = 50): [NotificationHistoryItem!]!
|
||||||
@@ -272,6 +294,22 @@ input ConnectMessengerInput {
|
|||||||
channelId: String!
|
channelId: String!
|
||||||
}
|
}
|
||||||
|
|
||||||
|
input UpsertMyCounterpartyProfileInput {
|
||||||
|
companyName: String!
|
||||||
|
companyFullName: String!
|
||||||
|
inn: String!
|
||||||
|
kpp: String
|
||||||
|
ogrn: String
|
||||||
|
legalAddress: String!
|
||||||
|
bankName: String!
|
||||||
|
bik: String!
|
||||||
|
correspondentAccount: String!
|
||||||
|
checkingAccount: String!
|
||||||
|
signerFullName: String!
|
||||||
|
signerPosition: String!
|
||||||
|
signerBasis: String!
|
||||||
|
}
|
||||||
|
|
||||||
input ReadyOrderItemInput {
|
input ReadyOrderItemInput {
|
||||||
productId: ID!
|
productId: ID!
|
||||||
quantity: Float!
|
quantity: Float!
|
||||||
@@ -329,6 +367,7 @@ type Mutation {
|
|||||||
createInvitation(input: CreateInvitationInput!): Invitation!
|
createInvitation(input: CreateInvitationInput!): Invitation!
|
||||||
acceptInvitation(input: AcceptInvitationInput!): User!
|
acceptInvitation(input: AcceptInvitationInput!): User!
|
||||||
connectMessenger(input: ConnectMessengerInput!): MessengerConnection!
|
connectMessenger(input: ConnectMessengerInput!): MessengerConnection!
|
||||||
|
upsertMyCounterpartyProfile(input: UpsertMyCounterpartyProfileInput!): CounterpartyProfile!
|
||||||
sendTestMessengerMessage(type: MessengerType!, channelId: String, message: String): MessengerDispatchResult!
|
sendTestMessengerMessage(type: MessengerType!, channelId: String, message: String): MessengerDispatchResult!
|
||||||
|
|
||||||
submitReadyOrder(input: SubmitReadyOrderInput!): Order!
|
submitReadyOrder(input: SubmitReadyOrderInput!): Order!
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ export default defineNuxtConfig({
|
|||||||
process.env.BACKEND_GRAPHQL_URL ||
|
process.env.BACKEND_GRAPHQL_URL ||
|
||||||
'http://localhost:4000/graphql',
|
'http://localhost:4000/graphql',
|
||||||
authCookieName,
|
authCookieName,
|
||||||
|
dadataApiToken: process.env.DADATA_API_TOKEN ?? '',
|
||||||
public: {
|
public: {
|
||||||
authCookieName,
|
authCookieName,
|
||||||
telegramBotUrl: process.env.NUXT_PUBLIC_TELEGRAM_BOT_URL ?? '',
|
telegramBotUrl: process.env.NUXT_PUBLIC_TELEGRAM_BOT_URL ?? '',
|
||||||
|
|||||||
45
server/api/dadata/bank.post.ts
Normal file
45
server/api/dadata/bank.post.ts
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
type DadataBankSuggestion = {
|
||||||
|
value: string;
|
||||||
|
unrestricted_value?: string;
|
||||||
|
data?: {
|
||||||
|
bic?: string;
|
||||||
|
correspondent_account?: string;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export default defineEventHandler(async (event) => {
|
||||||
|
const config = useRuntimeConfig(event);
|
||||||
|
const token = String(config.dadataApiToken || '').trim();
|
||||||
|
if (!token) {
|
||||||
|
throw createError({
|
||||||
|
statusCode: 500,
|
||||||
|
statusMessage: 'DADATA_API_TOKEN is not configured.',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const body = await readBody<{ query?: string }>(event);
|
||||||
|
const query = String(body?.query ?? '').trim();
|
||||||
|
if (query.length < 2) {
|
||||||
|
return { suggestions: [] as DadataBankSuggestion[] };
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await $fetch<{ suggestions?: DadataBankSuggestion[] }>(
|
||||||
|
'https://suggestions.dadata.ru/suggestions/api/4_1/rs/suggest/bank',
|
||||||
|
{
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
Accept: 'application/json',
|
||||||
|
Authorization: `Token ${token}`,
|
||||||
|
},
|
||||||
|
body: {
|
||||||
|
query,
|
||||||
|
count: 10,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
suggestions: response.suggestions ?? [],
|
||||||
|
};
|
||||||
|
});
|
||||||
53
server/api/dadata/party.post.ts
Normal file
53
server/api/dadata/party.post.ts
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
type DadataPartySuggestion = {
|
||||||
|
value: string;
|
||||||
|
unrestricted_value?: string;
|
||||||
|
data?: {
|
||||||
|
inn?: string;
|
||||||
|
kpp?: string;
|
||||||
|
ogrn?: string;
|
||||||
|
address?: {
|
||||||
|
value?: string;
|
||||||
|
};
|
||||||
|
management?: {
|
||||||
|
name?: string;
|
||||||
|
post?: string;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export default defineEventHandler(async (event) => {
|
||||||
|
const config = useRuntimeConfig(event);
|
||||||
|
const token = String(config.dadataApiToken || '').trim();
|
||||||
|
if (!token) {
|
||||||
|
throw createError({
|
||||||
|
statusCode: 500,
|
||||||
|
statusMessage: 'DADATA_API_TOKEN is not configured.',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const body = await readBody<{ query?: string }>(event);
|
||||||
|
const query = String(body?.query ?? '').trim();
|
||||||
|
if (query.length < 2) {
|
||||||
|
return { suggestions: [] as DadataPartySuggestion[] };
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await $fetch<{ suggestions?: DadataPartySuggestion[] }>(
|
||||||
|
'https://suggestions.dadata.ru/suggestions/api/4_1/rs/suggest/party',
|
||||||
|
{
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
Accept: 'application/json',
|
||||||
|
Authorization: `Token ${token}`,
|
||||||
|
},
|
||||||
|
body: {
|
||||||
|
query,
|
||||||
|
count: 10,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
suggestions: response.suggestions ?? [],
|
||||||
|
};
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user