Настройки работают

Иерархия групп работает
Полностью завязано на пользователя и серверы
This commit is contained in:
2025-12-18 07:21:31 +03:00
parent 542beafe0e
commit 4e4571b3db
23 changed files with 1572 additions and 385 deletions

View File

@@ -77,6 +77,51 @@ type UpdateItemDTO struct {
Price float64 `json:"price"`
}
// AddDraftItem - POST /api/drafts/:id/items
func (h *DraftsHandler) AddDraftItem(c *gin.Context) {
draftID, err := uuid.Parse(c.Param("id"))
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid draft id"})
return
}
item, err := h.service.AddItem(draftID)
if err != nil {
logger.Log.Error("Failed to add item", zap.Error(err))
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, item)
}
// DeleteDraftItem - DELETE /api/drafts/:id/items/:itemId
func (h *DraftsHandler) DeleteDraftItem(c *gin.Context) {
draftID, err := uuid.Parse(c.Param("id"))
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid draft id"})
return
}
itemID, err := uuid.Parse(c.Param("itemId"))
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid item id"})
return
}
newTotal, err := h.service.DeleteItem(draftID, itemID)
if err != nil {
logger.Log.Error("Failed to delete item", zap.Error(err))
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{
"status": "deleted",
"id": itemID.String(),
"total_sum": newTotal,
})
}
func (h *DraftsHandler) UpdateItem(c *gin.Context) {
// userID := c.MustGet("userID").(uuid.UUID) // Пока не используется в UpdateItem, но можно добавить проверку владельца
draftID, _ := uuid.Parse(c.Param("id"))

View File

@@ -0,0 +1,158 @@
package handlers
import (
"net/http"
"github.com/gin-gonic/gin"
"github.com/google/uuid"
"go.uber.org/zap"
"rmser/internal/domain/account"
"rmser/internal/domain/catalog"
"rmser/pkg/logger"
)
type SettingsHandler struct {
accountRepo account.Repository
catalogRepo catalog.Repository
}
func NewSettingsHandler(accRepo account.Repository, catRepo catalog.Repository) *SettingsHandler {
return &SettingsHandler{
accountRepo: accRepo,
catalogRepo: catRepo,
}
}
// GetSettings возвращает настройки активного сервера
func (h *SettingsHandler) GetSettings(c *gin.Context) {
userID := c.MustGet("userID").(uuid.UUID)
server, err := h.accountRepo.GetActiveServer(userID)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
if server == nil {
c.JSON(http.StatusNotFound, gin.H{"error": "No active server"})
return
}
c.JSON(http.StatusOK, server)
}
// UpdateSettingsDTO
type UpdateSettingsDTO struct {
Name string `json:"name"`
DefaultStoreID string `json:"default_store_id"`
RootGroupID string `json:"root_group_id"`
AutoProcess bool `json:"auto_process"`
}
// UpdateSettings сохраняет настройки
func (h *SettingsHandler) UpdateSettings(c *gin.Context) {
userID := c.MustGet("userID").(uuid.UUID)
var req UpdateSettingsDTO
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
server, err := h.accountRepo.GetActiveServer(userID)
if err != nil || server == nil {
c.JSON(http.StatusNotFound, gin.H{"error": "No active server"})
return
}
// Обновляем поля
if req.Name != "" {
server.Name = req.Name
}
server.AutoProcess = req.AutoProcess
if req.DefaultStoreID != "" {
if uid, err := uuid.Parse(req.DefaultStoreID); err == nil {
server.DefaultStoreID = &uid
}
} else {
server.DefaultStoreID = nil
}
// Теперь правильно ловим ID группы
if req.RootGroupID != "" {
if uid, err := uuid.Parse(req.RootGroupID); err == nil {
server.RootGroupGUID = &uid
}
} else {
server.RootGroupGUID = nil
}
if err := h.accountRepo.SaveServer(server); err != nil {
logger.Log.Error("Failed to save settings", zap.Error(err))
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, server)
}
// --- Group Tree Logic ---
type GroupNode struct {
Key string `json:"key"` // ID for Ant Design TreeSelect
Value string `json:"value"` // ID value
Title string `json:"title"` // Name
Children []*GroupNode `json:"children"` // Sub-groups
}
// GetGroupsTree возвращает иерархию групп
func (h *SettingsHandler) GetGroupsTree(c *gin.Context) {
userID := c.MustGet("userID").(uuid.UUID)
server, err := h.accountRepo.GetActiveServer(userID)
if err != nil || server == nil {
c.JSON(http.StatusNotFound, gin.H{"error": "No active server"})
return
}
groups, err := h.catalogRepo.GetGroups(server.ID)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
tree := buildTree(groups)
c.JSON(http.StatusOK, tree)
}
func buildTree(flat []catalog.Product) []*GroupNode {
// 1. Map ID -> Node
nodeMap := make(map[uuid.UUID]*GroupNode)
for _, g := range flat {
nodeMap[g.ID] = &GroupNode{
Key: g.ID.String(),
Value: g.ID.String(),
Title: g.Name,
Children: make([]*GroupNode, 0),
}
}
var roots []*GroupNode
// 2. Build Hierarchy
for _, g := range flat {
node := nodeMap[g.ID]
if g.ParentID != nil {
if parent, exists := nodeMap[*g.ParentID]; exists {
parent.Children = append(parent.Children, node)
} else {
// Если родителя нет в списке (например, он удален или мы выбрали подмножество),
// считаем узлом верхнего уровня
roots = append(roots, node)
}
} else {
roots = append(roots, node)
}
}
return roots
}

View File

@@ -141,6 +141,9 @@ func (bot *Bot) initHandlers() {
// 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: "Функция пополнения в разработке 🛠"})
})
@@ -151,6 +154,7 @@ func (bot *Bot) initHandlers() {
// Input Handlers
bot.b.Handle(tele.OnText, bot.handleText)
bot.b.Handle(tele.OnPhoto, bot.handlePhoto)
}
func (bot *Bot) Start() {
@@ -205,9 +209,10 @@ func (bot *Bot) renderServersMenu(c tele.Context) error {
}
btnAdd := menu.Data(" Добавить сервер", "act_add_server")
btnDel := menu.Data("🗑 Удалить", "act_del_server_menu")
btnBack := menu.Data("🔙 Назад", "nav_main")
rows = append(rows, menu.Row(btnAdd))
rows = append(rows, menu.Row(btnAdd, btnDel))
rows = append(rows, menu.Row(btnBack))
menu.Inline(rows...)
@@ -264,18 +269,21 @@ func (bot *Bot) renderBalanceMenu(c tele.Context) error {
func (bot *Bot) handleCallback(c tele.Context) error {
data := c.Callback().Data
// FIX: Telebot v3 добавляет префикс '\f' к Unique ID кнопки.
// Нам нужно удалить его, чтобы корректно парсить строку.
if len(data) > 0 && data[0] == '\f' {
data = data[1:]
}
// Обработка выбора сервера "set_server_..."
if strings.HasPrefix(data, "set_server_") {
serverIDStr := strings.TrimPrefix(data, "set_server_")
// Удаляем лишние пробелы/символы, которые telebot иногда добавляет (уникальный префикс \f)
serverIDStr = strings.TrimSpace(serverIDStr)
// Telebot v3: Callback data is prefixed with \f followed by unique id.
// But here we use 'data' which is the payload.
// NOTE: data variable contains what we passed in .Data() second arg.
// Split by | just in case middleware adds something, but usually raw string is fine.
parts := strings.Split(serverIDStr, "|") // Защита от старых форматов
serverIDStr = parts[0]
// Защита от старых форматов с разделителем |
if idx := strings.Index(serverIDStr, "|"); idx != -1 {
serverIDStr = serverIDStr[:idx]
}
userDB, _ := bot.accountRepo.GetUserByTelegramID(c.Sender().ID)
@@ -290,30 +298,74 @@ func (bot *Bot) handleCallback(c tele.Context) error {
}
if !found {
logger.Log.Warn("User tried to select unknown server",
zap.Int64("user_tg_id", c.Sender().ID),
zap.String("server_id_req", serverIDStr))
return c.Respond(&tele.CallbackResponse{Text: "Сервер не найден или доступ запрещен"})
}
// 2. Делаем активным
// Важно: нужно спарсить UUID
// Telebot sometimes sends garbage if Unique is not handled properly.
// But we handle OnCallback generally.
// Fix: В Telebot 3 Data() возвращает payload как есть.
// Но лучше быть аккуратным.
if err := bot.accountRepo.SetActiveServer(userDB.ID, parseUUID(serverIDStr)); err != nil {
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: "Ошибка смены сервера"})
}
// 3. Сбрасываем кэш фабрики клиентов (чтобы при следующем запросе создался клиент с новыми кредами, если бы они поменялись,
// но тут меняется сам сервер, так что Factory.GetClientForUser просто возьмет другой сервер)
// Для надежности можно ничего не делать, Factory сама разберется.
// 3. Успех
c.Respond(&tele.CallbackResponse{Text: "✅ Сервер выбран"})
return bot.renderServersMenu(c) // Перерисовываем меню
}
// --- ЛОГИКА УДАЛЕНИЯ (новая) ---
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)
if targetID == uuid.Nil {
return c.Respond(&tele.CallbackResponse{Text: "Некорректный ID"})
}
// 1. Проверяем, активен ли он сейчас
// Нам нужно знать это ДО удаления, чтобы переключить активность
// Но проще удалить, а потом проверить, остался ли активный сервер
// Удаляем
if err := bot.accountRepo.DeleteServer(targetID); err != nil {
logger.Log.Error("Failed to delete server", zap.Error(err))
return c.Respond(&tele.CallbackResponse{Text: "Ошибка удаления"})
}
// Сбрасываем кэш клиента в фабрике
bot.rmsFactory.ClearCache(targetID)
// 2. Проверяем, есть ли активный сервер у пользователя
userDB, _ := bot.accountRepo.GetUserByTelegramID(c.Sender().ID)
active, err := bot.accountRepo.GetActiveServer(userDB.ID)
// Если активного нет (мы удалили активный) или ошибка - назначаем новый
if active == nil || err != nil {
all, _ := bot.accountRepo.GetAllServers(userDB.ID)
if len(all) > 0 {
// Делаем активным первый попавшийся
_ = bot.accountRepo.SetActiveServer(userDB.ID, all[0].ID)
c.Respond(&tele.CallbackResponse{Text: "Сервер удален. Активным назначен " + all[0].Name})
} else {
c.Respond(&tele.CallbackResponse{Text: "Сервер удален. Список пуст."})
}
} else {
c.Respond(&tele.CallbackResponse{Text: "Сервер удален"})
}
// Возвращаемся в меню удаления (обновляем список)
return bot.renderDeleteServerMenu(c)
}
return nil
}
@@ -366,55 +418,65 @@ func (bot *Bot) handleText(c tele.Context) error {
ctx.TempURL = strings.TrimRight(text, "/")
ctx.State = StateAddServerLogin
})
return c.Send("👤 Введите <b>логин</b> пользователя iiko:")
return c.Send("👤 Введите логин пользователя iiko:")
case StateAddServerLogin:
bot.fsm.UpdateContext(userID, func(ctx *UserContext) {
ctx.TempLogin = text
ctx.State = StateAddServerPassword
})
return c.Send("🔑 Введите <b>пароль</b>:")
return c.Send("🔑 Введите пароль:")
case StateAddServerPassword:
password := text
ctx := bot.fsm.GetContext(userID)
msg, _ := bot.b.Send(c.Sender(), "⏳ Проверяю подключение...")
// Check connection
// 1. Проверяем авторизацию (креды)
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Попробуйте ввести пароль снова или начните сначала /add_server", err))
return c.Send(fmt.Sprintf("❌ Ошибка авторизации: %v\nПроверьте логин/пароль.", err))
}
// Save
encPass, _ := bot.cryptoManager.Encrypt(password)
userDB, _ := bot.accountRepo.GetOrCreateUser(userID, c.Sender().Username, "", "")
newServer := &account.RMSServer{
UserID: userDB.ID,
Name: "iiko Server " + time.Now().Format("15:04"), // Генерируем имя, чтобы не спрашивать лишнего
BaseURL: ctx.TempURL,
Login: ctx.TempLogin,
EncryptedPassword: encPass,
IsActive: true, // Сразу делаем активным
// 2. Пробуем узнать имя сервера
var detectedName string
info, err := rms.GetServerInfo(ctx.TempURL)
if err == nil && info.ServerName != "" {
detectedName = info.ServerName
}
// Сначала сохраняем, потом делаем активным (через репо сохранения)
if err := bot.accountRepo.SaveServer(newServer); err != nil {
return c.Send("Ошибка БД: " + err.Error())
}
// Устанавливаем активным (сбрасывая другие)
bot.accountRepo.SetActiveServer(userDB.ID, newServer.ID)
bot.fsm.Reset(userID)
bot.b.Delete(msg)
c.Send("✅ <b>Сервер добавлен и выбран активным!</b>", tele.ModeHTML)
// Auto-sync
go bot.syncService.SyncAllData(userDB.ID)
// Сохраняем пароль во временный контекст, он нам пригодится при финальном сохранении
bot.fsm.UpdateContext(userID, func(uCtx *UserContext) {
uCtx.TempPassword = password
uCtx.TempServerName = detectedName
})
return bot.renderMainMenu(c)
// Если имя нашли - предлагаем выбор
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("🔎 Обнаружено имя сервера: <b>%s</b>.\nИспользовать его?", detectedName), menu, tele.ModeHTML)
}
// Если имя не нашли - просим ввести вручную
bot.fsm.SetState(userID, StateAddServerInputName)
return c.Send("🏷 Введите <b>название</b> для этого сервера (для вашего удобства):")
case StateAddServerInputName:
// Пользователь ввел свое название
name := text
if len(name) < 3 {
return c.Send("⚠️ Название слишком короткое.")
}
return bot.saveServerFinal(c, userID, name)
}
return nil
@@ -488,3 +550,78 @@ func parseUUID(s string) uuid.UUID {
id, _ := uuid.Parse(s)
return id
}
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("🏷 Хорошо, введите желаемое <b>название</b>:")
}
// saveServerFinal - общая логика сохранения в БД
func (bot *Bot) saveServerFinal(c tele.Context, userID int64, serverName string) error {
ctx := bot.fsm.GetContext(userID)
encPass, _ := bot.cryptoManager.Encrypt(ctx.TempPassword)
userDB, _ := bot.accountRepo.GetOrCreateUser(userID, c.Sender().Username, "", "")
newServer := &account.RMSServer{
UserID: userDB.ID,
Name: serverName,
BaseURL: ctx.TempURL,
Login: ctx.TempLogin,
EncryptedPassword: encPass,
IsActive: true,
}
if err := bot.accountRepo.SaveServer(newServer); err != nil {
return c.Send("Ошибка сохранения в БД: " + err.Error())
}
bot.accountRepo.SetActiveServer(userDB.ID, newServer.ID)
bot.fsm.Reset(userID)
c.Send(fmt.Sprintf("✅ Сервер <b>%s</b> успешно добавлен!", serverName), tele.ModeHTML)
// Auto-sync
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.GetAllServers(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 {
// Кнопка удаления для каждого сервера
// Префикс do_del_server_
btn := menu.Data(fmt.Sprintf("❌ %s", s.Name), "do_del_server_"+s.ID.String())
rows = append(rows, menu.Row(btn))
}
btnBack := menu.Data("🔙 Назад к списку", "nav_servers")
rows = append(rows, menu.Row(btnBack))
menu.Inline(rows...)
return c.EditOrSend("🗑 <b>Удаление сервера</b>\n\nНажмите на сервер, который хотите удалить.\nЭто действие нельзя отменить.", menu, tele.ModeHTML)
}

View File

@@ -10,14 +10,17 @@ const (
StateAddServerURL
StateAddServerLogin
StateAddServerPassword
StateAddServerConfirmName
StateAddServerInputName
)
// UserContext хранит временные данные в процессе диалога
type UserContext struct {
State State
TempURL string
TempLogin string
TempPassword string
State State
TempURL string
TempLogin string
TempPassword string
TempServerName string
}
// StateManager управляет состояниями