package telegram import ( "context" "fmt" "io" "net/http" "strings" "time" "github.com/google/uuid" "go.uber.org/zap" tele "gopkg.in/telebot.v3" "gopkg.in/telebot.v3/middleware" "rmser/config" "rmser/internal/domain/account" "rmser/internal/infrastructure/rms" "rmser/internal/services/ocr" "rmser/internal/services/sync" "rmser/pkg/crypto" "rmser/pkg/logger" ) type Bot struct { b *tele.Bot ocrService *ocr.Service syncService *sync.Service accountRepo account.Repository rmsFactory *rms.Factory cryptoManager *crypto.CryptoManager fsm *StateManager adminIDs map[int64]struct{} webAppURL string // UI Elements (Menus) // menuMain удаляем как статическое поле, так как оно теперь динамическое menuServers *tele.ReplyMarkup menuDicts *tele.ReplyMarkup menuBalance *tele.ReplyMarkup } func NewBot( cfg config.TelegramConfig, ocrService *ocr.Service, syncService *sync.Service, accountRepo account.Repository, rmsFactory *rms.Factory, cryptoManager *crypto.CryptoManager, ) (*Bot, error) { pref := tele.Settings{ Token: cfg.Token, Poller: &tele.LongPoller{Timeout: 10 * time.Second}, OnError: func(err error, c tele.Context) { logger.Log.Error("Telegram error", zap.Error(err)) }, } b, err := tele.NewBot(pref) if err != nil { return nil, err } admins := make(map[int64]struct{}) for _, id := range cfg.AdminIDs { admins[id] = struct{}{} } bot := &Bot{ b: b, ocrService: ocrService, syncService: syncService, accountRepo: accountRepo, rmsFactory: rmsFactory, cryptoManager: cryptoManager, fsm: NewStateManager(), adminIDs: admins, webAppURL: cfg.WebAppURL, } if bot.webAppURL == "" { bot.webAppURL = "http://example.com" } bot.initMenus() bot.initHandlers() return bot, nil } // initMenus инициализирует статические кнопки (кроме Главного меню) func (bot *Bot) initMenus() { // --- SERVERS MENU (Dynamic part logic is in handler) --- bot.menuServers = &tele.ReplyMarkup{} // --- DICTIONARIES MENU --- bot.menuDicts = &tele.ReplyMarkup{} btnSync := bot.menuDicts.Data("⚡️ Обновить данные", "act_sync") btnBack := bot.menuDicts.Data("🔙 Назад", "nav_main") bot.menuDicts.Inline( bot.menuDicts.Row(btnSync), bot.menuDicts.Row(btnBack), ) // --- BALANCE MENU --- bot.menuBalance = &tele.ReplyMarkup{} btnDeposit := bot.menuBalance.Data("💳 Пополнить (Demo)", "act_deposit") bot.menuBalance.Inline( bot.menuBalance.Row(btnDeposit), bot.menuBalance.Row(btnBack), ) } func (bot *Bot) initHandlers() { bot.b.Use(middleware.Logger()) bot.b.Use(bot.registrationMiddleware) // Commands bot.b.Handle("/start", bot.handleStartCommand) // Admin Commands bot.b.Handle("/admin", bot.handleAdminCommand) // Navigation Callbacks bot.b.Handle(&tele.Btn{Unique: "nav_main"}, bot.renderMainMenu) bot.b.Handle(&tele.Btn{Unique: "nav_servers"}, bot.renderServersMenu) bot.b.Handle(&tele.Btn{Unique: "nav_dicts"}, bot.renderDictsMenu) bot.b.Handle(&tele.Btn{Unique: "nav_balance"}, bot.renderBalanceMenu) // Actions Callbacks bot.b.Handle(&tele.Btn{Unique: "act_add_server"}, bot.startAddServerFlow) bot.b.Handle(&tele.Btn{Unique: "act_sync"}, bot.triggerSync) bot.b.Handle(&tele.Btn{Unique: "act_del_server_menu"}, bot.renderDeleteServerMenu) bot.b.Handle(&tele.Btn{Unique: "confirm_name_yes"}, bot.handleConfirmNameYes) bot.b.Handle(&tele.Btn{Unique: "confirm_name_no"}, bot.handleConfirmNameNo) bot.b.Handle(&tele.Btn{Unique: "act_deposit"}, func(c tele.Context) error { return c.Respond(&tele.CallbackResponse{Text: "Функция пополнения в разработке 🛠"}) }) // Dynamic Handler for server selection ("set_server_UUID") bot.b.Handle(&tele.Btn{Unique: "adm_list_servers"}, bot.adminListServers) bot.b.Handle(tele.OnCallback, bot.handleCallback) // Input Handlers bot.b.Handle(tele.OnText, bot.handleText) bot.b.Handle(tele.OnPhoto, bot.handlePhoto) } func (bot *Bot) Start() { logger.Log.Info("Запуск Telegram бота...") bot.b.Start() } func (bot *Bot) Stop() { bot.b.Stop() } func (bot *Bot) registrationMiddleware(next tele.HandlerFunc) tele.HandlerFunc { return func(c tele.Context) error { user := c.Sender() _, err := bot.accountRepo.GetOrCreateUser(user.ID, user.Username, user.FirstName, user.LastName) if err != nil { logger.Log.Error("Failed to register user", zap.Error(err)) } return next(c) } } // handleStartCommand обрабатывает /start и deep linking (приглашения) func (bot *Bot) handleStartCommand(c tele.Context) error { payload := c.Message().Payload // То, что после /start // Если есть payload, пробуем разобрать как приглашение if payload != "" && strings.HasPrefix(payload, "invite_") { return bot.handleInviteLink(c, strings.TrimPrefix(payload, "invite_")) } return bot.renderMainMenu(c) } // handleInviteLink обрабатывает приглашение пользователя на сервер func (bot *Bot) handleInviteLink(c tele.Context, serverIDStr string) error { serverID, err := uuid.Parse(serverIDStr) if err != nil { return c.Send("❌ Некорректная ссылка приглашения.") } newUser := c.Sender() // Гарантируем, что юзер есть в БД (хотя middleware это делает, тут для надежности перед логикой) userDB, _ := bot.accountRepo.GetOrCreateUser(newUser.ID, newUser.Username, newUser.FirstName, newUser.LastName) // Добавляем пользователя (RoleOperator - желаемая, но репозиторий может оставить более высокую) err = bot.accountRepo.AddUserToServer(serverID, userDB.ID, account.RoleOperator) if err != nil { return c.Send(fmt.Sprintf("❌ Не удалось подключиться к серверу: %v", err)) } // Сбрасываем кэш подключений bot.rmsFactory.ClearCacheForUser(userDB.ID) // Получаем актуальные данные о роли и сервере ПОСЛЕ добавления activeServer, err := bot.accountRepo.GetActiveServer(userDB.ID) if err != nil || activeServer == nil || activeServer.ID != serverID { // Крайний случай, если что-то пошло не так с активацией return c.Send("✅ Доступ предоставлен, но сервер не стал активным автоматически. Выберите его в меню.") } role, _ := bot.accountRepo.GetUserRole(userDB.ID, serverID) // 1. Отправляем сообщение пользователю c.Send(fmt.Sprintf("✅ Вы подключены к серверу %s.\nВаша роль: %s.\nТеперь вы можете загружать чеки.", activeServer.Name, role), tele.ModeHTML) // 2. Уведомляем Владельца (только если это реально новый человек или роль изменилась, но упростим - шлем всегда при переходе по ссылке) // Но не шлем уведомление, если Владелец перешел по своей же ссылке if role != account.RoleOwner { go func() { users, err := bot.accountRepo.GetServerUsers(serverID) if err == nil { for _, u := range users { if u.Role == account.RoleOwner { // Не уведомляем, если это тот же человек (хотя проверка выше role != Owner уже отсекла это, но на всякий случай) if u.UserID == userDB.ID { continue } name := newUser.FirstName if newUser.LastName != "" { name += " " + newUser.LastName } if newUser.Username != "" { name += fmt.Sprintf(" (@%s)", newUser.Username) } msg := fmt.Sprintf("🔔 Обновление команды\n\nПользователь %s активировал приглашение на сервер «%s» (Роль: %s).", name, activeServer.Name, role) bot.b.Send(&tele.User{ID: u.User.TelegramID}, msg, tele.ModeHTML) break } } } }() } return bot.renderMainMenu(c) } // Реализация интерфейса handlers.Notifier func (bot *Bot) SendRoleChangeNotification(telegramID int64, serverName string, newRole string) { msg := fmt.Sprintf("ℹ️ Изменение прав доступа\n\nСервер: %s\nВаша новая роль: %s", serverName, newRole) bot.b.Send(&tele.User{ID: telegramID}, msg, tele.ModeHTML) } func (bot *Bot) SendRemovalNotification(telegramID int64, serverName string) { msg := fmt.Sprintf("⛔ Доступ закрыт\n\nВы были отключены от сервера %s.", serverName) bot.b.Send(&tele.User{ID: telegramID}, msg, tele.ModeHTML) } // handleAdminCommand - точка входа в админку func (bot *Bot) handleAdminCommand(c tele.Context) error { userID := c.Sender().ID if _, isAdmin := bot.adminIDs[userID]; !isAdmin { return nil // Игнорируем не админов } menu := &tele.ReplyMarkup{} btnServers := menu.Data("🏢 Список серверов", "adm_list_servers") menu.Inline(menu.Row(btnServers)) return c.Send("🕵️‍♂️ Super Admin Panel\n\nВыберите действие:", menu, tele.ModeHTML) } func (bot *Bot) adminListServers(c tele.Context) error { servers, err := bot.accountRepo.GetAllServersSystemWide() if err != nil { return c.Send("Error: " + err.Error()) } menu := &tele.ReplyMarkup{} var rows []tele.Row for _, s := range servers { // adm_srv_ btn := menu.Data(fmt.Sprintf("🖥 %s", s.Name), "adm_srv_"+s.ID.String()) rows = append(rows, menu.Row(btn)) } menu.Inline(rows...) return c.EditOrSend("Все серверы системы:", menu, tele.ModeHTML) } // --- RENDERERS (View Layer) --- // renderMainMenu строит меню динамически в зависимости от роли func (bot *Bot) renderMainMenu(c tele.Context) error { bot.fsm.Reset(c.Sender().ID) userDB, _ := bot.accountRepo.GetUserByTelegramID(c.Sender().ID) activeServer, _ := bot.accountRepo.GetActiveServer(userDB.ID) menu := &tele.ReplyMarkup{} btnServers := menu.Data("🖥 Серверы", "nav_servers") btnDicts := menu.Data("🔄 Справочники", "nav_dicts") btnBalance := menu.Data("💰 Баланс", "nav_balance") var rows []tele.Row rows = append(rows, menu.Row(btnServers, btnDicts)) rows = append(rows, menu.Row(btnBalance)) // Проверяем роль для отображения кнопки App showApp := false if activeServer != nil { role, _ := bot.accountRepo.GetUserRole(userDB.ID, activeServer.ID) if role == account.RoleOwner || role == account.RoleAdmin { showApp = true } } if showApp { btnApp := menu.WebApp("📱 Открыть приложение", &tele.WebApp{URL: bot.webAppURL}) rows = append(rows, menu.Row(btnApp)) } else { // Если оператор или нет сервера, можно добавить подсказку или просто ничего // Для оператора это нормально. Для нового юзера - он пойдет в "Серверы" } menu.Inline(rows...) txt := "👋 Панель управления RMSER\n\n" + "Здесь вы можете управлять подключенными серверами iiko и следить за актуальностью справочников." if activeServer != nil { role, _ := bot.accountRepo.GetUserRole(userDB.ID, activeServer.ID) txt += fmt.Sprintf("\n\nАктивный сервер: %s (%s)", activeServer.Name, role) } return c.EditOrSend(txt, menu, tele.ModeHTML) } func (bot *Bot) renderServersMenu(c tele.Context) error { userDB, _ := bot.accountRepo.GetUserByTelegramID(c.Sender().ID) servers, err := bot.accountRepo.GetAllAvailableServers(userDB.ID) if err != nil { return c.Send("Ошибка БД: " + err.Error()) } activeServer, _ := bot.accountRepo.GetActiveServer(userDB.ID) menu := &tele.ReplyMarkup{} var rows []tele.Row for _, s := range servers { icon := "🔴" if activeServer != nil && activeServer.ID == s.ID { icon = "🟢" } // Определяем роль для отображения role, _ := bot.accountRepo.GetUserRole(userDB.ID, s.ID) label := fmt.Sprintf("%s %s (%s)", icon, s.Name, role) btn := menu.Data(label, "set_server_"+s.ID.String()) rows = append(rows, menu.Row(btn)) } btnAdd := menu.Data("➕ Добавить сервер", "act_add_server") btnDel := menu.Data("⚙️ Управление / Удаление", "act_del_server_menu") btnBack := menu.Data("🔙 Назад", "nav_main") rows = append(rows, menu.Row(btnAdd, btnDel)) rows = append(rows, menu.Row(btnBack)) menu.Inline(rows...) txt := fmt.Sprintf("🖥 Ваши серверы (%d):\n\nНажмите на сервер, чтобы сделать его активным.", len(servers)) return c.EditOrSend(txt, menu, tele.ModeHTML) } func (bot *Bot) renderDictsMenu(c tele.Context) error { userDB, _ := bot.accountRepo.GetUserByTelegramID(c.Sender().ID) stats, err := bot.syncService.GetSyncStats(userDB.ID) var txt string if err != nil { txt = fmt.Sprintf("⚠️ Статус: Ошибка или нет активного сервера (%v)", err) } else { lastUpdate := "—" if stats.LastInvoice != nil { lastUpdate = stats.LastInvoice.Format("02.01.2006") } txt = fmt.Sprintf("🔄 Состояние справочников\n\n"+ "🏢 Сервер: %s\n"+ "📦 Товары: %d\n"+ "🚚 Поставщики: %d\n"+ "🏭 Склады: %d\n\n"+ "📄 Накладные (30дн): %d\n"+ "📅 Посл. документ: %s\n\n"+ "Нажмите «Обновить», чтобы синхронизировать данные.", stats.ServerName, stats.ProductsCount, stats.SuppliersCount, stats.StoresCount, stats.InvoicesLast30, lastUpdate) } return c.EditOrSend(txt, bot.menuDicts, tele.ModeHTML) } func (bot *Bot) renderBalanceMenu(c tele.Context) error { txt := "💰 Ваш баланс\n\n" + "💵 Текущий счет: 0.00 ₽\n" + "💎 Тариф: Free\n\n" + "Пока сервис работает в бета-режиме, использование бесплатно." return c.EditOrSend(txt, bot.menuBalance, tele.ModeHTML) } // --- LOGIC HANDLERS --- func (bot *Bot) handleCallback(c tele.Context) error { data := c.Callback().Data if len(data) > 0 && data[0] == '\f' { data = data[1:] } userDB, _ := bot.accountRepo.GetUserByTelegramID(c.Sender().ID) // --- SELECT SERVER --- if strings.HasPrefix(data, "set_server_") { serverIDStr := strings.TrimPrefix(data, "set_server_") serverIDStr = strings.TrimSpace(serverIDStr) if idx := strings.Index(serverIDStr, "|"); idx != -1 { serverIDStr = serverIDStr[:idx] } targetID := parseUUID(serverIDStr) if err := bot.accountRepo.SetActiveServer(userDB.ID, targetID); err != nil { logger.Log.Error("Failed to set active server", zap.Error(err)) return c.Respond(&tele.CallbackResponse{Text: "Ошибка: доступ запрещен"}) } bot.rmsFactory.ClearCacheForUser(userDB.ID) // Сброс кэша c.Respond(&tele.CallbackResponse{Text: "✅ Сервер выбран"}) // Важно: перерисовываем главное меню, чтобы обновилась кнопка App (появилась/пропала) // Но мы находимся в подменю. Логичнее остаться в ServersMenu, но кнопка App в MainMenu. // Пользователь нажмет "Назад" и попадет в MainMenu, где сработает renderMainMenu с новой логикой. return bot.renderServersMenu(c) } // --- DELETE / LEAVE SERVER --- if strings.HasPrefix(data, "do_del_server_") { serverIDStr := strings.TrimPrefix(data, "do_del_server_") serverIDStr = strings.TrimSpace(serverIDStr) if idx := strings.Index(serverIDStr, "|"); idx != -1 { serverIDStr = serverIDStr[:idx] } targetID := parseUUID(serverIDStr) role, err := bot.accountRepo.GetUserRole(userDB.ID, targetID) if err != nil { return c.Respond(&tele.CallbackResponse{Text: "Ошибка прав доступа"}) } if role == account.RoleOwner { if err := bot.accountRepo.DeleteServer(targetID); err != nil { return c.Respond(&tele.CallbackResponse{Text: "Ошибка удаления"}) } bot.rmsFactory.ClearCacheForUser(userDB.ID) c.Respond(&tele.CallbackResponse{Text: "Сервер полностью удален"}) } else { if err := bot.accountRepo.RemoveUserFromServer(targetID, userDB.ID); err != nil { return c.Respond(&tele.CallbackResponse{Text: "Ошибка выхода"}) } bot.rmsFactory.ClearCacheForUser(userDB.ID) c.Respond(&tele.CallbackResponse{Text: "Вы покинули сервер"}) } active, _ := bot.accountRepo.GetActiveServer(userDB.ID) if active == nil { all, _ := bot.accountRepo.GetAllAvailableServers(userDB.ID) if len(all) > 0 { _ = bot.accountRepo.SetActiveServer(userDB.ID, all[0].ID) } } return bot.renderDeleteServerMenu(c) } // --- INVITE LINK GENERATION --- if strings.HasPrefix(data, "gen_invite_") { serverIDStr := strings.TrimPrefix(data, "gen_invite_") link := fmt.Sprintf("https://t.me/%s?start=invite_%s", bot.b.Me.Username, serverIDStr) c.Respond() return c.Send(fmt.Sprintf("🔗 Ссылка для приглашения:\n\n%s\n\nОтправьте её сотруднику.", link), tele.ModeHTML) } // --- ADMIN: SELECT SERVER -> SHOW USERS --- if strings.HasPrefix(data, "adm_srv_") { serverIDStr := strings.TrimPrefix(data, "adm_srv_") serverID := parseUUID(serverIDStr) return bot.renderServerUsers(c, serverID) // <--- ВЫЗОВ НОВОГО МЕТОДА } // --- ADMIN: SELECT USER -> CONFIRM OWNERSHIP --- if strings.HasPrefix(data, "adm_usr_") { // Получаем ID связи connIDStr := strings.TrimPrefix(data, "adm_usr_") connID := parseUUID(connIDStr) // Загружаем детали связи через новый метод link, err := bot.accountRepo.GetConnectionByID(connID) if err != nil { return c.Respond(&tele.CallbackResponse{Text: "Ошибка: связь не найдена"}) } // Проверяем роль if link.Role == account.RoleOwner { return c.Respond(&tele.CallbackResponse{Text: "Этот пользователь уже Владелец"}) } menu := &tele.ReplyMarkup{} // ИСПРАВЛЕНИЕ: Для подтверждения тоже передаем ID связи // adm_own_yes_ + UUID = 12 + 36 = 48 байт (OK) btnYes := menu.Data("✅ Сделать Владельцем", fmt.Sprintf("adm_own_yes_%s", link.ID.String())) btnNo := menu.Data("Отмена", "adm_srv_"+link.ServerID.String()) menu.Inline(menu.Row(btnYes), menu.Row(btnNo)) txt := fmt.Sprintf("⚠️ Внимание!\n\nВы собираетесь передать права Владельца сервера %s пользователю %s.\n\nТекущий владелец станет Администратором.", link.Server.Name, link.User.FirstName) return c.EditOrSend(txt, menu, tele.ModeHTML) } // --- ADMIN: EXECUTE TRANSFER --- if strings.HasPrefix(data, "adm_own_yes_") { connIDStr := strings.TrimPrefix(data, "adm_own_yes_") connID := parseUUID(connIDStr) link, err := bot.accountRepo.GetConnectionByID(connID) if err != nil { return c.Respond(&tele.CallbackResponse{Text: "Ошибка: связь не найдена"}) } if err := bot.accountRepo.TransferOwnership(link.ServerID, link.UserID); err != nil { logger.Log.Error("Ownership transfer failed", zap.Error(err)) return c.Respond(&tele.CallbackResponse{Text: "Ошибка: " + err.Error()}) } // Уведомляем нового владельца go func() { msg := fmt.Sprintf("👑 Поздравляем!\n\nВам переданы права Владельца (OWNER) сервера %s.", link.Server.Name) bot.b.Send(&tele.User{ID: link.User.TelegramID}, msg, tele.ModeHTML) }() c.Respond(&tele.CallbackResponse{Text: "Успешно!"}) // Возвращаемся к списку return bot.renderServerUsers(c, link.ServerID) } return nil } // --- Вспомогательный метод рендера списка пользователей --- func (bot *Bot) renderServerUsers(c tele.Context, serverID uuid.UUID) error { users, err := bot.accountRepo.GetServerUsers(serverID) if err != nil { return c.Respond(&tele.CallbackResponse{Text: "Ошибка загрузки юзеров"}) } menu := &tele.ReplyMarkup{} var rows []tele.Row for _, u := range users { roleIcon := "👤" if u.Role == account.RoleOwner { roleIcon = "👑" } if u.Role == account.RoleAdmin { roleIcon = "⭐️" } label := fmt.Sprintf("%s %s %s", roleIcon, u.User.FirstName, u.User.LastName) // Используем ID связи payload := fmt.Sprintf("adm_usr_%s", u.ID.String()) btn := menu.Data(label, payload) rows = append(rows, menu.Row(btn)) } btnBack := menu.Data("🔙 К серверам", "adm_list_servers") rows = append(rows, menu.Row(btnBack)) menu.Inline(rows...) // Для заголовка нам нужно имя сервера, но в users[0].Server оно есть (Preload), // либо если юзеров нет (пустой сервер?), то имя не узнаем без доп запроса. // Но пустой сервер вряд ли будет, там как минимум Owner. serverName := "Unknown" if len(users) > 0 { serverName = users[0].Server.Name } return c.EditOrSend(fmt.Sprintf("👥 Пользователи сервера %s:", serverName), menu, tele.ModeHTML) } func (bot *Bot) triggerSync(c tele.Context) error { userDB, _ := bot.accountRepo.GetUserByTelegramID(c.Sender().ID) server, err := bot.accountRepo.GetActiveServer(userDB.ID) if err != nil || server == nil { return c.Respond(&tele.CallbackResponse{Text: "Нет активного сервера"}) } role, _ := bot.accountRepo.GetUserRole(userDB.ID, server.ID) if role == account.RoleOperator { return c.Respond(&tele.CallbackResponse{Text: "⚠️ Синхронизация доступна только Админам", ShowAlert: true}) } c.Respond(&tele.CallbackResponse{Text: "Запускаю синхронизацию..."}) go func() { if err := bot.syncService.SyncAllData(userDB.ID); err != nil { logger.Log.Error("Manual sync failed", zap.Error(err)) bot.b.Send(c.Sender(), "❌ Ошибка синхронизации. Проверьте настройки сервера.") } else { bot.b.Send(c.Sender(), "✅ Синхронизация успешно завершена!") } }() return nil } // --- FSM: ADD SERVER FLOW --- func (bot *Bot) startAddServerFlow(c tele.Context) error { bot.fsm.SetState(c.Sender().ID, StateAddServerURL) return c.EditOrSend("🔗 Введите URL вашего сервера iikoRMS.\nПример: https://resto.iiko.it\n\n(Напишите 'отмена' для выхода)", tele.ModeHTML) } func (bot *Bot) handleText(c tele.Context) error { userID := c.Sender().ID state := bot.fsm.GetState(userID) text := strings.TrimSpace(c.Text()) if strings.ToLower(text) == "отмена" || strings.ToLower(text) == "/cancel" { bot.fsm.Reset(userID) return bot.renderMainMenu(c) } if state == StateNone { return c.Send("Используйте меню для навигации 👇") } switch state { case StateAddServerURL: if !strings.HasPrefix(text, "http") { return c.Send("❌ URL должен начинаться с http:// или https://\nПопробуйте снова.") } bot.fsm.UpdateContext(userID, func(ctx *UserContext) { ctx.TempURL = strings.TrimRight(text, "/") ctx.State = StateAddServerLogin }) return c.Send("👤 Введите логин пользователя iiko:") case StateAddServerLogin: bot.fsm.UpdateContext(userID, func(ctx *UserContext) { ctx.TempLogin = text ctx.State = StateAddServerPassword }) return c.Send("🔑 Введите пароль:") case StateAddServerPassword: password := text ctx := bot.fsm.GetContext(userID) msg, _ := bot.b.Send(c.Sender(), "⏳ Проверяю подключение...") tempClient := bot.rmsFactory.CreateClientFromRawCredentials(ctx.TempURL, ctx.TempLogin, password) if err := tempClient.Auth(); err != nil { bot.b.Delete(msg) return c.Send(fmt.Sprintf("❌ Ошибка авторизации: %v\nПроверьте логин/пароль.", err)) } var detectedName string info, err := rms.GetServerInfo(ctx.TempURL) if err == nil && info.ServerName != "" { detectedName = info.ServerName } bot.b.Delete(msg) bot.fsm.UpdateContext(userID, func(uCtx *UserContext) { uCtx.TempPassword = password uCtx.TempServerName = detectedName }) if detectedName != "" { bot.fsm.SetState(userID, StateAddServerConfirmName) menu := &tele.ReplyMarkup{} btnYes := menu.Data("✅ Да, использовать это имя", "confirm_name_yes") btnNo := menu.Data("✏️ Ввести другое", "confirm_name_no") menu.Inline(menu.Row(btnYes), menu.Row(btnNo)) return c.Send(fmt.Sprintf("🔎 Обнаружено имя сервера: %s.\nИспользовать его?", detectedName), menu, tele.ModeHTML) } bot.fsm.SetState(userID, StateAddServerInputName) return c.Send("🏷 Введите название для этого сервера:") case StateAddServerInputName: name := text if len(name) < 3 { return c.Send("⚠️ Название слишком короткое.") } return bot.saveServerFinal(c, userID, name) } return nil } func (bot *Bot) handlePhoto(c tele.Context) error { userDB, err := bot.accountRepo.GetOrCreateUser(c.Sender().ID, c.Sender().Username, "", "") if err != nil { return c.Send("Ошибка базы данных пользователей") } _, err = bot.rmsFactory.GetClientForUser(userDB.ID) if err != nil { return c.Send("⛔ Ошибка доступа к iiko или сервер не выбран.\nПроверьте статус сервера в меню.") } photo := c.Message().Photo file, err := bot.b.FileByID(photo.FileID) if err != nil { return c.Send("Ошибка доступа к файлу.") } fileURL := fmt.Sprintf("https://api.telegram.org/file/bot%s/%s", bot.b.Token, file.FilePath) resp, err := http.Get(fileURL) if err != nil { return c.Send("Ошибка скачивания файла.") } defer resp.Body.Close() imgData, err := io.ReadAll(resp.Body) if err != nil { return c.Send("Ошибка чтения файла.") } c.Send("⏳ Обрабатываю чек: создаю черновик и распознаю...") ctx, cancel := context.WithTimeout(context.Background(), 45*time.Second) defer cancel() draft, err := bot.ocrService.ProcessReceiptImage(ctx, userDB.ID, imgData) if err != nil { logger.Log.Error("OCR processing failed", zap.Error(err)) return c.Send("❌ Ошибка обработки: " + err.Error()) } matchedCount := 0 for _, item := range draft.Items { if item.IsMatched { matchedCount++ } } baseURL := strings.TrimRight(bot.webAppURL, "/") fullURL := fmt.Sprintf("%s/invoice/%s", baseURL, draft.ID.String()) var msgText string if matchedCount == len(draft.Items) { msgText = fmt.Sprintf("✅ Успех! Все позиции (%d) распознаны.\n\nПереходите к созданию накладной.", len(draft.Items)) } else { msgText = fmt.Sprintf("⚠️ Внимание! Распознано %d из %d позиций.\n\nНекоторые товары требуют ручного сопоставления.", matchedCount, len(draft.Items)) } menu := &tele.ReplyMarkup{} role, _ := bot.accountRepo.GetUserRole(userDB.ID, draft.RMSServerID) if role != account.RoleOperator { btnOpen := menu.WebApp("📝 Открыть накладную", &tele.WebApp{URL: fullURL}) menu.Inline(menu.Row(btnOpen)) } else { msgText += "\n\n(Редактирование доступно Администратору)" } return c.Send(msgText, menu, tele.ModeHTML) } func (bot *Bot) handleConfirmNameYes(c tele.Context) error { userID := c.Sender().ID ctx := bot.fsm.GetContext(userID) if ctx.State != StateAddServerConfirmName { return c.Respond() } return bot.saveServerFinal(c, userID, ctx.TempServerName) } func (bot *Bot) handleConfirmNameNo(c tele.Context) error { userID := c.Sender().ID bot.fsm.SetState(userID, StateAddServerInputName) return c.EditOrSend("🏷 Хорошо, введите желаемое название:") } func (bot *Bot) saveServerFinal(c tele.Context, userID int64, serverName string) error { ctx := bot.fsm.GetContext(userID) userDB, _ := bot.accountRepo.GetOrCreateUser(userID, c.Sender().Username, "", "") encPass, _ := bot.cryptoManager.Encrypt(ctx.TempPassword) server, err := bot.accountRepo.ConnectServer(userDB.ID, ctx.TempURL, ctx.TempLogin, encPass, serverName) if err != nil { return c.Send("Ошибка подключения сервера: " + err.Error()) } bot.fsm.Reset(userID) role, _ := bot.accountRepo.GetUserRole(userDB.ID, server.ID) c.Send(fmt.Sprintf("✅ Сервер %s подключен!\nВаша роль: %s", server.Name, role), tele.ModeHTML) if role == account.RoleOwner { go bot.syncService.SyncAllData(userDB.ID) } else { go bot.syncService.SyncAllData(userDB.ID) } return bot.renderMainMenu(c) } func (bot *Bot) renderDeleteServerMenu(c tele.Context) error { userDB, _ := bot.accountRepo.GetUserByTelegramID(c.Sender().ID) servers, err := bot.accountRepo.GetAllAvailableServers(userDB.ID) if err != nil { return c.Send("Ошибка БД: " + err.Error()) } if len(servers) == 0 { return c.Respond(&tele.CallbackResponse{Text: "Список серверов пуст"}) } menu := &tele.ReplyMarkup{} var rows []tele.Row for _, s := range servers { role, _ := bot.accountRepo.GetUserRole(userDB.ID, s.ID) var label string if role == account.RoleOwner { label = fmt.Sprintf("❌ Удалить %s (Owner)", s.Name) } else { label = fmt.Sprintf("🚪 Покинуть %s", s.Name) } btnAction := menu.Data(label, "do_del_server_"+s.ID.String()) if role == account.RoleOwner || role == account.RoleAdmin { btnInvite := menu.Data(fmt.Sprintf("📩 Invite %s", s.Name), "gen_invite_"+s.ID.String()) rows = append(rows, menu.Row(btnAction, btnInvite)) } else { rows = append(rows, menu.Row(btnAction)) } } btnBack := menu.Data("🔙 Назад к списку", "nav_servers") rows = append(rows, menu.Row(btnBack)) menu.Inline(rows...) return c.EditOrSend("⚙️ Управление серверами\n\nЗдесь вы можете удалить сервер или пригласить сотрудников.", menu, tele.ModeHTML) } func parseUUID(s string) uuid.UUID { id, _ := uuid.Parse(s) return id }