Files
rmser/internal/transport/http/handlers/drafts.go
SERTY 10882f55c8 3001-фух.это был сильный спринт.
сервис стал сильно лучше
черновики сохраняются одним запросом
дто черновиков вынесен отдельно
2026-01-30 02:24:26 +03:00

439 lines
12 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.

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"`
}
func (h *DraftsHandler) CommitDraft(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 CommitRequestDTO
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
date, err := time.Parse("2006-01-02", req.DateIncoming)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid date format"})
return
}
storeID, err := uuid.Parse(req.StoreID)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid store id"})
return
}
supplierID, err := uuid.Parse(req.SupplierID)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid supplier id"})
return
}
if err := h.service.UpdateDraftHeader(draftID, &storeID, &supplierID, date, req.Comment, req.IncomingDocNum); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to update header: " + err.Error()})
return
}
docNum, err := h.service.CommitDraft(draftID, userID)
if err != nil {
logger.Log.Error("Commit failed", zap.Error(err))
c.JSON(http.StatusBadGateway, gin.H{"error": "RMS error: " + err.Error()})
return
}
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)
}