Enhance contact management by adding surname input and updating customer handling
- Introduced a new input for capturing the surname of the user during contact addition. - Updated the contact parsing logic to include surname alongside name and phone number. - Modified the customer creation and update processes to accommodate surname, ensuring full name is used in confirmation messages. - Adjusted localization files to reflect the new surname input prompt and updated confirmation messages. - Refactored components to utilize a unified method for retrieving full customer names, improving consistency across the application.
This commit is contained in:
parent
9244eaec26
commit
c7648e8bf9
@ -79,9 +79,10 @@ msg-send-client-contact = 👤 Отправьте контакт пользов
|
||||
msg-send-client-contact-or-phone = 👤 Отправьте контакт пользователя или введите его номер телефона в сообщении
|
||||
msg-send-contact = Пожалуйста, отправьте контакт пользователя через кнопку Telegram
|
||||
msg-send-client-name = ✍️ Введите имя пользователя одним сообщением
|
||||
msg-send-client-surname = ✍️ Введите фамилию пользователя одним сообщением
|
||||
msg-invalid-name = ❌ Некорректное имя. Попробуйте еще раз
|
||||
msg-contact-added =
|
||||
✅ Добавили { $name } в список ваших контактов
|
||||
✅ Добавили { $fullname } в список ваших контактов
|
||||
|
||||
Пригласите пользователя в приложение, чтобы вы могли добавлять с ним записи
|
||||
msg-contact-forward = Перешлите пользователю следующее сообщение, чтобы он мог начать пользоваться ботом ⬇️
|
||||
|
||||
@ -2,6 +2,7 @@
|
||||
/* eslint-disable id-length */
|
||||
import { type Context } from '@/bot/context';
|
||||
import { KEYBOARD_SHARE_BOT, KEYBOARD_SHARE_PHONE } from '@/config/keyboards';
|
||||
import { parseContact } from '@/utils/contact';
|
||||
import { combine } from '@/utils/messages';
|
||||
import { isValidPhoneNumber, normalizePhoneNumber } from '@/utils/phone';
|
||||
import { type Conversation } from '@grammyjs/conversations';
|
||||
@ -36,12 +37,15 @@ export async function addContact(conversation: Conversation<Context, Context>, c
|
||||
const firstCtx = await conversation.wait();
|
||||
|
||||
let name = '';
|
||||
let surname = '';
|
||||
let phone = '';
|
||||
|
||||
if (firstCtx.message?.contact) {
|
||||
const { contact } = firstCtx.message;
|
||||
name = `${contact.first_name || ''} ${contact.last_name || ''}`.trim();
|
||||
phone = normalizePhoneNumber(contact.phone_number);
|
||||
const parsedContact = parseContact(contact);
|
||||
name = parsedContact.name;
|
||||
surname = parsedContact.surname;
|
||||
phone = normalizePhoneNumber(parsedContact.phone);
|
||||
} else if (firstCtx.message?.text) {
|
||||
const typedPhone = normalizePhoneNumber(firstCtx.message.text);
|
||||
if (!isValidPhoneNumber(typedPhone)) {
|
||||
@ -64,6 +68,16 @@ export async function addContact(conversation: Conversation<Context, Context>, c
|
||||
}
|
||||
|
||||
name = typedName;
|
||||
|
||||
// Просим ввести фамилию клиента
|
||||
await ctx.reply(await conversation.external(({ t }) => t('msg-send-client-surname')));
|
||||
const surnameCtx = await conversation.wait();
|
||||
const typedSurname = surnameCtx.message?.text?.trim() || '';
|
||||
if (!typedSurname) {
|
||||
return ctx.reply(await conversation.external(({ t }) => t('msg-invalid-surname')));
|
||||
}
|
||||
|
||||
surname = typedSurname;
|
||||
} else {
|
||||
return ctx.reply(await conversation.external(({ t }) => t('msg-send-client-contact-or-phone')));
|
||||
}
|
||||
@ -81,7 +95,9 @@ export async function addContact(conversation: Conversation<Context, Context>, c
|
||||
// Если клиента нет, создаём нового
|
||||
if (!documentId) {
|
||||
const registrationService = new RegistrationService();
|
||||
const createCustomerResult = await registrationService.createCustomer({ name, phone });
|
||||
const createCustomerResult = await registrationService.createCustomer({
|
||||
data: { name, phone, surname },
|
||||
});
|
||||
|
||||
documentId = createCustomerResult?.createCustomer?.documentId;
|
||||
if (!documentId) throw new Error('Клиент не создан');
|
||||
@ -92,7 +108,11 @@ export async function addContact(conversation: Conversation<Context, Context>, c
|
||||
await customerService.addInvitedBy({ data: { invitedBy }, documentId });
|
||||
|
||||
// Отправляем подтверждения и инструкции
|
||||
await ctx.reply(await conversation.external(({ t }) => t('msg-contact-added', { name })));
|
||||
await ctx.reply(
|
||||
await conversation.external(({ t }) =>
|
||||
t('msg-contact-added', { fullname: [name, surname].filter(Boolean).join(' ') }),
|
||||
),
|
||||
);
|
||||
await ctx.reply(await conversation.external(({ t }) => t('msg-contact-forward')));
|
||||
await ctx.reply(await conversation.external(({ t }) => t('msg-share-bot')), KEYBOARD_SHARE_BOT);
|
||||
} catch (error) {
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
import { type Context } from '@/bot/context';
|
||||
import { logHandle } from '@/bot/helpers/logging';
|
||||
import { KEYBOARD_REMOVE, mainMenu } from '@/config/keyboards';
|
||||
import { parseContact } from '@/utils/contact';
|
||||
import { isValidPhoneNumber, normalizePhoneNumber } from '@/utils/phone';
|
||||
import { CustomersService } from '@repo/graphql/api/customers';
|
||||
import { RegistrationService } from '@repo/graphql/api/registration';
|
||||
@ -14,7 +15,7 @@ const feature = composer.chatType('private');
|
||||
feature.on(':contact', logHandle('contact-registration'), async (ctx) => {
|
||||
const telegramId = ctx.from.id;
|
||||
const { contact } = ctx.message;
|
||||
const name = `${contact.first_name || ''} ${contact.last_name || ''}`.trim();
|
||||
const { name, surname } = parseContact(contact);
|
||||
|
||||
// Проверяем, не зарегистрирован ли уже пользователь
|
||||
const customerService = new CustomersService({ telegramId });
|
||||
@ -46,7 +47,7 @@ feature.on(':contact', logHandle('contact-registration'), async (ctx) => {
|
||||
if (customer && !customer.telegramId) {
|
||||
// Пользователь добавлен ранее мастером — обновляем данные
|
||||
await registrationService.updateCustomer({
|
||||
data: { active: true, name, telegramId },
|
||||
data: { active: true, name, surname, telegramId },
|
||||
documentId: customer.documentId,
|
||||
});
|
||||
|
||||
@ -56,7 +57,9 @@ feature.on(':contact', logHandle('contact-registration'), async (ctx) => {
|
||||
}
|
||||
|
||||
// Новый пользователь — создаём и активируем
|
||||
const response = await registrationService.createCustomer({ name, phone, telegramId });
|
||||
const response = await registrationService.createCustomer({
|
||||
data: { name, phone, surname, telegramId },
|
||||
});
|
||||
|
||||
const documentId = response?.createCustomer?.documentId;
|
||||
if (!documentId) return ctx.reply(ctx.t('err-generic'));
|
||||
|
||||
9
apps/bot/src/utils/contact.ts
Normal file
9
apps/bot/src/utils/contact.ts
Normal file
@ -0,0 +1,9 @@
|
||||
import { type Contact } from '@grammyjs/types';
|
||||
|
||||
export function parseContact(contact: Contact) {
|
||||
return {
|
||||
name: contact?.first_name?.trim() || '',
|
||||
phone: contact?.phone_number?.trim() || '',
|
||||
surname: contact?.last_name?.trim() || '',
|
||||
};
|
||||
}
|
||||
@ -12,6 +12,7 @@ import { Card } from '@repo/ui/components/ui/card';
|
||||
import { Label } from '@repo/ui/components/ui/label';
|
||||
import { LoadingSpinner } from '@repo/ui/components/ui/spinner';
|
||||
import { cn } from '@repo/ui/lib/utils';
|
||||
import { getCustomerFullName } from '@repo/utils/customer';
|
||||
import { sift } from 'radashi';
|
||||
|
||||
type ContactsGridProps = {
|
||||
@ -103,7 +104,7 @@ export function ContactsGridBase({
|
||||
isCurrentUser && 'font-bold',
|
||||
)}
|
||||
>
|
||||
{contact.name}
|
||||
{getCustomerFullName(contact)}
|
||||
</span>
|
||||
</Label>
|
||||
);
|
||||
@ -162,6 +163,9 @@ function useContacts() {
|
||||
return {
|
||||
isLoading,
|
||||
...query,
|
||||
contacts: [{ ...customer, name: 'Я' } as CustomerFieldsFragment, ...contacts],
|
||||
contacts: [
|
||||
{ ...customer, name: 'Я', surname: undefined } as CustomerFieldsFragment,
|
||||
...contacts,
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
@ -75,6 +75,13 @@ export function ProfileDataCard() {
|
||||
onChange={(value) => updateField('name', value)}
|
||||
value={customer?.name ?? ''}
|
||||
/>
|
||||
<TextField
|
||||
id="surname"
|
||||
key={`surname-${resetTrigger}`}
|
||||
label="Фамилия"
|
||||
onChange={(value) => updateField('surname', value)}
|
||||
value={customer?.surname ?? ''}
|
||||
/>
|
||||
<TextField disabled id="phone" label="Телефон" readOnly value={customer?.phone ?? ''} />
|
||||
<CheckboxWithText
|
||||
checked={customer.role !== 'client'}
|
||||
|
||||
@ -4,6 +4,7 @@ import { type ProfileProps } from './types';
|
||||
import { UserAvatar } from '@/components/shared/user-avatar';
|
||||
import { useCustomerQuery } from '@/hooks/api/customers';
|
||||
import { Card } from '@repo/ui/components/ui/card';
|
||||
import { getCustomerFullName } from '@repo/utils/customer';
|
||||
|
||||
export function PersonCard({ telegramId }: Readonly<ProfileProps>) {
|
||||
const { data: { customer } = {}, isLoading } = useCustomerQuery({ telegramId });
|
||||
@ -24,7 +25,7 @@ export function PersonCard({ telegramId }: Readonly<ProfileProps>) {
|
||||
<Card className="bg-transparent p-4 shadow-none">
|
||||
<div className="flex flex-col items-center space-y-2">
|
||||
<UserAvatar {...customer} size="lg" />
|
||||
<h2 className="text-2xl font-bold">{customer?.name}</h2>
|
||||
<h2 className="text-2xl font-bold">{getCustomerFullName(customer)}</h2>
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
|
||||
@ -2,6 +2,7 @@ import { UserAvatar } from './user-avatar';
|
||||
import type * as GQL from '@repo/graphql/types';
|
||||
import { Badge } from '@repo/ui/components/ui/badge';
|
||||
import { cn } from '@repo/ui/lib/utils';
|
||||
import { getCustomerFullName } from '@repo/utils/customer';
|
||||
import Link from 'next/link';
|
||||
import { memo } from 'react';
|
||||
|
||||
@ -28,7 +29,7 @@ export const ContactRow = memo(function ({ className, description, ...contact }:
|
||||
<div className={cn('flex items-center space-x-4 rounded-lg transition-colors')}>
|
||||
<UserAvatar {...contact} size="sm" />
|
||||
<div>
|
||||
<p className="font-medium">{contact.name}</p>
|
||||
<p className="font-medium">{getCustomerFullName(contact)}</p>
|
||||
{description && (
|
||||
<p className="max-w-52 truncate text-xs text-muted-foreground">{description}</p>
|
||||
)}
|
||||
|
||||
@ -6,6 +6,7 @@ import { useSubscriptionQuery } from '@/hooks/api/subscriptions';
|
||||
import AvatarPlaceholder from '@/public/avatar/avatar_placeholder.png';
|
||||
import { type CustomerFieldsFragment } from '@repo/graphql/types';
|
||||
import { cn } from '@repo/ui/lib/utils';
|
||||
import { getCustomerFullName } from '@repo/utils/customer';
|
||||
import Image from 'next/image';
|
||||
|
||||
type Sizes = 'lg' | 'md' | 'sm' | 'xs';
|
||||
@ -37,7 +38,7 @@ export function UserAvatar({ className, size = 'sm', telegramId = null }: UserAv
|
||||
)}
|
||||
>
|
||||
<Image
|
||||
alt={customer?.name || 'contact-avatar'}
|
||||
alt={customer ? getCustomerFullName(customer) : 'contact-avatar'}
|
||||
className="size-full rounded-full object-cover"
|
||||
height={80}
|
||||
src={customer?.photoUrl || AvatarPlaceholder}
|
||||
|
||||
@ -4,13 +4,21 @@ import * as GQL from '../types';
|
||||
import { type VariablesOf } from '@graphql-typed-document-node/core';
|
||||
import { isCustomerBanned } from '@repo/utils/customer';
|
||||
|
||||
const DEFAULT_CUSTOMER_ROLE = GQL.Enum_Customer_Role.Client;
|
||||
|
||||
export class RegistrationService {
|
||||
async createCustomer(variables: VariablesOf<typeof GQL.CreateCustomerDocument>) {
|
||||
const { mutate } = await getClientWithToken();
|
||||
|
||||
const mutationResult = await mutate({
|
||||
mutation: GQL.CreateCustomerDocument,
|
||||
variables,
|
||||
variables: {
|
||||
...variables,
|
||||
data: {
|
||||
...variables.data,
|
||||
role: DEFAULT_CUSTOMER_ROLE,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const error = mutationResult.errors?.at(0);
|
||||
|
||||
@ -3,6 +3,7 @@ fragment CustomerFields on Customer {
|
||||
bannedUntil
|
||||
documentId
|
||||
name
|
||||
surname
|
||||
phone
|
||||
photoUrl
|
||||
role
|
||||
@ -13,8 +14,8 @@ fragment CustomerFields on Customer {
|
||||
}
|
||||
}
|
||||
|
||||
mutation CreateCustomer($name: String!, $telegramId: Long, $phone: String) {
|
||||
createCustomer(data: { name: $name, telegramId: $telegramId, phone: $phone, role: client }) {
|
||||
mutation CreateCustomer($data: CustomerInput!) {
|
||||
createCustomer(data: $data) {
|
||||
documentId
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because one or more lines are too long
@ -1,7 +1,9 @@
|
||||
import * as GQL from '../../graphql/types';
|
||||
import type * as GQL from '../../graphql/types';
|
||||
|
||||
export function getCustomerFullName(customer: GQL.CustomerFieldsFragment) {
|
||||
return [customer?.name?.trim(), customer?.surname?.trim()].filter(Boolean).join(' ');
|
||||
}
|
||||
|
||||
export function isCustomerBanned(customer: GQL.CustomerFieldsFragment): boolean {
|
||||
return Boolean(customer.bannedUntil && new Date() < new Date(customer.bannedUntil));
|
||||
}
|
||||
|
||||
// isCustomerMaster удален - больше не нужен при равенстве пользователей
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user