mirror of
https://github.com/serty2005/rmser.git
synced 2026-02-05 03:12:34 -06:00
827 lines
31 KiB
Go
827 lines
31 KiB
Go
// 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)
|
||
}
|