0202-финиш перед десктопом

пересчет поправил
редактирование с перепроведением
галка автопроведения работает
рекомендации починил
This commit is contained in:
2026-02-02 13:53:38 +03:00
parent 10882f55c8
commit 88620f3fb6
37 changed files with 1905 additions and 11162 deletions

View File

@@ -203,50 +203,98 @@ type CommitRequestDTO struct {
SupplierID string `json:"supplier_id"`
Comment string `json:"comment"`
IncomingDocNum string `json:"incoming_document_number"`
IsProcessed bool `json:"is_processed"`
}
func (h *DraftsHandler) CommitDraft(c *gin.Context) {
userID := c.MustGet("userID").(uuid.UUID)
// Защита от паники
defer func() {
if r := recover(); r != nil {
logger.Log.Error("CRITICAL PANIC in CommitDraft Handler",
zap.Any("panic", r),
zap.Stack("stack"),
)
c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("Internal Server Error: %v", r)})
}
}()
logger.Log.Info("--- HANDLER: Start CommitDraft ---", zap.String("path", c.Request.URL.Path))
userID, ok := c.Get("userID")
if !ok {
logger.Log.Error("HANDLER: UserID missing in context")
c.JSON(http.StatusUnauthorized, gin.H{"error": "unauthorized"})
return
}
userUUID := userID.(uuid.UUID)
logger.Log.Info("HANDLER: UserID extracted", zap.String("user_id", userUUID.String()))
draftID, err := uuid.Parse(c.Param("id"))
if err != nil {
logger.Log.Warn("HANDLER: Invalid DraftID", zap.String("param", c.Param("id")), zap.Error(err))
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid draft id"})
return
}
logger.Log.Info("HANDLER: DraftID parsed", zap.String("draft_id", draftID.String()))
var req CommitRequestDTO
if err := c.ShouldBindJSON(&req); err != nil {
logger.Log.Error("HANDLER: JSON Binding failed", zap.Error(err))
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
logger.Log.Info("HANDLER: Payload bound",
zap.String("date_incoming", req.DateIncoming),
zap.String("store_id", req.StoreID),
zap.String("supplier_id", req.SupplierID),
zap.String("incoming_doc_num", req.IncomingDocNum),
zap.Bool("is_processed", req.IsProcessed),
)
date, err := time.Parse("2006-01-02", req.DateIncoming)
if err != nil {
logger.Log.Error("HANDLER: Date parsing failed", zap.String("date", req.DateIncoming), zap.Error(err))
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid date format"})
return
}
storeID, err := uuid.Parse(req.StoreID)
if err != nil {
logger.Log.Error("HANDLER: StoreID parsing failed", zap.String("store_id", req.StoreID), zap.Error(err))
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid store id"})
return
}
supplierID, err := uuid.Parse(req.SupplierID)
if err != nil {
logger.Log.Error("HANDLER: SupplierID parsing failed", zap.String("supplier_id", req.SupplierID), zap.Error(err))
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid supplier id"})
return
}
logger.Log.Info("HANDLER: Calling UpdateDraftHeader...",
zap.String("draft_id", draftID.String()),
zap.String("store_id", storeID.String()),
zap.String("supplier_id", supplierID.String()),
zap.Time("date", date),
)
if err := h.service.UpdateDraftHeader(draftID, &storeID, &supplierID, date, req.Comment, req.IncomingDocNum); err != nil {
logger.Log.Error("HANDLER: UpdateDraftHeader failed", zap.Error(err))
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to update header: " + err.Error()})
return
}
docNum, err := h.service.CommitDraft(draftID, userID)
logger.Log.Info("HANDLER: Calling CommitDraft service...", zap.String("draft_id", draftID.String()), zap.String("user_id", userUUID.String()))
docNum, err := h.service.CommitDraft(draftID, userUUID, req.IsProcessed)
if err != nil {
logger.Log.Error("Commit failed", zap.Error(err))
logger.Log.Warn("HANDLER: CommitDraft service failed", zap.Error(err))
c.JSON(http.StatusBadGateway, gin.H{"error": "RMS error: " + err.Error()})
return
}
logger.Log.Info("HANDLER: Success!", zap.String("doc_num", docNum))
c.JSON(http.StatusOK, gin.H{"status": "completed", "document_number": docNum})
}

View File

@@ -110,3 +110,23 @@ func (h *InvoiceHandler) SyncInvoices(c *gin.Context) {
"message": "Синхронизация запущена",
})
}
// GetStats godoc
// @Summary Получить статистику по накладным
// @Description Возвращает статистику по накладным для текущего пользователя
// @Tags invoices
// @Produce json
// @Success 200 {object} invService.InvoiceStatsDTO
// @Failure 500 {object} map[string]string
func (h *InvoiceHandler) GetStats(c *gin.Context) {
userID := c.MustGet("userID").(uuid.UUID)
stats, err := h.service.GetStats(userID)
if err != nil {
logger.Log.Error("Ошибка получения статистики", zap.Error(err))
c.JSON(http.StatusInternalServerError, gin.H{"error": "Ошибка получения статистики"})
return
}
c.JSON(http.StatusOK, stats)
}

View File

@@ -4,29 +4,56 @@ import (
"net/http"
"github.com/gin-gonic/gin"
"github.com/google/uuid"
"go.uber.org/zap"
"rmser/internal/domain/account"
"rmser/internal/services/recommend"
"rmser/pkg/logger"
)
type RecommendationsHandler struct {
service *recommend.Service
service *recommend.Service
accountRepo account.Repository
}
func NewRecommendationsHandler(service *recommend.Service) *RecommendationsHandler {
return &RecommendationsHandler{service: service}
func NewRecommendationsHandler(service *recommend.Service, accountRepo account.Repository) *RecommendationsHandler {
return &RecommendationsHandler{
service: service,
accountRepo: accountRepo,
}
}
// GetRecommendations godoc
// @Summary Получить список рекомендаций
// @Description Возвращает сгенерированные рекомендации (проблемные зоны учета)
// @Description Возвращает сгенерированные рекомендации (проблемные зоны учета) для активного сервера
// @Tags recommendations
// @Produce json
// @Success 200 {array} recommendations.Recommendation
// @Failure 500 {object} map[string]string
func (h *RecommendationsHandler) GetRecommendations(c *gin.Context) {
recs, err := h.service.GetRecommendations()
userID := c.MustGet("userID").(uuid.UUID)
// Получаем активный сервер пользователя
server, err := h.accountRepo.GetActiveServer(userID)
if err != nil {
logger.Log.Error("Ошибка получения активного сервера", zap.Error(err))
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to get active server"})
return
}
if server == nil {
c.JSON(http.StatusNotFound, gin.H{"error": "no active server"})
return
}
// Сначала обновляем рекомендации
if err := h.service.RefreshRecommendations(server.ID); err != nil {
logger.Log.Error("Ошибка обновления рекомендаций", zap.Error(err))
// Не прерываем выполнение, продолжаем с текущими данными
}
// Затем получаем рекомендации
recs, err := h.service.GetRecommendations(server.ID)
if err != nil {
logger.Log.Error("Ошибка получения рекомендаций", zap.Error(err))
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})

View File

@@ -2,6 +2,7 @@ package handlers
import (
"net/http"
"time"
"github.com/gin-gonic/gin"
"github.com/google/uuid"
@@ -48,13 +49,16 @@ func (h *SettingsHandler) SetNotifier(n Notifier) {
// SettingsResponse - DTO для отдачи настроек
type SettingsResponse struct {
ID string `json:"id"`
Name string `json:"name"`
BaseURL string `json:"base_url"`
DefaultStoreID *string `json:"default_store_id"` // Nullable
RootGroupID *string `json:"root_group_id"` // Nullable
AutoConduct bool `json:"auto_conduct"`
Role string `json:"role"` // OWNER, ADMIN, OPERATOR
ID string `json:"id"`
Name string `json:"name"`
BaseURL string `json:"base_url"`
DefaultStoreID *string `json:"default_store_id"` // Nullable
RootGroupID *string `json:"root_group_id"` // Nullable
AutoConduct bool `json:"auto_conduct"`
Role string `json:"role"` // OWNER, ADMIN, OPERATOR
SyncInterval int `json:"sync_interval"` // Интервал синхронизации в минутах
LastSyncAt *time.Time `json:"last_sync_at"` // Время последней синхронизации
LastActivityAt *time.Time `json:"last_activity_at"` // Время последней активности
}
// GetSettings возвращает настройки активного сервера + роль пользователя
@@ -77,11 +81,14 @@ func (h *SettingsHandler) GetSettings(c *gin.Context) {
}
resp := SettingsResponse{
ID: server.ID.String(),
Name: server.Name,
BaseURL: server.BaseURL,
AutoConduct: server.AutoProcess,
Role: string(role),
ID: server.ID.String(),
Name: server.Name,
BaseURL: server.BaseURL,
AutoConduct: server.AutoProcess,
Role: string(role),
SyncInterval: server.SyncInterval,
LastSyncAt: server.LastSyncAt,
LastActivityAt: server.LastActivityAt,
}
if server.DefaultStoreID != nil {
@@ -96,16 +103,17 @@ func (h *SettingsHandler) GetSettings(c *gin.Context) {
c.JSON(http.StatusOK, resp)
}
// UpdateSettingsDTO
// UpdateSettingsDTO - DTO для частичного обновления настроек (PATCH-семантика)
type UpdateSettingsDTO struct {
Name string `json:"name"`
DefaultStoreID string `json:"default_store_id"`
RootGroupID string `json:"root_group_id"`
AutoProcess bool `json:"auto_process"`
AutoConduct bool `json:"auto_conduct"`
Name *string `json:"name"`
DefaultStoreID *string `json:"default_store_id"`
RootGroupID *string `json:"root_group_id"`
AutoProcess *bool `json:"auto_process"` // Legacy для обратной совместимости
AutoConduct *bool `json:"auto_conduct"` // Новое поле
SyncInterval *int `json:"sync_interval,omitempty"` // Интервал синхронизации в минутах (5 - 10080)
}
// UpdateSettings сохраняет настройки
// UpdateSettings сохраняет настройки с PATCH-семантикой
func (h *SettingsHandler) UpdateSettings(c *gin.Context) {
userID := c.MustGet("userID").(uuid.UUID)
@@ -115,6 +123,11 @@ func (h *SettingsHandler) UpdateSettings(c *gin.Context) {
return
}
// Логирование полученных данных для отладки
logger.Log.Info("Получен запрос на обновление настроек",
zap.Any("request", req),
)
server, err := h.accountRepo.GetActiveServer(userID)
if err != nil || server == nil {
c.JSON(http.StatusNotFound, gin.H{"error": "No active server"})
@@ -132,31 +145,56 @@ func (h *SettingsHandler) UpdateSettings(c *gin.Context) {
return
}
if req.Name != "" {
server.Name = req.Name
// Обновление имени (только если передано)
if req.Name != nil {
server.Name = *req.Name
}
if req.AutoConduct {
server.AutoProcess = true
} else {
server.AutoProcess = req.AutoProcess || req.AutoConduct
// Обновление флага авто-проведения
if req.AutoConduct != nil {
server.AutoProcess = *req.AutoConduct
} else if req.AutoProcess != nil {
// Fallback для старых клиентов, которые используют legacy поле
server.AutoProcess = *req.AutoProcess
}
if req.DefaultStoreID != "" {
if uid, err := uuid.Parse(req.DefaultStoreID); err == nil {
server.DefaultStoreID = &uid
// Обновление интервала синхронизации
if req.SyncInterval != nil {
// Валидация диапазона: от 5 минут до 1 недели (10080 минут)
if *req.SyncInterval < 5 || *req.SyncInterval > 10080 {
c.JSON(http.StatusBadRequest, gin.H{"error": "sync_interval должен быть от 5 минут до 1 недели (10080 минут)"})
return
}
} else {
server.DefaultStoreID = nil
server.SyncInterval = *req.SyncInterval
}
if req.RootGroupID != "" {
if uid, err := uuid.Parse(req.RootGroupID); err == nil {
server.RootGroupGUID = &uid
// Обновление DefaultStoreID
if req.DefaultStoreID != nil {
if *req.DefaultStoreID == "" {
// Пустая строка -> сбрасываем в nil
server.DefaultStoreID = nil
} else {
// UUID -> обновляем
if uid, err := uuid.Parse(*req.DefaultStoreID); err == nil {
server.DefaultStoreID = &uid
}
}
} else {
server.RootGroupGUID = nil
}
// Если nil -> не трогаем текущее значение
// Обновление RootGroupID
if req.RootGroupID != nil {
if *req.RootGroupID == "" {
// Пустая строка -> сбрасываем в nil
server.RootGroupGUID = nil
} else {
// UUID -> обновляем
if uid, err := uuid.Parse(*req.RootGroupID); err == nil {
server.RootGroupGUID = &uid
}
}
}
// Если nil -> не трогаем текущее значение
if err := h.accountRepo.SaveServerSettings(server); err != nil {
logger.Log.Error("Failed to save settings", zap.Error(err))