mirror of
https://github.com/serty2005/rmser.git
synced 2026-02-04 19:02:33 -06:00
сервис стал сильно лучше черновики сохраняются одним запросом дто черновиков вынесен отдельно
439 lines
12 KiB
Go
439 lines
12 KiB
Go
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)
|
||
}
|