Files
SERTY 88620f3fb6 0202-финиш перед десктопом
пересчет поправил
редактирование с перепроведением
галка автопроведения работает
рекомендации починил
2026-02-02 13:53:38 +03:00

487 lines
14 KiB
Go
Raw Permalink 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.

package handlers
import (
"fmt"
"io"
"net/http"
"time"
"github.com/gin-gonic/gin"
"github.com/google/uuid"
"github.com/shopspring/decimal"
"go.uber.org/zap"
draftsdomain "rmser/internal/domain/drafts"
"rmser/internal/services/drafts"
"rmser/internal/services/ocr"
"rmser/pkg/logger"
)
type DraftsHandler struct {
service *drafts.Service
ocrService *ocr.Service
}
func NewDraftsHandler(service *drafts.Service, ocrService *ocr.Service) *DraftsHandler {
return &DraftsHandler{
service: service,
ocrService: ocrService,
}
}
func (h *DraftsHandler) GetDraft(c *gin.Context) {
userID := c.MustGet("userID").(uuid.UUID)
idStr := c.Param("id")
id, err := uuid.Parse(idStr)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid id"})
return
}
draft, err := h.service.GetDraft(id, userID)
if err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "draft not found"})
return
}
c.JSON(http.StatusOK, draft)
}
func (h *DraftsHandler) GetDictionaries(c *gin.Context) {
userID := c.MustGet("userID").(uuid.UUID)
data, err := h.service.GetDictionaries(userID)
if err != nil {
logger.Log.Error("GetDictionaries error", zap.Error(err))
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, data)
}
func (h *DraftsHandler) GetStores(c *gin.Context) {
userID := c.MustGet("userID").(uuid.UUID)
dict, err := h.service.GetDictionaries(userID)
if err != nil {
logger.Log.Error("GetStores error", zap.Error(err))
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, dict["stores"])
}
// UpdateItemDTO обновлен: float64 -> *float64, добавлен edited_field
type UpdateItemDTO struct {
ProductID *string `json:"product_id"`
ContainerID *string `json:"container_id"`
Quantity *float64 `json:"quantity"`
Price *float64 `json:"price"`
Sum *float64 `json:"sum"`
EditedField string `json:"edited_field"` // "quantity", "price", "sum"
}
// ReorderItemsRequest DTO для переупорядочивания нескольких элементов
type ReorderItemsRequest struct {
Items []struct {
ID string `json:"id"`
Order int `json:"order"`
} `json:"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)
}
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,
})
}
// UpdateItem обновлен
func (h *DraftsHandler) UpdateItem(c *gin.Context) {
draftID, _ := uuid.Parse(c.Param("id"))
itemID, _ := uuid.Parse(c.Param("itemId"))
var req UpdateItemDTO
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
var pID *uuid.UUID
if req.ProductID != nil && *req.ProductID != "" {
if uid, err := uuid.Parse(*req.ProductID); err == nil {
pID = &uid
}
}
var cID *uuid.UUID
if req.ContainerID != nil {
if *req.ContainerID == "" {
// Сброс фасовки
empty := uuid.Nil
cID = &empty
} else if uid, err := uuid.Parse(*req.ContainerID); err == nil {
cID = &uid
}
}
qty := decimal.Zero
if req.Quantity != nil {
qty = decimal.NewFromFloat(*req.Quantity)
}
price := decimal.Zero
if req.Price != nil {
price = decimal.NewFromFloat(*req.Price)
}
sum := decimal.Zero
if req.Sum != nil {
sum = decimal.NewFromFloat(*req.Sum)
}
// Дефолт, если фронт не прислал (для совместимости)
editedField := req.EditedField
if editedField == "" {
if req.Sum != nil {
editedField = "sum"
} else if req.Price != nil {
editedField = "price"
} else {
editedField = "quantity"
}
}
if err := h.service.UpdateItem(draftID, itemID, pID, cID, qty, price, sum, editedField); err != nil {
logger.Log.Error("Failed to update item", zap.Error(err))
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"status": "updated"})
}
type CommitRequestDTO struct {
DateIncoming string `json:"date_incoming"` // YYYY-MM-DD
StoreID string `json:"store_id"`
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) {
// Защита от паники
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
}
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.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})
}
type AddContainerRequestDTO struct {
ProductID string `json:"product_id" binding:"required"`
Name string `json:"name" binding:"required"`
Count float64 `json:"count" binding:"required,gt=0"`
}
func (h *DraftsHandler) AddContainer(c *gin.Context) {
userID := c.MustGet("userID").(uuid.UUID)
var req AddContainerRequestDTO
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
pID, err := uuid.Parse(req.ProductID)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid product_id"})
return
}
countDec := decimal.NewFromFloat(req.Count)
newID, err := h.service.CreateProductContainer(userID, pID, req.Name, countDec)
if err != nil {
logger.Log.Error("Failed to create container", zap.Error(err))
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"status": "created", "container_id": newID.String()})
}
func (h *DraftsHandler) GetDrafts(c *gin.Context) {
userID := c.MustGet("userID").(uuid.UUID)
fromStr := c.DefaultQuery("from", time.Now().AddDate(0, 0, -30).Format("2006-01-02"))
toStr := c.DefaultQuery("to", time.Now().Format("2006-01-02"))
from, _ := time.Parse("2006-01-02", fromStr)
to, _ := time.Parse("2006-01-02", toStr)
to = to.Add(23*time.Hour + 59*time.Minute + 59*time.Second)
list, err := h.service.GetUnifiedList(userID, from, to)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, list)
}
func (h *DraftsHandler) DeleteDraft(c *gin.Context) {
idStr := c.Param("id")
id, err := uuid.Parse(idStr)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid id"})
return
}
newStatus, err := h.service.DeleteDraft(id)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"status": newStatus, "id": id.String()})
}
// ReorderItems изменяет порядок нескольких позиций в черновике
func (h *DraftsHandler) ReorderItems(c *gin.Context) {
var req ReorderItemsRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
// Получаем draftID из параметров пути
draftID, err := uuid.Parse(c.Param("id"))
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid draft id"})
return
}
// Преобразуем элементы в формат для сервиса
items := make([]struct {
ID uuid.UUID
Order int
}, len(req.Items))
for i, item := range req.Items {
id, err := uuid.Parse(item.ID)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": fmt.Sprintf("invalid item id: %s", item.ID)})
return
}
items[i] = struct {
ID uuid.UUID
Order int
}{
ID: id,
Order: item.Order,
}
}
if err := h.service.ReorderItems(draftID, items); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"success": true})
}
// UpdateDraft обновляет черновик (шапку и позиции) пакетно
func (h *DraftsHandler) UpdateDraft(c *gin.Context) {
userID := c.MustGet("userID").(uuid.UUID)
draftID, err := uuid.Parse(c.Param("id"))
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid draft id"})
return
}
var req draftsdomain.UpdateDraftRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
if err := h.service.SaveDraftFull(draftID, userID, req); err != nil {
logger.Log.Error("Failed to update draft", zap.Error(err))
c.JSON(http.StatusInternalServerError, gin.H{"error": "Не удалось сохранить изменения. Проверьте данные."})
return
}
c.JSON(http.StatusOK, gin.H{"status": "updated"})
}
// Upload обрабатывает загрузку файла и прогоняет через OCR
func (h *DraftsHandler) Upload(c *gin.Context) {
// Лимит размера тела запроса (20MB)
c.Request.Body = http.MaxBytesReader(c.Writer, c.Request.Body, 20<<20)
// Получаем файл из формы
fileHeader, err := c.FormFile("file")
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "file is required"})
return
}
// Открываем файл для чтения
file, err := fileHeader.Open()
if err != nil {
logger.Log.Error("Failed to open uploaded file", zap.Error(err))
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to open file"})
return
}
defer file.Close()
// Читаем байты файла
fileBytes, err := io.ReadAll(file)
if err != nil {
logger.Log.Error("Failed to read file bytes", zap.Error(err))
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to read file"})
return
}
// Получаем userID из контекста
userID := c.MustGet("userID").(uuid.UUID)
// Вызываем ProcessDocument
draft, err := h.ocrService.ProcessDocument(c.Request.Context(), userID, fileBytes, fileHeader.Filename)
if err != nil {
// Если черновик создан, но произошла ошибка OCR, возвращаем черновик со статусом ERROR
// Проверяем, что draft не nil (черновик был создан)
if draft != nil {
c.JSON(http.StatusOK, draft)
return
}
// Если черновик не был создан, возвращаем ошибку
logger.Log.Error("ProcessDocument failed", zap.Error(err))
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
// Успешная обработка
c.JSON(http.StatusOK, draft)
}