// internal/transport/telegram/draft_editor.go package telegram import ( "errors" "fmt" "html" "strconv" "strings" "github.com/google/uuid" "github.com/shopspring/decimal" "go.uber.org/zap" tele "gopkg.in/telebot.v3" "rmser/internal/domain/account" "rmser/internal/domain/drafts" draftsService "rmser/internal/services/drafts" "rmser/pkg/logger" ) // DraftEditor управляет редактированием черновиков накладных type DraftEditor struct { bot *tele.Bot draftsService *draftsService.Service accountRepo account.Repository fsm *StateManager } // NewDraftEditor создаёт новый экземпляр редактора черновиков func NewDraftEditor(bot *tele.Bot, draftsService *draftsService.Service, accountRepo account.Repository, fsm *StateManager) *DraftEditor { return &DraftEditor{ bot: bot, draftsService: draftsService, accountRepo: accountRepo, fsm: fsm, } } // ============================================ // === КОМАНДЫ ЗАПУСКА РЕДАКТОРА ЧЕРНОВИКОВ === // ============================================ // handleDraftCommand обрабатывает команду /draft // Запускает редактор для указанного черновика func (de *DraftEditor) handleDraftCommand(c tele.Context) error { userID := c.Sender().ID // Получаем аргументы команды args := c.Args() if len(args) == 0 { return c.Send("❌ Укажите ID черновика.\n\nПример: /draft 123e4567-e89b-12d3-a456-426614174000") } draftIDStr := args[0] // Парсим UUID draftID, err := uuid.Parse(draftIDStr) if err != nil { return c.Send("❌ Некорректный формат ID черновика.\n\nПример: /draft 123e4567-e89b-12d3-a456-426614174000") } // Получаем пользователя userDB, err := de.accountRepo.GetUserByTelegramID(userID) if err != nil { return c.Send("❌ Ошибка получения данных пользователя") } // Получаем черновик draft, err := de.draftsService.GetDraftForEditor(draftID, userDB.ID) if err != nil { return c.Send("❌ Черновик не найден или у вас нет прав на его редактирование") } // Проверяем статус черновика if draft.Status == drafts.StatusCanceled { return c.Send("❌ Этот черновик нельзя редактировать. Текущий статус: " + string(draft.Status)) } // Инициализируем FSM для редактора de.fsm.InitDraftEditor(userID, draft.ID) // Показываем редактор return de.renderDraftEditorMenu(c, draft, 0) } // handleNewDraftCommand обрабатывает команду /newdraft // Создаёт новый черновик и запускает редактор func (de *DraftEditor) handleNewDraftCommand(c tele.Context) error { userID := c.Sender().ID // Получаем пользователя userDB, err := de.accountRepo.GetUserByTelegramID(userID) if err != nil { return c.Send("❌ Ошибка получения данных пользователя") } // Создаём новый черновик draft, err := de.draftsService.CreateDraft(userDB.ID) if err != nil { return c.Send("❌ Ошибка создания черновика: " + err.Error()) } // Инициализируем FSM для редактора de.fsm.InitDraftEditor(userID, draft.ID) // Показываем редактор return de.renderDraftEditorMenu(c, draft, 0) } // ============================================ // === РЕНДЕРИНГ МЕНЮ РЕДАКТОРА ЧЕРНОВИКОВ === // ============================================ // renderDraftEditorMenu отображает интерактивный список позиций черновика // с пагинацией и кнопками управления. // Параметры: // - c: контекст Telegram // - draft: черновик для отображения // - page: номер страницы (0-based) // // Сохраняет ID сообщения в FSM для последующего редактирования. func (de *DraftEditor) renderDraftEditorMenu(c tele.Context, draft *drafts.DraftInvoice, page int) error { // Расчёт пагинации totalItems := len(draft.Items) totalPages := (totalItems + DraftEditorPageSize - 1) / DraftEditorPageSize if totalPages == 0 { totalPages = 1 } if page < 0 { page = 0 } if page >= totalPages { page = totalPages - 1 } startIdx := page * DraftEditorPageSize endIdx := startIdx + DraftEditorPageSize if endIdx > totalItems { endIdx = totalItems } // Расчёт общей суммы var totalSum decimal.Decimal for _, item := range draft.Items { if !item.Sum.IsZero() { totalSum = totalSum.Add(item.Sum) } else { totalSum = totalSum.Add(item.Quantity.Mul(item.Price)) } } // Формирование текста заголовка dateStr := "—" if draft.DateIncoming != nil { dateStr = draft.DateIncoming.Format("02.01.2006") } txt := fmt.Sprintf( "📄 Черновик от %s\n"+ "💰 Сумма: %.2f ₽ | 📦 Позиций: %d\n\n"+ "👇 Нажмите на позицию для редактирования:\n", dateStr, totalSum.InexactFloat64(), totalItems, ) // Формирование кнопок позиций menu := &tele.ReplyMarkup{} var rows []tele.Row draftShort := shortUUID(draft.ID) pageItems := draft.Items[startIdx:endIdx] for i, item := range pageItems { globalIdx := startIdx + i + 1 // 1-based для отображения // Формируем label: "1. Название — 2 шт × 80.00 ₽" qtyStr := item.Quantity.StringFixed(2) priceStr := item.Price.StringFixed(2) // Обрезаем название если слишком длинное name := item.RawName if len(name) > 25 { name = name[:22] + "..." } label := fmt.Sprintf("%d. %s — %s × %s", globalIdx, name, qtyStr, priceStr) callbackData := fmt.Sprintf("di:%s:%d", draftShort, globalIdx) btn := menu.Data(label, callbackData) rows = append(rows, menu.Row(btn)) } // Кнопки пагинации (если нужно) if totalPages > 1 { var navRow []tele.Btn if page > 0 { navRow = append(navRow, menu.Data("◀️ Назад", fmt.Sprintf("dp:%s:%d", draftShort, page-1))) } // Индикатор страницы (не кнопка, а часть текста - но в Telegram нельзя смешивать) // Поэтому добавляем как неактивную кнопку или в текст сообщения pageIndicator := menu.Data(fmt.Sprintf("📄 %d/%d", page+1, totalPages), "noop") navRow = append(navRow, pageIndicator) if page < totalPages-1 { navRow = append(navRow, menu.Data("Вперёд ▶️", fmt.Sprintf("dp:%s:%d", draftShort, page+1))) } rows = append(rows, navRow) } // Кнопки управления // Кнопка "Добавить позицию" btnAdd := menu.Data("➕ Добавить позицию", fmt.Sprintf("da:%s", draftShort)) rows = append(rows, menu.Row(btnAdd)) // Кнопки "Подтвердить" и "Отменить" btnConfirm := menu.Data("✅ Подтвердить", fmt.Sprintf("dc:%s", draftShort)) btnCancel := menu.Data("❌ Отменить", fmt.Sprintf("dx:%s", draftShort)) rows = append(rows, menu.Row(btnConfirm, btnCancel)) menu.Inline(rows...) // Обновляем контекст FSM de.fsm.UpdateContext(c.Sender().ID, func(ctx *UserContext) { ctx.EditingDraftID = draft.ID ctx.DraftCurrentPage = page }) // Отправляем или редактируем сообщение return c.EditOrSend(txt, menu, tele.ModeHTML) } // renderItemEditMenu отображает меню редактирования конкретной позиции черновика. // Параметры: // - c: контекст Telegram // - draft: черновик (для получения draftShort) // - item: редактируемая позиция // - itemIndex: порядковый номер позиции (1-based) func (de *DraftEditor) renderItemEditMenu(c tele.Context, draft *drafts.DraftInvoice, item *drafts.DraftInvoiceItem, itemIndex int) error { // Вычисление суммы sum := item.Sum if sum.IsZero() { sum = item.Quantity.Mul(item.Price) } // Формирование текста txt := fmt.Sprintf( "✏️ Редактирование позиции №%d\n\n"+ "📝 Название: %s\n"+ "📦 Количество: %s\n"+ "💰 Цена: %s ₽\n"+ "💵 Сумма: %s ₽\n\n"+ "Выберите, что изменить:", itemIndex, html.EscapeString(item.RawName), item.Quantity.StringFixed(2), item.Price.StringFixed(2), sum.StringFixed(2), ) // Формирование кнопок menu := &tele.ReplyMarkup{} draftShort := shortUUID(draft.ID) btnName := menu.Data("📝 Название", fmt.Sprintf("den:%s:%d", draftShort, itemIndex)) btnQty := menu.Data("📦 Количество", fmt.Sprintf("deq:%s:%d", draftShort, itemIndex)) btnPrice := menu.Data("💰 Цена", fmt.Sprintf("dep:%s:%d", draftShort, itemIndex)) btnDelete := menu.Data("🗑 Удалить", fmt.Sprintf("did:%s:%d", draftShort, itemIndex)) btnBack := menu.Data("🔙 Назад", fmt.Sprintf("dib:%s", draftShort)) menu.Inline( menu.Row(btnName, btnQty, btnPrice), menu.Row(btnDelete), menu.Row(btnBack), ) // Обновление FSM de.fsm.UpdateContext(c.Sender().ID, func(ctx *UserContext) { ctx.EditingItemID = item.ID ctx.EditingItemIndex = itemIndex }) return c.Edit(txt, menu, tele.ModeHTML) } // ============================================ // === ВСПОМОГАТЕЛЬНЫЕ МЕТОДЫ РЕДАКТОРА === // ============================================ // getItemByIndex возвращает позицию черновика по её порядковому номеру (1-based) func (de *DraftEditor) getItemByIndex(draft *drafts.DraftInvoice, index int) *drafts.DraftInvoiceItem { if index < 1 || index > len(draft.Items) { return nil } return &draft.Items[index-1] } // promptForItemName запрашивает ввод нового названия позиции func (de *DraftEditor) promptForItemName(c tele.Context, item *drafts.DraftInvoiceItem, itemIndex int) error { de.fsm.SetState(c.Sender().ID, StateDraftEditItemName) txt := fmt.Sprintf( "📝 Изменение названия позиции №%d\n\n"+ "Текущее название: %s\n\n"+ "Введите новое название:", itemIndex, html.EscapeString(item.RawName), ) // Кнопка отмены menu := &tele.ReplyMarkup{} btnCancel := menu.Data("❌ Отмена", fmt.Sprintf("dib:%s", shortUUID(de.fsm.GetContext(c.Sender().ID).EditingDraftID))) menu.Inline(menu.Row(btnCancel)) return c.Edit(txt, menu, tele.ModeHTML) } // promptForItemQuantity запрашивает ввод нового количества func (de *DraftEditor) promptForItemQuantity(c tele.Context, item *drafts.DraftInvoiceItem, itemIndex int) error { de.fsm.SetState(c.Sender().ID, StateDraftEditItemQty) txt := fmt.Sprintf( "📦 Изменение количества позиции №%d\n\n"+ "Текущее количество: %s\n\n"+ "Введите новое количество (число):", itemIndex, item.Quantity.StringFixed(2), ) menu := &tele.ReplyMarkup{} btnCancel := menu.Data("❌ Отмена", fmt.Sprintf("dib:%s", shortUUID(de.fsm.GetContext(c.Sender().ID).EditingDraftID))) menu.Inline(menu.Row(btnCancel)) return c.Edit(txt, menu, tele.ModeHTML) } // promptForItemPrice запрашивает ввод новой цены func (de *DraftEditor) promptForItemPrice(c tele.Context, item *drafts.DraftInvoiceItem, itemIndex int) error { de.fsm.SetState(c.Sender().ID, StateDraftEditItemPrice) txt := fmt.Sprintf( "💰 Изменение цены позиции №%d\n\n"+ "Текущая цена: %s ₽\n\n"+ "Введите новую цену (число):", itemIndex, item.Price.StringFixed(2), ) menu := &tele.ReplyMarkup{} btnCancel := menu.Data("❌ Отмена", fmt.Sprintf("dib:%s", shortUUID(de.fsm.GetContext(c.Sender().ID).EditingDraftID))) menu.Inline(menu.Row(btnCancel)) return c.Edit(txt, menu, tele.ModeHTML) } // ============================================ // === ОБРАБОТЧИКИ CALLBACK'ОВ РЕДАКТОРА ЧЕРНОВИКОВ === // ============================================ // parseDraftCallback разбирает callback data редактора черновиков. // Формат: "prefix:draftShort:param" или "prefix:draftShort" // Возвращает: prefix, draftShort, param (если есть), error func (de *DraftEditor) parseDraftCallback(data string) (prefix, draftShort string, param int, err error) { // Убираем возможный префикс \f от telebot if len(data) > 0 && data[0] == '\f' { data = data[1:] } parts := strings.Split(data, ":") if len(parts) < 2 { return "", "", 0, errors.New("invalid callback format") } prefix = parts[0] draftShort = parts[1] if len(parts) >= 3 { param, err = strconv.Atoi(parts[2]) if err != nil { return "", "", 0, fmt.Errorf("invalid param: %w", err) } } return prefix, draftShort, param, nil } // findDraftByShortID ищет черновик по первым 8 символам UUID. // Использует EditingDraftID из FSM контекста для точного совпадения. func (de *DraftEditor) findDraftByShortID(userID int64, draftShort string) (*drafts.DraftInvoice, error) { ctx := de.fsm.GetContext(userID) // Проверяем, совпадает ли shortUUID с сохранённым в контексте if shortUUID(ctx.EditingDraftID) == draftShort { userDB, err := de.accountRepo.GetUserByTelegramID(userID) if err != nil { return nil, err } return de.draftsService.GetDraftForEditor(ctx.EditingDraftID, userDB.ID) } return nil, errors.New("черновик не найден или сессия истекла") } // handleDraftEditorCallback обрабатывает все callback'ы редактора черновиков func (de *DraftEditor) handleDraftEditorCallback(c tele.Context, data string) error { prefix, draftShort, param, err := de.parseDraftCallback(data) if err != nil { return c.Respond(&tele.CallbackResponse{Text: "Ошибка: " + err.Error()}) } userID := c.Sender().ID // Получаем черновик draft, err := de.findDraftByShortID(userID, draftShort) if err != nil { return c.Respond(&tele.CallbackResponse{Text: "Черновик не найден. Начните заново."}) } switch prefix { case "di": // Draft Item — открыть меню позиции return de.handleDraftItemSelect(c, draft, param) case "dp": // Draft Page — пагинация return de.handleDraftPageChange(c, draft, param) case "da": // Draft Add — добавить позицию return de.handleDraftAddItem(c, draft) case "dc": // Draft Confirm — подтвердить return de.handleDraftConfirm(c, draft) case "dx": // Draft eXit — отменить return de.handleDraftCancel(c, draft) case "den": // Draft Edit Name return de.handleDraftEditName(c, draft, param) case "deq": // Draft Edit Quantity return de.handleDraftEditQuantity(c, draft, param) case "dep": // Draft Edit Price return de.handleDraftEditPrice(c, draft, param) case "did": // Draft Item Delete return de.handleDraftItemDelete(c, draft, param) case "dib": // Draft Item Back — назад к списку return de.handleDraftItemBack(c, draft) } return nil } // handleDraftItemSelect — открытие меню позиции func (de *DraftEditor) handleDraftItemSelect(c tele.Context, draft *drafts.DraftInvoice, itemIndex int) error { item := de.getItemByIndex(draft, itemIndex) if item == nil { return c.Respond(&tele.CallbackResponse{Text: "Позиция не найдена"}) } c.Respond() // Убираем "часики" return de.renderItemEditMenu(c, draft, item, itemIndex) } // handleDraftPageChange — переход на страницу func (de *DraftEditor) handleDraftPageChange(c tele.Context, draft *drafts.DraftInvoice, page int) error { c.Respond() return de.renderDraftEditorMenu(c, draft, page) } // handleDraftAddItem — добавление позиции func (de *DraftEditor) handleDraftAddItem(c tele.Context, draft *drafts.DraftInvoice) error { userDB, _ := de.accountRepo.GetUserByTelegramID(c.Sender().ID) _, err := de.draftsService.AddItem(draft.ID) if err != nil { return c.Respond(&tele.CallbackResponse{Text: "Ошибка: " + err.Error()}) } c.Respond(&tele.CallbackResponse{Text: "✅ Позиция добавлена"}) // Перезагружаем черновик и переходим на последнюю страницу updatedDraft, _ := de.draftsService.GetDraftForEditor(draft.ID, userDB.ID) lastPage := (len(updatedDraft.Items) - 1) / DraftEditorPageSize return de.renderDraftEditorMenu(c, updatedDraft, lastPage) } // handleDraftConfirm — подтверждение черновика func (de *DraftEditor) handleDraftConfirm(c tele.Context, draft *drafts.DraftInvoice) error { userDB, _ := de.accountRepo.GetUserByTelegramID(c.Sender().ID) // Переводим в статус READY_TO_VERIFY updatedDraft, err := de.draftsService.SetDraftReadyToVerify(draft.ID, userDB.ID) if err != nil { return c.Respond(&tele.CallbackResponse{Text: "Ошибка: " + err.Error()}) } // Сбрасываем FSM de.fsm.ResetDraftEditor(c.Sender().ID) c.Respond(&tele.CallbackResponse{Text: "✅ Черновик сохранён!"}) // Отправляем уведомление админам (будет реализовано на Этапе 9) go de.notifyAdminsAboutDraft(updatedDraft, userDB) // Получаем информацию о сервере для проверки авто-процессинга server, err := de.accountRepo.GetServerByID(updatedDraft.RMSServerID) if err != nil { logger.Log.Warn("Не удалось получить информацию о сервере", zap.Error(err)) } // Формируем финальное сообщение txt := fmt.Sprintf( "✅ Черновик успешно сохранён!\n\n"+ "📊 Статус: READY_TO_VERIFY\n"+ "📦 Позиций: %d\n"+ "📅 Дата: %s\n\n", len(updatedDraft.Items), updatedDraft.DateIncoming.Format("02.01.2006"), ) // Добавляем информацию о дальнейшей обработке if server != nil && server.AutoProcess { txt += "🔄 Авто-процессинг в iiko включён.\n" + "Черновик будет автоматически отправлен в систему iiko после проверки администратором.\n\n" } else { txt += "📋 Черновик готов к дальнейшей обработке.\n" + "Администраторы получили уведомление и скоро проверят накладную.\n\n" } txt += "Вы можете вернуться к работе через меню накладных." return c.Edit(txt, tele.ModeHTML) } // handleDraftCancel — отмена редактирования func (de *DraftEditor) handleDraftCancel(c tele.Context, draft *drafts.DraftInvoice) error { de.fsm.ResetDraftEditor(c.Sender().ID) c.Respond(&tele.CallbackResponse{Text: "Редактирование отменено"}) txt := "❌ Редактирование отменено\n\n" + "Черновик сохранён. Вы можете вернуться к нему позже через меню накладных." return c.Edit(txt, tele.ModeHTML) } // handleDraftEditName — переход к вводу названия func (de *DraftEditor) handleDraftEditName(c tele.Context, draft *drafts.DraftInvoice, itemIndex int) error { item := de.getItemByIndex(draft, itemIndex) if item == nil { return c.Respond(&tele.CallbackResponse{Text: "Позиция не найдена"}) } c.Respond() return de.promptForItemName(c, item, itemIndex) } // handleDraftEditQuantity — переход к вводу количества func (de *DraftEditor) handleDraftEditQuantity(c tele.Context, draft *drafts.DraftInvoice, itemIndex int) error { item := de.getItemByIndex(draft, itemIndex) if item == nil { return c.Respond(&tele.CallbackResponse{Text: "Позиция не найдена"}) } c.Respond() return de.promptForItemQuantity(c, item, itemIndex) } // handleDraftEditPrice — переход к вводу цены func (de *DraftEditor) handleDraftEditPrice(c tele.Context, draft *drafts.DraftInvoice, itemIndex int) error { item := de.getItemByIndex(draft, itemIndex) if item == nil { return c.Respond(&tele.CallbackResponse{Text: "Позиция не найдена"}) } c.Respond() return de.promptForItemPrice(c, item, itemIndex) } // handleDraftItemDelete — удаление позиции func (de *DraftEditor) handleDraftItemDelete(c tele.Context, draft *drafts.DraftInvoice, itemIndex int) error { item := de.getItemByIndex(draft, itemIndex) if item == nil { return c.Respond(&tele.CallbackResponse{Text: "Позиция не найдена"}) } userDB, _ := de.accountRepo.GetUserByTelegramID(c.Sender().ID) _, err := de.draftsService.DeleteItem(draft.ID, item.ID) if err != nil { return c.Respond(&tele.CallbackResponse{Text: "Ошибка удаления: " + err.Error()}) } c.Respond(&tele.CallbackResponse{Text: "🗑 Позиция удалена"}) // Перезагружаем черновик и возвращаемся к списку updatedDraft, _ := de.draftsService.GetDraftForEditor(draft.ID, userDB.ID) // Корректируем страницу если нужно ctx := de.fsm.GetContext(c.Sender().ID) page := ctx.DraftCurrentPage totalPages := (len(updatedDraft.Items) + DraftEditorPageSize - 1) / DraftEditorPageSize if page >= totalPages && page > 0 { page = totalPages - 1 } return de.renderDraftEditorMenu(c, updatedDraft, page) } // handleDraftItemBack — возврат к списку func (de *DraftEditor) handleDraftItemBack(c tele.Context, draft *drafts.DraftInvoice) error { c.Respond() ctx := de.fsm.GetContext(c.Sender().ID) return de.renderDraftEditorMenu(c, draft, ctx.DraftCurrentPage) } // notifyAdminsAboutDraft отправляет уведомление администраторам о новом черновике // // Функция должна: // 1. Получить список администраторов и владельцев сервера (исключая автора черновика) // 2. Отправить каждому администратору уведомление с информацией о черновике // 3. Учесть настройку MuteDraftNotifications для каждого пользователя // // TODO: Полная реализация на Этапе 9 // Для реализации использовать метод accountRepo.GetServerUsersForDraftNotification() func (de *DraftEditor) notifyAdminsAboutDraft(draft *drafts.DraftInvoice, author *account.User) { // Заглушка — будет реализовано позже logger.Log.Info("Draft ready for review", zap.String("draft_id", draft.ID.String()), zap.String("author", author.FirstName), ) } // ============================================ // === ОБРАБОТЧИКИ ТЕКСТОВЫХ СООБЩЕНИЙ РЕДАКТОРА ЧЕРНОВИКОВ === // ============================================ // handleDraftEditorText обрабатывает текстовые сообщения в состояниях редактора черновиков func (de *DraftEditor) handleDraftEditorText(c tele.Context) error { userID := c.Sender().ID ctx := de.fsm.GetContext(userID) // Проверяем, находимся ли мы в одном из состояний редактирования switch ctx.State { case StateDraftEditItemName: return de.handleDraftNameInput(c, ctx) case StateDraftEditItemQty: return de.handleDraftQuantityInput(c, ctx) case StateDraftEditItemPrice: return de.handleDraftPriceInput(c, ctx) } return nil // Не наше сообщение } // handleDraftNameInput обрабатывает ввод названия позиции func (de *DraftEditor) handleDraftNameInput(c tele.Context, ctx *UserContext) error { userID := c.Sender().ID text := c.Text() // Валидация if strings.TrimSpace(text) == "" { return c.Send("❌ Название не может быть пустым. Попробуйте ещё раз:") } if len(text) > 200 { return c.Send("❌ Название слишком длинное (максимум 200 символов). Попробуйте ещё раз:") } // Получаем пользователя userDB, err := de.accountRepo.GetUserByTelegramID(userID) if err != nil { return c.Send("❌ Ошибка получения данных пользователя") } // Получаем черновик draft, err := de.draftsService.GetDraftForEditor(ctx.EditingDraftID, userDB.ID) if err != nil { de.fsm.ResetDraftEditor(userID) return c.Send("❌ Черновик не найден. Редактирование отменено.") } // Получаем позицию item := de.getItemByIndex(draft, ctx.EditingItemIndex) if item == nil { de.fsm.ResetDraftEditor(userID) return c.Send("❌ Позиция не найдена. Редактирование отменено.") } // Обновляем название _, err = de.draftsService.UpdateItemRawName(draft.ID, item.ID, text) if err != nil { return c.Send("❌ Ошибка обновления названия: " + err.Error()) } // Сбрасываем состояние редактирования de.fsm.SetState(userID, StateNone) // Перезагружаем черновик и показываем меню позиции updatedDraft, _ := de.draftsService.GetDraftForEditor(draft.ID, userDB.ID) updatedItem := de.getItemByIndex(updatedDraft, ctx.EditingItemIndex) return de.renderItemEditMenu(c, updatedDraft, updatedItem, ctx.EditingItemIndex) } // handleDraftQuantityInput обрабатывает ввод количества позиции func (de *DraftEditor) handleDraftQuantityInput(c tele.Context, ctx *UserContext) error { userID := c.Sender().ID text := c.Text() // Парсинг количества qty, err := strconv.ParseFloat(text, 64) if err != nil { return c.Send("❌ Некорректное значение. Введите число (например: 10 или 5.5):") } // Валидация if qty <= 0 { return c.Send("❌ Количество должно быть больше 0. Попробуйте ещё раз:") } if qty > 1000000 { return c.Send("❌ Слишком большое количество. Попробуйте ещё раз:") } // Получаем пользователя userDB, err := de.accountRepo.GetUserByTelegramID(userID) if err != nil { return c.Send("❌ Ошибка получения данных пользователя") } // Получаем черновик draft, err := de.draftsService.GetDraftForEditor(ctx.EditingDraftID, userDB.ID) if err != nil { de.fsm.ResetDraftEditor(userID) return c.Send("❌ Черновик не найден. Редактирование отменено.") } // Получаем позицию item := de.getItemByIndex(draft, ctx.EditingItemIndex) if item == nil { de.fsm.ResetDraftEditor(userID) return c.Send("❌ Позиция не найдена. Редактирование отменено.") } // Обновляем количество qtyDecimal := decimal.NewFromFloat(qty) _, err = de.draftsService.UpdateItemQuantity(draft.ID, item.ID, qtyDecimal) if err != nil { return c.Send("❌ Ошибка обновления количества: " + err.Error()) } // Сбрасываем состояние редактирования de.fsm.SetState(userID, StateNone) // Перезагружаем черновик и показываем меню позиции updatedDraft, _ := de.draftsService.GetDraftForEditor(draft.ID, userDB.ID) updatedItem := de.getItemByIndex(updatedDraft, ctx.EditingItemIndex) return de.renderItemEditMenu(c, updatedDraft, updatedItem, ctx.EditingItemIndex) } // handleDraftPriceInput обрабатывает ввод цены позиции func (de *DraftEditor) handleDraftPriceInput(c tele.Context, ctx *UserContext) error { userID := c.Sender().ID text := c.Text() // Парсинг цены price, err := strconv.ParseFloat(text, 64) if err != nil { return c.Send("❌ Некорректное значение. Введите число (например: 100.50):") } // Валидация if price < 0 { return c.Send("❌ Цена не может быть отрицательной. Попробуйте ещё раз:") } if price > 1000000000 { return c.Send("❌ Слишком большая цена. Попробуйте ещё раз:") } // Получаем пользователя userDB, err := de.accountRepo.GetUserByTelegramID(userID) if err != nil { return c.Send("❌ Ошибка получения данных пользователя") } // Получаем черновик draft, err := de.draftsService.GetDraftForEditor(ctx.EditingDraftID, userDB.ID) if err != nil { de.fsm.ResetDraftEditor(userID) return c.Send("❌ Черновик не найден. Редактирование отменено.") } // Получаем позицию item := de.getItemByIndex(draft, ctx.EditingItemIndex) if item == nil { de.fsm.ResetDraftEditor(userID) return c.Send("❌ Позиция не найдена. Редактирование отменено.") } // Обновляем цену priceDecimal := decimal.NewFromFloat(price) _, err = de.draftsService.UpdateItemPrice(draft.ID, item.ID, priceDecimal) if err != nil { return c.Send("❌ Ошибка обновления цены: " + err.Error()) } // Сбрасываем состояние редактирования de.fsm.SetState(userID, StateNone) // Перезагружаем черновик и показываем меню позиции updatedDraft, _ := de.draftsService.GetDraftForEditor(draft.ID, userDB.ID) updatedItem := de.getItemByIndex(updatedDraft, ctx.EditingItemIndex) return de.renderItemEditMenu(c, updatedDraft, updatedItem, ctx.EditingItemIndex) }