Compare commits
35 Commits
feature/do
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7ecf72656b | ||
|
|
241effd3b8 | ||
|
|
64c9134cc2 | ||
|
|
19b53db5f3 | ||
|
|
a26c0eab8a | ||
|
|
3ac86cfeb0 | ||
|
|
d895433a65 | ||
|
|
3064887ecf | ||
|
|
02e9d5c529 | ||
|
|
f45140ef04 | ||
|
|
bdcd11d97e | ||
|
|
6a0d34d37b | ||
|
|
2df80c90f6 | ||
|
|
03145534a1 | ||
|
|
6a21f5911e | ||
|
|
b88ac07ba3 | ||
|
|
b517519e7e | ||
|
|
a1af00af69 | ||
|
|
311f6c183d | ||
|
|
67cf9a8e26 | ||
|
|
6080aadc93 | ||
|
|
ee4e70f43d | ||
|
|
4d40230864 | ||
|
|
92119baa5e | ||
|
|
1e9fd66e27 | ||
|
|
aa11ecfcec | ||
|
|
8e61fbbb40 | ||
|
|
d32a7dc54e | ||
|
|
0a43d63c2c | ||
|
|
01d8bb20d5 | ||
|
|
20b2b44572 | ||
|
|
7fe1b89f5b | ||
|
|
4a98ac5d3e | ||
|
|
f8d0b7619f | ||
|
|
0b64d8086c |
113
.github/workflows/deploy.yml
vendored
113
.github/workflows/deploy.yml
vendored
@ -4,6 +4,7 @@ on:
|
|||||||
push:
|
push:
|
||||||
branches:
|
branches:
|
||||||
- main
|
- main
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build-and-push:
|
build-and-push:
|
||||||
@ -13,11 +14,30 @@ jobs:
|
|||||||
web_tag: ${{ steps.vars.outputs.web_tag }}
|
web_tag: ${{ steps.vars.outputs.web_tag }}
|
||||||
bot_tag: ${{ steps.vars.outputs.bot_tag }}
|
bot_tag: ${{ steps.vars.outputs.bot_tag }}
|
||||||
cache_proxy_tag: ${{ steps.vars.outputs.cache_proxy_tag }}
|
cache_proxy_tag: ${{ steps.vars.outputs.cache_proxy_tag }}
|
||||||
|
# Добавляем output-ы для отслеживания, какие проекты были собраны
|
||||||
|
web_built: ${{ steps.filter.outputs.web }}
|
||||||
|
bot_built: ${{ steps.filter.outputs.bot }}
|
||||||
|
cache_proxy_built: ${{ steps.filter.outputs.cache_proxy }}
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@v3
|
uses: actions/checkout@v3
|
||||||
|
|
||||||
- name: Create fake .env file for build
|
# --- НОВОЕ: Шаг 1: dorny/paths-filter для условной сборки ---
|
||||||
|
- name: Filter changed paths
|
||||||
|
uses: dorny/paths-filter@v2
|
||||||
|
id: filter
|
||||||
|
with:
|
||||||
|
filters: |
|
||||||
|
web:
|
||||||
|
- 'apps/web/**'
|
||||||
|
- 'packages/**'
|
||||||
|
bot:
|
||||||
|
- 'apps/bot/**'
|
||||||
|
- 'packages/**'
|
||||||
|
cache_proxy:
|
||||||
|
- 'apps/cache-proxy/**'
|
||||||
|
# -----------------------------------------------------------
|
||||||
|
- name: Create .env file for build
|
||||||
run: |
|
run: |
|
||||||
echo "BOT_TOKEN=fake" > .env
|
echo "BOT_TOKEN=fake" > .env
|
||||||
echo "LOGIN_GRAPHQL=fake" >> .env
|
echo "LOGIN_GRAPHQL=fake" >> .env
|
||||||
@ -28,6 +48,10 @@ jobs:
|
|||||||
echo "BOT_URL=http://localhost:3000" >> .env
|
echo "BOT_URL=http://localhost:3000" >> .env
|
||||||
echo "REDIS_PASSWORD=fake" >> .env
|
echo "REDIS_PASSWORD=fake" >> .env
|
||||||
echo "BOT_PROVIDER_TOKEN=fake" >> .env
|
echo "BOT_PROVIDER_TOKEN=fake" >> .env
|
||||||
|
echo "SUPPORT_TELEGRAM_URL=${{ secrets.SUPPORT_TELEGRAM_URL }}" >> .env
|
||||||
|
echo "URL_OFFER=${{ secrets.URL_OFFER }}" >> .env
|
||||||
|
echo "URL_PRIVACY=${{ secrets.URL_PRIVACY }}" >> .env
|
||||||
|
echo "URL_FAQ=${{ secrets.URL_FAQ }}" >> .env
|
||||||
|
|
||||||
- name: Set image tags
|
- name: Set image tags
|
||||||
id: vars
|
id: vars
|
||||||
@ -39,29 +63,37 @@ jobs:
|
|||||||
- name: Login to Docker Hub
|
- name: Login to Docker Hub
|
||||||
run: echo "${{ secrets.DOCKERHUB_TOKEN }}" | docker login -u "${{ secrets.DOCKERHUB_USERNAME }}" --password-stdin
|
run: echo "${{ secrets.DOCKERHUB_TOKEN }}" | docker login -u "${{ secrets.DOCKERHUB_USERNAME }}" --password-stdin
|
||||||
|
|
||||||
|
# --- ИЗМЕНЕНО: Условное выполнение Build/Push ---
|
||||||
- name: Build web image
|
- name: Build web image
|
||||||
|
if: steps.filter.outputs.web == 'true'
|
||||||
run: |
|
run: |
|
||||||
docker build -t ${{ secrets.DOCKERHUB_USERNAME }}/zapishis-web:${{ steps.vars.outputs.web_tag }} -f ./apps/web/Dockerfile .
|
docker build -t ${{ secrets.DOCKERHUB_USERNAME }}/zapishis-web:${{ steps.vars.outputs.web_tag }} -f ./apps/web/Dockerfile .
|
||||||
|
|
||||||
- name: Push web image to Docker Hub
|
- name: Push web image to Docker Hub
|
||||||
|
if: steps.filter.outputs.web == 'true'
|
||||||
run: |
|
run: |
|
||||||
docker push ${{ secrets.DOCKERHUB_USERNAME }}/zapishis-web:${{ steps.vars.outputs.web_tag }}
|
docker push ${{ secrets.DOCKERHUB_USERNAME }}/zapishis-web:${{ steps.vars.outputs.web_tag }}
|
||||||
|
|
||||||
- name: Build bot image
|
- name: Build bot image
|
||||||
|
if: steps.filter.outputs.bot == 'true'
|
||||||
run: |
|
run: |
|
||||||
docker build -t ${{ secrets.DOCKERHUB_USERNAME }}/zapishis-bot:${{ steps.vars.outputs.bot_tag }} -f ./apps/bot/Dockerfile .
|
docker build -t ${{ secrets.DOCKERHUB_USERNAME }}/zapishis-bot:${{ steps.vars.outputs.bot_tag }} -f ./apps/bot/Dockerfile .
|
||||||
|
|
||||||
- name: Push bot image to Docker Hub
|
- name: Push bot image to Docker Hub
|
||||||
|
if: steps.filter.outputs.bot == 'true'
|
||||||
run: |
|
run: |
|
||||||
docker push ${{ secrets.DOCKERHUB_USERNAME }}/zapishis-bot:${{ steps.vars.outputs.bot_tag }}
|
docker push ${{ secrets.DOCKERHUB_USERNAME }}/zapishis-bot:${{ steps.vars.outputs.bot_tag }}
|
||||||
|
|
||||||
- name: Build cache-proxy image
|
- name: Build cache-proxy image
|
||||||
|
if: steps.filter.outputs.cache_proxy == 'true'
|
||||||
run: |
|
run: |
|
||||||
docker build -t ${{ secrets.DOCKERHUB_USERNAME }}/zapishis-cache-proxy:${{ steps.vars.outputs.cache_proxy_tag }} -f ./apps/cache-proxy/Dockerfile .
|
docker build -t ${{ secrets.DOCKERHUB_USERNAME }}/zapishis-cache-proxy:${{ steps.vars.outputs.cache_proxy_tag }} -f ./apps/cache-proxy/Dockerfile .
|
||||||
|
|
||||||
- name: Push cache-proxy image to Docker Hub
|
- name: Push cache-proxy image to Docker Hub
|
||||||
|
if: steps.filter.outputs.cache_proxy == 'true'
|
||||||
run: |
|
run: |
|
||||||
docker push ${{ secrets.DOCKERHUB_USERNAME }}/zapishis-cache-proxy:${{ steps.vars.outputs.cache_proxy_tag }}
|
docker push ${{ secrets.DOCKERHUB_USERNAME }}/zapishis-cache-proxy:${{ steps.vars.outputs.cache_proxy_tag }}
|
||||||
|
# -------------------------------------------------
|
||||||
|
|
||||||
deploy:
|
deploy:
|
||||||
name: Deploy to VPS
|
name: Deploy to VPS
|
||||||
@ -83,8 +115,10 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
ssh -i ~/.ssh/id_rsa -p ${{ secrets.VPS_PORT }} -o StrictHostKeyChecking=no ${{ secrets.VPS_USER }}@${{ secrets.VPS_HOST }} "mkdir -p /home/${{ secrets.VPS_USER }}/zapishis"
|
ssh -i ~/.ssh/id_rsa -p ${{ secrets.VPS_PORT }} -o StrictHostKeyChecking=no ${{ secrets.VPS_USER }}@${{ secrets.VPS_HOST }} "mkdir -p /home/${{ secrets.VPS_USER }}/zapishis"
|
||||||
|
|
||||||
- name: Create real .env file for production
|
# --- НОВОЕ: Шаг 2: Создание основного .env БЕЗ ТЕГОВ ---
|
||||||
|
- name: Create .env file for deploy
|
||||||
run: |
|
run: |
|
||||||
|
# Включаем все секреты, КРОМЕ тегов
|
||||||
echo "BOT_TOKEN=${{ secrets.BOT_TOKEN }}" > .env
|
echo "BOT_TOKEN=${{ secrets.BOT_TOKEN }}" > .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
|
||||||
@ -92,14 +126,26 @@ jobs:
|
|||||||
echo "EMAIL_GRAPHQL=${{ secrets.EMAIL_GRAPHQL }}" >> .env
|
echo "EMAIL_GRAPHQL=${{ secrets.EMAIL_GRAPHQL }}" >> .env
|
||||||
echo "NEXTAUTH_SECRET=${{ secrets.NEXTAUTH_SECRET }}" >> .env
|
echo "NEXTAUTH_SECRET=${{ secrets.NEXTAUTH_SECRET }}" >> .env
|
||||||
echo "BOT_URL=${{ secrets.BOT_URL }}" >> .env
|
echo "BOT_URL=${{ secrets.BOT_URL }}" >> .env
|
||||||
echo "WEB_IMAGE_TAG=${{ needs.build-and-push.outputs.web_tag }}" >> .env
|
|
||||||
echo "BOT_IMAGE_TAG=${{ needs.build-and-push.outputs.bot_tag }}" >> .env
|
|
||||||
echo "CACHE_PROXY_IMAGE_TAG=${{ needs.build-and-push.outputs.cache_proxy_tag }}" >> .env
|
|
||||||
echo "DOCKERHUB_USERNAME=${{ secrets.DOCKERHUB_USERNAME }}" >> .env
|
echo "DOCKERHUB_USERNAME=${{ secrets.DOCKERHUB_USERNAME }}" >> .env
|
||||||
echo "REDIS_PASSWORD=${{ secrets.REDIS_PASSWORD }}" >> .env
|
echo "REDIS_PASSWORD=${{ secrets.REDIS_PASSWORD }}" >> .env
|
||||||
echo "BOT_PROVIDER_TOKEN=${{ secrets.BOT_PROVIDER_TOKEN }}" >> .env
|
echo "BOT_PROVIDER_TOKEN=${{ secrets.BOT_PROVIDER_TOKEN }}" >> .env
|
||||||
|
echo "SUPPORT_TELEGRAM_URL=${{ secrets.SUPPORT_TELEGRAM_URL }}" >> .env
|
||||||
|
echo "URL_OFFER=${{ secrets.URL_OFFER }}" >> .env
|
||||||
|
echo "URL_PRIVACY=${{ secrets.URL_PRIVACY }}" >> .env
|
||||||
|
echo "URL_FAQ=${{ secrets.URL_FAQ }}" >> .env
|
||||||
|
|
||||||
- name: Copy .env to VPS via SCP
|
# --- НОВОЕ: Шаг 3: Создание файлов тегов (.project.env) ---
|
||||||
|
- name: Create Project Tag Env Files
|
||||||
|
run: |
|
||||||
|
# Создаем файлы, которые будут содержать только одну переменную с тегом
|
||||||
|
echo "WEB_IMAGE_TAG=${{ needs.build-and-push.outputs.web_tag }}" > .env.web
|
||||||
|
echo "BOT_IMAGE_TAG=${{ needs.build-and-push.outputs.bot_tag }}" > .env.bot
|
||||||
|
echo "CACHE_PROXY_IMAGE_TAG=${{ needs.build-and-push.outputs.cache_proxy_tag }}" > .env.cache-proxy
|
||||||
|
|
||||||
|
# --- Шаг 4: Копирование .env и УСЛОВНОЕ копирование тегов ---
|
||||||
|
|
||||||
|
# Копируем основной .env всегда
|
||||||
|
- name: Copy .env to VPS via SCP (Always)
|
||||||
uses: appleboy/scp-action@master
|
uses: appleboy/scp-action@master
|
||||||
with:
|
with:
|
||||||
host: ${{ secrets.VPS_HOST }}
|
host: ${{ secrets.VPS_HOST }}
|
||||||
@ -109,6 +155,42 @@ jobs:
|
|||||||
source: '.env'
|
source: '.env'
|
||||||
target: '/home/${{ secrets.VPS_USER }}/zapishis/'
|
target: '/home/${{ secrets.VPS_USER }}/zapishis/'
|
||||||
|
|
||||||
|
# Копируем .env.web ТОЛЬКО, если web был собран (обновляем тег на VPS)
|
||||||
|
- name: Copy .env.web to VPS
|
||||||
|
if: ${{ needs.build-and-push.outputs.web_built == 'true' }}
|
||||||
|
uses: appleboy/scp-action@master
|
||||||
|
with:
|
||||||
|
host: ${{ secrets.VPS_HOST }}
|
||||||
|
username: ${{ secrets.VPS_USER }}
|
||||||
|
key: ${{ secrets.VPS_SSH_KEY }}
|
||||||
|
port: ${{ secrets.VPS_PORT }}
|
||||||
|
source: '.env.web'
|
||||||
|
target: '/home/${{ secrets.VPS_USER }}/zapishis/'
|
||||||
|
|
||||||
|
# Копируем .env.bot ТОЛЬКО, если bot был собран
|
||||||
|
- name: Copy .env.bot to VPS
|
||||||
|
if: ${{ needs.build-and-push.outputs.bot_built == 'true' }}
|
||||||
|
uses: appleboy/scp-action@master
|
||||||
|
with:
|
||||||
|
host: ${{ secrets.VPS_HOST }}
|
||||||
|
username: ${{ secrets.VPS_USER }}
|
||||||
|
key: ${{ secrets.VPS_SSH_KEY }}
|
||||||
|
port: ${{ secrets.VPS_PORT }}
|
||||||
|
source: '.env.bot'
|
||||||
|
target: '/home/${{ secrets.VPS_USER }}/zapishis/'
|
||||||
|
|
||||||
|
# Копируем .env.cache-proxy ТОЛЬКО, если cache-proxy был собран
|
||||||
|
- name: Copy .env.cache-proxy to VPS
|
||||||
|
if: ${{ needs.build-and-push.outputs.cache_proxy_built == 'true' }}
|
||||||
|
uses: appleboy/scp-action@master
|
||||||
|
with:
|
||||||
|
host: ${{ secrets.VPS_HOST }}
|
||||||
|
username: ${{ secrets.VPS_USER }}
|
||||||
|
key: ${{ secrets.VPS_SSH_KEY }}
|
||||||
|
port: ${{ secrets.VPS_PORT }}
|
||||||
|
source: '.env.cache-proxy'
|
||||||
|
target: '/home/${{ secrets.VPS_USER }}/zapishis/'
|
||||||
|
|
||||||
- name: Copy docker-compose.yml to VPS via SCP
|
- name: Copy docker-compose.yml to VPS via SCP
|
||||||
uses: appleboy/scp-action@master
|
uses: appleboy/scp-action@master
|
||||||
with:
|
with:
|
||||||
@ -119,12 +201,27 @@ jobs:
|
|||||||
source: 'docker-compose.yml'
|
source: 'docker-compose.yml'
|
||||||
target: '/home/${{ secrets.VPS_USER }}/zapishis/'
|
target: '/home/${{ secrets.VPS_USER }}/zapishis/'
|
||||||
|
|
||||||
|
# --- ФИНАЛЬНЫЙ ДЕПЛОЙ ---
|
||||||
- name: Login and deploy on VPS
|
- name: Login and deploy on VPS
|
||||||
run: |
|
run: |
|
||||||
ssh -i ~/.ssh/id_rsa -p ${{ secrets.VPS_PORT }} -o StrictHostKeyChecking=no ${{ secrets.VPS_USER }}@${{ secrets.VPS_HOST }} "
|
ssh -i ~/.ssh/id_rsa -p ${{ secrets.VPS_PORT }} -o StrictHostKeyChecking=no ${{ secrets.VPS_USER }}@${{ secrets.VPS_HOST }} "
|
||||||
cd /home/${{ secrets.VPS_USER }}/zapishis && \
|
cd /home/${{ secrets.VPS_USER }}/zapishis && \
|
||||||
docker login -u ${{ secrets.DOCKERHUB_USERNAME }} -p ${{ secrets.DOCKERHUB_TOKEN }} && \
|
|
||||||
docker compose pull && \
|
# 1. Объединение ВСЕХ ENV-файлов в один основной .env
|
||||||
|
# Теги из .env.web/.env.bot переопределят любые старые/пустые значения,
|
||||||
|
# и .env станет полным и актуальным.
|
||||||
|
echo \"Merging environment files into .env...\" && \
|
||||||
|
cat .env .env.web .env.bot .env.cache-proxy > .temp_env && \
|
||||||
|
mv .temp_env .env && \
|
||||||
|
|
||||||
|
# 2. Логин
|
||||||
|
docker login -u ${{ secrets.DOCKERHUB_USERNAME }} -p ${{ secrets.DOCKERHUB_TOKEN }}
|
||||||
|
|
||||||
|
# 3. Pull ВСЕХ сервисов (Docker Compose автоматически использует обновленный .env)
|
||||||
|
echo \"Pulling all services...\" && \
|
||||||
|
docker compose pull
|
||||||
|
|
||||||
|
# 4. Перезапуск
|
||||||
docker compose down && \
|
docker compose down && \
|
||||||
docker compose up -d
|
docker compose up -d
|
||||||
"
|
"
|
||||||
|
|||||||
@ -25,7 +25,7 @@ description =
|
|||||||
start =
|
start =
|
||||||
.description = Запуск бота
|
.description = Запуск бота
|
||||||
addcontact =
|
addcontact =
|
||||||
.description = Добавить контакт пользователя
|
.description = Добавить контакт
|
||||||
sharebot =
|
sharebot =
|
||||||
.description = Поделиться ботом
|
.description = Поделиться ботом
|
||||||
subscribe =
|
subscribe =
|
||||||
@ -36,7 +36,7 @@ help =
|
|||||||
.description = Список команд и поддержка
|
.description = Список команд и поддержка
|
||||||
commands-list =
|
commands-list =
|
||||||
📋 Доступные команды:
|
📋 Доступные команды:
|
||||||
• /addcontact — добавить контакт пользователя
|
• /addcontact — добавить контакт
|
||||||
• /sharebot — поделиться ботом
|
• /sharebot — поделиться ботом
|
||||||
• /subscribe — приобрести Pro доступ
|
• /subscribe — приобрести Pro доступ
|
||||||
• /pro — информация о вашем Pro доступе
|
• /pro — информация о вашем Pro доступе
|
||||||
@ -55,6 +55,7 @@ btn-pro = 👑 Pro доступ
|
|||||||
btn-subscribe = 👑 Приобрести Pro
|
btn-subscribe = 👑 Приобрести Pro
|
||||||
btn-pro-info = ℹ️ Мой Pro доступ
|
btn-pro-info = ℹ️ Мой Pro доступ
|
||||||
btn-open-app = 📱 Открыть приложение
|
btn-open-app = 📱 Открыть приложение
|
||||||
|
btn-faq = 📖 Инструкция
|
||||||
btn-documents = 📋 Документы
|
btn-documents = 📋 Документы
|
||||||
btn-back = ◀️ Назад
|
btn-back = ◀️ Назад
|
||||||
|
|
||||||
|
|||||||
@ -29,6 +29,7 @@
|
|||||||
"dayjs": "catalog:",
|
"dayjs": "catalog:",
|
||||||
"grammy": "^1.38.1",
|
"grammy": "^1.38.1",
|
||||||
"ioredis": "^5.7.0",
|
"ioredis": "^5.7.0",
|
||||||
|
"libphonenumber-js": "^1.12.24",
|
||||||
"pino": "^9.9.0",
|
"pino": "^9.9.0",
|
||||||
"pino-pretty": "^13.1.1",
|
"pino-pretty": "^13.1.1",
|
||||||
"radashi": "catalog:",
|
"radashi": "catalog:",
|
||||||
|
|||||||
@ -5,10 +5,10 @@ import { env } from '@/config/env';
|
|||||||
import { KEYBOARD_SHARE_BOT, KEYBOARD_SHARE_PHONE } from '@/config/keyboards';
|
import { KEYBOARD_SHARE_BOT, KEYBOARD_SHARE_PHONE } from '@/config/keyboards';
|
||||||
import { parseContact } from '@/utils/contact';
|
import { parseContact } from '@/utils/contact';
|
||||||
import { combine } from '@/utils/messages';
|
import { combine } from '@/utils/messages';
|
||||||
import { isValidPhoneNumber, normalizePhoneNumber } from '@/utils/phone';
|
|
||||||
import { type Conversation } from '@grammyjs/conversations';
|
import { type Conversation } from '@grammyjs/conversations';
|
||||||
import { CustomersService } from '@repo/graphql/api/customers';
|
import { CustomersService } from '@repo/graphql/api/customers';
|
||||||
import { RegistrationService } from '@repo/graphql/api/registration';
|
import { RegistrationService } from '@repo/graphql/api/registration';
|
||||||
|
import parsePhoneNumber from 'libphonenumber-js';
|
||||||
|
|
||||||
export async function addContact(conversation: Conversation<Context, Context>, ctx: Context) {
|
export async function addContact(conversation: Conversation<Context, Context>, ctx: Context) {
|
||||||
// Все пользователи могут добавлять контакты
|
// Все пользователи могут добавлять контакты
|
||||||
@ -17,8 +17,8 @@ export async function addContact(conversation: Conversation<Context, Context>, c
|
|||||||
return ctx.reply(await conversation.external(({ t }) => t('err-generic')));
|
return ctx.reply(await conversation.external(({ t }) => t('err-generic')));
|
||||||
}
|
}
|
||||||
|
|
||||||
const customerService = new CustomersService({ telegramId });
|
const registrationService = new RegistrationService();
|
||||||
const { customer } = await customerService.getCustomer({ telegramId });
|
const { customer } = await registrationService._NOCACHE_GetCustomer({ telegramId });
|
||||||
|
|
||||||
if (!customer) {
|
if (!customer) {
|
||||||
return ctx.reply(
|
return ctx.reply(
|
||||||
@ -26,8 +26,8 @@ export async function addContact(conversation: Conversation<Context, Context>, c
|
|||||||
combine(
|
combine(
|
||||||
t('msg-need-phone'),
|
t('msg-need-phone'),
|
||||||
t('share-phone-agreement', {
|
t('share-phone-agreement', {
|
||||||
offerUrl: env.OFFER_URL,
|
offerUrl: env.URL_OFFER,
|
||||||
privacyUrl: env.PRIVACY_URL,
|
privacyUrl: env.URL_PRIVACY,
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@ -42,8 +42,8 @@ export async function addContact(conversation: Conversation<Context, Context>, c
|
|||||||
t('msg-send-client-contact-or-phone'),
|
t('msg-send-client-contact-or-phone'),
|
||||||
t('msg-cancel-operation'),
|
t('msg-cancel-operation'),
|
||||||
t('share-contact-agreement', {
|
t('share-contact-agreement', {
|
||||||
offerUrl: env.OFFER_URL,
|
offerUrl: env.URL_OFFER,
|
||||||
privacyUrl: env.PRIVACY_URL,
|
privacyUrl: env.URL_PRIVACY,
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@ -60,23 +60,36 @@ export async function addContact(conversation: Conversation<Context, Context>, c
|
|||||||
let phone = '';
|
let phone = '';
|
||||||
|
|
||||||
if (firstCtx.message?.contact) {
|
if (firstCtx.message?.contact) {
|
||||||
|
/**
|
||||||
|
* Отправлен контакт
|
||||||
|
*/
|
||||||
const { contact } = firstCtx.message;
|
const { contact } = firstCtx.message;
|
||||||
const parsedContact = parseContact(contact);
|
const parsedContact = parseContact(contact);
|
||||||
|
const parsedPhone = parsePhoneNumber(contact.phone_number, 'RU');
|
||||||
|
|
||||||
name = parsedContact.name;
|
name = parsedContact.name;
|
||||||
surname = parsedContact.surname;
|
surname = parsedContact.surname;
|
||||||
phone = normalizePhoneNumber(parsedContact.phone);
|
|
||||||
|
if (!parsedPhone?.isValid() || !parsedPhone.number) {
|
||||||
|
return ctx.reply(await conversation.external(({ t }) => t('msg-invalid-phone')));
|
||||||
|
}
|
||||||
|
|
||||||
|
phone = parsedPhone.number;
|
||||||
} else if (firstCtx.message?.text) {
|
} else if (firstCtx.message?.text) {
|
||||||
const typedPhone = normalizePhoneNumber(firstCtx.message.text);
|
/**
|
||||||
if (!isValidPhoneNumber(typedPhone)) {
|
* Номер в тексте сообщения
|
||||||
|
*/
|
||||||
|
const parsedPhone = parsePhoneNumber(firstCtx.message.text, 'RU');
|
||||||
|
if (!parsedPhone?.isValid() || !parsedPhone.number) {
|
||||||
return ctx.reply(await conversation.external(({ t }) => t('msg-invalid-phone')));
|
return ctx.reply(await conversation.external(({ t }) => t('msg-invalid-phone')));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Нельзя добавлять свой собственный номер телефона
|
// Нельзя добавлять свой собственный номер телефона
|
||||||
if (customer.phone && normalizePhoneNumber(customer.phone) === typedPhone) {
|
if (customer.phone && customer.phone === parsedPhone.number) {
|
||||||
return ctx.reply(await conversation.external(({ t }) => t('err-cannot-add-self')));
|
return ctx.reply(await conversation.external(({ t }) => t('err-cannot-add-self')));
|
||||||
}
|
}
|
||||||
|
|
||||||
phone = typedPhone;
|
phone = parsedPhone.number;
|
||||||
|
|
||||||
// Просим ввести имя клиента
|
// Просим ввести имя клиента
|
||||||
await ctx.reply(await conversation.external(({ t }) => t('msg-send-client-name')));
|
await ctx.reply(await conversation.external(({ t }) => t('msg-send-client-name')));
|
||||||
@ -101,19 +114,15 @@ export async function addContact(conversation: Conversation<Context, Context>, c
|
|||||||
return ctx.reply(await conversation.external(({ t }) => t('msg-send-client-contact-or-phone')));
|
return ctx.reply(await conversation.external(({ t }) => t('msg-send-client-contact-or-phone')));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Проверяем валидность номера телефона
|
|
||||||
if (!isValidPhoneNumber(phone)) {
|
|
||||||
return ctx.reply(await conversation.external(({ t }) => t('msg-invalid-phone')));
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Проверяем, есть ли клиент с таким номером
|
// Проверяем, есть ли клиент с таким номером
|
||||||
const { customer: existingCustomer } = await customerService.getCustomer({ phone });
|
const { customer: existingCustomer } = await registrationService._NOCACHE_GetCustomer({
|
||||||
|
phone,
|
||||||
|
});
|
||||||
let documentId = existingCustomer?.documentId;
|
let documentId = existingCustomer?.documentId;
|
||||||
|
|
||||||
// Если клиента нет, создаём нового
|
// Если клиента нет, создаём нового
|
||||||
if (!documentId) {
|
if (!documentId) {
|
||||||
const registrationService = new RegistrationService();
|
|
||||||
const createCustomerResult = await registrationService.createCustomer({
|
const createCustomerResult = await registrationService.createCustomer({
|
||||||
data: { name, phone, surname },
|
data: { name, phone, surname },
|
||||||
});
|
});
|
||||||
@ -124,6 +133,7 @@ export async function addContact(conversation: Conversation<Context, Context>, c
|
|||||||
|
|
||||||
// Добавляем текущего пользователя к приглашенному
|
// Добавляем текущего пользователя к приглашенному
|
||||||
const invitedBy = [customer.documentId];
|
const invitedBy = [customer.documentId];
|
||||||
|
const customerService = new CustomersService({ telegramId });
|
||||||
await customerService.addInvitedBy({ data: { invitedBy }, documentId });
|
await customerService.addInvitedBy({ data: { invitedBy }, documentId });
|
||||||
|
|
||||||
// Отправляем подтверждения и инструкции
|
// Отправляем подтверждения и инструкции
|
||||||
@ -132,7 +142,9 @@ export async function addContact(conversation: Conversation<Context, Context>, c
|
|||||||
t('msg-contact-added', { fullname: [name, surname].filter(Boolean).join(' ') }),
|
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-contact-forward')), {
|
||||||
|
parse_mode: 'HTML',
|
||||||
|
});
|
||||||
await ctx.reply(await conversation.external(({ t }) => t('msg-share-bot')), KEYBOARD_SHARE_BOT);
|
await ctx.reply(await conversation.external(({ t }) => t('msg-share-bot')), KEYBOARD_SHARE_BOT);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
await ctx.reply(
|
await ctx.reply(
|
||||||
|
|||||||
@ -5,6 +5,7 @@ import { formatMoney } from '@/utils/format';
|
|||||||
import { combine } from '@/utils/messages';
|
import { combine } from '@/utils/messages';
|
||||||
import { type Conversation } from '@grammyjs/conversations';
|
import { type Conversation } from '@grammyjs/conversations';
|
||||||
import { fmt, i } from '@grammyjs/parse-mode';
|
import { fmt, i } from '@grammyjs/parse-mode';
|
||||||
|
import { CustomersService } from '@repo/graphql/api/customers';
|
||||||
import { SubscriptionsService } from '@repo/graphql/api/subscriptions';
|
import { SubscriptionsService } from '@repo/graphql/api/subscriptions';
|
||||||
import * as GQL from '@repo/graphql/types';
|
import * as GQL from '@repo/graphql/types';
|
||||||
import { InlineKeyboard } from 'grammy';
|
import { InlineKeyboard } from 'grammy';
|
||||||
@ -98,8 +99,8 @@ export async function subscription(conversation: Conversation<Context, Context>,
|
|||||||
|
|
||||||
const agreementText = await conversation.external(({ t }) => {
|
const agreementText = await conversation.external(({ t }) => {
|
||||||
return t('payment-agreement', {
|
return t('payment-agreement', {
|
||||||
offerUrl: env.OFFER_URL,
|
offerUrl: env.URL_OFFER,
|
||||||
privacyUrl: env.PRIVACY_URL,
|
privacyUrl: env.URL_PRIVACY,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -107,6 +108,9 @@ export async function subscription(conversation: Conversation<Context, Context>,
|
|||||||
parse_mode: 'HTML',
|
parse_mode: 'HTML',
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const customerService = new CustomersService({ telegramId });
|
||||||
|
const { customer } = await customerService.getCustomer({ telegramId });
|
||||||
|
|
||||||
return ctx.replyWithInvoice(
|
return ctx.replyWithInvoice(
|
||||||
'Оплата Pro доступа',
|
'Оплата Pro доступа',
|
||||||
combine(
|
combine(
|
||||||
@ -123,6 +127,27 @@ export async function subscription(conversation: Conversation<Context, Context>,
|
|||||||
],
|
],
|
||||||
{
|
{
|
||||||
protect_content: true,
|
protect_content: true,
|
||||||
|
provider_data: JSON.stringify({
|
||||||
|
receipt: {
|
||||||
|
customer: {
|
||||||
|
phone: customer?.phone.replaceAll(/\D/gu, ''),
|
||||||
|
},
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
amount: {
|
||||||
|
currency: 'RUB',
|
||||||
|
value: selectedPrice.amount,
|
||||||
|
},
|
||||||
|
description: selectedPrice.description || 'Pro доступ',
|
||||||
|
payment_mode: 'full_payment',
|
||||||
|
payment_subject: 'payment',
|
||||||
|
quantity: 1,
|
||||||
|
vat_code: 1,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
tax_system_code: 1,
|
||||||
|
},
|
||||||
|
}),
|
||||||
provider_token: env.BOT_PROVIDER_TOKEN,
|
provider_token: env.BOT_PROVIDER_TOKEN,
|
||||||
start_parameter: 'get_access',
|
start_parameter: 'get_access',
|
||||||
},
|
},
|
||||||
|
|||||||
@ -2,10 +2,9 @@ import { type Context } from '@/bot/context';
|
|||||||
import { logHandle } from '@/bot/helpers/logging';
|
import { logHandle } from '@/bot/helpers/logging';
|
||||||
import { KEYBOARD_REMOVE, mainMenu } from '@/config/keyboards';
|
import { KEYBOARD_REMOVE, mainMenu } from '@/config/keyboards';
|
||||||
import { parseContact } from '@/utils/contact';
|
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';
|
import { RegistrationService } from '@repo/graphql/api/registration';
|
||||||
import { Composer } from 'grammy';
|
import { Composer } from 'grammy';
|
||||||
|
import parsePhoneNumber from 'libphonenumber-js';
|
||||||
|
|
||||||
const composer = new Composer<Context>();
|
const composer = new Composer<Context>();
|
||||||
|
|
||||||
@ -18,8 +17,10 @@ feature.on(':contact', logHandle('contact-registration'), async (ctx) => {
|
|||||||
const { name, surname } = parseContact(contact);
|
const { name, surname } = parseContact(contact);
|
||||||
|
|
||||||
// Проверяем, не зарегистрирован ли уже пользователь
|
// Проверяем, не зарегистрирован ли уже пользователь
|
||||||
const customerService = new CustomersService({ telegramId });
|
const registrationService = new RegistrationService();
|
||||||
const { customer: existingCustomer } = await customerService.getCustomer({ telegramId });
|
const { customer: existingCustomer } = await registrationService._NOCACHE_GetCustomer({
|
||||||
|
telegramId,
|
||||||
|
});
|
||||||
|
|
||||||
if (existingCustomer) {
|
if (existingCustomer) {
|
||||||
return ctx.reply(ctx.t('msg-already-registered'), {
|
return ctx.reply(ctx.t('msg-already-registered'), {
|
||||||
@ -34,15 +35,15 @@ feature.on(':contact', logHandle('contact-registration'), async (ctx) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Нормализация и валидация номера
|
// Нормализация и валидация номера
|
||||||
const phone = normalizePhoneNumber(contact.phone_number);
|
const parsedPhone = parsePhoneNumber(contact.phone_number, 'RU');
|
||||||
if (!isValidPhoneNumber(phone)) {
|
if (!parsedPhone?.isValid() || !parsedPhone?.number) {
|
||||||
return ctx.reply(ctx.t('msg-invalid-phone'));
|
return ctx.reply(ctx.t('msg-invalid-phone'));
|
||||||
}
|
}
|
||||||
|
|
||||||
const registrationService = new RegistrationService();
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const { customer } = await registrationService.getCustomer({ phone });
|
const { customer } = await registrationService._NOCACHE_GetCustomer({
|
||||||
|
phone: parsedPhone.number,
|
||||||
|
});
|
||||||
|
|
||||||
if (customer && !customer.telegramId) {
|
if (customer && !customer.telegramId) {
|
||||||
// Пользователь добавлен ранее мастером — обновляем данные
|
// Пользователь добавлен ранее мастером — обновляем данные
|
||||||
@ -58,7 +59,7 @@ feature.on(':contact', logHandle('contact-registration'), async (ctx) => {
|
|||||||
|
|
||||||
// Новый пользователь — создаём и активируем
|
// Новый пользователь — создаём и активируем
|
||||||
const response = await registrationService.createCustomer({
|
const response = await registrationService.createCustomer({
|
||||||
data: { name, phone, surname, telegramId },
|
data: { name, phone: parsedPhone.number, surname, telegramId },
|
||||||
});
|
});
|
||||||
|
|
||||||
const documentId = response?.createCustomer?.documentId;
|
const documentId = response?.createCustomer?.documentId;
|
||||||
|
|||||||
@ -14,7 +14,7 @@ feature.command('start', logHandle('command-start'), async (ctx) => {
|
|||||||
const telegramId = ctx.from.id;
|
const telegramId = ctx.from.id;
|
||||||
|
|
||||||
const registrationService = new RegistrationService();
|
const registrationService = new RegistrationService();
|
||||||
const { customer } = await registrationService.getCustomer({ telegramId });
|
const { customer } = await registrationService._NOCACHE_GetCustomer({ telegramId });
|
||||||
|
|
||||||
if (customer) {
|
if (customer) {
|
||||||
// Пользователь уже зарегистрирован — приветствуем
|
// Пользователь уже зарегистрирован — приветствуем
|
||||||
@ -28,8 +28,8 @@ feature.command('start', logHandle('command-start'), async (ctx) => {
|
|||||||
combine(
|
combine(
|
||||||
ctx.t('msg-welcome'),
|
ctx.t('msg-welcome'),
|
||||||
ctx.t('share-phone-agreement', {
|
ctx.t('share-phone-agreement', {
|
||||||
offerUrl: env.OFFER_URL,
|
offerUrl: env.URL_OFFER,
|
||||||
privacyUrl: env.PRIVACY_URL,
|
privacyUrl: env.URL_PRIVACY,
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
{
|
{
|
||||||
|
|||||||
@ -5,8 +5,8 @@ import { KEYBOARD_REMOVE } from '@/config/keyboards';
|
|||||||
async function handler(ctx: Context) {
|
async function handler(ctx: Context) {
|
||||||
await ctx.reply(
|
await ctx.reply(
|
||||||
ctx.t('agreement-links', {
|
ctx.t('agreement-links', {
|
||||||
offerUrl: env.OFFER_URL,
|
offerUrl: env.URL_OFFER,
|
||||||
privacyUrl: env.PRIVACY_URL,
|
privacyUrl: env.URL_PRIVACY,
|
||||||
}),
|
}),
|
||||||
{
|
{
|
||||||
...KEYBOARD_REMOVE,
|
...KEYBOARD_REMOVE,
|
||||||
|
|||||||
@ -4,8 +4,6 @@ export const envSchema = z.object({
|
|||||||
BOT_PROVIDER_TOKEN: z.string(),
|
BOT_PROVIDER_TOKEN: z.string(),
|
||||||
BOT_TOKEN: z.string(),
|
BOT_TOKEN: z.string(),
|
||||||
BOT_URL: z.string(),
|
BOT_URL: z.string(),
|
||||||
OFFER_URL: z.string(),
|
|
||||||
PRIVACY_URL: z.string(),
|
|
||||||
RATE_LIMIT: z
|
RATE_LIMIT: z
|
||||||
.string()
|
.string()
|
||||||
.transform((value) => Number.parseInt(value, 10))
|
.transform((value) => Number.parseInt(value, 10))
|
||||||
@ -20,6 +18,9 @@ export const envSchema = z.object({
|
|||||||
.string()
|
.string()
|
||||||
.transform((value) => Number.parseInt(value, 10))
|
.transform((value) => Number.parseInt(value, 10))
|
||||||
.default('6379'),
|
.default('6379'),
|
||||||
|
URL_FAQ: z.string(),
|
||||||
|
URL_OFFER: z.string(),
|
||||||
|
URL_PRIVACY: z.string(),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const env = envSchema.parse(process.env);
|
export const env = envSchema.parse(process.env);
|
||||||
|
|||||||
@ -58,6 +58,11 @@ export const mainMenu = new Menu<Context>('main-menu', { autoAnswer: true })
|
|||||||
.row()
|
.row()
|
||||||
.text((ctx) => ctx.t('btn-documents'), handleDocuments)
|
.text((ctx) => ctx.t('btn-documents'), handleDocuments)
|
||||||
.row()
|
.row()
|
||||||
|
.url(
|
||||||
|
(ctx) => ctx.t('btn-faq'),
|
||||||
|
() => env.URL_FAQ,
|
||||||
|
)
|
||||||
|
.row()
|
||||||
.url(
|
.url(
|
||||||
(ctx) => ctx.t('btn-open-app'),
|
(ctx) => ctx.t('btn-open-app'),
|
||||||
() => {
|
() => {
|
||||||
|
|||||||
@ -1,9 +0,0 @@
|
|||||||
export function isValidPhoneNumber(phone: string) {
|
|
||||||
return /^\+7\d{10}$/u.test(phone);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function normalizePhoneNumber(phone: string): string {
|
|
||||||
const digitsOnly = phone.replaceAll(/\D/gu, '');
|
|
||||||
|
|
||||||
return `+${digitsOnly}`;
|
|
||||||
}
|
|
||||||
@ -1,13 +1,16 @@
|
|||||||
import { seconds } from 'src/utils/time';
|
import { seconds } from 'src/utils/time';
|
||||||
|
|
||||||
export const queryTTL: Record<string, number | false> = {
|
export const queryTTL: Record<string, number | false> = {
|
||||||
Login: false,
|
GetCustomer: seconds().fromHours(12),
|
||||||
GetCustomer: seconds().fromHours(24),
|
GetCustomers: false,
|
||||||
GetOrder: seconds().fromHours(24),
|
GetInvited: false,
|
||||||
GetService: seconds().fromHours(24),
|
GetInvitedBy: false,
|
||||||
GetSlot: seconds().fromHours(24),
|
GetOrders: false,
|
||||||
|
GetServices: false,
|
||||||
|
GetSlots: false,
|
||||||
GetSlotsOrders: false,
|
GetSlotsOrders: false,
|
||||||
GetSubscriptionPrices: seconds().fromHours(24),
|
GetSubscriptionHistory: false,
|
||||||
GetSubscriptions: seconds().fromHours(24),
|
GetSubscriptions: false,
|
||||||
GetSubscriptionSettings: seconds().fromHours(1),
|
GetSubscriptionSettings: seconds().fromHours(12),
|
||||||
|
Login: false,
|
||||||
};
|
};
|
||||||
|
|||||||
8
apps/cache-proxy/src/proxy/lib/utils.ts
Normal file
8
apps/cache-proxy/src/proxy/lib/utils.ts
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
import { env } from 'src/config/env';
|
||||||
|
import { queryTTL } from './config';
|
||||||
|
|
||||||
|
export function getQueryTTL(operationName: string) {
|
||||||
|
if (operationName.includes('NOCACHE')) return false;
|
||||||
|
|
||||||
|
return queryTTL[operationName] ?? env.CACHE_TTL;
|
||||||
|
}
|
||||||
@ -15,8 +15,8 @@ import {
|
|||||||
import type { Cache } from 'cache-manager';
|
import type { Cache } from 'cache-manager';
|
||||||
import { FastifyReply, FastifyRequest } from 'fastify';
|
import { FastifyReply, FastifyRequest } from 'fastify';
|
||||||
import { env } from 'src/config/env';
|
import { env } from 'src/config/env';
|
||||||
import { queryTTL } from './lib/config';
|
|
||||||
import { extractDocumentId, getQueryType } from 'src/utils/query';
|
import { extractDocumentId, getQueryType } from 'src/utils/query';
|
||||||
|
import { getQueryTTL } from './lib/utils';
|
||||||
|
|
||||||
type RedisStore = Omit<Cache, 'set'> & {
|
type RedisStore = Omit<Cache, 'set'> & {
|
||||||
set: (key: string, value: unknown, { ttl }: { ttl: number }) => Promise<void>;
|
set: (key: string, value: unknown, { ttl }: { ttl: number }) => Promise<void>;
|
||||||
@ -78,9 +78,9 @@ export class ProxyController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const ttl = queryTTL[operationName];
|
const ttl = getQueryTTL(operationName);
|
||||||
if (queryType.action === 'query' && data && ttl !== false)
|
if (queryType.action === 'query' && data && ttl !== false)
|
||||||
await this.cacheManager.set(key, data, { ttl: ttl || env.CACHE_TTL });
|
await this.cacheManager.set(key, data, { ttl });
|
||||||
|
|
||||||
return reply.send(data);
|
return reply.send(data);
|
||||||
}
|
}
|
||||||
|
|||||||
6
apps/web/app/(documents)/offer/layout.tsx
Normal file
6
apps/web/app/(documents)/offer/layout.tsx
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
import { DocumentsLayout } from '@/components/documents/layout';
|
||||||
|
import { type PropsWithChildren } from 'react';
|
||||||
|
|
||||||
|
export default function Layout({ children }: Readonly<PropsWithChildren>) {
|
||||||
|
return <DocumentsLayout title="Публичная оферта">{children}</DocumentsLayout>;
|
||||||
|
}
|
||||||
99
apps/web/app/(documents)/offer/page.mdx
Normal file
99
apps/web/app/(documents)/offer/page.mdx
Normal file
@ -0,0 +1,99 @@
|
|||||||
|
import { env } from '@/config/env';
|
||||||
|
|
||||||
|
export const metadata = {
|
||||||
|
title: 'Публичная оферта',
|
||||||
|
description: 'Публичная оферта бота / мини-приложения «Запишись.онлайн» (@zapishis_online_bot)',
|
||||||
|
};
|
||||||
|
|
||||||
|
### Договор-оферта на использование сервиса «Запишись.онлайн» (@zapishis_online_bot)
|
||||||
|
|
||||||
|
Настоящий документ является публичной офертой в соответствии с пунктом 2 статьи 437 Гражданского кодекса Российской Федерации и представляет собой предложение индивидуального предпринимателя (самозанятого) — далее именуемого «Администрация», заключить Договор на использование Сервиса (далее – «Договор», «Оферта») с любым физическим лицом, принявшим условия настоящей Оферты (далее – «Пользователь»).
|
||||||
|
|
||||||
|
#### 1. Термины и определения
|
||||||
|
|
||||||
|
1.1. Оферта — настоящий документ, постоянно размещенный в сети Интернет по адресу <a href={env.URL_OFFER}>{env.URL_OFFER}</a>.
|
||||||
|
|
||||||
|
1.2. Акцепт — полное и безоговорочное принятие условий Оферты Пользователем путем оплаты доступа через встроенный платежный бот ЮKassa в Telegram.
|
||||||
|
|
||||||
|
1.3. Сервис — Telegram-бот и мини-приложение, позволяющее пользователям создавать и принимать заказы, управлять расписанием и взаимодействовать друг с другом без необходимости регистрации.
|
||||||
|
|
||||||
|
1.4. Администрация — самозанятое лицо, являющееся разработчиком и правообладателем Сервиса.
|
||||||
|
|
||||||
|
1.5. Пользователь — любое физическое лицо, использующее Сервис в личных или профессиональных целях.
|
||||||
|
|
||||||
|
1.6. Доступ — право использования функционала Сервиса на определённый оплаченный период (например, неделя, месяц, год).
|
||||||
|
|
||||||
|
1.7. Оплата — денежные средства, перечисленные Пользователем через платёжный бот ЮKassa в Telegram.
|
||||||
|
|
||||||
|
#### 2. Акцепт оферты и заключение договора
|
||||||
|
|
||||||
|
2.1. Акцептом настоящей Оферты считается оплата Пользователем доступа к Сервису любым доступным способом.
|
||||||
|
|
||||||
|
2.2. С момента совершения оплаты Пользователь считается заключившим Договор с Администрацией на условиях, изложенных в настоящей Оферте.
|
||||||
|
|
||||||
|
2.3. Пользователь подтверждает, что ему понятны все условия настоящей Оферты и он принимает их без ограничений.
|
||||||
|
|
||||||
|
#### 3. Предмет договора
|
||||||
|
|
||||||
|
3.1. Администрация предоставляет Пользователю неисключительное право (доступ) на использование функционала Сервиса в пределах оплаченного периода времени.
|
||||||
|
|
||||||
|
3.2. Сервис предоставляется в онлайн-формате через Telegram-бота без установки дополнительного программного обеспечения.
|
||||||
|
|
||||||
|
3.3. Пользователь получает право использовать функционал Сервиса в личных целях, в том числе для организации и планирования заказов, встреч и тренировок.
|
||||||
|
|
||||||
|
#### 4. Порядок оплаты и использование
|
||||||
|
|
||||||
|
4.1. Оплата производится через встроенные инструменты Telegram-бота с использованием платёжной системы ЮKassa.
|
||||||
|
|
||||||
|
4.2. Комиссия платёжной системы включена в итоговую стоимость. Администрация не взимает дополнительных платежей.
|
||||||
|
|
||||||
|
4.3. Доступ активируется автоматически после успешного подтверждения оплаты.
|
||||||
|
|
||||||
|
4.4. Пользователь может продлить доступ путём повторной оплаты. Автоматическое продление не применяется.
|
||||||
|
|
||||||
|
4.5. Возврат денежных средств возможен только в случае технических ошибок, по письменному обращению на адрес поддержки.
|
||||||
|
|
||||||
|
#### 5. Права и обязанности сторон
|
||||||
|
|
||||||
|
5.1. Пользователь обязуется:
|
||||||
|
|
||||||
|
- не использовать Сервис в противоправных целях;
|
||||||
|
- не вмешиваться в работу Сервиса и не предпринимать действий, направленных на нарушение его функционирования;
|
||||||
|
- предоставлять достоверную информацию при оплате и использовании Сервиса;
|
||||||
|
- при добавлении контактов других лиц (например, клиентов, мастеров) гарантировать, что у него есть согласие этих лиц на передачу и обработку их персональных данных в рамках Сервиса;
|
||||||
|
|
||||||
|
5.2. Администрация обязуется:
|
||||||
|
|
||||||
|
- обеспечивать бесперебойную работу Сервиса, за исключением периодов технического обслуживания;
|
||||||
|
- обрабатывать персональные данные Пользователей в соответствии с Политикой конфиденциальности;
|
||||||
|
- принимать обращения и запросы Пользователей по вопросам работы Сервиса. 6. Ответственность сторон;
|
||||||
|
|
||||||
|
6.1. Сервис предоставляется «как есть». Администрация не несёт ответственности за временные сбои, потерю данных или недоступность Сервиса, возникшие по причинам, не зависящим от неё.
|
||||||
|
|
||||||
|
6.2. Пользователь несёт полную ответственность за корректность совершаемых платежей и действий, совершаемых через свой Telegram-аккаунт.
|
||||||
|
|
||||||
|
#### 7. Обработка персональных данных
|
||||||
|
|
||||||
|
7.1. Администрация обрабатывает персональные данные Пользователя в соответствии с Федеральным законом №152-ФЗ «О персональных данных» и Политикой конфиденциальности.
|
||||||
|
|
||||||
|
7.2. Использование Сервиса означает согласие Пользователя на обработку его персональных данных.
|
||||||
|
|
||||||
|
#### 8. Срок действия и расторжение договора
|
||||||
|
|
||||||
|
8.1. Договор вступает в силу с момента оплаты доступа и действует в течение оплаченного периода.
|
||||||
|
|
||||||
|
8.2. Пользователь может прекратить использование Сервиса в любое время без возврата оплаченных средств.
|
||||||
|
|
||||||
|
8.3. Администрация вправе приостановить доступ в случае нарушения Пользователем условий настоящей Оферты.
|
||||||
|
|
||||||
|
#### 9. Заключительные положения
|
||||||
|
|
||||||
|
9.1. Настоящий Договор регулируется законодательством Российской Федерации.
|
||||||
|
|
||||||
|
9.2. Все споры и разногласия решаются путём переговоров, а при недостижении соглашения — в судебном порядке по месту нахождения Администрации.
|
||||||
|
|
||||||
|
9.3. Администрация оставляет за собой право изменять условия Оферты с размещением новой редакции на сайте.
|
||||||
|
|
||||||
|
#### 10. Контакты
|
||||||
|
|
||||||
|
Если у Вас есть вопросы по настоящему договору публичной оферты персональных данных, пожалуйста, свяжитесь с Разработчиком. Контакты указаны в описании бота.
|
||||||
@ -1,242 +0,0 @@
|
|||||||
import { Container } from '@/components/layout';
|
|
||||||
import { PageHeader } from '@/components/navigation';
|
|
||||||
import { env } from '@/config/env';
|
|
||||||
|
|
||||||
export default function OfferPage() {
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<PageHeader title="Публичная оферта" />
|
|
||||||
<Container className="prose prose-neutral dark:prose-invert">
|
|
||||||
<section className="mx-auto space-y-8">
|
|
||||||
<h1 className="text-2xl font-bold">
|
|
||||||
Договор-оферта на использование сервиса «Запишись.онлайн» (@zapishis_online_bot)
|
|
||||||
</h1>
|
|
||||||
<p className="mt-4">
|
|
||||||
Настоящий документ является публичной офертой в соответствии с пунктом 2 статьи 437
|
|
||||||
Гражданского кодекса Российской Федерации и представляет собой предложение
|
|
||||||
индивидуального предпринимателя (самозанятого) — далее именуемого «Администрация»,
|
|
||||||
заключить Договор на использование Сервиса (далее – «Договор», «Оферта») с любым
|
|
||||||
физическим лицом, принявшим условия настоящей Оферты (далее – «Пользователь»).
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<div className="my-6 border-t border-gray-300 dark:border-gray-600" />
|
|
||||||
|
|
||||||
<h2 className="text-2xl font-semibold">1. Термины и определения</h2>
|
|
||||||
<div className="space-y-3">
|
|
||||||
<p>
|
|
||||||
<strong>1.1.</strong> Оферта — настоящий документ, постоянно размещенный в сети
|
|
||||||
Интернет по адресу <a href="#">{env.OFFER_URL}</a>.
|
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
<strong>1.2.</strong> Акцепт — полное и безоговорочное принятие условий Оферты
|
|
||||||
Пользователем путем оплаты доступа через встроенный платежный бот ЮKassa в Telegram.
|
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
<strong>1.3.</strong> Сервис — Telegram-бот и мини-приложение, позволяющее
|
|
||||||
пользователям создавать и принимать заказы, управлять расписанием и взаимодействовать
|
|
||||||
друг с другом без необходимости регистрации.
|
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
<strong>1.4.</strong> Администрация — самозанятое лицо, являющееся разработчиком и
|
|
||||||
правообладателем Сервиса.
|
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
<strong>1.5.</strong> Пользователь — любое физическое лицо, использующее Сервис в
|
|
||||||
личных или профессиональных целях.
|
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
<strong>1.6.</strong> Доступ — право использования функционала Сервиса на определённый
|
|
||||||
оплаченный период (например, неделя, месяц, год).
|
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
<strong>1.7.</strong> Оплата — денежные средства, перечисленные Пользователем через
|
|
||||||
платёжный бот ЮKassa в Telegram.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="my-6 border-t border-gray-300 dark:border-gray-600" />
|
|
||||||
|
|
||||||
<h2 className="text-2xl font-semibold">2. Акцепт оферты и заключение договора</h2>
|
|
||||||
<div className="space-y-3">
|
|
||||||
<p>
|
|
||||||
<strong>2.1.</strong> Акцептом настоящей Оферты считается оплата Пользователем доступа
|
|
||||||
к Сервису любым доступным способом.
|
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
<strong>2.2.</strong> С момента совершения оплаты Пользователь считается заключившим
|
|
||||||
Договор с Администрацией на условиях, изложенных в настоящей Оферте.
|
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
<strong>2.3.</strong> Пользователь подтверждает, что ему понятны все условия настоящей
|
|
||||||
Оферты и он принимает их без ограничений.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="my-6 border-t border-gray-300 dark:border-gray-600" />
|
|
||||||
|
|
||||||
<h2 className="text-2xl font-semibold">3. Предмет договора</h2>
|
|
||||||
<div className="space-y-3">
|
|
||||||
<p>
|
|
||||||
<strong>3.1.</strong> Администрация предоставляет Пользователю неисключительное право
|
|
||||||
(доступ) на использование функционала Сервиса в пределах оплаченного периода времени.
|
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
<strong>3.2.</strong> Сервис предоставляется в онлайн-формате через Telegram-бота без
|
|
||||||
установки дополнительного программного обеспечения.
|
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
<strong>3.3.</strong> Пользователь получает право использовать функционал Сервиса в
|
|
||||||
личных целях, в том числе для организации и планирования заказов, встреч и тренировок.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="my-6 border-t border-gray-300 dark:border-gray-600" />
|
|
||||||
|
|
||||||
<h2 className="text-2xl font-semibold">4. Порядок оплаты и использование</h2>
|
|
||||||
<div className="space-y-3">
|
|
||||||
<p>
|
|
||||||
<strong>4.1.</strong> Оплата производится через встроенные инструменты Telegram-бота с
|
|
||||||
использованием платёжной системы ЮKassa.
|
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
<strong>4.2.</strong> Комиссия платёжной системы включена в итоговую стоимость.
|
|
||||||
Администрация не взимает дополнительных платежей.
|
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
<strong>4.3.</strong> Доступ активируется автоматически после успешного подтверждения
|
|
||||||
оплаты.
|
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
<strong>4.4.</strong> Пользователь может продлить доступ путём повторной оплаты.
|
|
||||||
Автоматическое продление не применяется.
|
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
<strong>4.5.</strong> Возврат денежных средств возможен только в случае технических
|
|
||||||
ошибок, по письменному обращению на адрес поддержки.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="my-6 border-t border-gray-300 dark:border-gray-600" />
|
|
||||||
|
|
||||||
<h2 className="text-2xl font-semibold">5. Права и обязанности сторон</h2>
|
|
||||||
<div className="space-y-3">
|
|
||||||
<p>
|
|
||||||
<strong>5.1.</strong> Пользователь обязуется:
|
|
||||||
</p>
|
|
||||||
<ul className="list-inside list-disc space-y-1">
|
|
||||||
<li>не использовать Сервис в противоправных целях;</li>
|
|
||||||
<li>
|
|
||||||
не вмешиваться в работу Сервиса и не предпринимать действий, направленных на
|
|
||||||
нарушение его функционирования;
|
|
||||||
</li>
|
|
||||||
<li>предоставлять достоверную информацию при оплате и использовании Сервиса.</li>
|
|
||||||
<li>
|
|
||||||
при добавлении контактов других лиц (например, клиентов, мастеров) гарантировать,
|
|
||||||
что у него есть согласие этих лиц на передачу и обработку их персональных данных в
|
|
||||||
рамках Сервиса.
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
<p>
|
|
||||||
<strong>5.2.</strong> Администрация обязуется:
|
|
||||||
</p>
|
|
||||||
<ul className="list-inside list-disc space-y-1">
|
|
||||||
<li>
|
|
||||||
обеспечивать бесперебойную работу Сервиса, за исключением периодов технического
|
|
||||||
обслуживания;
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
обрабатывать персональные данные Пользователей в соответствии с{' '}
|
|
||||||
<a href="/privacy">Политикой конфиденциальности</a>;
|
|
||||||
</li>
|
|
||||||
<li>принимать обращения и запросы Пользователей по вопросам работы Сервиса.</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="my-6 border-t border-gray-300 dark:border-gray-600" />
|
|
||||||
|
|
||||||
<h2 className="text-2xl font-semibold">6. Ответственность сторон</h2>
|
|
||||||
<div className="space-y-3">
|
|
||||||
<p>
|
|
||||||
<strong>6.1.</strong> Сервис предоставляется «как есть». Администрация не несёт
|
|
||||||
ответственности за временные сбои, потерю данных или недоступность Сервиса, возникшие
|
|
||||||
по причинам, не зависящим от неё.
|
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
<strong>6.2.</strong> Пользователь несёт полную ответственность за корректность
|
|
||||||
совершаемых платежей и действий, совершаемых через свой Telegram-аккаунт.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="my-6 border-t border-gray-300 dark:border-gray-600" />
|
|
||||||
|
|
||||||
<h2 className="text-2xl font-semibold">7. Обработка персональных данных</h2>
|
|
||||||
<div className="space-y-3">
|
|
||||||
<p>
|
|
||||||
<strong>7.1.</strong> Администрация обрабатывает персональные данные Пользователя в
|
|
||||||
соответствии с Федеральным законом №152-ФЗ «О персональных данных» и{' '}
|
|
||||||
<a href="/privacy">Политикой конфиденциальности</a>.
|
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
<strong>7.2.</strong> Использование Сервиса означает согласие Пользователя на
|
|
||||||
обработку его персональных данных.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="my-6 border-t border-gray-300 dark:border-gray-600" />
|
|
||||||
|
|
||||||
<h2 className="text-2xl font-semibold">8. Срок действия и расторжение договора</h2>
|
|
||||||
<div className="space-y-3">
|
|
||||||
<p>
|
|
||||||
<strong>8.1.</strong> Договор вступает в силу с момента оплаты доступа и действует в
|
|
||||||
течение оплаченного периода.
|
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
<strong>8.2.</strong> Пользователь может прекратить использование Сервиса в любое
|
|
||||||
время без возврата оплаченных средств.
|
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
<strong>8.3.</strong> Администрация вправе приостановить доступ в случае нарушения
|
|
||||||
Пользователем условий настоящей Оферты.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="my-6 border-t border-gray-300 dark:border-gray-600" />
|
|
||||||
|
|
||||||
<h2 className="text-2xl font-semibold">9. Заключительные положения</h2>
|
|
||||||
<div className="space-y-3">
|
|
||||||
<p>
|
|
||||||
<strong>9.1.</strong> Настоящий Договор регулируется законодательством Российской
|
|
||||||
Федерации.
|
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
<strong>9.2.</strong> Все споры и разногласия решаются путём переговоров, а при
|
|
||||||
недостижении соглашения — в судебном порядке по месту нахождения Администрации.
|
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
<strong>9.3.</strong> Администрация оставляет за собой право изменять условия Оферты с
|
|
||||||
размещением новой редакции на сайте.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="my-6 border-t border-gray-300 dark:border-gray-600" />
|
|
||||||
|
|
||||||
<h2 className="text-2xl font-semibold">10. Контакты</h2>
|
|
||||||
<p>
|
|
||||||
Если у Вас есть вопросы по настоящему договору публичной оферты персональных данных,
|
|
||||||
пожалуйста, свяжитесь с Разработчиком:
|
|
||||||
</p>
|
|
||||||
<ul>
|
|
||||||
<li>
|
|
||||||
Telegram:{' '}
|
|
||||||
<strong>
|
|
||||||
<a href={env.SUPPORT_TELEGRAM_URL}>{env.SUPPORT_TELEGRAM_URL}</a>
|
|
||||||
</strong>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
|
|
||||||
<div className="h-10" />
|
|
||||||
</section>
|
|
||||||
</Container>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
6
apps/web/app/(documents)/privacy/layout.tsx
Normal file
6
apps/web/app/(documents)/privacy/layout.tsx
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
import { DocumentsLayout } from '@/components/documents/layout';
|
||||||
|
import { type PropsWithChildren } from 'react';
|
||||||
|
|
||||||
|
export default function Layout({ children }: Readonly<PropsWithChildren>) {
|
||||||
|
return <DocumentsLayout title="Политика конфиденциальности">{children}</DocumentsLayout>;
|
||||||
|
}
|
||||||
131
apps/web/app/(documents)/privacy/page.mdx
Normal file
131
apps/web/app/(documents)/privacy/page.mdx
Normal file
@ -0,0 +1,131 @@
|
|||||||
|
export const metadata = {
|
||||||
|
title: 'Политика конфиденциальности',
|
||||||
|
description:
|
||||||
|
'Политика конфиденциальности бота / мини-приложения «Запишись.онлайн» (@zapishis_online_bot)',
|
||||||
|
};
|
||||||
|
|
||||||
|
### Политика конфиденциальности бота / мини-приложения «Запишись.онлайн» (@zapishis_online_bot)
|
||||||
|
|
||||||
|
#### 1. Термины и определения
|
||||||
|
|
||||||
|
- **Telegram** – Telegram Messenger Inc. (платформа, на которой работает бот и мини-приложение).
|
||||||
|
- **Платформа** – экосистема ботов и мини-приложений Telegram.
|
||||||
|
- **Разработчик** – физическое лицо, самозанятый, владелец и оператор сервиса «Запишись.онлайн» (@zapishis_online_bot) - (далее — «Разработчик»).
|
||||||
|
- **Сторонний сервис** – бот/мини-приложение Разработчика, предоставляемое в Платформе.
|
||||||
|
- **Пользователь** – лицо, использующее Сторонний сервис через свою учетную запись Telegram (далее — «Вы»).
|
||||||
|
- **Политика** – настоящий документ, регулирующий отношения между Разработчиком и Пользователем в части сбора и - обработки персональных данных.
|
||||||
|
|
||||||
|
#### 2. Общие положения
|
||||||
|
|
||||||
|
2.1. Настоящая Политика регулирует исключительно отношения между Разработчиком и Пользователем. Она не заменяет и не изменяет Политику конфиденциальности Telegram: [https://telegram.org/privacy](https://telegram.org/privacy).
|
||||||
|
|
||||||
|
2.2. Разработчик соблюдает применимые требования платформы Telegram к конфиденциальности и защите данных.
|
||||||
|
|
||||||
|
2.3. Использование Сервиса Пользователем и/или активация платного доступа означает согласие Пользователя с условиями настоящей Политики.
|
||||||
|
|
||||||
|
2.4. Если Вы не согласны с условиями Политики — прекратите использование Сервиса.
|
||||||
|
|
||||||
|
#### 3. Отказ от ответственности
|
||||||
|
|
||||||
|
3.1. Сторонний сервис является независимым приложением и не поддерживается, не одобряется и не аффилирован с Telegram (за исключением использования API и инфраструктуры Telegram).
|
||||||
|
|
||||||
|
3.2. Разработчик вправе изменять настоящую Политику — изменения вступают в силу с момента их публикации. Вы обязаны самостоятельно отслеживать обновления.
|
||||||
|
|
||||||
|
3.3. Используя Сервис, Вы подтверждаете, что ознакомлены и согласны с условиями использования Telegram для ботов и мини-приложений: [https://telegram.org/tos/bots](https://telegram.org/tos/bots), [https://telegram.org/tos/mini-apps](https://telegram.org/tos/mini-apps).
|
||||||
|
|
||||||
|
3.4. Вы гарантируете, что используете Сервис в соответствии с действующим законодательством и обладаете правом взаимодействовать с ним (например, достигли возраста, необходимого для использования услуг).
|
||||||
|
|
||||||
|
3.5. Вы обязуетесь предоставлять точную и актуальную информацию, если Сервис запрашивает её.
|
||||||
|
|
||||||
|
3.6. Любая информация, которую Вы делаете общедоступной самостоятельно (через профиль Telegram, публичные сообщения и т.п.), может стать доступна другим пользователям и не подпадает под защиту настоящей Политики в части конфиденциальности этой общедоступной информации.
|
||||||
|
|
||||||
|
#### 4. Сбор персональных данных
|
||||||
|
|
||||||
|
4.1. Telegram по умолчанию предоставляет сторонним сервисам ограниченный набор данных о Пользователе — подробнее: [https://telegram.org/privacy#6-bot-messages](https://telegram.org/privacy#6-bot-messages).
|
||||||
|
|
||||||
|
4.2. Сторонний сервис может дополнительно получать данные, которые Вы передаёте в чате бота или в мини-приложении (например, контакт, телефон), если Вы явно их отправляете.
|
||||||
|
|
||||||
|
4.3. В случае мини-приложения дополнительно могут передаваться данные в соответствии с правилами мини-приложений Telegram: [https://telegram.org/tos/mini-apps#4-privacy](https://telegram.org/tos/mini-apps#4-privacy).
|
||||||
|
|
||||||
|
4.4. Сторонний сервис может собирать также анонимную статистику использования (диагностика, события взаимодействия), не связываемую напрямую с персоной.
|
||||||
|
|
||||||
|
4.5. Пользователь может передавать данные третьих лиц (например, контактные данные клиентов или мастеров) для использования в Сервисе. При этом пользователь гарантирует, что эти лица дали согласие на обработку их персональных данных в рамках Сервиса.
|
||||||
|
|
||||||
|
#### 5. Какие данные мы собираем и как используем
|
||||||
|
|
||||||
|
5.1. Разработчик запрашивает, собирает и обрабатывает только те данные, которые необходимы для корректной работы функций Сервиса, в частности:
|
||||||
|
|
||||||
|
- Telegram ID и (опционально) отображаемое имя пользователя;
|
||||||
|
- телефон, только если Вы предоставили его добровольно (например, при регистрации);
|
||||||
|
- данные о заказах: дата/время, описание заказа, статус;
|
||||||
|
- информация о факте покупки Pro-доступа: период доступа, тип покупки (детали платёжной транзакции обрабатывает платёжный оператор — ЮKassa);
|
||||||
|
|
||||||
|
5.2. Цели обработки:
|
||||||
|
|
||||||
|
- предоставление и поддержка работы Сервиса (создание заказов, напоминания, управление доступом);
|
||||||
|
- подтверждение и учет оплат (взаимодействие с платёжным оператором для актуализации статуса доступа);
|
||||||
|
- реализация реферальной программы (хранение связей «кто пригласил/кого пригласили»);
|
||||||
|
- анализ использования и улучшение сервиса;
|
||||||
|
- выполнение юридических обязательств (хранение информации о транзакциях и др.);
|
||||||
|
|
||||||
|
> **Важно:** детальные платёжные данные (реквизиты карт и т.д.) не хранятся у Разработчика — их обрабатывает платёжный оператор (ЮKassa) и Telegram-платежный бот.
|
||||||
|
|
||||||
|
#### 6. Передача данных третьим лицам
|
||||||
|
|
||||||
|
6.1. Разработчик не передаёт персональные данные третьим лицам, за исключением следующих случаев:
|
||||||
|
|
||||||
|
- платёжному оператору (ЮKassa) и связанным службам для обработки платежей;
|
||||||
|
- Telegram как платформе для функционирования бота и мини-приложения;
|
||||||
|
- в случае необходимости — исполнителям, оказывающим техническую поддержку, при условии подписания ими обязательств о конфиденциальности;
|
||||||
|
- если передача требуется по закону (запросы уполномоченных органов и т.п.);
|
||||||
|
|
||||||
|
6.2. Разработчик не продаёт и не передаёт персональные данные для рекламных целей третьим лицам без Вашего отдельного согласия.
|
||||||
|
|
||||||
|
#### 7. Защита и хранение данных
|
||||||
|
|
||||||
|
7.1. Разработчик применяет разумные технические и организационные меры для защиты персональных данных (использование надежного VPS, ограничения доступа, резервное копирование и т.п.).
|
||||||
|
|
||||||
|
7.2. Доступ к персональным данным имеет только Разработчик (и/или доверенные исполнители технической поддержки при необходимости).
|
||||||
|
|
||||||
|
7.3. Данные хранятся на серверах, указанных Разработчиком. Если используются внешние сервисы/облачные хранилища — это будет указано в соответствующих местах Политики или сообщения при сборе данных.
|
||||||
|
|
||||||
|
#### 8. Права и обязанности сторон
|
||||||
|
|
||||||
|
8.1. Права Разработчика:
|
||||||
|
|
||||||
|
- вносить изменения в Политику с публикацией новой версии;
|
||||||
|
- ограничивать доступ к API/сервису при подозрении в злоупотреблениях;
|
||||||
|
- запросить подтверждение личности при необходимости обработки привилегированных запросов;
|
||||||
|
|
||||||
|
8.2. Обязанности Разработчика:
|
||||||
|
|
||||||
|
- обеспечивать доступность Политики и исполнять её условия;
|
||||||
|
- обрабатывать законные запросы пользователей о доступе, изменении или удалении данных в разумные сроки (не позднее 30 дней, если иное не установлено законом);
|
||||||
|
- соблюдать применимое законодательство о защите персональных данных;
|
||||||
|
|
||||||
|
8.3. Права Пользователя:
|
||||||
|
|
||||||
|
- запросить копию своих персональных данных, хранящихся у Разработчика;
|
||||||
|
- потребовать исправления неточных данных;
|
||||||
|
- потребовать удаления персональных данных в пределах, допустимых законом (с сохранением данных, необходимых для выполнения юридических обязательств, например, по учёту платежей);
|
||||||
|
- отозвать согласие на обработку персональных данных, если такое согласие предоставлялось добровольно;
|
||||||
|
- подать жалобу в уполномоченные органы по защите персональных данных, если считает, что его права нарушены;
|
||||||
|
|
||||||
|
8.4. Обязанности Пользователя:
|
||||||
|
|
||||||
|
- предоставлять точную и актуальную информацию;
|
||||||
|
- не использовать Сервис в нарушении законодательства и условий Telegram.
|
||||||
|
|
||||||
|
#### 9. Реклама и использование данных для аналитики
|
||||||
|
|
||||||
|
9.1. На текущем этапе Разработчик не использует персональные данные для демонстрации таргетированной рекламы третьих лиц без явного согласия Пользователя.
|
||||||
|
|
||||||
|
9.2. Разработчик может собирать агрегированную (анонимную) статистику использования Сервиса для улучшения функционала.
|
||||||
|
|
||||||
|
#### 10. Изменения Политики
|
||||||
|
|
||||||
|
10.1. Разработчик вправе вносить изменения в настоящую Политику. Все изменения публикуются на этой странице и вступают в силу с момента публикации.
|
||||||
|
|
||||||
|
#### 11. Контакты
|
||||||
|
|
||||||
|
Если у Вас есть вопросы по Политике конфиденциальности или запросы в отношении персональных данных, пожалуйста, свяжитесь с Разработчиком. Контакты указаны в описании бота.
|
||||||
@ -1,266 +0,0 @@
|
|||||||
import { Container } from '@/components/layout';
|
|
||||||
import { PageHeader } from '@/components/navigation';
|
|
||||||
import { env } from '@/config/env';
|
|
||||||
|
|
||||||
export default function PrivacyPolicyPage() {
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<PageHeader title="Политика конфиденциальности" />
|
|
||||||
<Container className="prose prose-neutral md:prose-lg dark:prose-invert max-w-none">
|
|
||||||
<h1 className="text-2xl font-bold">
|
|
||||||
Политика конфиденциальности бота / мини-приложения «Запишись.онлайн»
|
|
||||||
(@zapishis_online_bot)
|
|
||||||
</h1>
|
|
||||||
|
|
||||||
<h2 className="text-2xl font-semibold">1. Термины и определения</h2>
|
|
||||||
<ol>
|
|
||||||
<li>
|
|
||||||
<strong>Telegram</strong> – Telegram Messenger Inc. (платформа, на которой работает бот
|
|
||||||
и мини-приложение).
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<strong>Платформа</strong> – экосистема ботов и мини-приложений Telegram.
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<strong>Разработчик</strong> – физическое лицо, самозанятый, владелец и оператор сервиса
|
|
||||||
«Запишись.онлайн» (@zapishis_online_bot) (далее — «Разработчик»).
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<strong>Сторонний сервис</strong> – бот/мини-приложение Разработчика, предоставляемое в
|
|
||||||
Платформе.
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<strong>Пользователь</strong> – лицо, использующее Сторонний сервис через свою учетную
|
|
||||||
запись Telegram (далее — «Вы»).
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<strong>Политика</strong> – настоящий документ, регулирующий отношения между
|
|
||||||
Разработчиком и Пользователем в части сбора и обработки персональных данных.
|
|
||||||
</li>
|
|
||||||
</ol>
|
|
||||||
|
|
||||||
<h2 className="text-2xl font-semibold">2. Общие положения</h2>
|
|
||||||
<p>
|
|
||||||
2.1. Настоящая Политика регулирует исключительно отношения между Разработчиком и
|
|
||||||
Пользователем. Она не заменяет и не изменяет Политику конфиденциальности Telegram:{' '}
|
|
||||||
<a href="https://telegram.org/privacy">https://telegram.org/privacy</a>.
|
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
2.2. Разработчик соблюдает применимые требования платформы Telegram к конфиденциальности и
|
|
||||||
защите данных.
|
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
2.3. Использование Сервиса Пользователем и/или активация платного доступа означает
|
|
||||||
согласие Пользователя с условиями настоящей Политики.
|
|
||||||
</p>
|
|
||||||
<p>2.4. Если Вы не согласны с условиями Политики — прекратите использование Сервиса.</p>
|
|
||||||
|
|
||||||
<h2 className="text-2xl font-semibold">3. Отказ от ответственности</h2>
|
|
||||||
<p>
|
|
||||||
3.1. Сторонний сервис является независимым приложением и не поддерживается, не одобряется
|
|
||||||
и не аффилирован с Telegram (за исключением использования API и инфраструктуры Telegram).
|
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
3.2. Разработчик вправе изменять настоящую Политику — изменения вступают в силу с момента
|
|
||||||
их публикации. Вы обязаны самостоятельно отслеживать обновления.
|
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
3.3. Используя Сервис, Вы подтверждаете, что ознакомлены и согласны с условиями
|
|
||||||
использования Telegram для ботов и мини-приложений:{' '}
|
|
||||||
<a href="https://telegram.org/tos/bots">https://telegram.org/tos/bots</a>,{' '}
|
|
||||||
<a href="https://telegram.org/tos/mini-apps">https://telegram.org/tos/mini-apps</a>.
|
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
3.4. Вы гарантируете, что используете Сервис в соответствии с действующим
|
|
||||||
законодательством и обладаете правом взаимодействовать с ним (например, достигли возраста,
|
|
||||||
необходимого для использования услуг).
|
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
3.5. Вы обязуетесь предоставлять точную и актуальную информацию, если Сервис запрашивает
|
|
||||||
её.
|
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
3.6. Любая информация, которую Вы делаете общедоступной самостоятельно (через профиль
|
|
||||||
Telegram, публичные сообщения и т.п.), может стать доступна другим пользователям и не
|
|
||||||
подпадает под защиту настоящей Политики в части конфиденциальности этой общедоступной
|
|
||||||
информации.
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<h2 className="text-2xl font-semibold">4. Сбор персональных данных</h2>
|
|
||||||
<p>
|
|
||||||
4.1. Telegram по умолчанию предоставляет сторонним сервисам ограниченный набор данных о
|
|
||||||
Пользователе — подробнее:{' '}
|
|
||||||
<a href="https://telegram.org/privacy#6-bot-messages">
|
|
||||||
https://telegram.org/privacy#6-bot-messages
|
|
||||||
</a>
|
|
||||||
.
|
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
4.2. Сторонний сервис может дополнительно получать данные, которые Вы передаёте в чате
|
|
||||||
бота или в мини-приложении (например, контакт, телефон), если Вы явно их отправляете.
|
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
4.3. В случае мини-приложения дополнительно могут передаваться данные в соответствии с
|
|
||||||
правилами мини-приложений Telegram:{' '}
|
|
||||||
<a href="https://telegram.org/tos/mini-apps#4-privacy">
|
|
||||||
https://telegram.org/tos/mini-apps#4-privacy
|
|
||||||
</a>
|
|
||||||
.
|
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
4.4. Сторонний сервис может собирать также анонимную статистику использования
|
|
||||||
(диагностика, события взаимодействия), не связываемую напрямую с персоной.
|
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
4.5. Пользователь может передавать данные третьих лиц (например, контактные данные
|
|
||||||
клиентов или мастеров) для использования в Сервисе. При этом пользователь гарантирует, что
|
|
||||||
эти лица дали согласие на обработку их персональных данных в рамках Сервиса.
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<h2 className="text-2xl font-semibold">5. Какие данные мы собираем и как используем</h2>
|
|
||||||
<p>
|
|
||||||
5.1. Разработчик запрашивает, собирает и обрабатывает только те данные, которые необходимы
|
|
||||||
для корректной работы функций Сервиса, в частности:
|
|
||||||
</p>
|
|
||||||
<ul>
|
|
||||||
<li>Telegram ID и (опционально) отображаемое имя пользователя;</li>
|
|
||||||
<li>телефон, только если Вы предоставили его добровольно (например, при регистрации);</li>
|
|
||||||
<li>данные о заказах: дата/время, описание заказа, статус;</li>
|
|
||||||
<li>
|
|
||||||
информация о факте покупки Pro-доступа: период доступа, тип покупки (детали платёжной
|
|
||||||
транзакции обрабатывает платёжный оператор — ЮKassa).
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
<p>5.2. Цели обработки:</p>
|
|
||||||
<ul>
|
|
||||||
<li>
|
|
||||||
предоставление и поддержка работы Сервиса (создание заказов, напоминания, управление
|
|
||||||
доступом);
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
подтверждение и учет оплат (взаимодействие с платёжным оператором для актуализации
|
|
||||||
статуса доступа);
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
реализация реферальной программы (хранение связей «кто пригласил/кого пригласили»);
|
|
||||||
</li>
|
|
||||||
<li>анализ использования и улучшение сервиса;</li>
|
|
||||||
<li>выполнение юридических обязательств (хранение информации о транзакциях и др.).</li>
|
|
||||||
</ul>
|
|
||||||
<p className="note">
|
|
||||||
Важно: детальные платёжные данные (реквизиты карт и т.д.) не хранятся у Разработчика — их
|
|
||||||
обрабатывает платёжный оператор (ЮKassa) и Telegram-платежный бот.
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<h2 className="text-2xl font-semibold">6. Передача данных третьим лицам</h2>
|
|
||||||
<p>
|
|
||||||
6.1. Разработчик не передаёт персональные данные третьим лицам, за исключением следующих
|
|
||||||
случаев:
|
|
||||||
</p>
|
|
||||||
<ul>
|
|
||||||
<li>платёжному оператору (ЮKassa) и связанным службам для обработки платежей;</li>
|
|
||||||
<li>Telegram как платформе для функционирования бота и мини-приложения;</li>
|
|
||||||
<li>
|
|
||||||
в случае необходимости — исполнителям, оказывающим техническую поддержку, при условии
|
|
||||||
подписания ими обязательств о конфиденциальности;
|
|
||||||
</li>
|
|
||||||
<li>если передача требуется по закону (запросы уполномоченных органов и т.п.).</li>
|
|
||||||
</ul>
|
|
||||||
<p>
|
|
||||||
6.2. Разработчик не продаёт и не передаёт персональные данные для рекламных целей третьим
|
|
||||||
лицам без Вашего отдельного согласия.
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<h2 className="text-2xl font-semibold">7. Защита и хранение данных</h2>
|
|
||||||
<p>
|
|
||||||
7.1. Разработчик применяет разумные технические и организационные меры для защиты
|
|
||||||
персональных данных (использование надежного VPS, ограничения доступа, резервное
|
|
||||||
копирование и т.п.).
|
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
7.2. Доступ к персональным данным имеет только Разработчик (и/или доверенные исполнители
|
|
||||||
технической поддержки при необходимости).
|
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
7.3. Данные хранятся на серверах, указанных Разработчиком. Если используются внешние
|
|
||||||
сервисы/облачные хранилища — это будет указано в соответствующих местах Политики или
|
|
||||||
сообщения при сборе данных.
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<h2 className="text-2xl font-semibold">8. Права и обязанности сторон</h2>
|
|
||||||
<p>8.1. Права Разработчика:</p>
|
|
||||||
<ul>
|
|
||||||
<li>вносить изменения в Политику с публикацией новой версии;</li>
|
|
||||||
<li>ограничивать доступ к API/сервису при подозрении в злоупотреблениях;</li>
|
|
||||||
<li>
|
|
||||||
запросить подтверждение личности при необходимости обработки привилегированных запросов.
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
<p>8.2. Обязанности Разработчика:</p>
|
|
||||||
<ul>
|
|
||||||
<li>обеспечивать доступность Политики и исполнять её условия;</li>
|
|
||||||
<li>
|
|
||||||
обрабатывать законные запросы пользователей о доступе, изменении или удалении данных в
|
|
||||||
разумные сроки (не позднее 30 дней, если иное не установлено законом);
|
|
||||||
</li>
|
|
||||||
<li>соблюдать применимое законодательство о защите персональных данных.</li>
|
|
||||||
</ul>
|
|
||||||
<p>8.3. Права Пользователя:</p>
|
|
||||||
<ul>
|
|
||||||
<li>запросить копию своих персональных данных, хранящихся у Разработчика;</li>
|
|
||||||
<li>потребовать исправления неточных данных;</li>
|
|
||||||
<li>
|
|
||||||
потребовать удаления персональных данных в пределах, допустимых законом (с сохранением
|
|
||||||
данных, необходимых для выполнения юридических обязательств, например, по учёту
|
|
||||||
платежей);
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
отозвать согласие на обработку персональных данных, если такое согласие предоставлялось
|
|
||||||
добровольно;
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
подать жалобу в уполномоченные органы по защите персональных данных, если считает, что
|
|
||||||
его права нарушены.
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
<p>8.4. Обязанности Пользователя:</p>
|
|
||||||
<ul>
|
|
||||||
<li>предоставлять точную и актуальную информацию;</li>
|
|
||||||
<li>не использовать Сервис в нарушении законодательства и условий Telegram.</li>
|
|
||||||
</ul>
|
|
||||||
|
|
||||||
<h2 className="text-2xl font-semibold">9. Реклама и использование данных для аналитики</h2>
|
|
||||||
<p>
|
|
||||||
9.1. На текущем этапе Разработчик не использует персональные данные для демонстрации
|
|
||||||
таргетированной рекламы третьих лиц без явного согласия Пользователя.
|
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
9.2. Разработчик может собирать агрегированную (анонимную) статистику использования
|
|
||||||
Сервиса для улучшения функционала.
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<h2 className="text-2xl font-semibold">10. Изменения Политики</h2>
|
|
||||||
<p>
|
|
||||||
10.1. Разработчик вправе вносить изменения в настоящую Политику. Все изменения публикуются
|
|
||||||
на этой странице и вступают в силу с момента публикации.
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<h2 className="text-2xl font-semibold">11. Контакты</h2>
|
|
||||||
<p>
|
|
||||||
Если у Вас есть вопросы по Политике конфиденциальности или запросы в отношении
|
|
||||||
персональных данных, пожалуйста, свяжитесь с Разработчиком:
|
|
||||||
</p>
|
|
||||||
<ul>
|
|
||||||
<li>
|
|
||||||
Telegram:{' '}
|
|
||||||
<strong>
|
|
||||||
<a href={env.SUPPORT_TELEGRAM_URL}>{env.SUPPORT_TELEGRAM_URL}</a>
|
|
||||||
</strong>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
|
|
||||||
<div className="h-10" />
|
|
||||||
</Container>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@ -2,11 +2,9 @@ import { AuthProvider } from '@/providers/auth';
|
|||||||
import { ErrorProvider } from '@/providers/error';
|
import { ErrorProvider } from '@/providers/error';
|
||||||
import { QueryProvider } from '@/providers/query';
|
import { QueryProvider } from '@/providers/query';
|
||||||
import { ThemeProvider } from '@/providers/theme-provider';
|
import { ThemeProvider } from '@/providers/theme-provider';
|
||||||
import { I18nProvider } from '@/utils/i18n/provider';
|
|
||||||
import '@repo/ui/globals.css';
|
import '@repo/ui/globals.css';
|
||||||
import { cn } from '@repo/ui/lib/utils';
|
import { cn } from '@repo/ui/lib/utils';
|
||||||
import { type Metadata } from 'next';
|
import { type Metadata } from 'next';
|
||||||
import { getLocale } from 'next-intl/server';
|
|
||||||
import { Inter } from 'next/font/google';
|
import { Inter } from 'next/font/google';
|
||||||
import { type PropsWithChildren } from 'react';
|
import { type PropsWithChildren } from 'react';
|
||||||
|
|
||||||
@ -17,19 +15,15 @@ export const metadata: Metadata = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export default async function RootLayout({ children }: Readonly<PropsWithChildren>) {
|
export default async function RootLayout({ children }: Readonly<PropsWithChildren>) {
|
||||||
const locale = await getLocale();
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<html lang={locale}>
|
<html lang="ru">
|
||||||
<body className={cn(inter.className, 'flex min-h-screen flex-col bg-app-background')}>
|
<body className={cn(inter.className, 'flex min-h-screen flex-col bg-app-background')}>
|
||||||
<ErrorProvider>
|
<ErrorProvider>
|
||||||
<I18nProvider>
|
<ThemeProvider>
|
||||||
<ThemeProvider>
|
<AuthProvider>
|
||||||
<AuthProvider>
|
<QueryProvider>{children}</QueryProvider>
|
||||||
<QueryProvider>{children}</QueryProvider>
|
</AuthProvider>
|
||||||
</AuthProvider>
|
</ThemeProvider>
|
||||||
</ThemeProvider>
|
|
||||||
</I18nProvider>
|
|
||||||
</ErrorProvider>
|
</ErrorProvider>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
18
apps/web/components/documents/layout.tsx
Normal file
18
apps/web/components/documents/layout.tsx
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
import { Container } from '@/components/layout';
|
||||||
|
import { PageHeader } from '@/components/navigation';
|
||||||
|
import { type PropsWithChildren } from 'react';
|
||||||
|
|
||||||
|
export function DocumentsLayout({
|
||||||
|
children,
|
||||||
|
title,
|
||||||
|
}: Readonly<PropsWithChildren> & { readonly title: string }) {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<PageHeader title={title} />
|
||||||
|
<Container className="prose prose-neutral md:prose-lg dark:prose-invert max-w-none">
|
||||||
|
{children}
|
||||||
|
</Container>
|
||||||
|
<div className="h-10" />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
17
apps/web/components/documents/links.tsx
Normal file
17
apps/web/components/documents/links.tsx
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
import { env } from '@/config/env';
|
||||||
|
|
||||||
|
export function OfferLink() {
|
||||||
|
return (
|
||||||
|
<a href={env.URL_OFFER} rel="noreferrer" target="_blank">
|
||||||
|
{env.URL_OFFER}
|
||||||
|
</a>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function SupportLink() {
|
||||||
|
return (
|
||||||
|
<a href={env.SUPPORT_TELEGRAM_URL} rel="noreferrer" target="_blank">
|
||||||
|
{env.SUPPORT_TELEGRAM_URL}
|
||||||
|
</a>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -1,4 +1,5 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { BackButton } from './back-button';
|
import { BackButton } from './back-button';
|
||||||
import { cn } from '@repo/ui/lib/utils';
|
import { cn } from '@repo/ui/lib/utils';
|
||||||
import { isTMA } from '@telegram-apps/sdk-react';
|
import { isTMA } from '@telegram-apps/sdk-react';
|
||||||
@ -6,16 +7,16 @@ import { isTMA } from '@telegram-apps/sdk-react';
|
|||||||
type Props = { title: string | undefined };
|
type Props = { title: string | undefined };
|
||||||
|
|
||||||
export function PageHeader(props: Readonly<Props>) {
|
export function PageHeader(props: Readonly<Props>) {
|
||||||
const isTG = isTMA('simple');
|
const hideBackButton = process.env.NODE_ENV === 'production' || isTMA('simple');
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
'sticky top-0 z-50 flex h-12 items-center rounded-b-lg bg-transparent font-bold tracking-wide backdrop-blur-md',
|
'sticky top-0 z-50 flex h-12 items-center rounded-b-lg bg-transparent font-bold tracking-wide backdrop-blur-md',
|
||||||
isTG ? 'px-4' : 'px-2',
|
hideBackButton ? 'px-4' : 'px-2',
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{!isTG && <BackButton />}
|
{!hideBackButton && <BackButton />}
|
||||||
{props.title}
|
{props.title}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -26,8 +26,12 @@ type ContactsGridProps = {
|
|||||||
readonly title: string;
|
readonly title: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type UseContactsProps = Partial<{
|
||||||
|
showInactive: boolean;
|
||||||
|
}>;
|
||||||
|
|
||||||
export function ClientsGrid() {
|
export function ClientsGrid() {
|
||||||
const { contacts, fetchNextPage, hasNextPage, isLoading } = useContacts();
|
const { contacts, fetchNextPage, hasNextPage, isLoading } = useContacts({ showInactive: true });
|
||||||
|
|
||||||
const clientId = useOrderStore((store) => store.clientId);
|
const clientId = useOrderStore((store) => store.clientId);
|
||||||
const setClientId = useOrderStore((store) => store.setClientId);
|
const setClientId = useOrderStore((store) => store.setClientId);
|
||||||
@ -145,7 +149,7 @@ export function MastersGrid() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function useContacts() {
|
function useContacts({ showInactive = false }: UseContactsProps = {}) {
|
||||||
const { data: { customer } = {}, isLoading: isLoadingCustomer } = useCustomerQuery();
|
const { data: { customer } = {}, isLoading: isLoadingCustomer } = useCustomerQuery();
|
||||||
|
|
||||||
const {
|
const {
|
||||||
@ -156,16 +160,13 @@ function useContacts() {
|
|||||||
|
|
||||||
const isLoading = isLoadingContacts || isLoadingCustomer;
|
const isLoading = isLoadingContacts || isLoadingCustomer;
|
||||||
|
|
||||||
const contacts = sift(
|
const contacts = sift(pages.flatMap((page) => page.customers));
|
||||||
pages.flatMap((page) => page.customers).filter((contact) => Boolean(contact && contact.active)),
|
|
||||||
);
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
isLoading,
|
isLoading,
|
||||||
...query,
|
...query,
|
||||||
contacts: [
|
contacts: [{ ...customer, name: 'Я', surname: undefined } as CustomerFieldsFragment].concat(
|
||||||
{ ...customer, name: 'Я', surname: undefined } as CustomerFieldsFragment,
|
showInactive ? contacts : contacts.filter((contact) => contact.active),
|
||||||
...contacts,
|
),
|
||||||
],
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,94 +1,35 @@
|
|||||||
|
/* eslint-disable canonical/id-match */
|
||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import FloatingActionPanel from '@/components/shared/action-panel';
|
import FloatingActionPanel from '@/components/shared/action-panel';
|
||||||
import { useCustomerQuery } from '@/hooks/api/customers';
|
import { useCustomerQuery } from '@/hooks/api/customers';
|
||||||
import { usePushWithData } from '@/hooks/url';
|
import { usePushWithData } from '@/hooks/url';
|
||||||
import { Button } from '@repo/ui/components/ui/button';
|
import { Enum_Customer_Role } from '@repo/graphql/types';
|
||||||
import {
|
|
||||||
Drawer,
|
|
||||||
DrawerClose,
|
|
||||||
DrawerContent,
|
|
||||||
DrawerDescription,
|
|
||||||
DrawerFooter,
|
|
||||||
DrawerHeader,
|
|
||||||
DrawerTitle,
|
|
||||||
DrawerTrigger,
|
|
||||||
} from '@repo/ui/components/ui/drawer';
|
|
||||||
import { useState } from 'react';
|
|
||||||
|
|
||||||
type QuickAppointmentProps = {
|
type QuickAppointmentProps = {
|
||||||
readonly telegramId: number;
|
readonly telegramId: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function ProfileButtons({ telegramId }: Readonly<QuickAppointmentProps>) {
|
export function ProfileButtons({ telegramId }: Readonly<QuickAppointmentProps>) {
|
||||||
const [isPanelOpen, setIsPanelOpen] = useState(false);
|
|
||||||
const push = usePushWithData();
|
const push = usePushWithData();
|
||||||
|
|
||||||
const { data: { customer: profile } = {} } = useCustomerQuery({ telegramId });
|
const { data: { customer: profile } = {}, isLoading: isLoadingProfile } = useCustomerQuery({
|
||||||
const { data: { customer: currentUser } = {} } = useCustomerQuery();
|
telegramId,
|
||||||
|
});
|
||||||
|
const { data: { customer: currentUser } = {}, isLoading: isLoadingCurrentUser } =
|
||||||
|
useCustomerQuery();
|
||||||
|
|
||||||
const handleBookAsClient = () => {
|
const isLoading = isLoadingProfile || isLoadingCurrentUser;
|
||||||
push('/orders/add', {
|
|
||||||
client: currentUser,
|
|
||||||
slot: { master: profile },
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleBookAsMaster = () => {
|
const handleBook = () => {
|
||||||
push('/orders/add', {
|
if (profile?.role === Enum_Customer_Role.Client) {
|
||||||
client: profile,
|
push('/orders/add', { client: profile, slot: { master: currentUser } });
|
||||||
slot: { master: currentUser },
|
} else {
|
||||||
});
|
push('/orders/add', { client: currentUser, slot: { master: profile } });
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
if (!telegramId) return null;
|
if (!telegramId) return null;
|
||||||
|
|
||||||
return (
|
return <FloatingActionPanel isLoading={isLoading} onQuickBook={handleBook} />;
|
||||||
<>
|
|
||||||
<FloatingActionPanel onQuickBook={() => setIsPanelOpen(true)} />
|
|
||||||
<Drawer onOpenChange={setIsPanelOpen} open={isPanelOpen}>
|
|
||||||
<DrawerTrigger asChild>
|
|
||||||
<div />
|
|
||||||
</DrawerTrigger>
|
|
||||||
<DrawerContent>
|
|
||||||
<div className="mx-auto w-full max-w-sm">
|
|
||||||
<DrawerHeader>
|
|
||||||
<DrawerTitle>Быстрая запись</DrawerTitle>
|
|
||||||
<DrawerDescription>Выберите действие</DrawerDescription>
|
|
||||||
</DrawerHeader>
|
|
||||||
<div className="p-4 pt-0">
|
|
||||||
<div className="flex flex-col gap-3">
|
|
||||||
<DrawerClose asChild>
|
|
||||||
<Button
|
|
||||||
className="w-full text-sm"
|
|
||||||
disabled={!profile || !currentUser}
|
|
||||||
onClick={handleBookAsClient}
|
|
||||||
size="sm"
|
|
||||||
>
|
|
||||||
Записаться к мастеру {profile?.name}
|
|
||||||
</Button>
|
|
||||||
</DrawerClose>
|
|
||||||
<DrawerClose asChild>
|
|
||||||
<Button
|
|
||||||
className="w-full text-sm"
|
|
||||||
disabled={!profile || !currentUser}
|
|
||||||
onClick={handleBookAsMaster}
|
|
||||||
size="sm"
|
|
||||||
variant="secondary"
|
|
||||||
>
|
|
||||||
Записать клиента к себе
|
|
||||||
</Button>
|
|
||||||
</DrawerClose>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<DrawerFooter>
|
|
||||||
<DrawerClose asChild>
|
|
||||||
<Button variant="outline">Отмена</Button>
|
|
||||||
</DrawerClose>
|
|
||||||
</DrawerFooter>
|
|
||||||
</div>
|
|
||||||
</DrawerContent>
|
|
||||||
</Drawer>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,3 +1,4 @@
|
|||||||
|
/* eslint-disable canonical/id-match */
|
||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { type ProfileProps } from '../types';
|
import { type ProfileProps } from '../types';
|
||||||
@ -5,13 +6,17 @@ import { DataNotFound } from '@/components/shared/alert';
|
|||||||
import { ServiceCard } from '@/components/shared/service-card';
|
import { ServiceCard } from '@/components/shared/service-card';
|
||||||
import { useCustomerQuery } from '@/hooks/api/customers';
|
import { useCustomerQuery } from '@/hooks/api/customers';
|
||||||
import { useServicesQuery } from '@/hooks/api/services';
|
import { useServicesQuery } from '@/hooks/api/services';
|
||||||
|
import { Enum_Customer_Role } from '@repo/graphql/types';
|
||||||
import { LoadingSpinner } from '@repo/ui/components/ui/spinner';
|
import { LoadingSpinner } from '@repo/ui/components/ui/spinner';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
|
|
||||||
// Компонент для отображения услуг мастера (без ссылок, только просмотр)
|
// Компонент для отображения услуг мастера (без ссылок, только просмотр)
|
||||||
export function ReadonlyServicesList({ telegramId }: Readonly<ProfileProps>) {
|
export function ReadonlyServicesList({ telegramId }: Readonly<ProfileProps>) {
|
||||||
|
const { data: { customer } = {} } = useCustomerQuery({ telegramId });
|
||||||
const { isLoading, services } = useServices(telegramId);
|
const { isLoading, services } = useServices(telegramId);
|
||||||
|
|
||||||
|
if (customer?.role === Enum_Customer_Role.Client) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-2 px-4">
|
<div className="space-y-2 px-4">
|
||||||
<h1 className="font-bold">Услуги</h1>
|
<h1 className="font-bold">Услуги</h1>
|
||||||
|
|||||||
@ -15,6 +15,8 @@ export function SubscriptionInfoBar() {
|
|||||||
|
|
||||||
const { data: { customer } = {}, isLoading: isLoadingCustomer } = useCustomerQuery();
|
const { data: { customer } = {}, isLoading: isLoadingCustomer } = useCustomerQuery();
|
||||||
|
|
||||||
|
if (customer?.role === Enum_Customer_Role.Client) return null;
|
||||||
|
|
||||||
const isLoading = isLoadingCustomer || isLoadingSubscription || isLoadingSubscriptionSetting;
|
const isLoading = isLoadingCustomer || isLoadingSubscription || isLoadingSubscriptionSetting;
|
||||||
|
|
||||||
const isActive = data?.hasActiveSubscription;
|
const isActive = data?.hasActiveSubscription;
|
||||||
@ -36,8 +38,6 @@ export function SubscriptionInfoBar() {
|
|||||||
|
|
||||||
if (!subscriptionSetting?.proEnabled) return null;
|
if (!subscriptionSetting?.proEnabled) return null;
|
||||||
|
|
||||||
if (customer?.role === Enum_Customer_Role.Client) return null;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Link href="/pro" rel="noopener noreferrer">
|
<Link href="/pro" rel="noopener noreferrer">
|
||||||
<div className={cn('px-4', isLoading && 'animate-pulse')}>
|
<div className={cn('px-4', isLoading && 'animate-pulse')}>
|
||||||
|
|||||||
@ -4,7 +4,7 @@ import { Badge } from '@repo/ui/components/ui/badge';
|
|||||||
import { cn } from '@repo/ui/lib/utils';
|
import { cn } from '@repo/ui/lib/utils';
|
||||||
import { getCustomerFullName } from '@repo/utils/customer';
|
import { getCustomerFullName } from '@repo/utils/customer';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
import { memo } from 'react';
|
import { memo, type PropsWithChildren } from 'react';
|
||||||
|
|
||||||
type ContactRowProps = GQL.CustomerFieldsFragment & {
|
type ContactRowProps = GQL.CustomerFieldsFragment & {
|
||||||
readonly className?: string;
|
readonly className?: string;
|
||||||
@ -12,13 +12,30 @@ type ContactRowProps = GQL.CustomerFieldsFragment & {
|
|||||||
readonly showServices?: boolean;
|
readonly showServices?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
function Wrapper({
|
||||||
|
children,
|
||||||
|
contact,
|
||||||
|
}: PropsWithChildren<{ readonly contact: GQL.CustomerFieldsFragment }>) {
|
||||||
|
const isActive = contact.active && contact.telegramId;
|
||||||
|
|
||||||
|
if (isActive) {
|
||||||
|
return (
|
||||||
|
<Link
|
||||||
|
className="block"
|
||||||
|
href={contact.active ? `/profile/${contact.telegramId}` : ''}
|
||||||
|
key={contact.telegramId}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</Link>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return <>{children}</>;
|
||||||
|
}
|
||||||
|
|
||||||
export const ContactRow = memo(function ({ className, description, ...contact }: ContactRowProps) {
|
export const ContactRow = memo(function ({ className, description, ...contact }: ContactRowProps) {
|
||||||
return (
|
return (
|
||||||
<Link
|
<Wrapper contact={contact}>
|
||||||
className="block"
|
|
||||||
href={contact.active ? `/profile/${contact.telegramId}` : ''}
|
|
||||||
key={contact.telegramId}
|
|
||||||
>
|
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
'flex items-center justify-between',
|
'flex items-center justify-between',
|
||||||
@ -37,6 +54,6 @@ export const ContactRow = memo(function ({ className, description, ...contact }:
|
|||||||
</div>
|
</div>
|
||||||
{contact.active ? <div /> : <Badge variant="destructive">Неактивен</Badge>}
|
{contact.active ? <div /> : <Badge variant="destructive">Неактивен</Badge>}
|
||||||
</div>
|
</div>
|
||||||
</Link>
|
</Wrapper>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|||||||
@ -4,8 +4,8 @@ import { z } from 'zod';
|
|||||||
export const envSchema = z.object({
|
export const envSchema = z.object({
|
||||||
__DEV_TELEGRAM_ID: z.string().default(''),
|
__DEV_TELEGRAM_ID: z.string().default(''),
|
||||||
BOT_URL: z.string(),
|
BOT_URL: z.string(),
|
||||||
OFFER_URL: z.string(),
|
|
||||||
SUPPORT_TELEGRAM_URL: z.string(),
|
SUPPORT_TELEGRAM_URL: z.string(),
|
||||||
|
URL_OFFER: z.string(),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const env = envSchema.parse(process.env);
|
export const env = envSchema.parse(process.env);
|
||||||
|
|||||||
@ -34,7 +34,7 @@ export function useBackButton() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function isRootLevelPage(pathname: string) {
|
function isRootLevelPage(pathname: string) {
|
||||||
if (exclude.some((path) => pathname.includes(path))) return false;
|
if (exclude.includes(pathname)) return false;
|
||||||
|
|
||||||
return pathname.split('/').filter(Boolean).length === 1;
|
return pathname.split('/').filter(Boolean).length === 1;
|
||||||
}
|
}
|
||||||
|
|||||||
7
apps/web/mdx-components.tsx
Normal file
7
apps/web/mdx-components.tsx
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
import { type MDXComponents } from 'mdx/types';
|
||||||
|
|
||||||
|
const components = {} satisfies MDXComponents;
|
||||||
|
|
||||||
|
export function useMDXComponents(): MDXComponents {
|
||||||
|
return components;
|
||||||
|
}
|
||||||
@ -12,6 +12,6 @@ export default withAuth({
|
|||||||
|
|
||||||
export const config = {
|
export const config = {
|
||||||
matcher: [
|
matcher: [
|
||||||
'/((?!auth|browser|telegram|unregistered|privacy|public-offer|api|_next/static|_next/image|favicon.ico).*)',
|
'/((?!auth|browser|telegram|unregistered|privacy|offer|api|_next/static|_next/image|favicon.ico).*)',
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,12 +1,16 @@
|
|||||||
import createNextIntlPlugin from 'next-intl/plugin';
|
import createMDX from '@next/mdx';
|
||||||
|
|
||||||
const withNextIntl = createNextIntlPlugin('./utils/i18n/i18n.ts');
|
const nextConfig = createMDX({
|
||||||
|
extension: /\.mdx?$/u,
|
||||||
const nextConfig = withNextIntl({
|
})({
|
||||||
eslint: {
|
eslint: {
|
||||||
ignoreDuringBuilds: true,
|
ignoreDuringBuilds: true,
|
||||||
},
|
},
|
||||||
|
experimental: {
|
||||||
|
mdxRs: true,
|
||||||
|
},
|
||||||
output: 'standalone',
|
output: 'standalone',
|
||||||
|
pageExtensions: ['js', 'jsx', 'ts', 'tsx', 'md', 'mdx'],
|
||||||
reactStrictMode: true,
|
reactStrictMode: true,
|
||||||
transpilePackages: ['@repo/ui'],
|
transpilePackages: ['@repo/ui'],
|
||||||
});
|
});
|
||||||
|
|||||||
@ -14,6 +14,9 @@
|
|||||||
"test:e2e": "playwright test"
|
"test:e2e": "playwright test"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@mdx-js/loader": "^3.1.1",
|
||||||
|
"@mdx-js/react": "^3.1.1",
|
||||||
|
"@next/mdx": "^15.5.5",
|
||||||
"@playwright/test": "^1.49.1",
|
"@playwright/test": "^1.49.1",
|
||||||
"@repo/graphql": "workspace:*",
|
"@repo/graphql": "workspace:*",
|
||||||
"@repo/typescript-config": "workspace:*",
|
"@repo/typescript-config": "workspace:*",
|
||||||
@ -22,9 +25,10 @@
|
|||||||
"@tanstack/react-query": "^5.64.1",
|
"@tanstack/react-query": "^5.64.1",
|
||||||
"@telegram-apps/sdk-react": "^2.0.19",
|
"@telegram-apps/sdk-react": "^2.0.19",
|
||||||
"@testing-library/react": "^16.1.0",
|
"@testing-library/react": "^16.1.0",
|
||||||
|
"@types/mdx": "^2.0.13",
|
||||||
"@types/node": "catalog:",
|
"@types/node": "catalog:",
|
||||||
"@types/react-dom": "catalog:",
|
|
||||||
"@types/react": "catalog:",
|
"@types/react": "catalog:",
|
||||||
|
"@types/react-dom": "catalog:",
|
||||||
"@vitejs/plugin-react": "^4.3.4",
|
"@vitejs/plugin-react": "^4.3.4",
|
||||||
"autoprefixer": "catalog:",
|
"autoprefixer": "catalog:",
|
||||||
"dayjs": "catalog:",
|
"dayjs": "catalog:",
|
||||||
@ -32,14 +36,13 @@
|
|||||||
"graphql": "catalog:",
|
"graphql": "catalog:",
|
||||||
"jsdom": "^25.0.1",
|
"jsdom": "^25.0.1",
|
||||||
"lucide-react": "catalog:",
|
"lucide-react": "catalog:",
|
||||||
|
"next": "^15.5.9",
|
||||||
"next-auth": "^4.24.11",
|
"next-auth": "^4.24.11",
|
||||||
"next-intl": "^3.26.0",
|
|
||||||
"next-themes": "^0.4.4",
|
"next-themes": "^0.4.4",
|
||||||
"next": "^15.5.0",
|
|
||||||
"postcss": "catalog:",
|
"postcss": "catalog:",
|
||||||
"radashi": "catalog:",
|
"radashi": "catalog:",
|
||||||
"react-dom": "catalog:",
|
|
||||||
"react": "catalog:",
|
"react": "catalog:",
|
||||||
|
"react-dom": "catalog:",
|
||||||
"tailwindcss": "catalog:",
|
"tailwindcss": "catalog:",
|
||||||
"typescript": "catalog:",
|
"typescript": "catalog:",
|
||||||
"use-debounce": "^10.0.4",
|
"use-debounce": "^10.0.4",
|
||||||
|
|||||||
@ -2,10 +2,8 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useBackButton, useClientOnce, useDidMount, useViewport } from '@/hooks/telegram';
|
import { useBackButton, useClientOnce, useDidMount, useViewport } from '@/hooks/telegram';
|
||||||
import { setLocale } from '@/utils/i18n/locale';
|
|
||||||
import { init } from '@/utils/telegram/init';
|
import { init } from '@/utils/telegram/init';
|
||||||
import { initData, useSignal } from '@telegram-apps/sdk-react';
|
import { type PropsWithChildren } from 'react';
|
||||||
import { type PropsWithChildren, useEffect } from 'react';
|
|
||||||
|
|
||||||
export function TelegramProvider(props: Readonly<PropsWithChildren>) {
|
export function TelegramProvider(props: Readonly<PropsWithChildren>) {
|
||||||
// Unfortunately, Telegram Mini Apps does not allow us to use all features of
|
// Unfortunately, Telegram Mini Apps does not allow us to use all features of
|
||||||
@ -31,12 +29,5 @@ function RootInner({ children }: PropsWithChildren) {
|
|||||||
useViewport();
|
useViewport();
|
||||||
useBackButton();
|
useBackButton();
|
||||||
|
|
||||||
const initDataUser = useSignal(initData.user);
|
|
||||||
|
|
||||||
// Set the user locale.
|
|
||||||
useEffect(() => {
|
|
||||||
if (initDataUser) setLocale(initDataUser.languageCode);
|
|
||||||
}, [initDataUser]);
|
|
||||||
|
|
||||||
return children;
|
return children;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,6 +0,0 @@
|
|||||||
{
|
|
||||||
"i18n": {
|
|
||||||
"header": "Application supports i18n",
|
|
||||||
"footer": "You can select a different language from the dropdown menu."
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,6 +0,0 @@
|
|||||||
{
|
|
||||||
"i18n": {
|
|
||||||
"header": "Поддержка i18n",
|
|
||||||
"footer": "Вы можете выбрать другой язык в выпадающем меню."
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,10 +0,0 @@
|
|||||||
export const defaultLocale = 'ru';
|
|
||||||
|
|
||||||
export const timeZone = 'Europe/Moscow';
|
|
||||||
|
|
||||||
export const locales = [defaultLocale, 'ru'] as const;
|
|
||||||
|
|
||||||
export const localesMap = [
|
|
||||||
{ key: 'en', title: 'English' },
|
|
||||||
{ key: 'ru', title: 'Русский' },
|
|
||||||
];
|
|
||||||
@ -1,18 +0,0 @@
|
|||||||
import { defaultLocale, locales } from './config';
|
|
||||||
import { getLocale } from './locale';
|
|
||||||
import { type Locale } from './types';
|
|
||||||
import { getRequestConfig } from 'next-intl/server';
|
|
||||||
|
|
||||||
const requestConfig = getRequestConfig(async () => {
|
|
||||||
const locale = (await getLocale()) as Locale;
|
|
||||||
|
|
||||||
return {
|
|
||||||
locale,
|
|
||||||
messages:
|
|
||||||
locale === defaultLocale || !locales.includes(locale)
|
|
||||||
? (await import(`@/public/locales/${defaultLocale}.json`)).default
|
|
||||||
: (await import(`@/public/locales/${locale}.json`)).default,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
export default requestConfig;
|
|
||||||
@ -1,20 +0,0 @@
|
|||||||
// use server is required
|
|
||||||
'use server';
|
|
||||||
|
|
||||||
import { defaultLocale } from './config';
|
|
||||||
import { type Locale } from './types';
|
|
||||||
import { cookies } from 'next/headers';
|
|
||||||
|
|
||||||
// In this example the locale is read from a cookie. You could alternatively
|
|
||||||
// also read it from a database, backend service, or any other source.
|
|
||||||
const COOKIE_NAME = 'NEXT_LOCALE';
|
|
||||||
|
|
||||||
const getLocale = async () => {
|
|
||||||
return (await cookies()).get(COOKIE_NAME)?.value || defaultLocale;
|
|
||||||
};
|
|
||||||
|
|
||||||
const setLocale = async (locale?: string) => {
|
|
||||||
(await cookies()).set(COOKIE_NAME, (locale as Locale) || defaultLocale);
|
|
||||||
};
|
|
||||||
|
|
||||||
export { getLocale, setLocale };
|
|
||||||
@ -1,14 +0,0 @@
|
|||||||
/* eslint-disable canonical/id-match */
|
|
||||||
import { timeZone } from './config';
|
|
||||||
import { NextIntlClientProvider } from 'next-intl';
|
|
||||||
import { getMessages } from 'next-intl/server';
|
|
||||||
import { type PropsWithChildren } from 'react';
|
|
||||||
|
|
||||||
export async function I18nProvider({ children }: Readonly<PropsWithChildren>) {
|
|
||||||
const messages = await getMessages();
|
|
||||||
return (
|
|
||||||
<NextIntlClientProvider messages={messages} timeZone={timeZone}>
|
|
||||||
{children}
|
|
||||||
</NextIntlClientProvider>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@ -1,5 +0,0 @@
|
|||||||
import { type locales } from './config';
|
|
||||||
|
|
||||||
type Locale = (typeof locales)[number];
|
|
||||||
|
|
||||||
export type { Locale };
|
|
||||||
@ -1,3 +1,4 @@
|
|||||||
|
/* eslint-disable sonarjs/no-commented-code */
|
||||||
/* eslint-disable sonarjs/cognitive-complexity */
|
/* eslint-disable sonarjs/cognitive-complexity */
|
||||||
/* eslint-disable @typescript-eslint/naming-convention */
|
/* eslint-disable @typescript-eslint/naming-convention */
|
||||||
import { ERRORS as SHARED_ERRORS } from '../constants/errors';
|
import { ERRORS as SHARED_ERRORS } from '../constants/errors';
|
||||||
@ -283,10 +284,10 @@ export class OrdersService extends BaseService {
|
|||||||
|
|
||||||
if (!clientEntity) throw new Error(ERRORS.NOT_FOUND_CLIENT);
|
if (!clientEntity) throw new Error(ERRORS.NOT_FOUND_CLIENT);
|
||||||
|
|
||||||
// Проверка активности клиента
|
// // Проверка активности клиента
|
||||||
if (!clientEntity?.active) {
|
// if (!clientEntity?.active) {
|
||||||
throw new Error(ERRORS.INACTIVE_CLIENT);
|
// throw new Error(ERRORS.INACTIVE_CLIENT);
|
||||||
}
|
// }
|
||||||
|
|
||||||
// Получаем мастера слота
|
// Получаем мастера слота
|
||||||
const slotMaster = slot.master;
|
const slotMaster = slot.master;
|
||||||
|
|||||||
@ -2,11 +2,23 @@ import { getClientWithToken } from '../apollo/client';
|
|||||||
import { ERRORS } from '../constants/errors';
|
import { ERRORS } from '../constants/errors';
|
||||||
import * as GQL from '../types';
|
import * as GQL from '../types';
|
||||||
import { type VariablesOf } from '@graphql-typed-document-node/core';
|
import { type VariablesOf } from '@graphql-typed-document-node/core';
|
||||||
import { isCustomerBanned } from '@repo/utils/customer';
|
|
||||||
|
|
||||||
const DEFAULT_CUSTOMER_ROLE = GQL.Enum_Customer_Role.Client;
|
const DEFAULT_CUSTOMER_ROLE = GQL.Enum_Customer_Role.Client;
|
||||||
|
|
||||||
export class RegistrationService {
|
export class RegistrationService {
|
||||||
|
async _NOCACHE_GetCustomer(variables: VariablesOf<typeof GQL._Nocache_GetCustomerDocument>) {
|
||||||
|
const { query } = await getClientWithToken();
|
||||||
|
|
||||||
|
const result = await query({
|
||||||
|
query: GQL._Nocache_GetCustomerDocument,
|
||||||
|
variables,
|
||||||
|
});
|
||||||
|
|
||||||
|
const customer = result.data.customers.at(0);
|
||||||
|
|
||||||
|
return { customer };
|
||||||
|
}
|
||||||
|
|
||||||
async createCustomer(variables: VariablesOf<typeof GQL.CreateCustomerDocument>) {
|
async createCustomer(variables: VariablesOf<typeof GQL.CreateCustomerDocument>) {
|
||||||
const { mutate } = await getClientWithToken();
|
const { mutate } = await getClientWithToken();
|
||||||
|
|
||||||
@ -27,34 +39,8 @@ export class RegistrationService {
|
|||||||
return mutationResult.data;
|
return mutationResult.data;
|
||||||
}
|
}
|
||||||
|
|
||||||
async getCustomer(variables: VariablesOf<typeof GQL.GetCustomerDocument>) {
|
|
||||||
const { query } = await getClientWithToken();
|
|
||||||
|
|
||||||
const result = await query({
|
|
||||||
query: GQL.GetCustomerDocument,
|
|
||||||
variables,
|
|
||||||
});
|
|
||||||
|
|
||||||
const customer = result.data.customers.at(0);
|
|
||||||
|
|
||||||
return { customer };
|
|
||||||
}
|
|
||||||
|
|
||||||
async updateCustomer(variables: VariablesOf<typeof GQL.UpdateCustomerDocument>) {
|
async updateCustomer(variables: VariablesOf<typeof GQL.UpdateCustomerDocument>) {
|
||||||
// Проверяем бан для существующего пользователя
|
if (variables.data.bannedUntil || variables.data.phone) {
|
||||||
if (variables.documentId) {
|
|
||||||
const { query } = await getClientWithToken();
|
|
||||||
const result = await query({
|
|
||||||
query: GQL.GetCustomerDocument,
|
|
||||||
variables: { documentId: variables.documentId },
|
|
||||||
});
|
|
||||||
const customer = result.data.customers.at(0);
|
|
||||||
if (customer && isCustomerBanned(customer)) {
|
|
||||||
throw new Error(ERRORS.NO_PERMISSION);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (variables.data.bannedUntil) {
|
|
||||||
throw new Error(ERRORS.NO_PERMISSION);
|
throw new Error(ERRORS.NO_PERMISSION);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -170,6 +170,7 @@ export class SubscriptionsService extends BaseService {
|
|||||||
source: GQL.Enum_Subscriptionhistory_Source.Trial,
|
source: GQL.Enum_Subscriptionhistory_Source.Trial,
|
||||||
state: GQL.Enum_Subscriptionhistory_State.Success,
|
state: GQL.Enum_Subscriptionhistory_State.Success,
|
||||||
subscription: subscription.documentId,
|
subscription: subscription.documentId,
|
||||||
|
subscription_price: trialPrice.documentId,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -340,6 +341,8 @@ export class SubscriptionsService extends BaseService {
|
|||||||
|
|
||||||
const now = dayjs();
|
const now = dayjs();
|
||||||
|
|
||||||
|
const { customer } = await this._getUser();
|
||||||
|
|
||||||
const { orders } = await ordersService.getOrders({
|
const { orders } = await ordersService.getOrders({
|
||||||
filters: {
|
filters: {
|
||||||
datetime_end: {
|
datetime_end: {
|
||||||
@ -348,6 +351,13 @@ export class SubscriptionsService extends BaseService {
|
|||||||
datetime_start: {
|
datetime_start: {
|
||||||
gte: now.startOf('month').toISOString(),
|
gte: now.startOf('month').toISOString(),
|
||||||
},
|
},
|
||||||
|
slot: {
|
||||||
|
master: {
|
||||||
|
documentId: {
|
||||||
|
eq: customer.documentId,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
state: {
|
state: {
|
||||||
eq: GQL.Enum_Order_State.Completed,
|
eq: GQL.Enum_Order_State.Completed,
|
||||||
|
|||||||
@ -20,20 +20,20 @@ mutation CreateCustomer($data: CustomerInput!) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
query GetCustomer($phone: String, $telegramId: Long, $documentId: ID) {
|
query GetCustomer($telegramId: Long, $documentId: ID) {
|
||||||
customers(
|
customers(
|
||||||
filters: {
|
filters: { or: [{ telegramId: { eq: $telegramId } }, { documentId: { eq: $documentId } }] }
|
||||||
or: [
|
|
||||||
{ phone: { eq: $phone } }
|
|
||||||
{ telegramId: { eq: $telegramId } }
|
|
||||||
{ documentId: { eq: $documentId } }
|
|
||||||
]
|
|
||||||
}
|
|
||||||
) {
|
) {
|
||||||
...CustomerFields
|
...CustomerFields
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
query _NOCACHE_GetCustomer($phone: String, $telegramId: Long) {
|
||||||
|
customers(filters: { or: [{ phone: { eq: $phone } }, { telegramId: { eq: $telegramId } }] }) {
|
||||||
|
...CustomerFields
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
mutation UpdateCustomer($documentId: ID!, $data: CustomerInput!) {
|
mutation UpdateCustomer($documentId: ID!, $data: CustomerInput!) {
|
||||||
updateCustomer(documentId: $documentId, data: $data) {
|
updateCustomer(documentId: $documentId, data: $data) {
|
||||||
...CustomerFields
|
...CustomerFields
|
||||||
|
|||||||
@ -747,7 +747,6 @@ export type CreateCustomerMutationVariables = Exact<{
|
|||||||
export type CreateCustomerMutation = { __typename?: 'Mutation', createCustomer?: { __typename?: 'Customer', documentId: string } | null | undefined };
|
export type CreateCustomerMutation = { __typename?: 'Mutation', createCustomer?: { __typename?: 'Customer', documentId: string } | null | undefined };
|
||||||
|
|
||||||
export type GetCustomerQueryVariables = Exact<{
|
export type GetCustomerQueryVariables = Exact<{
|
||||||
phone?: InputMaybe<Scalars['String']['input']>;
|
|
||||||
telegramId?: InputMaybe<Scalars['Long']['input']>;
|
telegramId?: InputMaybe<Scalars['Long']['input']>;
|
||||||
documentId?: InputMaybe<Scalars['ID']['input']>;
|
documentId?: InputMaybe<Scalars['ID']['input']>;
|
||||||
}>;
|
}>;
|
||||||
@ -755,6 +754,14 @@ export type GetCustomerQueryVariables = Exact<{
|
|||||||
|
|
||||||
export type GetCustomerQuery = { __typename?: 'Query', customers: Array<{ __typename?: 'Customer', active?: boolean | null | undefined, bannedUntil?: string | null | undefined, documentId: string, name: string, surname?: string | null | undefined, phone: string, photoUrl?: string | null | undefined, role: Enum_Customer_Role, telegramId?: number | null | undefined, services: Array<{ __typename?: 'Service', documentId: string, name: string } | null | undefined> } | null | undefined> };
|
export type GetCustomerQuery = { __typename?: 'Query', customers: Array<{ __typename?: 'Customer', active?: boolean | null | undefined, bannedUntil?: string | null | undefined, documentId: string, name: string, surname?: string | null | undefined, phone: string, photoUrl?: string | null | undefined, role: Enum_Customer_Role, telegramId?: number | null | undefined, services: Array<{ __typename?: 'Service', documentId: string, name: string } | null | undefined> } | null | undefined> };
|
||||||
|
|
||||||
|
export type _Nocache_GetCustomerQueryVariables = Exact<{
|
||||||
|
phone?: InputMaybe<Scalars['String']['input']>;
|
||||||
|
telegramId?: InputMaybe<Scalars['Long']['input']>;
|
||||||
|
}>;
|
||||||
|
|
||||||
|
|
||||||
|
export type _Nocache_GetCustomerQuery = { __typename?: 'Query', customers: Array<{ __typename?: 'Customer', active?: boolean | null | undefined, bannedUntil?: string | null | undefined, documentId: string, name: string, surname?: string | null | undefined, phone: string, photoUrl?: string | null | undefined, role: Enum_Customer_Role, telegramId?: number | null | undefined, services: Array<{ __typename?: 'Service', documentId: string, name: string } | null | undefined> } | null | undefined> };
|
||||||
|
|
||||||
export type UpdateCustomerMutationVariables = Exact<{
|
export type UpdateCustomerMutationVariables = Exact<{
|
||||||
documentId: Scalars['ID']['input'];
|
documentId: Scalars['ID']['input'];
|
||||||
data: CustomerInput;
|
data: CustomerInput;
|
||||||
@ -976,7 +983,8 @@ export const SubscriptionRewardFieldsFragmentDoc = {"kind":"Document","definitio
|
|||||||
export const RegisterDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"Register"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"identifier"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"password"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"email"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"register"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"ObjectValue","fields":[{"kind":"ObjectField","name":{"kind":"Name","value":"username"},"value":{"kind":"Variable","name":{"kind":"Name","value":"identifier"}}},{"kind":"ObjectField","name":{"kind":"Name","value":"password"},"value":{"kind":"Variable","name":{"kind":"Name","value":"password"}}},{"kind":"ObjectField","name":{"kind":"Name","value":"email"},"value":{"kind":"Variable","name":{"kind":"Name","value":"email"}}}]}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"jwt"}},{"kind":"Field","name":{"kind":"Name","value":"user"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"username"}}]}}]}}]}}]} as unknown as DocumentNode<RegisterMutation, RegisterMutationVariables>;
|
export const RegisterDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"Register"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"identifier"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"password"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"email"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"register"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"ObjectValue","fields":[{"kind":"ObjectField","name":{"kind":"Name","value":"username"},"value":{"kind":"Variable","name":{"kind":"Name","value":"identifier"}}},{"kind":"ObjectField","name":{"kind":"Name","value":"password"},"value":{"kind":"Variable","name":{"kind":"Name","value":"password"}}},{"kind":"ObjectField","name":{"kind":"Name","value":"email"},"value":{"kind":"Variable","name":{"kind":"Name","value":"email"}}}]}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"jwt"}},{"kind":"Field","name":{"kind":"Name","value":"user"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"username"}}]}}]}}]}}]} as unknown as DocumentNode<RegisterMutation, RegisterMutationVariables>;
|
||||||
export const LoginDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"Login"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"identifier"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"password"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"login"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"ObjectValue","fields":[{"kind":"ObjectField","name":{"kind":"Name","value":"identifier"},"value":{"kind":"Variable","name":{"kind":"Name","value":"identifier"}}},{"kind":"ObjectField","name":{"kind":"Name","value":"password"},"value":{"kind":"Variable","name":{"kind":"Name","value":"password"}}}]}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"jwt"}}]}}]}}]} as unknown as DocumentNode<LoginMutation, LoginMutationVariables>;
|
export const LoginDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"Login"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"identifier"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"password"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"login"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"ObjectValue","fields":[{"kind":"ObjectField","name":{"kind":"Name","value":"identifier"},"value":{"kind":"Variable","name":{"kind":"Name","value":"identifier"}}},{"kind":"ObjectField","name":{"kind":"Name","value":"password"},"value":{"kind":"Variable","name":{"kind":"Name","value":"password"}}}]}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"jwt"}}]}}]}}]} as unknown as DocumentNode<LoginMutation, LoginMutationVariables>;
|
||||||
export const CreateCustomerDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"CreateCustomer"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"data"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"CustomerInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"createCustomer"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"data"},"value":{"kind":"Variable","name":{"kind":"Name","value":"data"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"documentId"}}]}}]}}]} as unknown as DocumentNode<CreateCustomerMutation, CreateCustomerMutationVariables>;
|
export const CreateCustomerDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"CreateCustomer"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"data"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"CustomerInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"createCustomer"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"data"},"value":{"kind":"Variable","name":{"kind":"Name","value":"data"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"documentId"}}]}}]}}]} as unknown as DocumentNode<CreateCustomerMutation, CreateCustomerMutationVariables>;
|
||||||
export const GetCustomerDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetCustomer"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"phone"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"telegramId"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"Long"}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"documentId"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"ID"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"customers"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"filters"},"value":{"kind":"ObjectValue","fields":[{"kind":"ObjectField","name":{"kind":"Name","value":"or"},"value":{"kind":"ListValue","values":[{"kind":"ObjectValue","fields":[{"kind":"ObjectField","name":{"kind":"Name","value":"phone"},"value":{"kind":"ObjectValue","fields":[{"kind":"ObjectField","name":{"kind":"Name","value":"eq"},"value":{"kind":"Variable","name":{"kind":"Name","value":"phone"}}}]}}]},{"kind":"ObjectValue","fields":[{"kind":"ObjectField","name":{"kind":"Name","value":"telegramId"},"value":{"kind":"ObjectValue","fields":[{"kind":"ObjectField","name":{"kind":"Name","value":"eq"},"value":{"kind":"Variable","name":{"kind":"Name","value":"telegramId"}}}]}}]},{"kind":"ObjectValue","fields":[{"kind":"ObjectField","name":{"kind":"Name","value":"documentId"},"value":{"kind":"ObjectValue","fields":[{"kind":"ObjectField","name":{"kind":"Name","value":"eq"},"value":{"kind":"Variable","name":{"kind":"Name","value":"documentId"}}}]}}]}]}}]}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"CustomerFields"}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"CustomerFields"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Customer"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"active"}},{"kind":"Field","name":{"kind":"Name","value":"bannedUntil"}},{"kind":"Field","name":{"kind":"Name","value":"documentId"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"surname"}},{"kind":"Field","name":{"kind":"Name","value":"phone"}},{"kind":"Field","name":{"kind":"Name","value":"photoUrl"}},{"kind":"Field","name":{"kind":"Name","value":"role"}},{"kind":"Field","name":{"kind":"Name","value":"telegramId"}},{"kind":"Field","name":{"kind":"Name","value":"services"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"filters"},"value":{"kind":"ObjectValue","fields":[{"kind":"ObjectField","name":{"kind":"Name","value":"active"},"value":{"kind":"ObjectValue","fields":[{"kind":"ObjectField","name":{"kind":"Name","value":"eq"},"value":{"kind":"BooleanValue","value":true}}]}}]}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"documentId"}},{"kind":"Field","name":{"kind":"Name","value":"name"}}]}}]}}]} as unknown as DocumentNode<GetCustomerQuery, GetCustomerQueryVariables>;
|
export const GetCustomerDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetCustomer"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"telegramId"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"Long"}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"documentId"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"ID"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"customers"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"filters"},"value":{"kind":"ObjectValue","fields":[{"kind":"ObjectField","name":{"kind":"Name","value":"or"},"value":{"kind":"ListValue","values":[{"kind":"ObjectValue","fields":[{"kind":"ObjectField","name":{"kind":"Name","value":"telegramId"},"value":{"kind":"ObjectValue","fields":[{"kind":"ObjectField","name":{"kind":"Name","value":"eq"},"value":{"kind":"Variable","name":{"kind":"Name","value":"telegramId"}}}]}}]},{"kind":"ObjectValue","fields":[{"kind":"ObjectField","name":{"kind":"Name","value":"documentId"},"value":{"kind":"ObjectValue","fields":[{"kind":"ObjectField","name":{"kind":"Name","value":"eq"},"value":{"kind":"Variable","name":{"kind":"Name","value":"documentId"}}}]}}]}]}}]}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"CustomerFields"}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"CustomerFields"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Customer"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"active"}},{"kind":"Field","name":{"kind":"Name","value":"bannedUntil"}},{"kind":"Field","name":{"kind":"Name","value":"documentId"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"surname"}},{"kind":"Field","name":{"kind":"Name","value":"phone"}},{"kind":"Field","name":{"kind":"Name","value":"photoUrl"}},{"kind":"Field","name":{"kind":"Name","value":"role"}},{"kind":"Field","name":{"kind":"Name","value":"telegramId"}},{"kind":"Field","name":{"kind":"Name","value":"services"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"filters"},"value":{"kind":"ObjectValue","fields":[{"kind":"ObjectField","name":{"kind":"Name","value":"active"},"value":{"kind":"ObjectValue","fields":[{"kind":"ObjectField","name":{"kind":"Name","value":"eq"},"value":{"kind":"BooleanValue","value":true}}]}}]}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"documentId"}},{"kind":"Field","name":{"kind":"Name","value":"name"}}]}}]}}]} as unknown as DocumentNode<GetCustomerQuery, GetCustomerQueryVariables>;
|
||||||
|
export const _Nocache_GetCustomerDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"_NOCACHE_GetCustomer"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"phone"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"telegramId"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"Long"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"customers"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"filters"},"value":{"kind":"ObjectValue","fields":[{"kind":"ObjectField","name":{"kind":"Name","value":"or"},"value":{"kind":"ListValue","values":[{"kind":"ObjectValue","fields":[{"kind":"ObjectField","name":{"kind":"Name","value":"phone"},"value":{"kind":"ObjectValue","fields":[{"kind":"ObjectField","name":{"kind":"Name","value":"eq"},"value":{"kind":"Variable","name":{"kind":"Name","value":"phone"}}}]}}]},{"kind":"ObjectValue","fields":[{"kind":"ObjectField","name":{"kind":"Name","value":"telegramId"},"value":{"kind":"ObjectValue","fields":[{"kind":"ObjectField","name":{"kind":"Name","value":"eq"},"value":{"kind":"Variable","name":{"kind":"Name","value":"telegramId"}}}]}}]}]}}]}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"CustomerFields"}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"CustomerFields"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Customer"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"active"}},{"kind":"Field","name":{"kind":"Name","value":"bannedUntil"}},{"kind":"Field","name":{"kind":"Name","value":"documentId"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"surname"}},{"kind":"Field","name":{"kind":"Name","value":"phone"}},{"kind":"Field","name":{"kind":"Name","value":"photoUrl"}},{"kind":"Field","name":{"kind":"Name","value":"role"}},{"kind":"Field","name":{"kind":"Name","value":"telegramId"}},{"kind":"Field","name":{"kind":"Name","value":"services"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"filters"},"value":{"kind":"ObjectValue","fields":[{"kind":"ObjectField","name":{"kind":"Name","value":"active"},"value":{"kind":"ObjectValue","fields":[{"kind":"ObjectField","name":{"kind":"Name","value":"eq"},"value":{"kind":"BooleanValue","value":true}}]}}]}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"documentId"}},{"kind":"Field","name":{"kind":"Name","value":"name"}}]}}]}}]} as unknown as DocumentNode<_Nocache_GetCustomerQuery, _Nocache_GetCustomerQueryVariables>;
|
||||||
export const UpdateCustomerDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"UpdateCustomer"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"documentId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ID"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"data"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"CustomerInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"updateCustomer"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"documentId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"documentId"}}},{"kind":"Argument","name":{"kind":"Name","value":"data"},"value":{"kind":"Variable","name":{"kind":"Name","value":"data"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"CustomerFields"}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"CustomerFields"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Customer"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"active"}},{"kind":"Field","name":{"kind":"Name","value":"bannedUntil"}},{"kind":"Field","name":{"kind":"Name","value":"documentId"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"surname"}},{"kind":"Field","name":{"kind":"Name","value":"phone"}},{"kind":"Field","name":{"kind":"Name","value":"photoUrl"}},{"kind":"Field","name":{"kind":"Name","value":"role"}},{"kind":"Field","name":{"kind":"Name","value":"telegramId"}},{"kind":"Field","name":{"kind":"Name","value":"services"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"filters"},"value":{"kind":"ObjectValue","fields":[{"kind":"ObjectField","name":{"kind":"Name","value":"active"},"value":{"kind":"ObjectValue","fields":[{"kind":"ObjectField","name":{"kind":"Name","value":"eq"},"value":{"kind":"BooleanValue","value":true}}]}}]}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"documentId"}},{"kind":"Field","name":{"kind":"Name","value":"name"}}]}}]}}]} as unknown as DocumentNode<UpdateCustomerMutation, UpdateCustomerMutationVariables>;
|
export const UpdateCustomerDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"UpdateCustomer"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"documentId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ID"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"data"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"CustomerInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"updateCustomer"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"documentId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"documentId"}}},{"kind":"Argument","name":{"kind":"Name","value":"data"},"value":{"kind":"Variable","name":{"kind":"Name","value":"data"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"CustomerFields"}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"CustomerFields"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Customer"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"active"}},{"kind":"Field","name":{"kind":"Name","value":"bannedUntil"}},{"kind":"Field","name":{"kind":"Name","value":"documentId"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"surname"}},{"kind":"Field","name":{"kind":"Name","value":"phone"}},{"kind":"Field","name":{"kind":"Name","value":"photoUrl"}},{"kind":"Field","name":{"kind":"Name","value":"role"}},{"kind":"Field","name":{"kind":"Name","value":"telegramId"}},{"kind":"Field","name":{"kind":"Name","value":"services"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"filters"},"value":{"kind":"ObjectValue","fields":[{"kind":"ObjectField","name":{"kind":"Name","value":"active"},"value":{"kind":"ObjectValue","fields":[{"kind":"ObjectField","name":{"kind":"Name","value":"eq"},"value":{"kind":"BooleanValue","value":true}}]}}]}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"documentId"}},{"kind":"Field","name":{"kind":"Name","value":"name"}}]}}]}}]} as unknown as DocumentNode<UpdateCustomerMutation, UpdateCustomerMutationVariables>;
|
||||||
export const GetCustomersDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetCustomers"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"filters"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"CustomerFiltersInput"}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"pagination"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"PaginationArg"}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"sort"}},"type":{"kind":"ListType","type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"customers"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"filters"},"value":{"kind":"Variable","name":{"kind":"Name","value":"filters"}}},{"kind":"Argument","name":{"kind":"Name","value":"pagination"},"value":{"kind":"Variable","name":{"kind":"Name","value":"pagination"}}},{"kind":"Argument","name":{"kind":"Name","value":"sort"},"value":{"kind":"Variable","name":{"kind":"Name","value":"sort"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"CustomerFields"}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"CustomerFields"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Customer"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"active"}},{"kind":"Field","name":{"kind":"Name","value":"bannedUntil"}},{"kind":"Field","name":{"kind":"Name","value":"documentId"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"surname"}},{"kind":"Field","name":{"kind":"Name","value":"phone"}},{"kind":"Field","name":{"kind":"Name","value":"photoUrl"}},{"kind":"Field","name":{"kind":"Name","value":"role"}},{"kind":"Field","name":{"kind":"Name","value":"telegramId"}},{"kind":"Field","name":{"kind":"Name","value":"services"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"filters"},"value":{"kind":"ObjectValue","fields":[{"kind":"ObjectField","name":{"kind":"Name","value":"active"},"value":{"kind":"ObjectValue","fields":[{"kind":"ObjectField","name":{"kind":"Name","value":"eq"},"value":{"kind":"BooleanValue","value":true}}]}}]}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"documentId"}},{"kind":"Field","name":{"kind":"Name","value":"name"}}]}}]}}]} as unknown as DocumentNode<GetCustomersQuery, GetCustomersQueryVariables>;
|
export const GetCustomersDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetCustomers"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"filters"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"CustomerFiltersInput"}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"pagination"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"PaginationArg"}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"sort"}},"type":{"kind":"ListType","type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"customers"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"filters"},"value":{"kind":"Variable","name":{"kind":"Name","value":"filters"}}},{"kind":"Argument","name":{"kind":"Name","value":"pagination"},"value":{"kind":"Variable","name":{"kind":"Name","value":"pagination"}}},{"kind":"Argument","name":{"kind":"Name","value":"sort"},"value":{"kind":"Variable","name":{"kind":"Name","value":"sort"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"CustomerFields"}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"CustomerFields"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Customer"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"active"}},{"kind":"Field","name":{"kind":"Name","value":"bannedUntil"}},{"kind":"Field","name":{"kind":"Name","value":"documentId"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"surname"}},{"kind":"Field","name":{"kind":"Name","value":"phone"}},{"kind":"Field","name":{"kind":"Name","value":"photoUrl"}},{"kind":"Field","name":{"kind":"Name","value":"role"}},{"kind":"Field","name":{"kind":"Name","value":"telegramId"}},{"kind":"Field","name":{"kind":"Name","value":"services"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"filters"},"value":{"kind":"ObjectValue","fields":[{"kind":"ObjectField","name":{"kind":"Name","value":"active"},"value":{"kind":"ObjectValue","fields":[{"kind":"ObjectField","name":{"kind":"Name","value":"eq"},"value":{"kind":"BooleanValue","value":true}}]}}]}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"documentId"}},{"kind":"Field","name":{"kind":"Name","value":"name"}}]}}]}}]} as unknown as DocumentNode<GetCustomersQuery, GetCustomersQueryVariables>;
|
||||||
export const GetInvitedByDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetInvitedBy"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"phone"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"telegramId"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"Long"}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"documentId"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"ID"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"customers"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"filters"},"value":{"kind":"ObjectValue","fields":[{"kind":"ObjectField","name":{"kind":"Name","value":"or"},"value":{"kind":"ListValue","values":[{"kind":"ObjectValue","fields":[{"kind":"ObjectField","name":{"kind":"Name","value":"phone"},"value":{"kind":"ObjectValue","fields":[{"kind":"ObjectField","name":{"kind":"Name","value":"eq"},"value":{"kind":"Variable","name":{"kind":"Name","value":"phone"}}}]}}]},{"kind":"ObjectValue","fields":[{"kind":"ObjectField","name":{"kind":"Name","value":"telegramId"},"value":{"kind":"ObjectValue","fields":[{"kind":"ObjectField","name":{"kind":"Name","value":"eq"},"value":{"kind":"Variable","name":{"kind":"Name","value":"telegramId"}}}]}}]},{"kind":"ObjectValue","fields":[{"kind":"ObjectField","name":{"kind":"Name","value":"documentId"},"value":{"kind":"ObjectValue","fields":[{"kind":"ObjectField","name":{"kind":"Name","value":"eq"},"value":{"kind":"Variable","name":{"kind":"Name","value":"documentId"}}}]}}]}]}}]}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"documentId"}},{"kind":"Field","name":{"kind":"Name","value":"invitedBy"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"CustomerFields"}}]}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"CustomerFields"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Customer"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"active"}},{"kind":"Field","name":{"kind":"Name","value":"bannedUntil"}},{"kind":"Field","name":{"kind":"Name","value":"documentId"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"surname"}},{"kind":"Field","name":{"kind":"Name","value":"phone"}},{"kind":"Field","name":{"kind":"Name","value":"photoUrl"}},{"kind":"Field","name":{"kind":"Name","value":"role"}},{"kind":"Field","name":{"kind":"Name","value":"telegramId"}},{"kind":"Field","name":{"kind":"Name","value":"services"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"filters"},"value":{"kind":"ObjectValue","fields":[{"kind":"ObjectField","name":{"kind":"Name","value":"active"},"value":{"kind":"ObjectValue","fields":[{"kind":"ObjectField","name":{"kind":"Name","value":"eq"},"value":{"kind":"BooleanValue","value":true}}]}}]}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"documentId"}},{"kind":"Field","name":{"kind":"Name","value":"name"}}]}}]}}]} as unknown as DocumentNode<GetInvitedByQuery, GetInvitedByQueryVariables>;
|
export const GetInvitedByDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetInvitedBy"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"phone"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"telegramId"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"Long"}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"documentId"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"ID"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"customers"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"filters"},"value":{"kind":"ObjectValue","fields":[{"kind":"ObjectField","name":{"kind":"Name","value":"or"},"value":{"kind":"ListValue","values":[{"kind":"ObjectValue","fields":[{"kind":"ObjectField","name":{"kind":"Name","value":"phone"},"value":{"kind":"ObjectValue","fields":[{"kind":"ObjectField","name":{"kind":"Name","value":"eq"},"value":{"kind":"Variable","name":{"kind":"Name","value":"phone"}}}]}}]},{"kind":"ObjectValue","fields":[{"kind":"ObjectField","name":{"kind":"Name","value":"telegramId"},"value":{"kind":"ObjectValue","fields":[{"kind":"ObjectField","name":{"kind":"Name","value":"eq"},"value":{"kind":"Variable","name":{"kind":"Name","value":"telegramId"}}}]}}]},{"kind":"ObjectValue","fields":[{"kind":"ObjectField","name":{"kind":"Name","value":"documentId"},"value":{"kind":"ObjectValue","fields":[{"kind":"ObjectField","name":{"kind":"Name","value":"eq"},"value":{"kind":"Variable","name":{"kind":"Name","value":"documentId"}}}]}}]}]}}]}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"documentId"}},{"kind":"Field","name":{"kind":"Name","value":"invitedBy"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"CustomerFields"}}]}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"CustomerFields"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Customer"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"active"}},{"kind":"Field","name":{"kind":"Name","value":"bannedUntil"}},{"kind":"Field","name":{"kind":"Name","value":"documentId"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"surname"}},{"kind":"Field","name":{"kind":"Name","value":"phone"}},{"kind":"Field","name":{"kind":"Name","value":"photoUrl"}},{"kind":"Field","name":{"kind":"Name","value":"role"}},{"kind":"Field","name":{"kind":"Name","value":"telegramId"}},{"kind":"Field","name":{"kind":"Name","value":"services"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"filters"},"value":{"kind":"ObjectValue","fields":[{"kind":"ObjectField","name":{"kind":"Name","value":"active"},"value":{"kind":"ObjectValue","fields":[{"kind":"ObjectField","name":{"kind":"Name","value":"eq"},"value":{"kind":"BooleanValue","value":true}}]}}]}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"documentId"}},{"kind":"Field","name":{"kind":"Name","value":"name"}}]}}]}}]} as unknown as DocumentNode<GetInvitedByQuery, GetInvitedByQueryVariables>;
|
||||||
|
|||||||
@ -40,6 +40,7 @@
|
|||||||
"@radix-ui/react-select": "^2.1.4",
|
"@radix-ui/react-select": "^2.1.4",
|
||||||
"@radix-ui/react-slot": "^1.1.1",
|
"@radix-ui/react-slot": "^1.1.1",
|
||||||
"@repo/typescript-config": "workspace:*",
|
"@repo/typescript-config": "workspace:*",
|
||||||
|
"@tailwindcss/typography": "^0.5.19",
|
||||||
"@types/react": "catalog:",
|
"@types/react": "catalog:",
|
||||||
"autoprefixer": "catalog:",
|
"autoprefixer": "catalog:",
|
||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
|
|||||||
@ -1,3 +1,4 @@
|
|||||||
|
import typography from '@tailwindcss/typography';
|
||||||
import { type Config } from 'tailwindcss';
|
import { type Config } from 'tailwindcss';
|
||||||
import tailwindcssAnimate from 'tailwindcss-animate';
|
import tailwindcssAnimate from 'tailwindcss-animate';
|
||||||
|
|
||||||
@ -5,12 +6,12 @@ const config = {
|
|||||||
content: [
|
content: [
|
||||||
'./pages/**/*.{ts,tsx}',
|
'./pages/**/*.{ts,tsx}',
|
||||||
'./components/**/*.{ts,tsx}',
|
'./components/**/*.{ts,tsx}',
|
||||||
'./app/**/*.{ts,tsx}',
|
'./app/**/*.{ts,tsx,mdx}',
|
||||||
'./src/**/*.{ts,tsx}',
|
'./src/**/*.{ts,tsx}',
|
||||||
'../../packages/ui/src/**/*.{ts,tsx}',
|
'../../packages/ui/src/**/*.{ts,tsx}',
|
||||||
],
|
],
|
||||||
darkMode: ['class'],
|
darkMode: ['class'],
|
||||||
plugins: [tailwindcssAnimate],
|
plugins: [tailwindcssAnimate, typography],
|
||||||
prefix: '',
|
prefix: '',
|
||||||
theme: {
|
theme: {
|
||||||
container: {
|
container: {
|
||||||
|
|||||||
2142
pnpm-lock.yaml
generated
2142
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.11
|
"@types/react": ^19.1.17
|
||||||
"@types/react-dom": ^19.1.8
|
"@types/react-dom": ^19.1.17
|
||||||
"@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.1
|
react: ^19.1.4
|
||||||
react-dom: ^19.1.1
|
react-dom: ^19.1.4
|
||||||
radashi: ^12.5.1
|
radashi: ^12.5.1
|
||||||
rimraf: ^6.0.1
|
rimraf: ^6.0.1
|
||||||
tailwindcss: ^3.4.15
|
tailwindcss: ^3.4.15
|
||||||
|
|||||||
@ -18,8 +18,9 @@
|
|||||||
"REDIS_HOST",
|
"REDIS_HOST",
|
||||||
"REDIS_PORT",
|
"REDIS_PORT",
|
||||||
"REDIS_PASSWORD",
|
"REDIS_PASSWORD",
|
||||||
"OFFER_URL",
|
"URL_OFFER",
|
||||||
"PRIVACY_URL"
|
"URL_PRIVACY",
|
||||||
|
"URL_FAQ"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"lint": {
|
"lint": {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user