Compare commits
12 Commits
main
...
fix/bugs-a
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b53a276540 | ||
|
|
e2d92ade16 | ||
|
|
76de850bf5 | ||
|
|
6eb421bfd4 | ||
|
|
0bfebce1e3 | ||
|
|
687142f9af | ||
|
|
b9371c34ad | ||
|
|
f6354d41f6 | ||
|
|
d587ea23b6 | ||
|
|
4983e7b36b | ||
|
|
7f86fc164d | ||
|
|
b7a217787d |
2
.github/workflows/deploy.yml
vendored
2
.github/workflows/deploy.yml
vendored
@ -16,6 +16,7 @@ jobs:
|
|||||||
- name: Create fake .env file for build
|
- name: Create fake .env file for build
|
||||||
run: |
|
run: |
|
||||||
echo "BOT_TOKEN=fake" > .env
|
echo "BOT_TOKEN=fake" > .env
|
||||||
|
echo "BOT_URL=fake" > .env
|
||||||
echo "LOGIN_GRAPHQL=fake" >> .env
|
echo "LOGIN_GRAPHQL=fake" >> .env
|
||||||
echo "PASSWORD_GRAPHQL=fake" >> .env
|
echo "PASSWORD_GRAPHQL=fake" >> .env
|
||||||
echo "URL_GRAPHQL=http://localhost/graphql" >> .env
|
echo "URL_GRAPHQL=http://localhost/graphql" >> .env
|
||||||
@ -64,6 +65,7 @@ jobs:
|
|||||||
- name: Create real .env file for production
|
- name: Create real .env file for production
|
||||||
run: |
|
run: |
|
||||||
echo "BOT_TOKEN=${{ secrets.BOT_TOKEN }}" > .env
|
echo "BOT_TOKEN=${{ secrets.BOT_TOKEN }}" > .env
|
||||||
|
echo "BOT_URL=${{ secrets.BOT_URL }}" > .env
|
||||||
echo "LOGIN_GRAPHQL=${{ secrets.LOGIN_GRAPHQL }}" >> .env
|
echo "LOGIN_GRAPHQL=${{ secrets.LOGIN_GRAPHQL }}" >> .env
|
||||||
echo "PASSWORD_GRAPHQL=${{ secrets.PASSWORD_GRAPHQL }}" >> .env
|
echo "PASSWORD_GRAPHQL=${{ secrets.PASSWORD_GRAPHQL }}" >> .env
|
||||||
echo "URL_GRAPHQL=${{ secrets.URL_GRAPHQL }}" >> .env
|
echo "URL_GRAPHQL=${{ secrets.URL_GRAPHQL }}" >> .env
|
||||||
|
|||||||
@ -3,6 +3,7 @@ import { z } from 'zod';
|
|||||||
|
|
||||||
export const envSchema = z.object({
|
export const envSchema = z.object({
|
||||||
BOT_TOKEN: z.string(),
|
BOT_TOKEN: z.string(),
|
||||||
|
BOT_URL: z.string(),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const env = envSchema.parse(process.env);
|
export const env = envSchema.parse(process.env);
|
||||||
|
|||||||
@ -4,11 +4,15 @@ import { env as environment } from './config/env';
|
|||||||
import {
|
import {
|
||||||
commandsList,
|
commandsList,
|
||||||
KEYBOARD_REMOVE,
|
KEYBOARD_REMOVE,
|
||||||
|
KEYBOARD_SHARE_BOT,
|
||||||
KEYBOARD_SHARE_PHONE,
|
KEYBOARD_SHARE_PHONE,
|
||||||
|
MESSAGE_CANCEL,
|
||||||
MESSAGE_NOT_MASTER,
|
MESSAGE_NOT_MASTER,
|
||||||
|
MESSAGE_SHARE_BOT,
|
||||||
MSG_ALREADY_MASTER,
|
MSG_ALREADY_MASTER,
|
||||||
MSG_BECOME_MASTER,
|
MSG_BECOME_MASTER,
|
||||||
MSG_CONTACT_ADDED,
|
MSG_CONTACT_ADDED,
|
||||||
|
MSG_CONTACT_FORWARD,
|
||||||
MSG_ERROR,
|
MSG_ERROR,
|
||||||
MSG_NEED_PHONE,
|
MSG_NEED_PHONE,
|
||||||
MSG_PHONE_SAVED,
|
MSG_PHONE_SAVED,
|
||||||
@ -16,13 +20,91 @@ import {
|
|||||||
MSG_WELCOME,
|
MSG_WELCOME,
|
||||||
MSG_WELCOME_BACK,
|
MSG_WELCOME_BACK,
|
||||||
} from './message';
|
} from './message';
|
||||||
|
import { isCustomerMaster } from './utils/customer';
|
||||||
import { normalizePhoneNumber } from './utils/phone';
|
import { normalizePhoneNumber } from './utils/phone';
|
||||||
import { CustomersService } from '@repo/graphql/api/customers';
|
import { CustomersService } from '@repo/graphql/api/customers';
|
||||||
import { Enum_Customer_Role } from '@repo/graphql/types';
|
import { Enum_Customer_Role } from '@repo/graphql/types';
|
||||||
import { Telegraf } from 'telegraf';
|
import { Scenes, session, Telegraf, type Context as TelegrafContext } from 'telegraf';
|
||||||
import { message } from 'telegraf/filters';
|
import { message } from 'telegraf/filters';
|
||||||
|
import {
|
||||||
|
type SceneContextScene,
|
||||||
|
type SceneSession,
|
||||||
|
type WizardContextWizard,
|
||||||
|
type WizardSessionData,
|
||||||
|
} from 'telegraf/typings/scenes';
|
||||||
|
|
||||||
const bot = new Telegraf(environment.BOT_TOKEN);
|
type BotContext = TelegrafContext & {
|
||||||
|
scene: SceneContextScene<BotContext, WizardSessionData>;
|
||||||
|
session: SceneSession<WizardSessionData>;
|
||||||
|
wizard: WizardContextWizard<BotContext>;
|
||||||
|
};
|
||||||
|
|
||||||
|
const bot = new Telegraf<BotContext>(environment.BOT_TOKEN);
|
||||||
|
|
||||||
|
const stage = new Scenes.Stage<BotContext>();
|
||||||
|
bot.use(session({ defaultSession: () => ({ __scenes: { cursor: 0, state: {} } }) }));
|
||||||
|
bot.use(stage.middleware());
|
||||||
|
|
||||||
|
const addContactScene = new Scenes.WizardScene<BotContext>(
|
||||||
|
'add-contact',
|
||||||
|
async (context) => {
|
||||||
|
await context.reply(MSG_SEND_CLIENT_CONTACT, { parse_mode: 'HTML' });
|
||||||
|
return context.wizard.next();
|
||||||
|
},
|
||||||
|
async (context) => {
|
||||||
|
if (!context.from) {
|
||||||
|
await context.reply('Ошибка: не удалось определить пользователя');
|
||||||
|
return context.scene.leave();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (context.message && 'text' in context.message && context.message.text === '/cancel') {
|
||||||
|
await context.reply(MESSAGE_CANCEL + commandsList, { parse_mode: 'HTML' });
|
||||||
|
return context.scene.leave();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!('message' in context && context.message && 'contact' in context.message)) {
|
||||||
|
await context.reply('Пожалуйста, отправьте контакт клиента через кнопку Telegram');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const telegramId = context.from.id;
|
||||||
|
const customerService = new CustomersService({ telegramId });
|
||||||
|
const { customer } = await customerService.getCustomer({ telegramId });
|
||||||
|
if (!customer || !isCustomerMaster(customer)) {
|
||||||
|
await context.reply(MESSAGE_NOT_MASTER, { parse_mode: 'HTML' });
|
||||||
|
return context.scene.leave();
|
||||||
|
}
|
||||||
|
|
||||||
|
const { contact } = context.message;
|
||||||
|
const name = (contact.first_name || '') + ' ' + (contact.last_name || '').trim();
|
||||||
|
const phone = normalizePhoneNumber(contact.phone_number);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const { customer: existingCustomer } = await customerService.getCustomer({ phone });
|
||||||
|
|
||||||
|
let documentId = existingCustomer?.documentId;
|
||||||
|
|
||||||
|
if (!documentId) {
|
||||||
|
const createCustomerResult = await customerService.createCustomer({ name, phone });
|
||||||
|
documentId = createCustomerResult?.createCustomer?.documentId;
|
||||||
|
if (!documentId) throw new Error('Customer not created');
|
||||||
|
}
|
||||||
|
|
||||||
|
const masters = [customer.documentId];
|
||||||
|
await customerService.addMasters({ data: { masters }, documentId });
|
||||||
|
await context.reply(MSG_CONTACT_ADDED(name), { parse_mode: 'HTML' });
|
||||||
|
await context.reply(MSG_CONTACT_FORWARD, { parse_mode: 'HTML' });
|
||||||
|
await context.reply(MESSAGE_SHARE_BOT, { ...KEYBOARD_SHARE_BOT, parse_mode: 'HTML' });
|
||||||
|
} catch (error) {
|
||||||
|
await context.reply(MSG_ERROR(error), { parse_mode: 'HTML' });
|
||||||
|
} finally {
|
||||||
|
await context.reply(commandsList, { ...KEYBOARD_REMOVE, parse_mode: 'HTML' });
|
||||||
|
context.scene.leave();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
stage.register(addContactScene);
|
||||||
|
|
||||||
bot.start(async (context) => {
|
bot.start(async (context) => {
|
||||||
const telegramId = context.from.id;
|
const telegramId = context.from.id;
|
||||||
@ -40,21 +122,23 @@ bot.start(async (context) => {
|
|||||||
return context.reply(MSG_WELCOME, { ...KEYBOARD_SHARE_PHONE, parse_mode: 'HTML' });
|
return context.reply(MSG_WELCOME, { ...KEYBOARD_SHARE_PHONE, parse_mode: 'HTML' });
|
||||||
});
|
});
|
||||||
|
|
||||||
|
bot.command('help', async (context) => {
|
||||||
|
return context.reply(commandsList, { ...KEYBOARD_REMOVE, parse_mode: 'HTML' });
|
||||||
|
});
|
||||||
|
|
||||||
bot.command('addcontact', async (context) => {
|
bot.command('addcontact', async (context) => {
|
||||||
const telegramId = context.from.id;
|
const telegramId = context.from.id;
|
||||||
|
|
||||||
const customerService = new CustomersService({ telegramId });
|
const customerService = new CustomersService({ telegramId });
|
||||||
const { customer } = await customerService.getCustomer({ telegramId });
|
const { customer } = await customerService.getCustomer({ telegramId });
|
||||||
|
|
||||||
if (!customer) {
|
if (!customer) {
|
||||||
return context.reply(MSG_NEED_PHONE, { ...KEYBOARD_SHARE_PHONE, parse_mode: 'HTML' });
|
return context.reply(MSG_NEED_PHONE, { ...KEYBOARD_SHARE_PHONE, parse_mode: 'HTML' });
|
||||||
}
|
}
|
||||||
|
|
||||||
if (customer.role !== Enum_Customer_Role.Master) {
|
if (!isCustomerMaster(customer)) {
|
||||||
return context.reply(MESSAGE_NOT_MASTER, { parse_mode: 'HTML' });
|
return context.reply(MESSAGE_NOT_MASTER, { parse_mode: 'HTML' });
|
||||||
}
|
}
|
||||||
|
|
||||||
return context.reply(MSG_SEND_CLIENT_CONTACT, { parse_mode: 'HTML' });
|
return context.scene.enter('add-contact');
|
||||||
});
|
});
|
||||||
|
|
||||||
bot.command('becomemaster', async (context) => {
|
bot.command('becomemaster', async (context) => {
|
||||||
@ -67,7 +151,7 @@ bot.command('becomemaster', async (context) => {
|
|||||||
return context.reply(MSG_NEED_PHONE, { ...KEYBOARD_SHARE_PHONE, parse_mode: 'HTML' });
|
return context.reply(MSG_NEED_PHONE, { ...KEYBOARD_SHARE_PHONE, parse_mode: 'HTML' });
|
||||||
}
|
}
|
||||||
|
|
||||||
if (customer.role === Enum_Customer_Role.Master) {
|
if (isCustomerMaster(customer)) {
|
||||||
return context.reply(MSG_ALREADY_MASTER, { parse_mode: 'HTML' });
|
return context.reply(MSG_ALREADY_MASTER, { parse_mode: 'HTML' });
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -86,60 +170,32 @@ bot.command('becomemaster', async (context) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
bot.command('sharebot', async (context) => {
|
||||||
|
await context.reply(MSG_CONTACT_FORWARD, { parse_mode: 'HTML' });
|
||||||
|
await context.reply(MESSAGE_SHARE_BOT, { ...KEYBOARD_SHARE_BOT, parse_mode: 'HTML' });
|
||||||
|
});
|
||||||
|
|
||||||
bot.on(message('contact'), async (context) => {
|
bot.on(message('contact'), async (context) => {
|
||||||
const telegramId = context.from.id;
|
const telegramId = context.from.id;
|
||||||
|
|
||||||
const customerService = new CustomersService({ telegramId });
|
const customerService = new CustomersService({ telegramId });
|
||||||
const { customer } = await customerService.getCustomer({ telegramId });
|
const { customer } = await customerService.getCustomer({ telegramId });
|
||||||
|
|
||||||
const isRegistration = !customer;
|
if (!customer) {
|
||||||
|
|
||||||
const { contact } = context.message;
|
const { contact } = context.message;
|
||||||
const name = (contact.first_name || '') + ' ' + (contact.last_name || '').trim();
|
const name = (contact.first_name || '') + ' ' + (contact.last_name || '').trim();
|
||||||
const phone = normalizePhoneNumber(contact.phone_number);
|
const phone = normalizePhoneNumber(contact.phone_number);
|
||||||
|
|
||||||
if (isRegistration) {
|
|
||||||
const response = await customerService
|
const response = await customerService
|
||||||
.createCustomer({
|
.createCustomer({ name, phone, telegramId: context.from.id })
|
||||||
name,
|
|
||||||
phone,
|
|
||||||
telegramId: context.from.id,
|
|
||||||
})
|
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
context.reply(MSG_ERROR(error), { parse_mode: 'HTML' });
|
context.reply(MSG_ERROR(error), { parse_mode: 'HTML' });
|
||||||
});
|
});
|
||||||
|
|
||||||
if (response) {
|
if (response) {
|
||||||
return context.reply(MSG_PHONE_SAVED + commandsList, {
|
return context.reply(MSG_PHONE_SAVED + commandsList, {
|
||||||
...KEYBOARD_REMOVE,
|
...KEYBOARD_REMOVE,
|
||||||
parse_mode: 'HTML',
|
parse_mode: 'HTML',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
if (customer.role !== Enum_Customer_Role.Master) {
|
|
||||||
return context.reply(MESSAGE_NOT_MASTER, { parse_mode: 'HTML' });
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const createCustomerResult = await customerService.createCustomer({ name, phone });
|
|
||||||
|
|
||||||
const documentId = createCustomerResult?.createCustomer?.documentId;
|
|
||||||
|
|
||||||
if (!documentId) {
|
|
||||||
throw new Error('Customer not created');
|
|
||||||
}
|
|
||||||
|
|
||||||
const masters = [customer.documentId];
|
|
||||||
|
|
||||||
await customerService.addMasters({
|
|
||||||
data: { masters },
|
|
||||||
documentId,
|
|
||||||
});
|
|
||||||
|
|
||||||
return context.reply(MSG_CONTACT_ADDED(name), { parse_mode: 'HTML' });
|
|
||||||
} catch (error) {
|
|
||||||
context.reply(MSG_ERROR(error), { parse_mode: 'HTML' });
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@ -1,9 +1,12 @@
|
|||||||
|
import { env as environment } from './config/env';
|
||||||
import { type ReplyKeyboardRemove } from 'telegraf/types';
|
import { type ReplyKeyboardRemove } from 'telegraf/types';
|
||||||
|
|
||||||
export const commandsList = `
|
export const commandsList = `
|
||||||
\n<b>📋 Доступные команды:</b>
|
\n<b>📋 Доступные команды:</b>
|
||||||
• <b>/addcontact</b> — добавить контакт клиента
|
• <b>/addcontact</b> — добавить контакт клиента
|
||||||
• <b>/becomemaster</b> — стать мастером
|
• <b>/becomemaster</b> — стать мастером
|
||||||
|
• <b>/sharebot</b> — поделиться ботом
|
||||||
|
• <b>/help</b> — список команд
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const KEYBOARD_SHARE_PHONE = {
|
export const KEYBOARD_SHARE_PHONE = {
|
||||||
@ -26,30 +29,51 @@ export const KEYBOARD_REMOVE = {
|
|||||||
} as ReplyKeyboardRemove,
|
} as ReplyKeyboardRemove,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const KEYBOARD_SHARE_BOT = {
|
||||||
|
reply_markup: {
|
||||||
|
inline_keyboard: [
|
||||||
|
[
|
||||||
|
{
|
||||||
|
text: '🤖Воспользоваться ботом',
|
||||||
|
url: environment.BOT_URL,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
export const MESSAGE_NOT_MASTER =
|
export const MESSAGE_NOT_MASTER =
|
||||||
'⛔️ <b>Только мастер может добавлять контакты.</b>\nСтать мастером можно на странице профиля в приложении или с помощью команды <b>/becomemaster</b>.';
|
'⛔️ <b>Только мастер может добавлять контакты</b>\nСтать мастером можно на странице профиля в приложении или с помощью команды <b>/becomemaster</b>';
|
||||||
|
|
||||||
export const MSG_WELCOME =
|
export const MSG_WELCOME =
|
||||||
'👋 <b>Добро пожаловать!</b>\nПожалуйста, поделитесь своим номером телефона для регистрации.';
|
'👋 <b>Добро пожаловать!</b>\nПожалуйста, поделитесь своим номером телефона для регистрации';
|
||||||
|
|
||||||
export const MSG_WELCOME_BACK = (name: string) =>
|
export const MSG_WELCOME_BACK = (name: string) =>
|
||||||
`👋 <b>С возвращением, ${name}!</b>\nЧтобы воспользоваться сервисом, откройте приложение.\n`;
|
`👋 <b>С возвращением, ${name}!</b>\nЧтобы воспользоваться сервисом, откройте приложение.\n`;
|
||||||
|
|
||||||
export const MSG_NEED_PHONE =
|
export const MSG_NEED_PHONE =
|
||||||
'📱 <b>Чтобы добавить контакт, сначала поделитесь своим номером телефона.</b>';
|
'📱 <b>Чтобы добавить контакт, сначала поделитесь своим номером телефона</b>';
|
||||||
|
|
||||||
export const MSG_SEND_CLIENT_CONTACT =
|
export const MSG_SEND_CLIENT_CONTACT =
|
||||||
'👤 <b>Отправьте контакт клиента, которого вы хотите добавить.</b>';
|
'👤 <b>Отправьте контакт клиента, которого вы хотите добавить. \n<em>Для отмены операции используйте команду /cancel</em></b>';
|
||||||
|
|
||||||
export const MSG_ALREADY_MASTER = '🎉 <b>Вы уже являетесь мастером!</b>';
|
export const MSG_ALREADY_MASTER = '🎉 <b>Вы уже являетесь мастером!</b>';
|
||||||
|
|
||||||
export const MSG_BECOME_MASTER = '🥳 <b>Поздравляем! Теперь вы мастер.</b>';
|
export const MSG_BECOME_MASTER = '🥳 <b>Поздравляем! Теперь вы мастер</b>';
|
||||||
|
|
||||||
export const MSG_ERROR = (error?: unknown) =>
|
export const MSG_ERROR = (error?: unknown) =>
|
||||||
`❌ <b>Произошла ошибка.</b>\n${error ? String(error) : ''}`;
|
`❌ <b>Произошла ошибка</b>\n${error ? String(error) : ''}`;
|
||||||
|
|
||||||
export const MSG_PHONE_SAVED =
|
export const MSG_PHONE_SAVED =
|
||||||
'✅ <b>Спасибо! Мы сохранили ваш номер телефона.</b>\nТеперь вы можете открыть приложение или воспользоваться командами бота.';
|
'✅ <b>Спасибо! Мы сохранили ваш номер телефона</b>\nТеперь вы можете открыть приложение или воспользоваться командами бота';
|
||||||
|
|
||||||
export const MSG_CONTACT_ADDED = (name: string) =>
|
export const MSG_CONTACT_ADDED = (name: string) =>
|
||||||
`✅ <b>Добавили контакт:</b> <b>${name}</b>\nПригласите пользователя в приложение, чтобы вы могли добавлять записи с этим контактом.`;
|
`✅ <b>Добавили контакт в список ваших клиентов</b>\n\nИмя: <b>${name}</b>\n\nПригласите клиента в приложение, чтобы вы могли добавлять записи с этим контактом`;
|
||||||
|
|
||||||
|
export const MSG_CONTACT_FORWARD =
|
||||||
|
'<em>Перешлите пользователю следующее сообщение, чтобы он мог начать пользоваться ботом ⬇️</em>';
|
||||||
|
|
||||||
|
export const MESSAGE_SHARE_BOT =
|
||||||
|
'📅 <b>Воспользуйтесь этим ботом для записи к вашему мастеру!</b>\nНажмите кнопку ниже, чтобы начать';
|
||||||
|
|
||||||
|
export const MESSAGE_CANCEL = '<b>❌ Отменена операции</b>';
|
||||||
|
|||||||
5
apps/bot/src/utils/customer.ts
Normal file
5
apps/bot/src/utils/customer.ts
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
import * as GQL from '@repo/graphql/types';
|
||||||
|
|
||||||
|
export function isCustomerMaster(customer: GQL.CustomerFieldsFragment) {
|
||||||
|
return customer?.role === GQL.Enum_Customer_Role.Master;
|
||||||
|
}
|
||||||
@ -1,7 +1,12 @@
|
|||||||
import { getCustomer } from '@/actions/api/customers';
|
import { getCustomer } from '@/actions/api/customers';
|
||||||
import { Container } from '@/components/layout';
|
import { Container } from '@/components/layout';
|
||||||
import { PageHeader } from '@/components/navigation';
|
import { PageHeader } from '@/components/navigation';
|
||||||
import { ContactDataCard, PersonCard, ProfileOrdersList } from '@/components/profile';
|
import {
|
||||||
|
BookContactButton,
|
||||||
|
ContactDataCard,
|
||||||
|
PersonCard,
|
||||||
|
ProfileOrdersList,
|
||||||
|
} from '@/components/profile';
|
||||||
import { dehydrate, HydrationBoundary, QueryClient } from '@tanstack/react-query';
|
import { dehydrate, HydrationBoundary, QueryClient } from '@tanstack/react-query';
|
||||||
|
|
||||||
type Props = { params: Promise<{ telegramId: string }> };
|
type Props = { params: Promise<{ telegramId: string }> };
|
||||||
@ -24,6 +29,7 @@ export default async function ProfilePage(props: Readonly<Props>) {
|
|||||||
<PersonCard telegramId={telegramId} />
|
<PersonCard telegramId={telegramId} />
|
||||||
<ContactDataCard telegramId={telegramId} />
|
<ContactDataCard telegramId={telegramId} />
|
||||||
<ProfileOrdersList telegramId={telegramId} />
|
<ProfileOrdersList telegramId={telegramId} />
|
||||||
|
<BookContactButton telegramId={telegramId} />
|
||||||
</Container>
|
</Container>
|
||||||
</HydrationBoundary>
|
</HydrationBoundary>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
|
import { DataNotFound } from '../shared/alert';
|
||||||
import { ContactRow } from '../shared/contact-row';
|
import { ContactRow } from '../shared/contact-row';
|
||||||
import { useCustomerContacts } from '@/hooks/api/contacts';
|
import { useCustomerContacts } from '@/hooks/api/contacts';
|
||||||
import { LoadingSpinner } from '@repo/ui/components/ui/spinner';
|
import { LoadingSpinner } from '@repo/ui/components/ui/spinner';
|
||||||
@ -9,7 +10,7 @@ export function ContactsList() {
|
|||||||
|
|
||||||
if (isLoading) return <LoadingSpinner />;
|
if (isLoading) return <LoadingSpinner />;
|
||||||
|
|
||||||
if (!contacts.length) return <div>Контакты не найдены</div>;
|
if (!contacts.length) return <DataNotFound title="Контакты не найдены" />;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
|
|||||||
@ -19,6 +19,7 @@ export function OrderButtons({ documentId }: Readonly<OrderComponentProps>) {
|
|||||||
|
|
||||||
if (!order) return null;
|
if (!order) return null;
|
||||||
|
|
||||||
|
const isCreated = order?.state === Enum_Order_State.Created;
|
||||||
const isApproved = order?.state === Enum_Order_State.Approved;
|
const isApproved = order?.state === Enum_Order_State.Approved;
|
||||||
const isCompleted = order?.state === Enum_Order_State.Completed;
|
const isCompleted = order?.state === Enum_Order_State.Completed;
|
||||||
const isCancelling = order?.state === Enum_Order_State.Cancelling;
|
const isCancelling = order?.state === Enum_Order_State.Cancelling;
|
||||||
@ -51,20 +52,11 @@ export function OrderButtons({ documentId }: Readonly<OrderComponentProps>) {
|
|||||||
return (
|
return (
|
||||||
<FloatingActionPanel
|
<FloatingActionPanel
|
||||||
isLoading={isPending}
|
isLoading={isPending}
|
||||||
onCancel={
|
onCancel={isCreated || (isMaster && isCancelling) || isApproved ? handleCancel : undefined}
|
||||||
isCancelled || (!isMaster && isCancelling) || isCompleted ? undefined : () => handleCancel()
|
onComplete={isMaster && isApproved ? handleOnComplete : undefined}
|
||||||
}
|
onConfirm={isMaster && isCreated ? handleApprove : undefined}
|
||||||
onComplete={isApproved && isMaster ? handleOnComplete : undefined}
|
|
||||||
onConfirm={
|
|
||||||
!isMaster ||
|
|
||||||
isApproved ||
|
|
||||||
(!isMaster && isCancelled) ||
|
|
||||||
(!isMaster && isCancelling) ||
|
|
||||||
isCompleted
|
|
||||||
? undefined
|
|
||||||
: () => handleApprove()
|
|
||||||
}
|
|
||||||
onRepeat={isCancelled || isCompleted ? handleOnRepeat : undefined}
|
onRepeat={isCancelled || isCompleted ? handleOnRepeat : undefined}
|
||||||
|
onReturn={isMaster && isCancelled ? handleApprove : undefined}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,33 +1,25 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
|
import { DataNotFound } from '@/components/shared/alert';
|
||||||
import { useServicesQuery } from '@/hooks/api/services';
|
import { useServicesQuery } from '@/hooks/api/services';
|
||||||
import { useOrderStore } from '@/stores/order';
|
import { useOrderStore } from '@/stores/order';
|
||||||
import { type ServiceFieldsFragment } from '@repo/graphql/types';
|
import { type ServiceFieldsFragment } from '@repo/graphql/types';
|
||||||
import { cn } from '@repo/ui/lib/utils';
|
import { cn } from '@repo/ui/lib/utils';
|
||||||
|
import { formatTime } from '@repo/utils/datetime-format';
|
||||||
|
|
||||||
export function ServiceSelect() {
|
export function ServiceSelect() {
|
||||||
const masterId = useOrderStore((store) => store.masterId);
|
const { data: { services } = {} } = useServicesQuery({});
|
||||||
|
|
||||||
const { data: { services } = {} } = useServicesQuery({
|
if (!services?.length) return <DataNotFound title="Услуги не найдены" />;
|
||||||
filters: {
|
|
||||||
master: {
|
|
||||||
documentId: {
|
|
||||||
eq: masterId,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!services?.length) return null;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div className="space-y-2 px-6">
|
||||||
{services.map((service) => service && <ServiceCard key={service.documentId} {...service} />)}
|
{services.map((service) => service && <ServiceCard key={service.documentId} {...service} />)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function ServiceCard({ documentId, name }: Readonly<ServiceFieldsFragment>) {
|
function ServiceCard({ documentId, duration, name }: Readonly<ServiceFieldsFragment>) {
|
||||||
const serviceId = useOrderStore((store) => store.serviceId);
|
const serviceId = useOrderStore((store) => store.serviceId);
|
||||||
const setServiceId = useOrderStore((store) => store.setServiceId);
|
const setServiceId = useOrderStore((store) => store.setServiceId);
|
||||||
|
|
||||||
@ -40,7 +32,7 @@ function ServiceCard({ documentId, name }: Readonly<ServiceFieldsFragment>) {
|
|||||||
return (
|
return (
|
||||||
<label
|
<label
|
||||||
className={cn(
|
className={cn(
|
||||||
'flex items-center justify-between border-2 rounded-2xl bg-background p-4 px-6 cursor-pointer dark:bg-primary/5',
|
'flex items-center justify-between border-2 rounded-2xl bg-background p-4 cursor-pointer dark:bg-primary/5',
|
||||||
selected ? 'border-primary' : 'border-transparent',
|
selected ? 'border-primary' : 'border-transparent',
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
@ -52,9 +44,23 @@ function ServiceCard({ documentId, name }: Readonly<ServiceFieldsFragment>) {
|
|||||||
type="radio"
|
type="radio"
|
||||||
value={documentId}
|
value={documentId}
|
||||||
/>
|
/>
|
||||||
<div className="flex flex-col gap-2">
|
<div className="flex w-full items-center justify-between gap-2">
|
||||||
{name}
|
<span className="text-base font-semibold text-foreground">{name}</span>
|
||||||
{/* <span className={cn('text-xs font-normal', 'text-muted-foreground')} /> */}
|
<span
|
||||||
|
className={cn(
|
||||||
|
'inline-flex items-center gap-1 px-4 p-2 rounded-full bg-secondary dark:bg-background text-primary text-xs font-medium',
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
className="size-4 opacity-70"
|
||||||
|
fill="currentColor"
|
||||||
|
viewBox="0 0 16 16"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<path d="M8 1.5a6.5 6.5 0 1 0 0 13a6.5 6.5 0 0 0 0-13ZM2.5 8a5.5 5.5 0 1 1 11 0a5.5 5.5 0 0 1-11 0Zm6-2.75a.5.5 0 0 0-1 0v3c0 .28.22.5.5.5h2a.5.5 0 0 0 0-1H8.5V5.25Z" />
|
||||||
|
</svg>
|
||||||
|
{formatTime(duration).user()}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</label>
|
</label>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -4,6 +4,62 @@ import { OrderCard } from '../shared/order-card';
|
|||||||
import { type ProfileProps } from './types';
|
import { type ProfileProps } from './types';
|
||||||
import { useCustomerQuery, useIsMaster } from '@/hooks/api/customers';
|
import { useCustomerQuery, useIsMaster } from '@/hooks/api/customers';
|
||||||
import { useOrdersQuery } from '@/hooks/api/orders';
|
import { useOrdersQuery } from '@/hooks/api/orders';
|
||||||
|
import { usePushWithData } from '@/hooks/url';
|
||||||
|
import { CalendarPlus } from 'lucide-react';
|
||||||
|
|
||||||
|
export function BookContactButton({ telegramId }: Readonly<ProfileProps>) {
|
||||||
|
const { data: { customer } = {}, isLoading: isCustomerLoading } = useCustomerQuery();
|
||||||
|
const isMaster = useIsMaster();
|
||||||
|
|
||||||
|
const { data: { customer: profile } = {}, isLoading: isProfileLoading } = useCustomerQuery({
|
||||||
|
telegramId,
|
||||||
|
});
|
||||||
|
|
||||||
|
const push = usePushWithData();
|
||||||
|
|
||||||
|
const handleBook = () => {
|
||||||
|
if (!profile || !customer) return;
|
||||||
|
|
||||||
|
if (isMaster) {
|
||||||
|
push('/orders/add', {
|
||||||
|
client: {
|
||||||
|
documentId: profile.documentId,
|
||||||
|
},
|
||||||
|
slot: {
|
||||||
|
master: {
|
||||||
|
documentId: customer.documentId,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
push('/orders/add', {
|
||||||
|
client: {
|
||||||
|
documentId: customer.documentId,
|
||||||
|
},
|
||||||
|
slot: {
|
||||||
|
master: {
|
||||||
|
documentId: profile.documentId,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (isCustomerLoading || isProfileLoading || !profile || !customer) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col items-center justify-center">
|
||||||
|
<button
|
||||||
|
className="flex items-center justify-center gap-2 self-center rounded-xl bg-primary/5 px-6 py-2 font-semibold text-primary shadow-sm transition-colors hover:bg-primary/10"
|
||||||
|
onClick={handleBook}
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
|
<CalendarPlus className="size-5 text-primary" />
|
||||||
|
<span>{isMaster ? 'Записать' : 'Записаться'}</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export function ProfileOrdersList({ telegramId }: Readonly<ProfileProps>) {
|
export function ProfileOrdersList({ telegramId }: Readonly<ProfileProps>) {
|
||||||
const { data: { customer } = {} } = useCustomerQuery();
|
const { data: { customer } = {} } = useCustomerQuery();
|
||||||
|
|||||||
@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
import { Button } from '@repo/ui/components/ui/button';
|
import { Button } from '@repo/ui/components/ui/button';
|
||||||
import { Card } from '@repo/ui/components/ui/card';
|
import { Card } from '@repo/ui/components/ui/card';
|
||||||
import { Ban, Check, Lock, RotateCcw, Trash2, Unlock } from 'lucide-react';
|
import { Ban, Check, Lock, RotateCcw, Trash2, Undo, Unlock } from 'lucide-react';
|
||||||
|
|
||||||
type FloatingActionPanelProps = {
|
type FloatingActionPanelProps = {
|
||||||
readonly isLoading?: boolean;
|
readonly isLoading?: boolean;
|
||||||
@ -12,6 +12,7 @@ type FloatingActionPanelProps = {
|
|||||||
readonly onConfirm?: () => void;
|
readonly onConfirm?: () => void;
|
||||||
readonly onDelete?: () => void;
|
readonly onDelete?: () => void;
|
||||||
readonly onRepeat?: () => void;
|
readonly onRepeat?: () => void;
|
||||||
|
readonly onReturn?: () => void;
|
||||||
readonly onToggle?: () => void;
|
readonly onToggle?: () => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -23,10 +24,12 @@ export default function FloatingActionPanel({
|
|||||||
onConfirm,
|
onConfirm,
|
||||||
onDelete,
|
onDelete,
|
||||||
onRepeat,
|
onRepeat,
|
||||||
|
onReturn,
|
||||||
onToggle,
|
onToggle,
|
||||||
}: FloatingActionPanelProps) {
|
}: FloatingActionPanelProps) {
|
||||||
// Если не переданы обработчики, скрываем панель
|
// Если не переданы обработчики, скрываем панель
|
||||||
if (!onCancel && !onConfirm && !onDelete && !onComplete && !onRepeat && !onToggle) return null;
|
if (!onCancel && !onConfirm && !onDelete && !onComplete && !onRepeat && !onToggle && !onReturn)
|
||||||
|
return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card className="fixed inset-x-4 bottom-4 z-50 rounded-3xl border-0 bg-background/95 p-4 shadow-2xl backdrop-blur-sm dark:bg-primary/5 md:bottom-6 md:left-auto md:right-6 md:p-6">
|
<Card className="fixed inset-x-4 bottom-4 z-50 rounded-3xl border-0 bg-background/95 p-4 shadow-2xl backdrop-blur-sm dark:bg-primary/5 md:bottom-6 md:left-auto md:right-6 md:p-6">
|
||||||
@ -124,6 +127,19 @@ export default function FloatingActionPanel({
|
|||||||
<span>Подтвердить</span>
|
<span>Подтвердить</span>
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* Кнопка вернуть */}
|
||||||
|
{onReturn && (
|
||||||
|
<Button
|
||||||
|
className="w-full rounded-2xl bg-blue-400 text-sm text-white transition-all duration-200 hover:bg-blue-500 dark:bg-blue-500 dark:hover:bg-blue-600 sm:w-auto"
|
||||||
|
disabled={isLoading}
|
||||||
|
onClick={onReturn}
|
||||||
|
size="sm"
|
||||||
|
>
|
||||||
|
<Undo className="mr-2 size-4" />
|
||||||
|
<span>Вернуть</span>
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
);
|
);
|
||||||
|
|||||||
12
apps/web/components/shared/alert.tsx
Normal file
12
apps/web/components/shared/alert.tsx
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
import { AlertCircleIcon } from 'lucide-react';
|
||||||
|
|
||||||
|
export function DataNotFound({ title }: { readonly title: string }) {
|
||||||
|
return (
|
||||||
|
<div className="p-4">
|
||||||
|
<div className="line-clamp-1 flex items-center justify-center text-sm font-medium tracking-tight text-secondary-foreground">
|
||||||
|
<AlertCircleIcon className="mr-2 size-4" />
|
||||||
|
<span className="line-clamp-1 font-medium tracking-tight">{title}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -35,7 +35,7 @@
|
|||||||
"next-auth": "^4.24.11",
|
"next-auth": "^4.24.11",
|
||||||
"next-intl": "^3.26.0",
|
"next-intl": "^3.26.0",
|
||||||
"next-themes": "^0.4.4",
|
"next-themes": "^0.4.4",
|
||||||
"next": "15.3.0",
|
"next": "15.3.4",
|
||||||
"postcss": "catalog:",
|
"postcss": "catalog:",
|
||||||
"radashi": "catalog:",
|
"radashi": "catalog:",
|
||||||
"react-dom": "catalog:",
|
"react-dom": "catalog:",
|
||||||
|
|||||||
@ -4,7 +4,7 @@ import { useOrderStore } from './context';
|
|||||||
import { type Steps } from './types';
|
import { type Steps } from './types';
|
||||||
import { useCustomerQuery, useIsMaster } from '@/hooks/api/customers';
|
import { useCustomerQuery, useIsMaster } from '@/hooks/api/customers';
|
||||||
import { type OrderFieldsFragment } from '@repo/graphql/types';
|
import { type OrderFieldsFragment } from '@repo/graphql/types';
|
||||||
import { useCallback, useEffect, useRef } from 'react';
|
import { useEffect, useRef } from 'react';
|
||||||
|
|
||||||
const STEPS: Steps[] = [
|
const STEPS: Steps[] = [
|
||||||
'master-select',
|
'master-select',
|
||||||
@ -18,7 +18,6 @@ export const CLIENT_STEPS: Steps[] = STEPS.filter((step) => step !== 'client-sel
|
|||||||
|
|
||||||
export function useInitOrderStore(initData: null | OrderFieldsFragment) {
|
export function useInitOrderStore(initData: null | OrderFieldsFragment) {
|
||||||
const initialized = useRef(false);
|
const initialized = useRef(false);
|
||||||
|
|
||||||
const { data: { customer } = {} } = useCustomerQuery();
|
const { data: { customer } = {} } = useCustomerQuery();
|
||||||
const isMaster = useIsMaster();
|
const isMaster = useIsMaster();
|
||||||
|
|
||||||
@ -29,19 +28,17 @@ export function useInitOrderStore(initData: null | OrderFieldsFragment) {
|
|||||||
const setStepsSequence = useOrderStore((store) => store._setStepSequence);
|
const setStepsSequence = useOrderStore((store) => store._setStepSequence);
|
||||||
const step = useOrderStore((store) => store.step);
|
const step = useOrderStore((store) => store.step);
|
||||||
|
|
||||||
const setFirstStep = useCallback(() => {
|
|
||||||
const steps = isMaster ? MASTER_STEPS : CLIENT_STEPS;
|
|
||||||
setStepsSequence(steps);
|
|
||||||
setStep(steps[0] as Steps);
|
|
||||||
}, [isMaster, setStep, setStepsSequence]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (initialized.current || !customer || step !== 'loading') return;
|
if (initialized.current || !customer || step !== 'loading') return;
|
||||||
|
|
||||||
|
const steps = isMaster ? MASTER_STEPS : CLIENT_STEPS;
|
||||||
|
setStepsSequence(steps);
|
||||||
|
|
||||||
|
// Инициализация из initData (например, для повторного заказа)
|
||||||
if (initData) {
|
if (initData) {
|
||||||
const masterId = initData.slot?.master?.documentId;
|
const masterId = initData.slot?.master?.documentId;
|
||||||
const clientId = initData.client?.documentId;
|
const clientId = initData.client?.documentId;
|
||||||
const serviceId = initData.services[0]?.documentId;
|
const serviceId = initData.services?.[0]?.documentId;
|
||||||
|
|
||||||
if (masterId) setMasterId(masterId);
|
if (masterId) setMasterId(masterId);
|
||||||
if (clientId) setClientId(clientId);
|
if (clientId) setClientId(clientId);
|
||||||
@ -49,17 +46,20 @@ export function useInitOrderStore(initData: null | OrderFieldsFragment) {
|
|||||||
|
|
||||||
if (masterId && clientId && serviceId) {
|
if (masterId && clientId && serviceId) {
|
||||||
setStep('datetime-select');
|
setStep('datetime-select');
|
||||||
|
} else if (masterId && clientId) {
|
||||||
|
setStep('service-select');
|
||||||
} else {
|
} else {
|
||||||
setFirstStep();
|
setStep(steps[0] ?? 'loading');
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
// Обычная инициализация (новый заказ)
|
||||||
if (isMaster) {
|
if (isMaster) {
|
||||||
setMasterId(customer.documentId);
|
setMasterId(customer.documentId);
|
||||||
} else {
|
} else {
|
||||||
setClientId(customer.documentId);
|
setClientId(customer.documentId);
|
||||||
}
|
}
|
||||||
|
|
||||||
setFirstStep();
|
setStep(steps[0] ?? 'loading');
|
||||||
}
|
}
|
||||||
|
|
||||||
initialized.current = true;
|
initialized.current = true;
|
||||||
@ -68,7 +68,6 @@ export function useInitOrderStore(initData: null | OrderFieldsFragment) {
|
|||||||
initData,
|
initData,
|
||||||
isMaster,
|
isMaster,
|
||||||
setClientId,
|
setClientId,
|
||||||
setFirstStep,
|
|
||||||
setMasterId,
|
setMasterId,
|
||||||
setServiceId,
|
setServiceId,
|
||||||
setStep,
|
setStep,
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import type * as GQL from '../types';
|
import * as GQL from '../types';
|
||||||
import { notifyByTelegramId } from '../utils/notify';
|
import { notifyByTelegramId } from '../utils/notify';
|
||||||
import { BaseService } from './base';
|
import { BaseService } from './base';
|
||||||
import { CustomersService } from './customers';
|
import { CustomersService } from './customers';
|
||||||
@ -29,6 +29,9 @@ export class NotifyService extends BaseService {
|
|||||||
const serviceId = String(variables.input.services?.[0] ?? '');
|
const serviceId = String(variables.input.services?.[0] ?? '');
|
||||||
const timeStart = String(variables.input.time_start ?? '');
|
const timeStart = String(variables.input.time_start ?? '');
|
||||||
const clientId = String(variables.input.client ?? '');
|
const clientId = String(variables.input.client ?? '');
|
||||||
|
const state = String(variables.input.state ?? '');
|
||||||
|
|
||||||
|
const isApproved = state === GQL.Enum_Order_State.Approved;
|
||||||
|
|
||||||
const { slot } = await slotsService.getSlot({ documentId: slotId });
|
const { slot } = await slotsService.getSlot({ documentId: slotId });
|
||||||
const { service } = await servicesService.getService({ documentId: serviceId });
|
const { service } = await servicesService.getService({ documentId: serviceId });
|
||||||
@ -45,13 +48,13 @@ export class NotifyService extends BaseService {
|
|||||||
|
|
||||||
// Мастеру
|
// Мастеру
|
||||||
if (master?.telegramId) {
|
if (master?.telegramId) {
|
||||||
const message = `✅ <b>Запись создана!</b>\n<b>Дата:</b> ${slotDate}\n<b>Время:</b> ${timeStartString} - ${timeEndString}\n<b>Клиент:</b> ${client?.name ?? '-'}\n<b>Услуга:</b> ${service?.name ?? '-'}`;
|
const message = `✅ <b>Запись создана${isApproved ? ' и подтверждена' : ''}!</b>\n<b>Дата:</b> ${slotDate}\n<b>Время:</b> ${timeStartString} - ${timeEndString}\n<b>Клиент:</b> ${client?.name ?? '-'}\n<b>Услуга:</b> ${service?.name ?? '-'}`;
|
||||||
await notifyByTelegramId(String(master.telegramId), message);
|
await notifyByTelegramId(String(master.telegramId), message);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Клиенту
|
// Клиенту
|
||||||
if (client?.telegramId) {
|
if (client?.telegramId) {
|
||||||
const message = `✅ <b>Запись создана!</b>\n<b>Дата:</b> ${slotDate}\n<b>Время:</b> ${timeStartString} - ${timeEndString}\n<b>Мастер:</b> ${master?.name ?? '-'}\n<b>Услуга:</b> ${service?.name ?? '-'}`;
|
const message = `✅ <b>Запись создана${isApproved ? ' и подтверждена' : ''}!</b>\n<b>Дата:</b> ${slotDate}\n<b>Время:</b> ${timeStartString} - ${timeEndString}\n<b>Мастер:</b> ${master?.name ?? '-'}\n<b>Услуга:</b> ${service?.name ?? '-'}`;
|
||||||
await notifyByTelegramId(String(client.telegramId), message);
|
await notifyByTelegramId(String(client.telegramId), message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
/* eslint-disable canonical/id-match */
|
/* eslint-disable canonical/id-match */
|
||||||
import { getClientWithToken } from '../apollo/client';
|
import { getClientWithToken } from '../apollo/client';
|
||||||
import * as GQL from '../types';
|
import * as GQL from '../types';
|
||||||
import { Enum_Customer_Role, Enum_Slot_State } from '../types';
|
import { Enum_Slot_State } from '../types';
|
||||||
import { BaseService } from './base';
|
import { BaseService } from './base';
|
||||||
import { CustomersService } from './customers';
|
import { CustomersService } from './customers';
|
||||||
import { NotifyService } from './notify';
|
import { NotifyService } from './notify';
|
||||||
@ -41,29 +41,32 @@ export class OrdersService extends BaseService {
|
|||||||
const servicesService = new ServicesService(this.customer);
|
const servicesService = new ServicesService(this.customer);
|
||||||
|
|
||||||
const { customer } = await customersService.getCustomer(this.customer);
|
const { customer } = await customersService.getCustomer(this.customer);
|
||||||
|
|
||||||
|
if (!customer) throw new Error(ERRORS.MISSING_USER);
|
||||||
|
|
||||||
const { slot } = await slotsService.getSlot({ documentId: variables.input.slot });
|
const { slot } = await slotsService.getSlot({ documentId: variables.input.slot });
|
||||||
|
|
||||||
if (slot?.state === Enum_Slot_State.Closed) {
|
if (slot?.state === Enum_Slot_State.Closed) {
|
||||||
throw new Error(ERRORS.SLOT_CLOSED);
|
throw new Error(ERRORS.SLOT_CLOSED);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (customer?.role === Enum_Customer_Role.Client) {
|
const isMaster = isCustomerMaster(customer);
|
||||||
|
|
||||||
|
if (!isMaster) {
|
||||||
if (customer.documentId !== variables.input.client) {
|
if (customer.documentId !== variables.input.client) {
|
||||||
throw new Error(ERRORS.INVALID_CLIENT);
|
throw new Error(ERRORS.INVALID_CLIENT);
|
||||||
}
|
}
|
||||||
|
|
||||||
const masters = await customersService.getMasters(this.customer);
|
const { customers } = await customersService.getMasters(this.customer);
|
||||||
|
const masters = customers.at(0)?.masters;
|
||||||
|
|
||||||
const masterId = slot?.master?.documentId;
|
const masterId = slot?.master?.documentId;
|
||||||
if (!masters.customers.some((master) => master?.documentId === masterId)) {
|
if (!masters?.some((master) => master?.documentId === masterId)) {
|
||||||
throw new Error(ERRORS.INVALID_MASTER);
|
throw new Error(ERRORS.INVALID_MASTER);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (isMaster && slot?.master?.documentId !== customer.documentId) {
|
||||||
customer?.role === Enum_Customer_Role.Master &&
|
|
||||||
slot?.master?.documentId !== customer.documentId
|
|
||||||
) {
|
|
||||||
throw new Error(ERRORS.INVALID_MASTER);
|
throw new Error(ERRORS.INVALID_MASTER);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -83,6 +86,7 @@ export class OrdersService extends BaseService {
|
|||||||
...variables,
|
...variables,
|
||||||
input: {
|
input: {
|
||||||
...variables.input,
|
...variables.input,
|
||||||
|
state: isMaster ? GQL.Enum_Order_State.Approved : GQL.Enum_Order_State.Created,
|
||||||
time_end: formatTime(endTime).db(),
|
time_end: formatTime(endTime).db(),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
2385
pnpm-lock.yaml
generated
2385
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@ -4,8 +4,8 @@ packages:
|
|||||||
catalog:
|
catalog:
|
||||||
"@apollo/client": ^3.12.4
|
"@apollo/client": ^3.12.4
|
||||||
"@types/node": ^20
|
"@types/node": ^20
|
||||||
"@types/react": ^19.1.2
|
"@types/react": ^19
|
||||||
"@types/react-dom": ^19.1.2
|
"@types/react-dom": ^19
|
||||||
"@vchikalkin/eslint-config-awesome": ^2.2.2
|
"@vchikalkin/eslint-config-awesome": ^2.2.2
|
||||||
autoprefixer: ^10.4.20
|
autoprefixer: ^10.4.20
|
||||||
dayjs: ^1.11.3
|
dayjs: ^1.11.3
|
||||||
@ -19,8 +19,8 @@ catalog:
|
|||||||
postcss: ^8.4.49
|
postcss: ^8.4.49
|
||||||
postcss-load-config: ^6.0.1
|
postcss-load-config: ^6.0.1
|
||||||
prettier: ^3.2.5
|
prettier: ^3.2.5
|
||||||
react: ^19.1.0
|
react: ^19
|
||||||
react-dom: ^19.1.0
|
react-dom: ^19
|
||||||
radashi: ^12.5.1
|
radashi: ^12.5.1
|
||||||
rimraf: ^6.0.1
|
rimraf: ^6.0.1
|
||||||
tailwindcss: ^3.4.15
|
tailwindcss: ^3.4.15
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user