From 8893b4ad22dc85853aeeac1598e9032d274fb10d Mon Sep 17 00:00:00 2001 From: "Andrey K. Choi" Date: Fri, 12 Sep 2025 22:13:26 +0900 Subject: [PATCH] feat: add VIP search option and profile editing functionality MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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. --- src/controllers/profileEditController.ts | 294 ++++++ .../migrations/add_profile_fields.sql | 40 + .../add_scheduled_notifications.sql | 46 + src/handlers/callbackHandlers.ts | 866 +++++++++++++++++- src/handlers/commandHandlers.ts | 3 + src/handlers/messageHandlers.ts | 216 +++++ src/models/Profile.ts | 39 + src/services/matchingService.ts | 30 + src/services/profileService.ts | 44 +- 9 files changed, 1528 insertions(+), 50 deletions(-) create mode 100644 src/controllers/profileEditController.ts create mode 100644 src/database/migrations/add_profile_fields.sql create mode 100644 src/database/migrations/add_scheduled_notifications.sql diff --git a/src/controllers/profileEditController.ts b/src/controllers/profileEditController.ts new file mode 100644 index 0000000..7db6fae --- /dev/null +++ b/src/controllers/profileEditController.ts @@ -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 { + 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 { + 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 { + 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; + } +} diff --git a/src/database/migrations/add_profile_fields.sql b/src/database/migrations/add_profile_fields.sql new file mode 100644 index 0000000..1e2b93a --- /dev/null +++ b/src/database/migrations/add_profile_fields.sql @@ -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(); diff --git a/src/database/migrations/add_scheduled_notifications.sql b/src/database/migrations/add_scheduled_notifications.sql new file mode 100644 index 0000000..568d65f --- /dev/null +++ b/src/database/migrations/add_scheduled_notifications.sql @@ -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 данные для кастомизации уведомления'; diff --git a/src/handlers/callbackHandlers.ts b/src/handlers/callbackHandlers.ts index ed815c3..0189f0a 100644 --- a/src/handlers/callbackHandlers.ts +++ b/src/handlers/callbackHandlers.ts @@ -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 { - 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 { - 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� Интересы: ' + profile.interests.join(', '); } let keyboard: InlineKeyboardMarkup; @@ -752,12 +818,17 @@ 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 += '�� ' + 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 { + await this.profileEditController.showProfilePreview(this.bot, chatId, parseInt(telegramId)); + } + + // Редактирование имени + async handleEditName(chatId: number, telegramId: string): Promise { + this.messageHandlers.setWaitingForInput(parseInt(telegramId), 'name'); + await this.bot.sendMessage(chatId, '📝 *Введите ваше новое имя:*\n\nНапример: Анна', { + parse_mode: 'Markdown' + }); + } + + // Редактирование возраста + async handleEditAge(chatId: number, telegramId: string): Promise { + 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 { + 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 { + 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 { + this.messageHandlers.setWaitingForInput(parseInt(telegramId), 'city'); + await this.bot.sendMessage(chatId, '🏙️ *Введите ваш город:*\n\nНапример: Москва', { + parse_mode: 'Markdown' + }); + } + + // Редактирование работы + async handleEditJob(chatId: number, telegramId: string): Promise { + this.messageHandlers.setWaitingForInput(parseInt(telegramId), 'job'); + await this.bot.sendMessage(chatId, '💼 *Введите вашу профессию или место работы:*\n\nНапример: Дизайнер в IT-компании', { + parse_mode: 'Markdown' + }); + } + + // Редактирование образования + async handleEditEducation(chatId: number, telegramId: string): Promise { + this.messageHandlers.setWaitingForInput(parseInt(telegramId), 'education'); + await this.bot.sendMessage(chatId, '🎓 *Введите ваше образование:*\n\nНапример: МГУ, факультет журналистики', { + parse_mode: 'Markdown' + }); + } + + // Редактирование роста + async handleEditHeight(chatId: number, telegramId: string): Promise { + this.messageHandlers.setWaitingForInput(parseInt(telegramId), 'height'); + await this.bot.sendMessage(chatId, '📏 *Введите ваш рост в сантиметрах:*\n\nНапример: 175', { + parse_mode: 'Markdown' + }); + } + + // Редактирование религии + async handleEditReligion(chatId: number, telegramId: string): Promise { + this.messageHandlers.setWaitingForInput(parseInt(telegramId), 'religion'); + await this.bot.sendMessage(chatId, '🕊️ *Введите вашу религию или напишите "нет":*\n\nНапример: православие, ислам, атеизм, нет', { + parse_mode: 'Markdown' + }); + } + + // Редактирование цели знакомства + async handleEditDatingGoal(chatId: number, telegramId: string): Promise { + 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 { + 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 { + 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 { + this.messageHandlers.setWaitingForInput(parseInt(telegramId), 'photo'); + await this.bot.sendMessage(chatId, '📷 *Отправьте фотографию:*\n\nМаксимум 9 фотографий в профиле', { + parse_mode: 'Markdown' + }); + } + + // Удаление фото + async handleDeletePhoto(chatId: number, telegramId: string): Promise { + 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 { + 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 { + 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 { + 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 { + 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 { + 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 { + 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 { + 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 { + 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 { + 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 { + 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 { + 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 { + 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 { + 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(', '); + } } diff --git a/src/handlers/commandHandlers.ts b/src/handlers/commandHandlers.ts index ead6a73..f9ba01c 100644 --- a/src/handlers/commandHandlers.ts +++ b/src/handlers/commandHandlers.ts @@ -40,6 +40,9 @@ export class CommandHandlers { ], [ { text: '💕 Мои матчи', callback_data: 'view_matches' }, + { text: '⭐ VIP поиск', callback_data: 'vip_search' } + ], + [ { text: '⚙️ Настройки', callback_data: 'settings' } ] ] diff --git a/src/handlers/messageHandlers.ts b/src/handlers/messageHandlers.ts index e1c1af5..284d03f 100644 --- a/src/handlers/messageHandlers.ts +++ b/src/handlers/messageHandlers.ts @@ -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 = new Map(); private chatStates: Map = new Map(); + private profileEditStates: Map = 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 { + 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 { + 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); + } + } } diff --git a/src/models/Profile.ts b/src/models/Profile.ts index f208838..c6c1a33 100644 --- a/src/models/Profile.ts +++ b/src/models/Profile.ts @@ -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 }; } diff --git a/src/services/matchingService.ts b/src/services/matchingService.ts index befb684..4572133 100644 --- a/src/services/matchingService.ts +++ b/src/services/matchingService.ts @@ -381,4 +381,34 @@ export class MatchingService { // Используем ProfileService для правильного маппинга данных return this.profileService.mapEntityToProfile(candidateData); } + + // VIP функция: поиск кандидатов по цели знакомства + async getCandidatesWithGoal(userProfile: Profile, targetGoal: string): Promise { + 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)); + } } \ No newline at end of file diff --git a/src/services/profileService.ts b/src/services/profileService.ts index d6f9efb..d83d73e 100644 --- a/src/services/profileService.ts +++ b/src/services/profileService.ts @@ -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.city, profile.education, profile.job, profile.height, + 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 { + 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 { // Используем 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()}`); }