Files
rmser/internal/transport/telegram/draft_editor.go

827 lines
31 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// 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 <draft_id>
// Запускает редактор для указанного черновика
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(
"📄 <b>Черновик</b> от %s\n"+
"💰 Сумма: <b>%.2f ₽</b> | 📦 Позиций: <b>%d</b>\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(
"✏️ <b>Редактирование позиции №%d</b>\n\n"+
"📝 Название: <code>%s</code>\n"+
"📦 Количество: <b>%s</b>\n"+
"💰 Цена: <b>%s ₽</b>\n"+
"💵 Сумма: <b>%s ₽</b>\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(
"📝 <b>Изменение названия позиции №%d</b>\n\n"+
"Текущее название: <code>%s</code>\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(
"📦 <b>Изменение количества позиции №%d</b>\n\n"+
"Текущее количество: <b>%s</b>\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(
"💰 <b>Изменение цены позиции №%d</b>\n\n"+
"Текущая цена: <b>%s ₽</b>\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(
"✅ <b>Черновик успешно сохранён!</b>\n\n"+
"📊 <b>Статус:</b> <code>READY_TO_VERIFY</code>\n"+
"📦 <b>Позиций:</b> %d\n"+
"📅 <b>Дата:</b> %s\n\n",
len(updatedDraft.Items),
updatedDraft.DateIncoming.Format("02.01.2006"),
)
// Добавляем информацию о дальнейшей обработке
if server != nil && server.AutoProcess {
txt += "🔄 <b>Авто-процессинг в iiko включён.</b>\n" +
"Черновик будет автоматически отправлен в систему iiko после проверки администратором.\n\n"
} else {
txt += "📋 <b>Черновик готов к дальнейшей обработке.</b>\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 := "❌ <b>Редактирование отменено</b>\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)
}