feat: add VIP search option and profile editing functionality
- Added a new button for '⭐ VIP поиск' in command handlers.
- Implemented profile editing states and methods in message handlers.
- Enhanced profile model to include hobbies, religion, dating goals, and lifestyle preferences.
- Updated profile service to handle new fields and ensure proper database interactions.
- Introduced a VIP function in matching service to find candidates based on dating goals.
This commit is contained in:
294
src/controllers/profileEditController.ts
Normal file
294
src/controllers/profileEditController.ts
Normal file
@@ -0,0 +1,294 @@
|
||||
import { ProfileService } from '../services/profileService';
|
||||
import { Profile } from '../models/Profile';
|
||||
import TelegramBot from 'node-telegram-bot-api';
|
||||
|
||||
export class ProfileEditController {
|
||||
private profileService: ProfileService;
|
||||
|
||||
constructor(profileService: ProfileService) {
|
||||
this.profileService = profileService;
|
||||
}
|
||||
|
||||
// Показать главное меню редактирования профиля
|
||||
async showProfileEditMenu(bot: TelegramBot, chatId: number, telegramId: number): Promise<void> {
|
||||
try {
|
||||
const profile = await this.profileService.getProfileByTelegramId(telegramId.toString());
|
||||
|
||||
if (!profile) {
|
||||
await bot.sendMessage(chatId, '❌ Профиль не найден. Сначала создайте профиль.');
|
||||
return;
|
||||
}
|
||||
|
||||
const keyboard = {
|
||||
inline_keyboard: [
|
||||
[
|
||||
{ text: '📝 Изменить имя', callback_data: 'edit_name' },
|
||||
{ text: '🎂 Изменить возраст', callback_data: 'edit_age' }
|
||||
],
|
||||
[
|
||||
{ text: '📖 Изменить "О себе"', callback_data: 'edit_bio' },
|
||||
{ text: '🎯 Хобби', callback_data: 'edit_hobbies' }
|
||||
],
|
||||
[
|
||||
{ text: '📷 Управление фото', callback_data: 'manage_photos' },
|
||||
{ text: '🏙️ Город', callback_data: 'edit_city' }
|
||||
],
|
||||
[
|
||||
{ text: '💼 Работа', callback_data: 'edit_job' },
|
||||
{ text: '🎓 Образование', callback_data: 'edit_education' }
|
||||
],
|
||||
[
|
||||
{ text: '📏 Рост', callback_data: 'edit_height' },
|
||||
{ text: '🕊️ Религия', callback_data: 'edit_religion' }
|
||||
],
|
||||
[
|
||||
{ text: '💕 Цель знакомства', callback_data: 'edit_dating_goal' },
|
||||
{ text: '🚬 Образ жизни', callback_data: 'edit_lifestyle' }
|
||||
],
|
||||
[
|
||||
{ text: '⚙️ Настройки поиска', callback_data: 'edit_search_preferences' }
|
||||
],
|
||||
[
|
||||
{ text: '👀 Предпросмотр профиля', callback_data: 'preview_profile' }
|
||||
],
|
||||
[
|
||||
{ text: '⬅️ Назад в главное меню', callback_data: 'main_menu' }
|
||||
]
|
||||
]
|
||||
};
|
||||
|
||||
const profileText = this.getProfileSummary(profile);
|
||||
|
||||
await bot.sendMessage(chatId,
|
||||
`🛠️ *Редактирование профиля*\n\n${profileText}\n\n*Выберите что хотите изменить:*`,
|
||||
{
|
||||
parse_mode: 'Markdown',
|
||||
reply_markup: keyboard
|
||||
}
|
||||
);
|
||||
} catch (error) {
|
||||
console.error('Error showing profile edit menu:', error);
|
||||
await bot.sendMessage(chatId, '❌ Произошла ошибка при загрузке профиля.');
|
||||
}
|
||||
}
|
||||
|
||||
// Показать меню управления фотографиями
|
||||
async showPhotoManagementMenu(bot: TelegramBot, chatId: number, telegramId: number): Promise<void> {
|
||||
try {
|
||||
const profile = await this.profileService.getProfileByTelegramId(telegramId.toString());
|
||||
|
||||
if (!profile) {
|
||||
await bot.sendMessage(chatId, '❌ Профиль не найден.');
|
||||
return;
|
||||
}
|
||||
|
||||
const keyboard = {
|
||||
inline_keyboard: [
|
||||
[
|
||||
{ text: '➕ Добавить фото', callback_data: 'add_photo' }
|
||||
],
|
||||
[
|
||||
{ text: '🗑️ Удалить фото', callback_data: 'delete_photo' },
|
||||
{ text: '⭐ Главное фото', callback_data: 'set_main_photo' }
|
||||
],
|
||||
[
|
||||
{ text: '⬅️ Назад', callback_data: 'edit_profile' }
|
||||
]
|
||||
]
|
||||
};
|
||||
|
||||
let photoText = '📷 *Управление фотографиями*\n\n';
|
||||
|
||||
if (profile.photos.length === 0) {
|
||||
photoText += 'У вас пока нет фотографий.\n';
|
||||
} else {
|
||||
photoText += `Количество фото: ${profile.photos.length}/9\n`;
|
||||
if (profile.getMainPhoto()) {
|
||||
photoText += '⭐ Главное фото установлено\n';
|
||||
}
|
||||
}
|
||||
|
||||
await bot.sendMessage(chatId, photoText, {
|
||||
parse_mode: 'Markdown',
|
||||
reply_markup: keyboard
|
||||
});
|
||||
|
||||
// Если есть фото, покажем их
|
||||
if (profile.photos.length > 0) {
|
||||
for (let i = 0; i < Math.min(profile.photos.length, 3); i++) {
|
||||
const caption = i === 0 ? '⭐ Главное фото' : `Фото ${i + 1}`;
|
||||
await bot.sendPhoto(chatId, profile.photos[i], { caption });
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error showing photo management menu:', error);
|
||||
await bot.sendMessage(chatId, '❌ Произошла ошибка при загрузке фотографий.');
|
||||
}
|
||||
}
|
||||
|
||||
// Предпросмотр профиля
|
||||
async showProfilePreview(bot: TelegramBot, chatId: number, telegramId: number): Promise<void> {
|
||||
try {
|
||||
const profile = await this.profileService.getProfileByTelegramId(telegramId.toString());
|
||||
|
||||
if (!profile) {
|
||||
await bot.sendMessage(chatId, '❌ Профиль не найден.');
|
||||
return;
|
||||
}
|
||||
|
||||
const displayProfile = profile.getDisplayProfile();
|
||||
let previewText = `👤 *${displayProfile.name}, ${displayProfile.age}*\n\n`;
|
||||
|
||||
if (displayProfile.bio) {
|
||||
previewText += `📖 *О себе:*\n${displayProfile.bio}\n\n`;
|
||||
}
|
||||
|
||||
if (displayProfile.hobbies) {
|
||||
previewText += `🎯 *Хобби:* ${displayProfile.hobbies}\n\n`;
|
||||
}
|
||||
|
||||
if (displayProfile.city) {
|
||||
previewText += `🏙️ *Город:* ${displayProfile.city}\n`;
|
||||
}
|
||||
|
||||
if (displayProfile.job) {
|
||||
previewText += `💼 *Работа:* ${displayProfile.job}\n`;
|
||||
}
|
||||
|
||||
if (displayProfile.education) {
|
||||
previewText += `🎓 *Образование:* ${displayProfile.education}\n`;
|
||||
}
|
||||
|
||||
if (displayProfile.height) {
|
||||
previewText += `📏 *Рост:* ${displayProfile.height} см\n`;
|
||||
}
|
||||
|
||||
if (displayProfile.religion) {
|
||||
previewText += `🕊️ *Религия:* ${displayProfile.religion}\n`;
|
||||
}
|
||||
|
||||
if (displayProfile.datingGoal) {
|
||||
const goalText = this.getDatingGoalText(displayProfile.datingGoal);
|
||||
previewText += `💕 *Цель знакомства:* ${goalText}\n`;
|
||||
}
|
||||
|
||||
if (displayProfile.lifestyle) {
|
||||
previewText += `\n🚬 *Образ жизни:*\n`;
|
||||
if (displayProfile.lifestyle.smoking) {
|
||||
previewText += `Курение: ${this.getLifestyleText('smoking', displayProfile.lifestyle.smoking)}\n`;
|
||||
}
|
||||
if (displayProfile.lifestyle.drinking) {
|
||||
previewText += `Алкоголь: ${this.getLifestyleText('drinking', displayProfile.lifestyle.drinking)}\n`;
|
||||
}
|
||||
if (displayProfile.lifestyle.kids) {
|
||||
previewText += `Дети: ${this.getLifestyleText('kids', displayProfile.lifestyle.kids)}\n`;
|
||||
}
|
||||
}
|
||||
|
||||
const keyboard = {
|
||||
inline_keyboard: [
|
||||
[
|
||||
{ text: '✏️ Редактировать', callback_data: 'edit_profile' },
|
||||
{ text: '⬅️ Главное меню', callback_data: 'main_menu' }
|
||||
]
|
||||
]
|
||||
};
|
||||
|
||||
// Отправляем главное фото, если есть
|
||||
if (displayProfile.photos.length > 0) {
|
||||
await bot.sendPhoto(chatId, displayProfile.photos[0], {
|
||||
caption: previewText,
|
||||
parse_mode: 'Markdown',
|
||||
reply_markup: keyboard
|
||||
});
|
||||
} else {
|
||||
await bot.sendMessage(chatId, previewText + '\n\n⚠️ *Добавьте фотографии для лучшего результата!*', {
|
||||
parse_mode: 'Markdown',
|
||||
reply_markup: keyboard
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error showing profile preview:', error);
|
||||
await bot.sendMessage(chatId, '❌ Произошла ошибка при загрузке предпросмотра.');
|
||||
}
|
||||
}
|
||||
|
||||
// Получить краткую сводку профиля
|
||||
private getProfileSummary(profile: Profile): string {
|
||||
let summary = `👤 *${profile.name}, ${profile.age}*\n`;
|
||||
|
||||
const completeness = this.calculateProfileCompleteness(profile);
|
||||
summary += `📊 Заполненность профиля: ${completeness}%\n\n`;
|
||||
|
||||
summary += `📷 Фото: ${profile.photos.length}/9\n`;
|
||||
summary += `📖 О себе: ${profile.bio ? '✅' : '❌'}\n`;
|
||||
summary += `🎯 Хобби: ${profile.hobbies ? '✅' : '❌'}\n`;
|
||||
summary += `🏙️ Город: ${profile.city ? '✅' : '❌'}\n`;
|
||||
summary += `💼 Работа: ${profile.job ? '✅' : '❌'}\n`;
|
||||
|
||||
return summary;
|
||||
}
|
||||
|
||||
// Рассчитать процент заполненности профиля
|
||||
private calculateProfileCompleteness(profile: Profile): number {
|
||||
const fields = [
|
||||
profile.name,
|
||||
profile.age,
|
||||
profile.photos.length > 0,
|
||||
profile.bio,
|
||||
profile.hobbies,
|
||||
profile.city,
|
||||
profile.job,
|
||||
profile.education,
|
||||
profile.height,
|
||||
profile.religion,
|
||||
profile.datingGoal
|
||||
];
|
||||
|
||||
const filledFields = fields.filter(field =>
|
||||
field !== null && field !== undefined && field !== ''
|
||||
).length;
|
||||
|
||||
return Math.round((filledFields / fields.length) * 100);
|
||||
}
|
||||
|
||||
// Получить текст цели знакомства
|
||||
private getDatingGoalText(goal: string): string {
|
||||
const goals: { [key: string]: string } = {
|
||||
'serious': 'Серьёзные отношения',
|
||||
'casual': 'Лёгкие отношения',
|
||||
'friends': 'Дружба',
|
||||
'unsure': 'Пока не определился',
|
||||
'one_night': 'Отношения на одну ночь',
|
||||
'fwb': 'Друзья с привилегиями',
|
||||
'marriage_abroad': 'Брак с переездом',
|
||||
'sugar': 'Спонсорство',
|
||||
'polyamory': 'Полиамория'
|
||||
};
|
||||
return goals[goal] || goal;
|
||||
}
|
||||
|
||||
// Получить текст образа жизни
|
||||
private getLifestyleText(type: string, value: string): string {
|
||||
const lifestyleTexts: { [key: string]: { [key: string]: string } } = {
|
||||
smoking: {
|
||||
'never': 'Не курю',
|
||||
'sometimes': 'Иногда',
|
||||
'regularly': 'Регулярно'
|
||||
},
|
||||
drinking: {
|
||||
'never': 'Не пью',
|
||||
'sometimes': 'Иногда',
|
||||
'regularly': 'Регулярно'
|
||||
},
|
||||
kids: {
|
||||
'have': 'Есть дети',
|
||||
'want': 'Хочу детей',
|
||||
'dont_want': 'Не хочу детей',
|
||||
'unsure': 'Пока не знаю'
|
||||
}
|
||||
};
|
||||
|
||||
return lifestyleTexts[type]?.[value] || value;
|
||||
}
|
||||
}
|
||||
40
src/database/migrations/add_profile_fields.sql
Normal file
40
src/database/migrations/add_profile_fields.sql
Normal file
@@ -0,0 +1,40 @@
|
||||
-- Add new profile fields for enhanced profile management
|
||||
|
||||
-- Add hobbies field
|
||||
ALTER TABLE profiles
|
||||
ADD COLUMN IF NOT EXISTS hobbies TEXT;
|
||||
|
||||
-- Add religion field
|
||||
ALTER TABLE profiles
|
||||
ADD COLUMN IF NOT EXISTS religion VARCHAR(100);
|
||||
|
||||
-- Add dating goal field
|
||||
ALTER TABLE profiles
|
||||
ADD COLUMN IF NOT EXISTS dating_goal VARCHAR(20)
|
||||
CHECK (dating_goal IN ('serious', 'casual', 'friends', 'unsure'));
|
||||
|
||||
-- Add lifestyle preferences
|
||||
ALTER TABLE profiles
|
||||
ADD COLUMN IF NOT EXISTS has_kids VARCHAR(20)
|
||||
CHECK (has_kids IN ('have', 'want', 'dont_want', 'unsure'));
|
||||
|
||||
-- Create indexes for better performance
|
||||
CREATE INDEX IF NOT EXISTS idx_profiles_dating_goal ON profiles(dating_goal);
|
||||
CREATE INDEX IF NOT EXISTS idx_profiles_religion ON profiles(religion);
|
||||
CREATE INDEX IF NOT EXISTS idx_profiles_has_kids ON profiles(has_kids);
|
||||
|
||||
-- Update updated_at timestamp function
|
||||
CREATE OR REPLACE FUNCTION update_updated_at_column()
|
||||
RETURNS TRIGGER AS $$
|
||||
BEGIN
|
||||
NEW.updated_at = CURRENT_TIMESTAMP;
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$ language 'plpgsql';
|
||||
|
||||
-- Apply trigger to profiles table if not exists
|
||||
DROP TRIGGER IF EXISTS update_profiles_updated_at ON profiles;
|
||||
CREATE TRIGGER update_profiles_updated_at
|
||||
BEFORE UPDATE ON profiles
|
||||
FOR EACH ROW
|
||||
EXECUTE FUNCTION update_updated_at_column();
|
||||
46
src/database/migrations/add_scheduled_notifications.sql
Normal file
46
src/database/migrations/add_scheduled_notifications.sql
Normal file
@@ -0,0 +1,46 @@
|
||||
-- Миграция для добавления таблицы запланированных уведомлений
|
||||
-- Версия: 2025-09-12-002
|
||||
|
||||
-- Создание таблицы запланированных уведомлений
|
||||
CREATE TABLE IF NOT EXISTS scheduled_notifications (
|
||||
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
||||
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
type VARCHAR(50) NOT NULL, -- 'inactivity_reminder', 'likes_summary', 'match_reminder', etc.
|
||||
scheduled_at TIMESTAMP WITH TIME ZONE NOT NULL,
|
||||
sent BOOLEAN DEFAULT FALSE,
|
||||
data JSONB, -- Дополнительные данные для уведомления
|
||||
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
-- Индексы для оптимизации запросов
|
||||
CREATE INDEX IF NOT EXISTS idx_scheduled_notifications_user_id ON scheduled_notifications(user_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_scheduled_notifications_scheduled_at ON scheduled_notifications(scheduled_at) WHERE sent = FALSE;
|
||||
CREATE INDEX IF NOT EXISTS idx_scheduled_notifications_type ON scheduled_notifications(type);
|
||||
CREATE INDEX IF NOT EXISTS idx_scheduled_notifications_sent ON scheduled_notifications(sent);
|
||||
|
||||
-- Триггер для обновления updated_at
|
||||
CREATE TRIGGER scheduled_notifications_updated_at BEFORE UPDATE ON scheduled_notifications
|
||||
FOR EACH ROW EXECUTE FUNCTION update_updated_at();
|
||||
|
||||
-- Функция для очистки старых уведомлений (старше 30 дней)
|
||||
CREATE OR REPLACE FUNCTION cleanup_old_notifications()
|
||||
RETURNS INTEGER AS $$
|
||||
DECLARE
|
||||
deleted_count INTEGER;
|
||||
BEGIN
|
||||
DELETE FROM scheduled_notifications
|
||||
WHERE sent = TRUE
|
||||
AND created_at < CURRENT_TIMESTAMP - INTERVAL '30 days';
|
||||
|
||||
GET DIAGNOSTICS deleted_count = ROW_COUNT;
|
||||
RETURN deleted_count;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
-- Комментарии к таблице и колонкам
|
||||
COMMENT ON TABLE scheduled_notifications IS 'Таблица для хранения запланированных уведомлений';
|
||||
COMMENT ON COLUMN scheduled_notifications.type IS 'Тип уведомления: inactivity_reminder, likes_summary, match_reminder и т.д.';
|
||||
COMMENT ON COLUMN scheduled_notifications.scheduled_at IS 'Время, когда должно быть отправлено уведомление';
|
||||
COMMENT ON COLUMN scheduled_notifications.sent IS 'Флаг, указывающий было ли отправлено уведомление';
|
||||
COMMENT ON COLUMN scheduled_notifications.data IS 'JSON данные для кастомизации уведомления';
|
||||
@@ -4,6 +4,7 @@ import { MatchingService } from '../services/matchingService';
|
||||
import { ChatService } from '../services/chatService';
|
||||
import { Profile } from '../models/Profile';
|
||||
import { MessageHandlers } from './messageHandlers';
|
||||
import { ProfileEditController } from '../controllers/profileEditController';
|
||||
|
||||
export class CallbackHandlers {
|
||||
private bot: TelegramBot;
|
||||
@@ -11,6 +12,7 @@ export class CallbackHandlers {
|
||||
private matchingService: MatchingService;
|
||||
private chatService: ChatService;
|
||||
private messageHandlers: MessageHandlers;
|
||||
private profileEditController: ProfileEditController;
|
||||
|
||||
constructor(bot: TelegramBot, messageHandlers: MessageHandlers) {
|
||||
this.bot = bot;
|
||||
@@ -18,6 +20,7 @@ export class CallbackHandlers {
|
||||
this.matchingService = new MatchingService();
|
||||
this.chatService = new ChatService();
|
||||
this.messageHandlers = messageHandlers;
|
||||
this.profileEditController = new ProfileEditController(this.profileService);
|
||||
}
|
||||
|
||||
register(): void {
|
||||
@@ -44,11 +47,91 @@ export class CallbackHandlers {
|
||||
await this.handleEditProfile(chatId, telegramId);
|
||||
} else if (data === 'manage_photos') {
|
||||
await this.handleManagePhotos(chatId, telegramId);
|
||||
} else if (data === 'preview_profile') {
|
||||
await this.handlePreviewProfile(chatId, telegramId);
|
||||
}
|
||||
|
||||
// Редактирование полей профиля
|
||||
else if (data === 'edit_name') {
|
||||
await this.handleEditName(chatId, telegramId);
|
||||
} else if (data === 'edit_age') {
|
||||
await this.handleEditAge(chatId, telegramId);
|
||||
} else if (data === 'edit_bio') {
|
||||
await this.handleEditBio(chatId, telegramId);
|
||||
} else if (data === 'edit_hobbies') {
|
||||
await this.handleEditHobbies(chatId, telegramId);
|
||||
} else if (data === 'edit_city') {
|
||||
await this.handleEditCity(chatId, telegramId);
|
||||
} else if (data === 'edit_job') {
|
||||
await this.handleEditJob(chatId, telegramId);
|
||||
} else if (data === 'edit_education') {
|
||||
await this.handleEditEducation(chatId, telegramId);
|
||||
} else if (data === 'edit_height') {
|
||||
await this.handleEditHeight(chatId, telegramId);
|
||||
} else if (data === 'edit_religion') {
|
||||
await this.handleEditReligion(chatId, telegramId);
|
||||
} else if (data === 'edit_dating_goal') {
|
||||
await this.handleEditDatingGoal(chatId, telegramId);
|
||||
} else if (data === 'edit_lifestyle') {
|
||||
await this.handleEditLifestyle(chatId, telegramId);
|
||||
} else if (data === 'edit_search_preferences') {
|
||||
await this.handleEditSearchPreferences(chatId, telegramId);
|
||||
}
|
||||
|
||||
// Управление фотографиями
|
||||
else if (data === 'add_photo') {
|
||||
await this.handleAddPhoto(chatId, telegramId);
|
||||
} else if (data === 'delete_photo') {
|
||||
await this.handleDeletePhoto(chatId, telegramId);
|
||||
} else if (data === 'set_main_photo') {
|
||||
await this.handleSetMainPhoto(chatId, telegramId);
|
||||
} else if (data.startsWith('delete_photo_')) {
|
||||
const photoIndex = parseInt(data.replace('delete_photo_', ''));
|
||||
await this.handleDeletePhotoByIndex(chatId, telegramId, photoIndex);
|
||||
} else if (data.startsWith('set_main_photo_')) {
|
||||
const photoIndex = parseInt(data.replace('set_main_photo_', ''));
|
||||
await this.handleSetMainPhotoByIndex(chatId, telegramId, photoIndex);
|
||||
}
|
||||
|
||||
// Цели знакомства
|
||||
else if (data.startsWith('set_dating_goal_')) {
|
||||
const goal = data.replace('set_dating_goal_', '');
|
||||
await this.handleSetDatingGoal(chatId, telegramId, goal);
|
||||
}
|
||||
|
||||
// Образ жизни
|
||||
else if (data === 'edit_smoking') {
|
||||
await this.handleEditSmoking(chatId, telegramId);
|
||||
} else if (data === 'edit_drinking') {
|
||||
await this.handleEditDrinking(chatId, telegramId);
|
||||
} else if (data === 'edit_kids') {
|
||||
await this.handleEditKids(chatId, telegramId);
|
||||
} else if (data.startsWith('set_smoking_')) {
|
||||
const value = data.replace('set_smoking_', '');
|
||||
await this.handleSetLifestyle(chatId, telegramId, 'smoking', value);
|
||||
} else if (data.startsWith('set_drinking_')) {
|
||||
const value = data.replace('set_drinking_', '');
|
||||
await this.handleSetLifestyle(chatId, telegramId, 'drinking', value);
|
||||
} else if (data.startsWith('set_kids_')) {
|
||||
const value = data.replace('set_kids_', '');
|
||||
await this.handleSetLifestyle(chatId, telegramId, 'kids', value);
|
||||
}
|
||||
|
||||
// Настройки поиска
|
||||
else if (data === 'edit_age_range') {
|
||||
await this.handleEditAgeRange(chatId, telegramId);
|
||||
} else if (data === 'edit_distance') {
|
||||
await this.handleEditDistance(chatId, telegramId);
|
||||
}
|
||||
|
||||
// Просмотр анкет и свайпы
|
||||
else if (data === 'start_browsing') {
|
||||
await this.handleStartBrowsing(chatId, telegramId);
|
||||
} else if (data === 'vip_search') {
|
||||
await this.handleVipSearch(chatId, telegramId);
|
||||
} else if (data.startsWith('search_by_goal_')) {
|
||||
const goal = data.replace('search_by_goal_', '');
|
||||
await this.handleSearchByGoal(chatId, telegramId, goal);
|
||||
} else if (data === 'next_candidate') {
|
||||
await this.handleNextCandidate(chatId, telegramId);
|
||||
} else if (data.startsWith('like_')) {
|
||||
@@ -167,52 +250,12 @@ export class CallbackHandlers {
|
||||
|
||||
// Редактирование профиля
|
||||
async handleEditProfile(chatId: number, telegramId: string): Promise<void> {
|
||||
const keyboard: InlineKeyboardMarkup = {
|
||||
inline_keyboard: [
|
||||
[
|
||||
{ text: '📝 Имя', callback_data: 'edit_name' },
|
||||
{ text: '📅 Возраст', callback_data: 'edit_age' }
|
||||
],
|
||||
[
|
||||
{ text: '📍 Город', callback_data: 'edit_city' },
|
||||
{ text: '💼 Работа', callback_data: 'edit_job' }
|
||||
],
|
||||
[
|
||||
{ text: '📖 О себе', callback_data: 'edit_bio' },
|
||||
{ text: '🎯 Интересы', callback_data: 'edit_interests' }
|
||||
],
|
||||
[{ text: '👈 Назад к профилю', callback_data: 'view_my_profile' }]
|
||||
]
|
||||
};
|
||||
|
||||
await this.bot.sendMessage(
|
||||
chatId,
|
||||
'✏️ Что хотите изменить в профиле?',
|
||||
{ reply_markup: keyboard }
|
||||
);
|
||||
await this.profileEditController.showProfileEditMenu(this.bot, chatId, parseInt(telegramId));
|
||||
}
|
||||
|
||||
// Управление фотографиями
|
||||
async handleManagePhotos(chatId: number, telegramId: string): Promise<void> {
|
||||
const keyboard: InlineKeyboardMarkup = {
|
||||
inline_keyboard: [
|
||||
[
|
||||
{ text: '📷 Добавить фото', callback_data: 'add_photo' },
|
||||
{ text: '🗑 Удалить фото', callback_data: 'delete_photo' }
|
||||
],
|
||||
[
|
||||
{ text: '⭐ Сделать главным', callback_data: 'set_main_photo' },
|
||||
{ text: '🔄 Изменить порядок', callback_data: 'reorder_photos' }
|
||||
],
|
||||
[{ text: '👈 Назад к профилю', callback_data: 'view_my_profile' }]
|
||||
]
|
||||
};
|
||||
|
||||
await this.bot.sendMessage(
|
||||
chatId,
|
||||
'📸 Управление фотографиями\n\nВыберите действие:',
|
||||
{ reply_markup: keyboard }
|
||||
);
|
||||
await this.profileEditController.showPhotoManagementMenu(this.bot, chatId, parseInt(telegramId));
|
||||
}
|
||||
|
||||
// Начать просмотр анкет
|
||||
@@ -669,10 +712,33 @@ export class CallbackHandlers {
|
||||
if (profile.job) profileText += '💼 ' + profile.job + '\n';
|
||||
if (profile.education) profileText += '🎓 ' + profile.education + '\n';
|
||||
if (profile.height) profileText += '📏 ' + profile.height + ' см\n';
|
||||
if (profile.religion) profileText += '🕊️ ' + profile.religion + '\n';
|
||||
|
||||
// Цель знакомства
|
||||
if (profile.datingGoal) {
|
||||
const goalText = this.getDatingGoalText(profile.datingGoal);
|
||||
profileText += '💕 ' + goalText + '\n';
|
||||
}
|
||||
|
||||
// Образ жизни
|
||||
if (profile.lifestyle) {
|
||||
const lifestyleText = this.getLifestyleText(profile.lifestyle);
|
||||
if (lifestyleText) {
|
||||
profileText += lifestyleText + '\n';
|
||||
}
|
||||
}
|
||||
|
||||
profileText += '\n📝 ' + (profile.bio || 'Описание не указано') + '\n';
|
||||
|
||||
// Хобби с хэштегами
|
||||
if (profile.hobbies && profile.hobbies.trim()) {
|
||||
const hobbiesArray = profile.hobbies.split(',').map(hobby => hobby.trim()).filter(hobby => hobby);
|
||||
const formattedHobbies = hobbiesArray.map(hobby => '#' + hobby).join(' ');
|
||||
profileText += '\n🎯 ' + formattedHobbies + '\n';
|
||||
}
|
||||
|
||||
if (profile.interests.length > 0) {
|
||||
profileText += '\n🎯 Интересы: ' + profile.interests.join(', ');
|
||||
profileText += '\n<EFBFBD> Интересы: ' + profile.interests.join(', ');
|
||||
}
|
||||
|
||||
let keyboard: InlineKeyboardMarkup;
|
||||
@@ -752,13 +818,18 @@ export class CallbackHandlers {
|
||||
candidateText += '📍 ' + (candidate.city || 'Не указан') + '\n';
|
||||
if (candidate.job) candidateText += '💼 ' + candidate.job + '\n';
|
||||
if (candidate.education) candidateText += '🎓 ' + candidate.education + '\n';
|
||||
if (candidate.height) candidateText += '<EFBFBD><EFBFBD> ' + candidate.height + ' см\n';
|
||||
if (candidate.height) candidateText += '📏 ' + candidate.height + ' см\n';
|
||||
if (candidate.religion) candidateText += '🕊️ ' + candidate.religion + '\n';
|
||||
candidateText += '\n📝 ' + (candidate.bio || 'Описание отсутствует') + '\n';
|
||||
|
||||
if (candidate.interests.length > 0) {
|
||||
candidateText += '\n🎯 Интересы: ' + candidate.interests.join(', ');
|
||||
}
|
||||
|
||||
if (candidate.hobbies) {
|
||||
candidateText += '\n🎮 Хобби: ' + candidate.hobbies;
|
||||
}
|
||||
|
||||
const keyboard: InlineKeyboardMarkup = {
|
||||
inline_keyboard: [
|
||||
[
|
||||
@@ -799,4 +870,711 @@ export class CallbackHandlers {
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// ===== НОВЫЕ МЕТОДЫ ДЛЯ РЕДАКТИРОВАНИЯ ПРОФИЛЯ =====
|
||||
|
||||
// Предпросмотр профиля
|
||||
async handlePreviewProfile(chatId: number, telegramId: string): Promise<void> {
|
||||
await this.profileEditController.showProfilePreview(this.bot, chatId, parseInt(telegramId));
|
||||
}
|
||||
|
||||
// Редактирование имени
|
||||
async handleEditName(chatId: number, telegramId: string): Promise<void> {
|
||||
this.messageHandlers.setWaitingForInput(parseInt(telegramId), 'name');
|
||||
await this.bot.sendMessage(chatId, '📝 *Введите ваше новое имя:*\n\nНапример: Анна', {
|
||||
parse_mode: 'Markdown'
|
||||
});
|
||||
}
|
||||
|
||||
// Редактирование возраста
|
||||
async handleEditAge(chatId: number, telegramId: string): Promise<void> {
|
||||
this.messageHandlers.setWaitingForInput(parseInt(telegramId), 'age');
|
||||
await this.bot.sendMessage(chatId, '🎂 *Введите ваш возраст:*\n\nВозраст должен быть от 18 до 100 лет', {
|
||||
parse_mode: 'Markdown'
|
||||
});
|
||||
}
|
||||
|
||||
// Редактирование описания "О себе"
|
||||
async handleEditBio(chatId: number, telegramId: string): Promise<void> {
|
||||
this.messageHandlers.setWaitingForInput(parseInt(telegramId), 'bio');
|
||||
await this.bot.sendMessage(chatId,
|
||||
'📖 *Расскажите о себе:*\n\n' +
|
||||
'Напишите несколько предложений, которые помогут людям лучше вас узнать.\n\n' +
|
||||
'_Максимум 500 символов_', {
|
||||
parse_mode: 'Markdown'
|
||||
});
|
||||
}
|
||||
|
||||
// Редактирование хобби
|
||||
async handleEditHobbies(chatId: number, telegramId: string): Promise<void> {
|
||||
this.messageHandlers.setWaitingForInput(parseInt(telegramId), 'hobbies');
|
||||
await this.bot.sendMessage(chatId,
|
||||
'🎯 *Введите ваши хобби через запятую:*\n\n' +
|
||||
'Например: футбол, чтение, путешествия, кулинария\n\n' +
|
||||
'_В анкете они будут отображаться как хэштеги: #футбол #чтение #путешествия_', {
|
||||
parse_mode: 'Markdown'
|
||||
});
|
||||
}
|
||||
|
||||
// Редактирование города
|
||||
async handleEditCity(chatId: number, telegramId: string): Promise<void> {
|
||||
this.messageHandlers.setWaitingForInput(parseInt(telegramId), 'city');
|
||||
await this.bot.sendMessage(chatId, '🏙️ *Введите ваш город:*\n\nНапример: Москва', {
|
||||
parse_mode: 'Markdown'
|
||||
});
|
||||
}
|
||||
|
||||
// Редактирование работы
|
||||
async handleEditJob(chatId: number, telegramId: string): Promise<void> {
|
||||
this.messageHandlers.setWaitingForInput(parseInt(telegramId), 'job');
|
||||
await this.bot.sendMessage(chatId, '💼 *Введите вашу профессию или место работы:*\n\nНапример: Дизайнер в IT-компании', {
|
||||
parse_mode: 'Markdown'
|
||||
});
|
||||
}
|
||||
|
||||
// Редактирование образования
|
||||
async handleEditEducation(chatId: number, telegramId: string): Promise<void> {
|
||||
this.messageHandlers.setWaitingForInput(parseInt(telegramId), 'education');
|
||||
await this.bot.sendMessage(chatId, '🎓 *Введите ваше образование:*\n\nНапример: МГУ, факультет журналистики', {
|
||||
parse_mode: 'Markdown'
|
||||
});
|
||||
}
|
||||
|
||||
// Редактирование роста
|
||||
async handleEditHeight(chatId: number, telegramId: string): Promise<void> {
|
||||
this.messageHandlers.setWaitingForInput(parseInt(telegramId), 'height');
|
||||
await this.bot.sendMessage(chatId, '📏 *Введите ваш рост в сантиметрах:*\n\nНапример: 175', {
|
||||
parse_mode: 'Markdown'
|
||||
});
|
||||
}
|
||||
|
||||
// Редактирование религии
|
||||
async handleEditReligion(chatId: number, telegramId: string): Promise<void> {
|
||||
this.messageHandlers.setWaitingForInput(parseInt(telegramId), 'religion');
|
||||
await this.bot.sendMessage(chatId, '🕊️ *Введите вашу религию или напишите "нет":*\n\nНапример: православие, ислам, атеизм, нет', {
|
||||
parse_mode: 'Markdown'
|
||||
});
|
||||
}
|
||||
|
||||
// Редактирование цели знакомства
|
||||
async handleEditDatingGoal(chatId: number, telegramId: string): Promise<void> {
|
||||
const keyboard = {
|
||||
inline_keyboard: [
|
||||
[
|
||||
{ text: '💕 Серьёзные отношения', callback_data: 'set_dating_goal_serious' },
|
||||
{ text: '🎉 Лёгкие отношения', callback_data: 'set_dating_goal_casual' }
|
||||
],
|
||||
[
|
||||
{ text: '👥 Дружба', callback_data: 'set_dating_goal_friends' },
|
||||
{ text: '🔥 Одна ночь', callback_data: 'set_dating_goal_one_night' }
|
||||
],
|
||||
[
|
||||
{ text: '😏 FWB', callback_data: 'set_dating_goal_fwb' },
|
||||
{ text: '💎 Спонсорство', callback_data: 'set_dating_goal_sugar' }
|
||||
],
|
||||
[
|
||||
{ text: '💍 Брак с переездом', callback_data: 'set_dating_goal_marriage_abroad' },
|
||||
{ text: '💫 Полиамория', callback_data: 'set_dating_goal_polyamory' }
|
||||
],
|
||||
[
|
||||
{ text: '🤷♀️ Пока не определился', callback_data: 'set_dating_goal_unsure' }
|
||||
],
|
||||
[
|
||||
{ text: '⬅️ Назад', callback_data: 'edit_profile' }
|
||||
]
|
||||
]
|
||||
};
|
||||
|
||||
await this.bot.sendMessage(chatId, '💕 *Выберите цель знакомства:*', {
|
||||
parse_mode: 'Markdown',
|
||||
reply_markup: keyboard
|
||||
});
|
||||
}
|
||||
|
||||
// Редактирование образа жизни
|
||||
async handleEditLifestyle(chatId: number, telegramId: string): Promise<void> {
|
||||
const keyboard = {
|
||||
inline_keyboard: [
|
||||
[
|
||||
{ text: '🚬 Курение', callback_data: 'edit_smoking' },
|
||||
{ text: '🍷 Алкоголь', callback_data: 'edit_drinking' }
|
||||
],
|
||||
[
|
||||
{ text: '👶 Отношение к детям', callback_data: 'edit_kids' }
|
||||
],
|
||||
[
|
||||
{ text: '⬅️ Назад', callback_data: 'edit_profile' }
|
||||
]
|
||||
]
|
||||
};
|
||||
|
||||
await this.bot.sendMessage(chatId, '🚬 *Выберите что хотите изменить в образе жизни:*', {
|
||||
parse_mode: 'Markdown',
|
||||
reply_markup: keyboard
|
||||
});
|
||||
}
|
||||
|
||||
// Редактирование предпочтений поиска
|
||||
async handleEditSearchPreferences(chatId: number, telegramId: string): Promise<void> {
|
||||
const keyboard = {
|
||||
inline_keyboard: [
|
||||
[
|
||||
{ text: '🔢 Возрастной диапазон', callback_data: 'edit_age_range' },
|
||||
{ text: '📍 Максимальное расстояние', callback_data: 'edit_distance' }
|
||||
],
|
||||
[
|
||||
{ text: '⬅️ Назад', callback_data: 'edit_profile' }
|
||||
]
|
||||
]
|
||||
};
|
||||
|
||||
await this.bot.sendMessage(chatId, '⚙️ *Настройки поиска:*', {
|
||||
parse_mode: 'Markdown',
|
||||
reply_markup: keyboard
|
||||
});
|
||||
}
|
||||
|
||||
// Добавление фото
|
||||
async handleAddPhoto(chatId: number, telegramId: string): Promise<void> {
|
||||
this.messageHandlers.setWaitingForInput(parseInt(telegramId), 'photo');
|
||||
await this.bot.sendMessage(chatId, '📷 *Отправьте фотографию:*\n\nМаксимум 9 фотографий в профиле', {
|
||||
parse_mode: 'Markdown'
|
||||
});
|
||||
}
|
||||
|
||||
// Удаление фото
|
||||
async handleDeletePhoto(chatId: number, telegramId: string): Promise<void> {
|
||||
try {
|
||||
const profile = await this.profileService.getProfileByTelegramId(telegramId);
|
||||
if (!profile || profile.photos.length === 0) {
|
||||
await this.bot.sendMessage(chatId, '❌ У вас нет фотографий для удаления');
|
||||
return;
|
||||
}
|
||||
|
||||
const keyboard = {
|
||||
inline_keyboard: [
|
||||
...profile.photos.map((photo, index) => [
|
||||
{ text: `🗑️ Удалить фото ${index + 1}`, callback_data: `delete_photo_${index}` }
|
||||
]),
|
||||
[{ text: '⬅️ Назад', callback_data: 'manage_photos' }]
|
||||
]
|
||||
};
|
||||
|
||||
await this.bot.sendMessage(chatId, '🗑️ *Выберите фото для удаления:*', {
|
||||
parse_mode: 'Markdown',
|
||||
reply_markup: keyboard
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error in handleDeletePhoto:', error);
|
||||
await this.bot.sendMessage(chatId, '❌ Произошла ошибка');
|
||||
}
|
||||
}
|
||||
|
||||
// Установка главного фото
|
||||
async handleSetMainPhoto(chatId: number, telegramId: string): Promise<void> {
|
||||
try {
|
||||
const profile = await this.profileService.getProfileByTelegramId(telegramId);
|
||||
if (!profile || profile.photos.length <= 1) {
|
||||
await this.bot.sendMessage(chatId, '❌ У вас недостаточно фотографий');
|
||||
return;
|
||||
}
|
||||
|
||||
const keyboard = {
|
||||
inline_keyboard: [
|
||||
...profile.photos.map((photo, index) => [
|
||||
{ text: `⭐ Сделать главным фото ${index + 1}`, callback_data: `set_main_photo_${index}` }
|
||||
]),
|
||||
[{ text: '⬅️ Назад', callback_data: 'manage_photos' }]
|
||||
]
|
||||
};
|
||||
|
||||
await this.bot.sendMessage(chatId, '⭐ *Выберите главное фото:*', {
|
||||
parse_mode: 'Markdown',
|
||||
reply_markup: keyboard
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error in handleSetMainPhoto:', error);
|
||||
await this.bot.sendMessage(chatId, '❌ Произошла ошибка');
|
||||
}
|
||||
}
|
||||
|
||||
// ===== НОВЫЕ МЕТОДЫ ДЛЯ РАСШИРЕННОГО РЕДАКТИРОВАНИЯ =====
|
||||
|
||||
// Удаление фото по индексу
|
||||
async handleDeletePhotoByIndex(chatId: number, telegramId: string, photoIndex: number): Promise<void> {
|
||||
try {
|
||||
const profile = await this.profileService.getProfileByTelegramId(telegramId);
|
||||
if (!profile || photoIndex >= profile.photos.length) {
|
||||
await this.bot.sendMessage(chatId, '❌ Фото не найдено');
|
||||
return;
|
||||
}
|
||||
|
||||
profile.removePhoto(profile.photos[photoIndex]);
|
||||
await this.profileService.updateProfile(profile.userId, {
|
||||
photos: profile.photos
|
||||
});
|
||||
|
||||
await this.bot.sendMessage(chatId, '✅ Фото удалено!');
|
||||
|
||||
setTimeout(() => {
|
||||
this.handleManagePhotos(chatId, telegramId);
|
||||
}, 1000);
|
||||
} catch (error) {
|
||||
console.error('Error deleting photo:', error);
|
||||
await this.bot.sendMessage(chatId, '❌ Произошла ошибка');
|
||||
}
|
||||
}
|
||||
|
||||
// Установка главного фото по индексу
|
||||
async handleSetMainPhotoByIndex(chatId: number, telegramId: string, photoIndex: number): Promise<void> {
|
||||
try {
|
||||
const profile = await this.profileService.getProfileByTelegramId(telegramId);
|
||||
if (!profile || photoIndex >= profile.photos.length) {
|
||||
await this.bot.sendMessage(chatId, '❌ Фото не найдено');
|
||||
return;
|
||||
}
|
||||
|
||||
profile.setMainPhoto(profile.photos[photoIndex]);
|
||||
await this.profileService.updateProfile(profile.userId, {
|
||||
photos: profile.photos
|
||||
});
|
||||
|
||||
await this.bot.sendMessage(chatId, '✅ Главное фото установлено!');
|
||||
|
||||
setTimeout(() => {
|
||||
this.handleManagePhotos(chatId, telegramId);
|
||||
}, 1000);
|
||||
} catch (error) {
|
||||
console.error('Error setting main photo:', error);
|
||||
await this.bot.sendMessage(chatId, '❌ Произошла ошибка');
|
||||
}
|
||||
}
|
||||
|
||||
// Установка цели знакомства
|
||||
async handleSetDatingGoal(chatId: number, telegramId: string, goal: string): Promise<void> {
|
||||
try {
|
||||
const profile = await this.profileService.getProfileByTelegramId(telegramId);
|
||||
if (!profile) {
|
||||
await this.bot.sendMessage(chatId, '❌ Профиль не найден');
|
||||
return;
|
||||
}
|
||||
|
||||
await this.profileService.updateProfile(profile.userId, {
|
||||
datingGoal: goal as any
|
||||
});
|
||||
|
||||
const goalTexts: { [key: string]: string } = {
|
||||
'serious': 'Серьёзные отношения',
|
||||
'casual': 'Лёгкие отношения',
|
||||
'friends': 'Дружба',
|
||||
'unsure': 'Пока не определился',
|
||||
'one_night': 'Отношения на одну ночь',
|
||||
'fwb': 'Друзья с привилегиями',
|
||||
'marriage_abroad': 'Брак с переездом',
|
||||
'sugar': 'Спонсорство',
|
||||
'polyamory': 'Полиамория'
|
||||
};
|
||||
|
||||
await this.bot.sendMessage(chatId, `✅ Цель знакомства установлена: ${goalTexts[goal]}`);
|
||||
|
||||
setTimeout(() => {
|
||||
this.profileEditController.showProfileEditMenu(this.bot, chatId, parseInt(telegramId));
|
||||
}, 1500);
|
||||
} catch (error) {
|
||||
console.error('Error setting dating goal:', error);
|
||||
await this.bot.sendMessage(chatId, '❌ Произошла ошибка');
|
||||
}
|
||||
}
|
||||
|
||||
// Редактирование курения
|
||||
async handleEditSmoking(chatId: number, telegramId: string): Promise<void> {
|
||||
const keyboard = {
|
||||
inline_keyboard: [
|
||||
[
|
||||
{ text: '🚭 Не курю', callback_data: 'set_smoking_never' },
|
||||
{ text: '🚬 Иногда', callback_data: 'set_smoking_sometimes' }
|
||||
],
|
||||
[
|
||||
{ text: '🚬 Регулярно', callback_data: 'set_smoking_regularly' }
|
||||
],
|
||||
[
|
||||
{ text: '⬅️ Назад', callback_data: 'edit_lifestyle' }
|
||||
]
|
||||
]
|
||||
};
|
||||
|
||||
await this.bot.sendMessage(chatId, '🚬 *Выберите ваше отношение к курению:*', {
|
||||
parse_mode: 'Markdown',
|
||||
reply_markup: keyboard
|
||||
});
|
||||
}
|
||||
|
||||
// Редактирование алкоголя
|
||||
async handleEditDrinking(chatId: number, telegramId: string): Promise<void> {
|
||||
const keyboard = {
|
||||
inline_keyboard: [
|
||||
[
|
||||
{ text: '🚫 Не пью', callback_data: 'set_drinking_never' },
|
||||
{ text: '🍷 Иногда', callback_data: 'set_drinking_sometimes' }
|
||||
],
|
||||
[
|
||||
{ text: '🍺 Регулярно', callback_data: 'set_drinking_regularly' }
|
||||
],
|
||||
[
|
||||
{ text: '⬅️ Назад', callback_data: 'edit_lifestyle' }
|
||||
]
|
||||
]
|
||||
};
|
||||
|
||||
await this.bot.sendMessage(chatId, '🍷 *Выберите ваше отношение к алкоголю:*', {
|
||||
parse_mode: 'Markdown',
|
||||
reply_markup: keyboard
|
||||
});
|
||||
}
|
||||
|
||||
// Редактирование отношения к детям
|
||||
async handleEditKids(chatId: number, telegramId: string): Promise<void> {
|
||||
const keyboard = {
|
||||
inline_keyboard: [
|
||||
[
|
||||
{ text: '👶 Есть дети', callback_data: 'set_kids_have' },
|
||||
{ text: '💕 Хочу детей', callback_data: 'set_kids_want' }
|
||||
],
|
||||
[
|
||||
{ text: '🚫 Не хочу детей', callback_data: 'set_kids_dont_want' },
|
||||
{ text: '🤷♀️ Пока не знаю', callback_data: 'set_kids_unsure' }
|
||||
],
|
||||
[
|
||||
{ text: '⬅️ Назад', callback_data: 'edit_lifestyle' }
|
||||
]
|
||||
]
|
||||
};
|
||||
|
||||
await this.bot.sendMessage(chatId, '👶 *Выберите ваше отношение к детям:*', {
|
||||
parse_mode: 'Markdown',
|
||||
reply_markup: keyboard
|
||||
});
|
||||
}
|
||||
|
||||
// Установка параметра образа жизни
|
||||
async handleSetLifestyle(chatId: number, telegramId: string, type: string, value: string): Promise<void> {
|
||||
try {
|
||||
const profile = await this.profileService.getProfileByTelegramId(telegramId);
|
||||
if (!profile) {
|
||||
await this.bot.sendMessage(chatId, '❌ Профиль не найден');
|
||||
return;
|
||||
}
|
||||
|
||||
const lifestyle = profile.lifestyle || {};
|
||||
lifestyle[type as keyof typeof lifestyle] = value as any;
|
||||
|
||||
await this.profileService.updateProfile(profile.userId, {
|
||||
lifestyle: lifestyle
|
||||
});
|
||||
|
||||
const typeTexts: { [key: string]: string } = {
|
||||
'smoking': 'курение',
|
||||
'drinking': 'алкоголь',
|
||||
'kids': 'отношение к детям'
|
||||
};
|
||||
|
||||
const valueTexts: { [key: string]: { [key: string]: string } } = {
|
||||
smoking: { 'never': 'не курю', 'sometimes': 'иногда', 'regularly': 'регулярно' },
|
||||
drinking: { 'never': 'не пью', 'sometimes': 'иногда', 'regularly': 'регулярно' },
|
||||
kids: { 'have': 'есть дети', 'want': 'хочу детей', 'dont_want': 'не хочу детей', 'unsure': 'пока не знаю' }
|
||||
};
|
||||
|
||||
const typeText = typeTexts[type] || type;
|
||||
const valueText = valueTexts[type]?.[value] || value;
|
||||
|
||||
await this.bot.sendMessage(chatId, `✅ ${typeText}: ${valueText}`);
|
||||
|
||||
setTimeout(() => {
|
||||
this.profileEditController.showProfileEditMenu(this.bot, chatId, parseInt(telegramId));
|
||||
}, 1500);
|
||||
} catch (error) {
|
||||
console.error('Error setting lifestyle:', error);
|
||||
await this.bot.sendMessage(chatId, '❌ Произошла ошибка');
|
||||
}
|
||||
}
|
||||
|
||||
// Редактирование возрастного диапазона
|
||||
async handleEditAgeRange(chatId: number, telegramId: string): Promise<void> {
|
||||
this.messageHandlers.setWaitingForInput(parseInt(telegramId), 'age_range');
|
||||
await this.bot.sendMessage(chatId,
|
||||
'🔢 *Введите возрастной диапазон:*\n\n' +
|
||||
'Формат: минимальный-максимальный возраст\n' +
|
||||
'Например: 18-35', {
|
||||
parse_mode: 'Markdown'
|
||||
});
|
||||
}
|
||||
|
||||
// Редактирование максимального расстояния
|
||||
async handleEditDistance(chatId: number, telegramId: string): Promise<void> {
|
||||
this.messageHandlers.setWaitingForInput(parseInt(telegramId), 'distance');
|
||||
await this.bot.sendMessage(chatId,
|
||||
'📍 *Введите максимальное расстояние для поиска:*\n\n' +
|
||||
'В километрах (например: 50)', {
|
||||
parse_mode: 'Markdown'
|
||||
});
|
||||
}
|
||||
|
||||
// ===== VIP ФУНКЦИИ =====
|
||||
|
||||
// VIP поиск по целям знакомства
|
||||
async handleVipSearch(chatId: number, telegramId: string): Promise<void> {
|
||||
try {
|
||||
// Проверяем VIP статус пользователя
|
||||
const user = await this.profileService.getUserByTelegramId(telegramId);
|
||||
if (!user || !user.isPremium) {
|
||||
const keyboard = {
|
||||
inline_keyboard: [
|
||||
[
|
||||
{ text: '💎 Получить VIP', callback_data: 'get_vip' },
|
||||
{ text: '⬅️ Назад', callback_data: 'main_menu' }
|
||||
]
|
||||
]
|
||||
};
|
||||
|
||||
await this.bot.sendMessage(chatId,
|
||||
'🔒 *VIP Поиск*\n\n' +
|
||||
'Эта функция доступна только для VIP пользователей!\n\n' +
|
||||
'✨ *VIP возможности:*\n' +
|
||||
'• Поиск по целям знакомства\n' +
|
||||
'• Расширенные фильтры\n' +
|
||||
'• Приоритет в показе анкет\n' +
|
||||
'• Безлимитные суперлайки', {
|
||||
parse_mode: 'Markdown',
|
||||
reply_markup: keyboard
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const keyboard = {
|
||||
inline_keyboard: [
|
||||
[
|
||||
{ text: '💕 Серьёзные отношения', callback_data: 'search_by_goal_serious' },
|
||||
{ text: '🎉 Лёгкие отношения', callback_data: 'search_by_goal_casual' }
|
||||
],
|
||||
[
|
||||
{ text: '👥 Дружба', callback_data: 'search_by_goal_friends' },
|
||||
{ text: '🔥 Одна ночь', callback_data: 'search_by_goal_one_night' }
|
||||
],
|
||||
[
|
||||
{ text: '😏 FWB', callback_data: 'search_by_goal_fwb' },
|
||||
{ text: '💎 Спонсорство', callback_data: 'search_by_goal_sugar' }
|
||||
],
|
||||
[
|
||||
{ text: '💍 Брак с переездом', callback_data: 'search_by_goal_marriage_abroad' },
|
||||
{ text: '💫 Полиамория', callback_data: 'search_by_goal_polyamory' }
|
||||
],
|
||||
[
|
||||
{ text: '🎲 Все цели', callback_data: 'start_browsing' }
|
||||
],
|
||||
[
|
||||
{ text: '⬅️ Главное меню', callback_data: 'main_menu' }
|
||||
]
|
||||
]
|
||||
};
|
||||
|
||||
await this.bot.sendMessage(chatId,
|
||||
'🔍 *VIP Поиск по целям знакомства*\n\n' +
|
||||
'Выберите интересующую вас цель:', {
|
||||
parse_mode: 'Markdown',
|
||||
reply_markup: keyboard
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error in VIP search:', error);
|
||||
await this.bot.sendMessage(chatId, '❌ Произошла ошибка');
|
||||
}
|
||||
}
|
||||
|
||||
// Поиск по конкретной цели
|
||||
async handleSearchByGoal(chatId: number, telegramId: string, goal: string): Promise<void> {
|
||||
try {
|
||||
const profile = await this.profileService.getProfileByTelegramId(telegramId);
|
||||
if (!profile) {
|
||||
await this.bot.sendMessage(chatId, '❌ Сначала создайте профиль!');
|
||||
return;
|
||||
}
|
||||
|
||||
// Получаем кандидатов с определенной целью знакомства
|
||||
const candidates = await this.matchingService.getCandidatesWithGoal(profile, goal);
|
||||
|
||||
if (candidates.length === 0) {
|
||||
const goalTexts: { [key: string]: string } = {
|
||||
'serious': 'серьёзные отношения',
|
||||
'casual': 'лёгкие отношения',
|
||||
'friends': 'дружбу',
|
||||
'one_night': 'отношения на одну ночь',
|
||||
'fwb': 'друзей с привилегиями',
|
||||
'marriage_abroad': 'брак с переездом',
|
||||
'sugar': 'спонсорство',
|
||||
'polyamory': 'полиаморию'
|
||||
};
|
||||
|
||||
const keyboard = {
|
||||
inline_keyboard: [
|
||||
[
|
||||
{ text: '🔍 Другие цели', callback_data: 'vip_search' },
|
||||
{ text: '🎲 Обычный поиск', callback_data: 'start_browsing' }
|
||||
],
|
||||
[
|
||||
{ text: '⬅️ Главное меню', callback_data: 'main_menu' }
|
||||
]
|
||||
]
|
||||
};
|
||||
|
||||
await this.bot.sendMessage(chatId,
|
||||
`😔 *Пока нет анкет*\n\n` +
|
||||
`К сожалению, сейчас нет пользователей, которые ищут ${goalTexts[goal] || goal}.\n\n` +
|
||||
'Попробуйте позже или выберите другую цель!', {
|
||||
parse_mode: 'Markdown',
|
||||
reply_markup: keyboard
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Показываем первого кандидата
|
||||
const candidate = candidates[0];
|
||||
await this.displayCandidate(chatId, candidate);
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error searching by goal:', error);
|
||||
await this.bot.sendMessage(chatId, '❌ Произошла ошибка при поиске');
|
||||
}
|
||||
}
|
||||
|
||||
// Показ конкретного кандидата (для VIP поиска)
|
||||
async displayCandidate(chatId: number, candidate: Profile): Promise<void> {
|
||||
const candidatePhotoFileId = candidate.photos[0]; // Первое фото - главное
|
||||
|
||||
let candidateText = candidate.name + ', ' + candidate.age + '\n';
|
||||
candidateText += '📍 ' + (candidate.city || 'Не указан') + '\n';
|
||||
if (candidate.job) candidateText += '💼 ' + candidate.job + '\n';
|
||||
if (candidate.education) candidateText += '🎓 ' + candidate.education + '\n';
|
||||
if (candidate.height) candidateText += '📏 ' + candidate.height + ' см\n';
|
||||
if (candidate.religion) candidateText += '🕊️ ' + candidate.religion + '\n';
|
||||
|
||||
// Цель знакомства
|
||||
if (candidate.datingGoal) {
|
||||
const goalText = this.getDatingGoalText(candidate.datingGoal);
|
||||
candidateText += '💕 ' + goalText + '\n';
|
||||
}
|
||||
|
||||
// Образ жизни
|
||||
if (candidate.lifestyle) {
|
||||
const lifestyleText = this.getLifestyleText(candidate.lifestyle);
|
||||
if (lifestyleText) {
|
||||
candidateText += lifestyleText + '\n';
|
||||
}
|
||||
}
|
||||
|
||||
candidateText += '\n📝 ' + (candidate.bio || 'Описание отсутствует') + '\n';
|
||||
|
||||
// Хобби с хэштегами
|
||||
if (candidate.hobbies && candidate.hobbies.trim()) {
|
||||
const hobbiesArray = candidate.hobbies.split(',').map(hobby => hobby.trim()).filter(hobby => hobby);
|
||||
const formattedHobbies = hobbiesArray.map(hobby => '#' + hobby).join(' ');
|
||||
candidateText += '\n🎯 ' + formattedHobbies + '\n';
|
||||
}
|
||||
|
||||
if (candidate.interests.length > 0) {
|
||||
candidateText += '\n<> Интересы: ' + candidate.interests.join(', ');
|
||||
}
|
||||
|
||||
const keyboard: InlineKeyboardMarkup = {
|
||||
inline_keyboard: [
|
||||
[
|
||||
{ text: '👎 Не нравится', callback_data: 'dislike_' + candidate.userId },
|
||||
{ text: '💖 Супер лайк', callback_data: 'superlike_' + candidate.userId },
|
||||
{ text: '👍 Нравится', callback_data: 'like_' + candidate.userId }
|
||||
],
|
||||
[
|
||||
{ text: '👤 Профиль', callback_data: 'view_profile_' + candidate.userId },
|
||||
{ text: '📸 Еще фото', callback_data: 'more_photos_' + candidate.userId }
|
||||
],
|
||||
[
|
||||
{ text: '⏭ Следующий', callback_data: 'next_candidate' },
|
||||
{ text: '🔍 VIP поиск', callback_data: 'vip_search' }
|
||||
]
|
||||
]
|
||||
};
|
||||
|
||||
// Проверяем, есть ли валидное фото (file_id или URL)
|
||||
const hasValidPhoto = candidatePhotoFileId &&
|
||||
(candidatePhotoFileId.startsWith('http') ||
|
||||
candidatePhotoFileId.startsWith('AgAC') ||
|
||||
candidatePhotoFileId.length > 20); // file_id обычно длинные
|
||||
|
||||
if (hasValidPhoto) {
|
||||
try {
|
||||
await this.bot.sendPhoto(chatId, candidatePhotoFileId, {
|
||||
caption: candidateText,
|
||||
reply_markup: keyboard
|
||||
});
|
||||
} catch (error) {
|
||||
// Если не удалось отправить фото, отправляем текст
|
||||
await this.bot.sendMessage(chatId, '🖼 Фото недоступно\n\n' + candidateText, {
|
||||
reply_markup: keyboard
|
||||
});
|
||||
}
|
||||
} else {
|
||||
// Отправляем как текстовое сообщение
|
||||
await this.bot.sendMessage(chatId, '📝 ' + candidateText, {
|
||||
reply_markup: keyboard
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Получить текст цели знакомства
|
||||
private getDatingGoalText(goal: string): string {
|
||||
const goals: { [key: string]: string } = {
|
||||
'serious': 'Серьёзные отношения',
|
||||
'casual': 'Лёгкие отношения',
|
||||
'friends': 'Дружба',
|
||||
'unsure': 'Пока не определился',
|
||||
'one_night': 'Отношения на одну ночь',
|
||||
'fwb': 'Друзья с привилегиями',
|
||||
'marriage_abroad': 'Брак с переездом',
|
||||
'sugar': 'Спонсорство',
|
||||
'polyamory': 'Полиамория'
|
||||
};
|
||||
return goals[goal] || goal;
|
||||
}
|
||||
|
||||
// Получить текст образа жизни
|
||||
private getLifestyleText(lifestyle: any): string {
|
||||
const parts: string[] = [];
|
||||
|
||||
if (lifestyle?.smoking) {
|
||||
const smokingTexts: { [key: string]: string } = {
|
||||
'never': 'Не курю',
|
||||
'sometimes': 'Иногда курю',
|
||||
'regularly': 'Курю'
|
||||
};
|
||||
parts.push('🚬 ' + (smokingTexts[lifestyle.smoking] || lifestyle.smoking));
|
||||
}
|
||||
|
||||
if (lifestyle?.drinking) {
|
||||
const drinkingTexts: { [key: string]: string } = {
|
||||
'never': 'Не пью',
|
||||
'sometimes': 'Иногда пью',
|
||||
'regularly': 'Пью'
|
||||
};
|
||||
parts.push('🍷 ' + (drinkingTexts[lifestyle.drinking] || lifestyle.drinking));
|
||||
}
|
||||
|
||||
if (lifestyle?.kids) {
|
||||
const kidsTexts: { [key: string]: string } = {
|
||||
'have': 'Есть дети',
|
||||
'want': 'Хочу детей',
|
||||
'dont_want': 'Не хочу детей',
|
||||
'unsure': 'Пока не знаю'
|
||||
};
|
||||
parts.push('👶 ' + (kidsTexts[lifestyle.kids] || lifestyle.kids));
|
||||
}
|
||||
|
||||
return parts.join(', ');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -40,6 +40,9 @@ export class CommandHandlers {
|
||||
],
|
||||
[
|
||||
{ text: '💕 Мои матчи', callback_data: 'view_matches' },
|
||||
{ text: '⭐ VIP поиск', callback_data: 'vip_search' }
|
||||
],
|
||||
[
|
||||
{ text: '⚙️ Настройки', callback_data: 'settings' }
|
||||
]
|
||||
]
|
||||
|
||||
@@ -14,12 +14,19 @@ interface ChatState {
|
||||
matchId: string;
|
||||
}
|
||||
|
||||
// Состояния пользователей для редактирования профиля
|
||||
interface ProfileEditState {
|
||||
waitingForInput: boolean;
|
||||
field: string;
|
||||
}
|
||||
|
||||
export class MessageHandlers {
|
||||
private bot: TelegramBot;
|
||||
private profileService: ProfileService;
|
||||
private chatService: ChatService;
|
||||
private userStates: Map<string, UserState> = new Map();
|
||||
private chatStates: Map<string, ChatState> = new Map();
|
||||
private profileEditStates: Map<string, ProfileEditState> = new Map();
|
||||
|
||||
constructor(bot: TelegramBot) {
|
||||
this.bot = bot;
|
||||
@@ -42,6 +49,7 @@ export class MessageHandlers {
|
||||
|
||||
const userState = this.userStates.get(userId);
|
||||
const chatState = this.chatStates.get(userId);
|
||||
const profileEditState = this.profileEditStates.get(userId);
|
||||
|
||||
// Если пользователь в процессе отправки сообщения в чат
|
||||
if (chatState?.waitingForMessage && msg.text) {
|
||||
@@ -49,6 +57,12 @@ export class MessageHandlers {
|
||||
return;
|
||||
}
|
||||
|
||||
// Если пользователь редактирует профиль
|
||||
if (profileEditState?.waitingForInput) {
|
||||
await this.handleProfileEdit(msg, userId, profileEditState.field);
|
||||
return;
|
||||
}
|
||||
|
||||
// Если пользователь в процессе создания профиля
|
||||
if (userState) {
|
||||
await this.handleProfileCreation(msg, userId, userState);
|
||||
@@ -312,4 +326,206 @@ export class MessageHandlers {
|
||||
await this.bot.sendMessage(msg.chat.id, '❌ Не удалось отправить сообщение. Попробуйте еще раз.');
|
||||
}
|
||||
}
|
||||
|
||||
// ===== МЕТОДЫ ДЛЯ РЕДАКТИРОВАНИЯ ПРОФИЛЯ =====
|
||||
|
||||
// Установить состояние ожидания ввода для редактирования профиля
|
||||
setWaitingForInput(telegramId: number, field: string): void {
|
||||
this.profileEditStates.set(telegramId.toString(), {
|
||||
waitingForInput: true,
|
||||
field: field
|
||||
});
|
||||
}
|
||||
|
||||
// Очистить состояние редактирования профиля
|
||||
clearProfileEditState(userId: string): void {
|
||||
this.profileEditStates.delete(userId);
|
||||
}
|
||||
|
||||
// Обработка редактирования профиля
|
||||
async handleProfileEdit(msg: Message, userId: string, field: string): Promise<void> {
|
||||
const chatId = msg.chat.id;
|
||||
|
||||
try {
|
||||
let value: any = msg.text;
|
||||
let isValid = true;
|
||||
let errorMessage = '';
|
||||
|
||||
// Валидация в зависимости от поля
|
||||
switch (field) {
|
||||
case 'name':
|
||||
if (!value || value.length < 2 || value.length > 50) {
|
||||
isValid = false;
|
||||
errorMessage = 'Имя должно быть от 2 до 50 символов';
|
||||
}
|
||||
break;
|
||||
|
||||
case 'age':
|
||||
const age = parseInt(value);
|
||||
if (isNaN(age) || age < 18 || age > 100) {
|
||||
isValid = false;
|
||||
errorMessage = 'Возраст должен быть числом от 18 до 100';
|
||||
}
|
||||
value = age;
|
||||
break;
|
||||
|
||||
case 'bio':
|
||||
if (value && value.length > 500) {
|
||||
isValid = false;
|
||||
errorMessage = 'Описание не должно превышать 500 символов';
|
||||
}
|
||||
break;
|
||||
|
||||
case 'height':
|
||||
const height = parseInt(value);
|
||||
if (isNaN(height) || height < 100 || height > 250) {
|
||||
isValid = false;
|
||||
errorMessage = 'Рост должен быть числом от 100 до 250 см';
|
||||
}
|
||||
value = height;
|
||||
break;
|
||||
|
||||
case 'photo':
|
||||
if (!msg.photo || !msg.photo.length) {
|
||||
isValid = false;
|
||||
errorMessage = 'Отправьте фотографию';
|
||||
} else {
|
||||
// Берём фото наибольшего размера
|
||||
value = msg.photo[msg.photo.length - 1].file_id;
|
||||
}
|
||||
break;
|
||||
|
||||
case 'age_range':
|
||||
const ageRangeParts = value.split('-');
|
||||
if (ageRangeParts.length !== 2) {
|
||||
isValid = false;
|
||||
errorMessage = 'Неверный формат. Используйте: минимальный-максимальный возраст (например: 18-35)';
|
||||
} else {
|
||||
const minAge = parseInt(ageRangeParts[0]);
|
||||
const maxAge = parseInt(ageRangeParts[1]);
|
||||
if (isNaN(minAge) || isNaN(maxAge) || minAge < 18 || maxAge > 100 || minAge >= maxAge) {
|
||||
isValid = false;
|
||||
errorMessage = 'Возраст должен быть от 18 до 100, минимальный меньше максимального';
|
||||
}
|
||||
value = { minAge, maxAge };
|
||||
}
|
||||
break;
|
||||
|
||||
case 'distance':
|
||||
const distance = parseInt(value);
|
||||
if (isNaN(distance) || distance < 1 || distance > 1000) {
|
||||
isValid = false;
|
||||
errorMessage = 'Расстояние должно быть числом от 1 до 1000 км';
|
||||
}
|
||||
value = distance;
|
||||
break;
|
||||
}
|
||||
|
||||
if (!isValid) {
|
||||
await this.bot.sendMessage(chatId, `❌ ${errorMessage}\n\nПопробуйте еще раз:`);
|
||||
return;
|
||||
}
|
||||
|
||||
// Обновляем профиль
|
||||
await this.updateProfileField(userId, field, value);
|
||||
|
||||
// Очищаем состояние
|
||||
this.clearProfileEditState(userId);
|
||||
|
||||
// Отправляем подтверждение и возвращаем к меню редактирования
|
||||
await this.bot.sendMessage(chatId, '✅ Данные успешно обновлены!');
|
||||
|
||||
setTimeout(async () => {
|
||||
const keyboard = {
|
||||
inline_keyboard: [
|
||||
[
|
||||
{ text: '✏️ Продолжить редактирование', callback_data: 'edit_profile' },
|
||||
{ text: '👀 Предпросмотр', callback_data: 'preview_profile' }
|
||||
],
|
||||
[
|
||||
{ text: '⬅️ Главное меню', callback_data: 'main_menu' }
|
||||
]
|
||||
]
|
||||
};
|
||||
|
||||
await this.bot.sendMessage(
|
||||
chatId,
|
||||
'🛠️ Что дальше?',
|
||||
{ reply_markup: keyboard }
|
||||
);
|
||||
}, 1000);
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error handling profile edit:', error);
|
||||
await this.bot.sendMessage(chatId, '❌ Произошла ошибка. Попробуйте еще раз.');
|
||||
this.clearProfileEditState(userId);
|
||||
}
|
||||
}
|
||||
|
||||
// Обновление поля профиля
|
||||
async updateProfileField(userId: string, field: string, value: any): Promise<void> {
|
||||
const profile = await this.profileService.getProfileByTelegramId(userId);
|
||||
if (!profile) {
|
||||
throw new Error('Profile not found');
|
||||
}
|
||||
|
||||
const updates: any = {};
|
||||
|
||||
switch (field) {
|
||||
case 'name':
|
||||
updates.name = value;
|
||||
break;
|
||||
case 'age':
|
||||
updates.age = value;
|
||||
break;
|
||||
case 'bio':
|
||||
updates.bio = value;
|
||||
break;
|
||||
case 'hobbies':
|
||||
updates.hobbies = value;
|
||||
break;
|
||||
case 'city':
|
||||
// В БД поле называется 'location', но мы используем city в модели
|
||||
updates.city = value;
|
||||
break;
|
||||
case 'job':
|
||||
// В БД поле называется 'occupation', но мы используем job в модели
|
||||
updates.job = value;
|
||||
break;
|
||||
case 'education':
|
||||
updates.education = value;
|
||||
break;
|
||||
case 'height':
|
||||
updates.height = value;
|
||||
break;
|
||||
case 'religion':
|
||||
updates.religion = value === 'нет' ? null : value;
|
||||
break;
|
||||
case 'age_range':
|
||||
updates.searchPreferences = {
|
||||
minAge: value.minAge,
|
||||
maxAge: value.maxAge,
|
||||
maxDistance: profile.searchPreferences?.maxDistance || 50
|
||||
};
|
||||
break;
|
||||
case 'distance':
|
||||
updates.searchPreferences = {
|
||||
minAge: profile.searchPreferences?.minAge || 18,
|
||||
maxAge: profile.searchPreferences?.maxAge || 50,
|
||||
maxDistance: value
|
||||
};
|
||||
break;
|
||||
case 'photo':
|
||||
// Добавляем фото к существующим
|
||||
profile.addPhoto(value);
|
||||
await this.profileService.updateProfile(profile.userId, {
|
||||
photos: profile.photos
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (Object.keys(updates).length > 0) {
|
||||
await this.profileService.updateProfile(profile.userId, updates);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,10 +7,18 @@ export interface ProfileData {
|
||||
bio?: string;
|
||||
photos: string[]; // Просто массив file_id
|
||||
interests: string[];
|
||||
hobbies?: string; // Хобби через запятую
|
||||
city?: string;
|
||||
education?: string;
|
||||
job?: string;
|
||||
height?: number;
|
||||
religion?: string;
|
||||
datingGoal?: 'serious' | 'casual' | 'friends' | 'unsure' | 'one_night' | 'fwb' | 'marriage_abroad' | 'sugar' | 'polyamory';
|
||||
lifestyle?: {
|
||||
smoking?: 'never' | 'sometimes' | 'regularly';
|
||||
drinking?: 'never' | 'sometimes' | 'regularly';
|
||||
kids?: 'have' | 'want' | 'dont_want' | 'unsure';
|
||||
};
|
||||
location?: {
|
||||
latitude: number;
|
||||
longitude: number;
|
||||
@@ -35,10 +43,18 @@ export class Profile {
|
||||
bio?: string;
|
||||
photos: string[];
|
||||
interests: string[];
|
||||
hobbies?: string;
|
||||
city?: string;
|
||||
education?: string;
|
||||
job?: string;
|
||||
height?: number;
|
||||
religion?: string;
|
||||
datingGoal?: 'serious' | 'casual' | 'friends' | 'unsure' | 'one_night' | 'fwb' | 'marriage_abroad' | 'sugar' | 'polyamory';
|
||||
lifestyle?: {
|
||||
smoking?: 'never' | 'sometimes' | 'regularly';
|
||||
drinking?: 'never' | 'sometimes' | 'regularly';
|
||||
kids?: 'have' | 'want' | 'dont_want' | 'unsure';
|
||||
};
|
||||
location?: {
|
||||
latitude: number;
|
||||
longitude: number;
|
||||
@@ -62,10 +78,14 @@ export class Profile {
|
||||
this.bio = data.bio;
|
||||
this.photos = data.photos || [];
|
||||
this.interests = data.interests || [];
|
||||
this.hobbies = data.hobbies;
|
||||
this.city = data.city;
|
||||
this.education = data.education;
|
||||
this.job = data.job;
|
||||
this.height = data.height;
|
||||
this.religion = data.religion;
|
||||
this.datingGoal = data.datingGoal;
|
||||
this.lifestyle = data.lifestyle;
|
||||
this.location = data.location;
|
||||
this.searchPreferences = data.searchPreferences || {
|
||||
minAge: 18,
|
||||
@@ -109,6 +129,21 @@ export class Profile {
|
||||
return this.photos[0];
|
||||
}
|
||||
|
||||
// Получить хобби как хэштеги
|
||||
getFormattedHobbies(): string {
|
||||
if (!this.hobbies) return '';
|
||||
return this.hobbies
|
||||
.split(',')
|
||||
.map(hobby => `#${hobby.trim()}`)
|
||||
.join(' ');
|
||||
}
|
||||
|
||||
// Установить хобби из строки
|
||||
setHobbies(hobbiesString: string): void {
|
||||
this.hobbies = hobbiesString;
|
||||
this.updatedAt = new Date();
|
||||
}
|
||||
|
||||
// Получить профиль для показа
|
||||
getDisplayProfile() {
|
||||
return {
|
||||
@@ -118,10 +153,14 @@ export class Profile {
|
||||
bio: this.bio,
|
||||
photos: this.photos,
|
||||
interests: this.interests,
|
||||
hobbies: this.getFormattedHobbies(),
|
||||
city: this.city,
|
||||
education: this.education,
|
||||
job: this.job,
|
||||
height: this.height,
|
||||
religion: this.religion,
|
||||
datingGoal: this.datingGoal,
|
||||
lifestyle: this.lifestyle,
|
||||
isVerified: this.isVerified
|
||||
};
|
||||
}
|
||||
|
||||
@@ -381,4 +381,34 @@ export class MatchingService {
|
||||
// Используем ProfileService для правильного маппинга данных
|
||||
return this.profileService.mapEntityToProfile(candidateData);
|
||||
}
|
||||
|
||||
// VIP функция: поиск кандидатов по цели знакомства
|
||||
async getCandidatesWithGoal(userProfile: Profile, targetGoal: string): Promise<Profile[]> {
|
||||
const swipedUsersResult = await query(`
|
||||
SELECT swiped_id
|
||||
FROM swipes
|
||||
WHERE swiper_id = $1
|
||||
`, [userProfile.userId]);
|
||||
|
||||
const swipedUserIds = swipedUsersResult.rows.map((row: any) => row.swiped_id);
|
||||
swipedUserIds.push(userProfile.userId); // Исключаем себя
|
||||
|
||||
let candidateQuery = `
|
||||
SELECT DISTINCT p.*, u.telegram_id, u.username, u.first_name, u.last_name
|
||||
FROM profiles p
|
||||
JOIN users u ON p.user_id = u.id
|
||||
WHERE p.is_visible = true
|
||||
AND p.is_active = true
|
||||
AND p.gender = $1
|
||||
AND p.dating_goal = $2
|
||||
AND p.user_id NOT IN (${swipedUserIds.map((_: any, i: number) => `$${i + 3}`).join(', ')})
|
||||
ORDER BY p.created_at DESC
|
||||
LIMIT 50
|
||||
`;
|
||||
|
||||
const params = [userProfile.interestedIn, targetGoal, ...swipedUserIds];
|
||||
const result = await query(candidateQuery, params);
|
||||
|
||||
return result.rows.map((row: any) => this.profileService.mapEntityToProfile(row));
|
||||
}
|
||||
}
|
||||
@@ -50,13 +50,15 @@ export class ProfileService {
|
||||
await query(`
|
||||
INSERT INTO profiles (
|
||||
id, user_id, name, age, gender, looking_for, bio, photos, interests,
|
||||
location, education, occupation, height, latitude, longitude,
|
||||
verification_status, is_active, is_visible, created_at, updated_at
|
||||
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20)
|
||||
hobbies, location, education, occupation, height, religion, dating_goal,
|
||||
latitude, longitude, verification_status, is_active, is_visible,
|
||||
created_at, updated_at
|
||||
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21, $22, $23)
|
||||
`, [
|
||||
profileId, userId, profile.name, profile.age, profile.gender, profile.interestedIn,
|
||||
profile.bio, profile.photos, profile.interests,
|
||||
profile.bio, profile.photos, profile.interests, profile.hobbies,
|
||||
profile.city, profile.education, profile.job, profile.height,
|
||||
profile.religion, profile.datingGoal,
|
||||
profile.location?.latitude, profile.location?.longitude,
|
||||
'unverified', true, profile.isVisible, profile.createdAt, profile.updatedAt
|
||||
]);
|
||||
@@ -106,6 +108,15 @@ export class ProfileService {
|
||||
return result.rows[0].id;
|
||||
}
|
||||
|
||||
// Получение пользователя по Telegram ID
|
||||
async getUserByTelegramId(telegramId: string): Promise<any | null> {
|
||||
const result = await query(`
|
||||
SELECT * FROM users WHERE telegram_id = $1
|
||||
`, [parseInt(telegramId)]);
|
||||
|
||||
return result.rows.length > 0 ? result.rows[0] : null;
|
||||
}
|
||||
|
||||
// Создание пользователя если не существует
|
||||
async ensureUser(telegramId: string, userData: any): Promise<string> {
|
||||
// Используем UPSERT для избежания дублирования
|
||||
@@ -146,7 +157,7 @@ export class ProfileService {
|
||||
|
||||
// Строим динамический запрос обновления
|
||||
Object.entries(updates).forEach(([key, value]) => {
|
||||
if (value !== undefined) {
|
||||
if (value !== undefined && key !== 'updatedAt') { // Исключаем updatedAt из цикла
|
||||
switch (key) {
|
||||
case 'photos':
|
||||
case 'interests':
|
||||
@@ -175,6 +186,7 @@ export class ProfileService {
|
||||
return existingProfile;
|
||||
}
|
||||
|
||||
// Всегда добавляем updated_at в конце
|
||||
updateFields.push(`updated_at = $${paramIndex++}`);
|
||||
updateValues.push(new Date());
|
||||
updateValues.push(userId);
|
||||
@@ -409,10 +421,18 @@ export class ProfileService {
|
||||
bio: entity.bio,
|
||||
photos: parsePostgresArray(entity.photos),
|
||||
interests: parsePostgresArray(entity.interests),
|
||||
hobbies: entity.hobbies,
|
||||
city: entity.location || entity.city,
|
||||
education: entity.education,
|
||||
job: entity.occupation || entity.job,
|
||||
height: entity.height,
|
||||
religion: entity.religion,
|
||||
datingGoal: entity.dating_goal,
|
||||
lifestyle: {
|
||||
smoking: entity.smoking,
|
||||
drinking: entity.drinking,
|
||||
kids: entity.has_kids
|
||||
},
|
||||
location: entity.latitude && entity.longitude ? {
|
||||
latitude: entity.latitude,
|
||||
longitude: entity.longitude
|
||||
@@ -431,6 +451,18 @@ export class ProfileService {
|
||||
|
||||
// Преобразование camelCase в snake_case
|
||||
private camelToSnake(str: string): string {
|
||||
// Специальные случаи для некоторых полей
|
||||
const specialCases: { [key: string]: string } = {
|
||||
'interestedIn': 'looking_for',
|
||||
'job': 'occupation',
|
||||
'city': 'location',
|
||||
'datingGoal': 'dating_goal'
|
||||
};
|
||||
|
||||
if (specialCases[str]) {
|
||||
return specialCases[str];
|
||||
}
|
||||
|
||||
return str.replace(/[A-Z]/g, letter => `_${letter.toLowerCase()}`);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user