mirror of
https://github.com/serty2005/rmser.git
synced 2026-02-04 19:02:33 -06:00
2701-как будто ок днд работает
This commit is contained in:
@@ -155,7 +155,7 @@ func main() {
|
|||||||
api.POST("/drafts/:id/items", draftsHandler.AddDraftItem)
|
api.POST("/drafts/:id/items", draftsHandler.AddDraftItem)
|
||||||
api.DELETE("/drafts/:id/items/:itemId", draftsHandler.DeleteDraftItem)
|
api.DELETE("/drafts/:id/items/:itemId", draftsHandler.DeleteDraftItem)
|
||||||
api.PATCH("/drafts/:id/items/:itemId", draftsHandler.UpdateItem)
|
api.PATCH("/drafts/:id/items/:itemId", draftsHandler.UpdateItem)
|
||||||
api.PUT("/drafts/:id/items/reorder", draftsHandler.ReorderItems)
|
api.POST("/drafts/:id/reorder", draftsHandler.ReorderItems)
|
||||||
api.POST("/drafts/:id/commit", draftsHandler.CommitDraft)
|
api.POST("/drafts/:id/commit", draftsHandler.CommitDraft)
|
||||||
api.POST("/drafts/container", draftsHandler.AddContainer)
|
api.POST("/drafts/container", draftsHandler.AddContainer)
|
||||||
|
|
||||||
|
|||||||
@@ -4,8 +4,8 @@ app:
|
|||||||
drop_tables: false
|
drop_tables: false
|
||||||
storage_path: "./uploads"
|
storage_path: "./uploads"
|
||||||
public_url: "https://rmser.serty.top"
|
public_url: "https://rmser.serty.top"
|
||||||
maintenance_mode: true
|
maintenance_mode: false
|
||||||
dev_ids: [665599275,2126923472] # Укажите здесь ваш ID и ID тестировщиков
|
dev_ids: [] # Укажите здесь ваш ID и ID тестировщиков
|
||||||
|
|
||||||
db:
|
db:
|
||||||
dsn: "host=postgres user=rmser password=mhrcadmin994525 dbname=rmser_db port=5432 sslmode=disable TimeZone=Europe/Moscow"
|
dsn: "host=postgres user=rmser password=mhrcadmin994525 dbname=rmser_db port=5432 sslmode=disable TimeZone=Europe/Moscow"
|
||||||
@@ -15,11 +15,6 @@ redis:
|
|||||||
password: ""
|
password: ""
|
||||||
db: 0
|
db: 0
|
||||||
|
|
||||||
rms:
|
|
||||||
base_url: "https://rest-mesto-vstrechi.iiko.it" # Например http://95.12.34.56:8080
|
|
||||||
login: "MH"
|
|
||||||
password: "MhLevfqkexit632597" # Пароль в открытом виде (приложение само хеширует)
|
|
||||||
|
|
||||||
ocr:
|
ocr:
|
||||||
service_url: "http://ocr-service:5005"
|
service_url: "http://ocr-service:5005"
|
||||||
|
|
||||||
|
|||||||
@@ -91,6 +91,10 @@ type Repository interface {
|
|||||||
Update(draft *DraftInvoice) error
|
Update(draft *DraftInvoice) error
|
||||||
UpdateItem(itemID uuid.UUID, updates map[string]interface{}) error
|
UpdateItem(itemID uuid.UUID, updates map[string]interface{}) error
|
||||||
UpdateItemOrder(itemID uuid.UUID, newOrder int) error
|
UpdateItemOrder(itemID uuid.UUID, newOrder int) error
|
||||||
|
ReorderItems(draftID uuid.UUID, items []struct {
|
||||||
|
ID uuid.UUID
|
||||||
|
Order int
|
||||||
|
}) error
|
||||||
CreateItem(item *DraftInvoiceItem) error
|
CreateItem(item *DraftInvoiceItem) error
|
||||||
DeleteItem(itemID uuid.UUID) error
|
DeleteItem(itemID uuid.UUID) error
|
||||||
Delete(id uuid.UUID) error
|
Delete(id uuid.UUID) error
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
package drafts
|
package drafts
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
"rmser/internal/domain/drafts"
|
"rmser/internal/domain/drafts"
|
||||||
|
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
@@ -105,6 +107,35 @@ func (r *pgRepository) UpdateItemOrder(itemID uuid.UUID, newOrder int) error {
|
|||||||
Update("order", newOrder).Error
|
Update("order", newOrder).Error
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ReorderItems обновляет порядок нескольких элементов в одной транзакции
|
||||||
|
func (r *pgRepository) ReorderItems(draftID uuid.UUID, items []struct {
|
||||||
|
ID uuid.UUID
|
||||||
|
Order int
|
||||||
|
}) error {
|
||||||
|
return r.db.Transaction(func(tx *gorm.DB) error {
|
||||||
|
for _, item := range items {
|
||||||
|
// Проверяем, что элемент принадлежит указанному черновику
|
||||||
|
var count int64
|
||||||
|
if err := tx.Model(&drafts.DraftInvoiceItem{}).
|
||||||
|
Where("id = ? AND draft_id = ?", item.ID, draftID).
|
||||||
|
Count(&count).Error; err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if count == 0 {
|
||||||
|
return fmt.Errorf("элемент с id %s не принадлежит черновику %s", item.ID.String(), draftID.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Обновляем только колонку order
|
||||||
|
if err := tx.Model(&drafts.DraftInvoiceItem{}).
|
||||||
|
Where("id = ?", item.ID).
|
||||||
|
Update("order", item.Order).Error; err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
func (r *pgRepository) Delete(id uuid.UUID) error {
|
func (r *pgRepository) Delete(id uuid.UUID) error {
|
||||||
return r.db.Delete(&drafts.DraftInvoice{}, id).Error
|
return r.db.Delete(&drafts.DraftInvoice{}, id).Error
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -921,3 +921,32 @@ func (s *Service) ReorderItem(draftID, itemID uuid.UUID, newOrder int) error {
|
|||||||
// Обновляем порядок целевого элемента
|
// Обновляем порядок целевого элемента
|
||||||
return s.draftRepo.UpdateItemOrder(itemID, newOrder)
|
return s.draftRepo.UpdateItemOrder(itemID, newOrder)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ReorderItems обновляет порядок нескольких элементов в черновике
|
||||||
|
func (s *Service) ReorderItems(draftID uuid.UUID, items []struct {
|
||||||
|
ID uuid.UUID
|
||||||
|
Order int
|
||||||
|
}) error {
|
||||||
|
// Проверяем, что черновик существует
|
||||||
|
draft, err := s.draftRepo.GetByID(draftID)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("черновик не найден: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Проверяем, что все элементы принадлежат указанному черновику
|
||||||
|
for _, item := range items {
|
||||||
|
found := false
|
||||||
|
for _, draftItem := range draft.Items {
|
||||||
|
if draftItem.ID == item.ID {
|
||||||
|
found = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !found {
|
||||||
|
return fmt.Errorf("элемент с id %s не принадлежит черновику %s", item.ID.String(), draftID.String())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Вызываем метод репозитория для обновления порядка
|
||||||
|
return s.draftRepo.ReorderItems(draftID, items)
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package handlers
|
package handlers
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -73,6 +74,14 @@ type UpdateItemDTO struct {
|
|||||||
EditedField string `json:"edited_field"` // "quantity", "price", "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) {
|
func (h *DraftsHandler) AddDraftItem(c *gin.Context) {
|
||||||
draftID, err := uuid.Parse(c.Param("id"))
|
draftID, err := uuid.Parse(c.Param("id"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -303,14 +312,9 @@ func (h *DraftsHandler) DeleteDraft(c *gin.Context) {
|
|||||||
c.JSON(http.StatusOK, gin.H{"status": newStatus, "id": id.String()})
|
c.JSON(http.StatusOK, gin.H{"status": newStatus, "id": id.String()})
|
||||||
}
|
}
|
||||||
|
|
||||||
// ReorderItems изменяет порядок позиции в черновике
|
// ReorderItems изменяет порядок нескольких позиций в черновике
|
||||||
func (h *DraftsHandler) ReorderItems(c *gin.Context) {
|
func (h *DraftsHandler) ReorderItems(c *gin.Context) {
|
||||||
type ReorderRequest struct {
|
var req ReorderItemsRequest
|
||||||
ItemID uuid.UUID `json:"item_id" binding:"required"`
|
|
||||||
NewOrder int `json:"new_order" binding:"required"`
|
|
||||||
}
|
|
||||||
|
|
||||||
var req ReorderRequest
|
|
||||||
if err := c.ShouldBindJSON(&req); err != nil {
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||||
return
|
return
|
||||||
@@ -323,7 +327,27 @@ func (h *DraftsHandler) ReorderItems(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := h.service.ReorderItem(draftID, req.ItemID, req.NewOrder); err != nil {
|
// Преобразуем элементы в формат для сервиса
|
||||||
|
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()})
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
95
rmser-view/package-lock.json
generated
95
rmser-view/package-lock.json
generated
@@ -8,11 +8,13 @@
|
|||||||
"name": "rmser-view",
|
"name": "rmser-view",
|
||||||
"version": "0.0.0",
|
"version": "0.0.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@hello-pangea/dnd": "^18.0.1",
|
||||||
"@tanstack/react-query": "^5.90.12",
|
"@tanstack/react-query": "^5.90.12",
|
||||||
"@twa-dev/sdk": "^8.0.2",
|
"@twa-dev/sdk": "^8.0.2",
|
||||||
"antd": "^6.1.0",
|
"antd": "^6.1.0",
|
||||||
"axios": "^1.13.2",
|
"axios": "^1.13.2",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
|
"lucide-react": "^0.563.0",
|
||||||
"react": "^19.2.0",
|
"react": "^19.2.0",
|
||||||
"react-dom": "^19.2.0",
|
"react-dom": "^19.2.0",
|
||||||
"react-router-dom": "^7.10.1",
|
"react-router-dom": "^7.10.1",
|
||||||
@@ -1029,6 +1031,23 @@
|
|||||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@hello-pangea/dnd": {
|
||||||
|
"version": "18.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@hello-pangea/dnd/-/dnd-18.0.1.tgz",
|
||||||
|
"integrity": "sha512-xojVWG8s/TGrKT1fC8K2tIWeejJYTAeJuj36zM//yEm/ZrnZUSFGS15BpO+jGZT1ybWvyXmeDJwPYb4dhWlbZQ==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"@babel/runtime": "^7.26.7",
|
||||||
|
"css-box-model": "^1.2.1",
|
||||||
|
"raf-schd": "^4.0.3",
|
||||||
|
"react-redux": "^9.2.0",
|
||||||
|
"redux": "^5.0.1"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": "^18.0.0 || ^19.0.0",
|
||||||
|
"react-dom": "^18.0.0 || ^19.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@humanfs/core": {
|
"node_modules/@humanfs/core": {
|
||||||
"version": "0.19.1",
|
"version": "0.19.1",
|
||||||
"resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz",
|
"resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz",
|
||||||
@@ -2294,6 +2313,12 @@
|
|||||||
"@types/react": "^19.2.0"
|
"@types/react": "^19.2.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/use-sync-external-store": {
|
||||||
|
"version": "0.0.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.6.tgz",
|
||||||
|
"integrity": "sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/@typescript-eslint/eslint-plugin": {
|
"node_modules/@typescript-eslint/eslint-plugin": {
|
||||||
"version": "8.49.0",
|
"version": "8.49.0",
|
||||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.49.0.tgz",
|
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.49.0.tgz",
|
||||||
@@ -2943,6 +2968,15 @@
|
|||||||
"node": ">= 8"
|
"node": ">= 8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/css-box-model": {
|
||||||
|
"version": "1.2.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/css-box-model/-/css-box-model-1.2.1.tgz",
|
||||||
|
"integrity": "sha512-a7Vr4Q/kd/aw96bnJG332W9V9LkJO69JRcaCYDUqjp6/z0w6VcZjgAcTbgFxEPfBgdnAwlh3iwu+hLopa+flJw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"tiny-invariant": "^1.0.6"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/csstype": {
|
"node_modules/csstype": {
|
||||||
"version": "3.2.3",
|
"version": "3.2.3",
|
||||||
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
|
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
|
||||||
@@ -3813,6 +3847,15 @@
|
|||||||
"yallist": "^3.0.2"
|
"yallist": "^3.0.2"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/lucide-react": {
|
||||||
|
"version": "0.563.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.563.0.tgz",
|
||||||
|
"integrity": "sha512-8dXPB2GI4dI8jV4MgUDGBeLdGk8ekfqVZ0BdLcrRzocGgG75ltNEmWS+gE7uokKF/0oSUuczNDT+g9hFJ23FkA==",
|
||||||
|
"license": "ISC",
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/math-intrinsics": {
|
"node_modules/math-intrinsics": {
|
||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
|
||||||
@@ -4055,12 +4098,17 @@
|
|||||||
"node": ">=6"
|
"node": ">=6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/raf-schd": {
|
||||||
|
"version": "4.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/raf-schd/-/raf-schd-4.0.3.tgz",
|
||||||
|
"integrity": "sha512-tQkJl2GRWh83ui2DiPTJz9wEiMN20syf+5oKfB03yYP7ioZcJwsIK8FjrtLwH1m7C7e+Tt2yYBlrOpdT+dyeIQ==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/react": {
|
"node_modules/react": {
|
||||||
"version": "19.2.1",
|
"version": "19.2.1",
|
||||||
"resolved": "https://registry.npmjs.org/react/-/react-19.2.1.tgz",
|
"resolved": "https://registry.npmjs.org/react/-/react-19.2.1.tgz",
|
||||||
"integrity": "sha512-DGrYcCWK7tvYMnWh79yrPHt+vdx9tY+1gPZa7nJQtO/p8bLTDaHp4dzwEhQB7pZ4Xe3ok4XKuEPrVuc+wlpkmw==",
|
"integrity": "sha512-DGrYcCWK7tvYMnWh79yrPHt+vdx9tY+1gPZa7nJQtO/p8bLTDaHp4dzwEhQB7pZ4Xe3ok4XKuEPrVuc+wlpkmw==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
@@ -4084,6 +4132,29 @@
|
|||||||
"integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==",
|
"integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/react-redux": {
|
||||||
|
"version": "9.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.2.0.tgz",
|
||||||
|
"integrity": "sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/use-sync-external-store": "^0.0.6",
|
||||||
|
"use-sync-external-store": "^1.4.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "^18.2.25 || ^19",
|
||||||
|
"react": "^18.0 || ^19",
|
||||||
|
"redux": "^5.0.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"redux": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/react-refresh": {
|
"node_modules/react-refresh": {
|
||||||
"version": "0.18.0",
|
"version": "0.18.0",
|
||||||
"resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.18.0.tgz",
|
"resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.18.0.tgz",
|
||||||
@@ -4132,6 +4203,13 @@
|
|||||||
"react-dom": ">=18"
|
"react-dom": ">=18"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/redux": {
|
||||||
|
"version": "5.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz",
|
||||||
|
"integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==",
|
||||||
|
"license": "MIT",
|
||||||
|
"peer": true
|
||||||
|
},
|
||||||
"node_modules/resolve-from": {
|
"node_modules/resolve-from": {
|
||||||
"version": "4.0.0",
|
"version": "4.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz",
|
||||||
@@ -4295,6 +4373,12 @@
|
|||||||
"node": ">=12.22"
|
"node": ">=12.22"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/tiny-invariant": {
|
||||||
|
"version": "1.3.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz",
|
||||||
|
"integrity": "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/tinyglobby": {
|
"node_modules/tinyglobby": {
|
||||||
"version": "0.2.15",
|
"version": "0.2.15",
|
||||||
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz",
|
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz",
|
||||||
@@ -4425,6 +4509,15 @@
|
|||||||
"punycode": "^2.1.0"
|
"punycode": "^2.1.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/use-sync-external-store": {
|
||||||
|
"version": "1.6.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz",
|
||||||
|
"integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==",
|
||||||
|
"license": "MIT",
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/vite": {
|
"node_modules/vite": {
|
||||||
"version": "7.2.7",
|
"version": "7.2.7",
|
||||||
"resolved": "https://registry.npmjs.org/vite/-/vite-7.2.7.tgz",
|
"resolved": "https://registry.npmjs.org/vite/-/vite-7.2.7.tgz",
|
||||||
|
|||||||
@@ -10,11 +10,13 @@
|
|||||||
"preview": "vite preview"
|
"preview": "vite preview"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@hello-pangea/dnd": "^18.0.1",
|
||||||
"@tanstack/react-query": "^5.90.12",
|
"@tanstack/react-query": "^5.90.12",
|
||||||
"@twa-dev/sdk": "^8.0.2",
|
"@twa-dev/sdk": "^8.0.2",
|
||||||
"antd": "^6.1.0",
|
"antd": "^6.1.0",
|
||||||
"axios": "^1.13.2",
|
"axios": "^1.13.2",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
|
"lucide-react": "^0.563.0",
|
||||||
"react": "^19.2.0",
|
"react": "^19.2.0",
|
||||||
"react-dom": "^19.2.0",
|
"react-dom": "^19.2.0",
|
||||||
"react-router-dom": "^7.10.1",
|
"react-router-dom": "^7.10.1",
|
||||||
|
|||||||
9693
rmser-view/project_context.md
Normal file
9693
rmser-view/project_context.md
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,4 +1,5 @@
|
|||||||
import React, { useMemo, useState, useEffect, useRef } from "react";
|
import React, { useMemo, useState, useEffect, useRef } from "react";
|
||||||
|
import { Draggable } from "@hello-pangea/dnd";
|
||||||
import {
|
import {
|
||||||
Card,
|
Card,
|
||||||
Flex,
|
Flex,
|
||||||
@@ -17,6 +18,7 @@ import {
|
|||||||
WarningFilled,
|
WarningFilled,
|
||||||
DeleteOutlined,
|
DeleteOutlined,
|
||||||
} from "@ant-design/icons";
|
} from "@ant-design/icons";
|
||||||
|
import { GripVertical } from "lucide-react";
|
||||||
import { CatalogSelect } from "../ocr/CatalogSelect";
|
import { CatalogSelect } from "../ocr/CatalogSelect";
|
||||||
import { CreateContainerModal } from "./CreateContainerModal";
|
import { CreateContainerModal } from "./CreateContainerModal";
|
||||||
import type {
|
import type {
|
||||||
@@ -31,6 +33,7 @@ const { Text } = Typography;
|
|||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
item: DraftItem;
|
item: DraftItem;
|
||||||
|
index: number;
|
||||||
onUpdate: (itemId: string, changes: UpdateDraftItemRequest) => void;
|
onUpdate: (itemId: string, changes: UpdateDraftItemRequest) => void;
|
||||||
onDelete: (itemId: string) => void;
|
onDelete: (itemId: string) => void;
|
||||||
isUpdating: boolean;
|
isUpdating: boolean;
|
||||||
@@ -41,6 +44,7 @@ type FieldType = "quantity" | "price" | "sum";
|
|||||||
|
|
||||||
export const DraftItemRow: React.FC<Props> = ({
|
export const DraftItemRow: React.FC<Props> = ({
|
||||||
item,
|
item,
|
||||||
|
index,
|
||||||
onUpdate,
|
onUpdate,
|
||||||
onDelete,
|
onDelete,
|
||||||
isUpdating,
|
isUpdating,
|
||||||
@@ -287,161 +291,227 @@ export const DraftItemRow: React.FC<Props> = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Card
|
<Draggable draggableId={item.id} index={index}>
|
||||||
size="small"
|
{(provided, snapshot) => {
|
||||||
style={{
|
const style = {
|
||||||
marginBottom: 8,
|
marginBottom: "8px",
|
||||||
borderLeft: `4px solid ${cardBorderColor}`,
|
backgroundColor: snapshot.isDragging ? "#e6f7ff" : "transparent",
|
||||||
background: item.product_id ? "#fff" : "#fff1f0",
|
boxShadow: snapshot.isDragging
|
||||||
}}
|
? "0 4px 12px rgba(0, 0, 0, 0.15)"
|
||||||
bodyStyle={{ padding: 12 }}
|
: "none",
|
||||||
>
|
borderRadius: "4px",
|
||||||
<Flex vertical gap={10}>
|
transition: "background-color 0.2s ease, box-shadow 0.2s ease",
|
||||||
<Flex justify="space-between" align="start">
|
...provided.draggableProps.style,
|
||||||
<div style={{ flex: 1 }}>
|
};
|
||||||
<Text
|
|
||||||
type="secondary"
|
return (
|
||||||
style={{ fontSize: 12, lineHeight: 1.2, display: "block" }}
|
<div
|
||||||
|
ref={provided.innerRef}
|
||||||
|
{...provided.draggableProps}
|
||||||
|
style={style}
|
||||||
|
>
|
||||||
|
<Card
|
||||||
|
size="small"
|
||||||
|
style={{
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
padding: "12px 16px",
|
||||||
|
borderLeft: `4px solid ${cardBorderColor}`,
|
||||||
|
border: snapshot.isDragging
|
||||||
|
? "2px solid #1890ff"
|
||||||
|
: "1px solid #d9d9d9",
|
||||||
|
background: item.product_id ? "#fff" : "#fff1f0",
|
||||||
|
borderRadius: "4px",
|
||||||
|
}}
|
||||||
|
bodyStyle={{ padding: 0 }}
|
||||||
>
|
>
|
||||||
{item.raw_name || "Новая позиция"}
|
{/* Drag handle - иконка для перетаскивания */}
|
||||||
</Text>
|
<div
|
||||||
{item.raw_amount > 0 && (
|
{...provided.dragHandleProps}
|
||||||
<Text
|
style={{
|
||||||
type="secondary"
|
cursor: "grab",
|
||||||
style={{ fontSize: 10, display: "block" }}
|
padding: "4px 8px 4px 0",
|
||||||
|
color: "#8c8c8c",
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
transition: "color 0.2s ease",
|
||||||
|
}}
|
||||||
|
onMouseEnter={(e) => {
|
||||||
|
e.currentTarget.style.color = "#1890ff";
|
||||||
|
}}
|
||||||
|
onMouseLeave={(e) => {
|
||||||
|
e.currentTarget.style.color = "#8c8c8c";
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
(чек: {item.raw_amount} x {item.raw_price})
|
<GripVertical size={20} />
|
||||||
</Text>
|
</div>
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
marginLeft: 8,
|
|
||||||
display: "flex",
|
|
||||||
alignItems: "center",
|
|
||||||
gap: 6,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{isUpdating && <SyncOutlined spin style={{ color: "#1890ff" }} />}
|
|
||||||
|
|
||||||
{activeWarning && (
|
<Flex vertical gap={10} style={{ flex: 1 }}>
|
||||||
<WarningFilled
|
<Flex justify="space-between" align="start">
|
||||||
style={{ color: "#faad14", fontSize: 16, cursor: "pointer" }}
|
<div style={{ flex: 1 }}>
|
||||||
onClick={showWarningModal}
|
<Text
|
||||||
/>
|
type="secondary"
|
||||||
)}
|
style={{
|
||||||
|
fontSize: 12,
|
||||||
|
lineHeight: 1.2,
|
||||||
|
display: "block",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{item.raw_name || "Новая позиция"}
|
||||||
|
</Text>
|
||||||
|
{item.raw_amount > 0 && (
|
||||||
|
<Text
|
||||||
|
type="secondary"
|
||||||
|
style={{ fontSize: 10, display: "block" }}
|
||||||
|
>
|
||||||
|
(чек: {item.raw_amount} x {item.raw_price})
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
marginLeft: 8,
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
gap: 6,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{isUpdating && (
|
||||||
|
<SyncOutlined spin style={{ color: "#1890ff" }} />
|
||||||
|
)}
|
||||||
|
|
||||||
{!item.product_id && (
|
{activeWarning && (
|
||||||
<Tag color="error" style={{ margin: 0 }}>
|
<WarningFilled
|
||||||
?
|
style={{
|
||||||
</Tag>
|
color: "#faad14",
|
||||||
)}
|
fontSize: 16,
|
||||||
|
cursor: "pointer",
|
||||||
|
}}
|
||||||
|
onClick={showWarningModal}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
<Popconfirm
|
{!item.product_id && (
|
||||||
title="Удалить строку?"
|
<Tag color="error" style={{ margin: 0 }}>
|
||||||
onConfirm={() => onDelete(item.id)}
|
?
|
||||||
okText="Да"
|
</Tag>
|
||||||
cancelText="Нет"
|
)}
|
||||||
placement="left"
|
|
||||||
>
|
|
||||||
<Button
|
|
||||||
type="text"
|
|
||||||
size="small"
|
|
||||||
icon={<DeleteOutlined />}
|
|
||||||
danger
|
|
||||||
style={{ marginLeft: 4 }}
|
|
||||||
/>
|
|
||||||
</Popconfirm>
|
|
||||||
</div>
|
|
||||||
</Flex>
|
|
||||||
|
|
||||||
<CatalogSelect
|
<Popconfirm
|
||||||
value={item.product_id || undefined}
|
title="Удалить строку?"
|
||||||
onChange={handleProductChange}
|
onConfirm={() => onDelete(item.id)}
|
||||||
initialProduct={activeProduct}
|
okText="Да"
|
||||||
/>
|
cancelText="Нет"
|
||||||
|
placement="left"
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
type="text"
|
||||||
|
size="small"
|
||||||
|
icon={<DeleteOutlined />}
|
||||||
|
danger
|
||||||
|
style={{ marginLeft: 4 }}
|
||||||
|
/>
|
||||||
|
</Popconfirm>
|
||||||
|
</div>
|
||||||
|
</Flex>
|
||||||
|
|
||||||
{activeProduct && (
|
<CatalogSelect
|
||||||
<Select
|
value={item.product_id || undefined}
|
||||||
style={{ width: "100%" }}
|
onChange={handleProductChange}
|
||||||
placeholder="Выберите единицу измерения"
|
initialProduct={activeProduct}
|
||||||
options={containerOptions}
|
/>
|
||||||
value={item.container_id || "BASE_UNIT"}
|
|
||||||
onChange={handleContainerChange}
|
{activeProduct && (
|
||||||
dropdownRender={(menu) => (
|
<Select
|
||||||
<>
|
style={{ width: "100%" }}
|
||||||
{menu}
|
placeholder="Выберите единицу измерения"
|
||||||
<Divider style={{ margin: "4px 0" }} />
|
options={containerOptions}
|
||||||
<Button
|
value={item.container_id || "BASE_UNIT"}
|
||||||
type="text"
|
onChange={handleContainerChange}
|
||||||
block
|
dropdownRender={(menu) => (
|
||||||
icon={<PlusOutlined />}
|
<>
|
||||||
onClick={() => setIsModalOpen(true)}
|
{menu}
|
||||||
style={{ textAlign: "left" }}
|
<Divider style={{ margin: "4px 0" }} />
|
||||||
|
<Button
|
||||||
|
type="text"
|
||||||
|
block
|
||||||
|
icon={<PlusOutlined />}
|
||||||
|
onClick={() => setIsModalOpen(true)}
|
||||||
|
style={{ textAlign: "left" }}
|
||||||
|
>
|
||||||
|
Добавить фасовку...
|
||||||
|
</Button>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "space-between",
|
||||||
|
background: "#fafafa",
|
||||||
|
margin: "0 -12px -12px -12px",
|
||||||
|
padding: "8px 12px",
|
||||||
|
borderTop: "1px solid #f0f0f0",
|
||||||
|
borderBottomLeftRadius: 8,
|
||||||
|
borderBottomRightRadius: 8,
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
Добавить фасовку...
|
<div
|
||||||
</Button>
|
style={{
|
||||||
</>
|
display: "flex",
|
||||||
)}
|
gap: 8,
|
||||||
/>
|
alignItems: "center",
|
||||||
)}
|
flex: 1,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<InputNumber
|
||||||
|
style={{ width: 70 }}
|
||||||
|
controls={false}
|
||||||
|
placeholder="Кол"
|
||||||
|
min={0}
|
||||||
|
value={localQty}
|
||||||
|
onChange={(val) => handleValueChange("quantity", val)}
|
||||||
|
onBlur={() => handleBlur("quantity")}
|
||||||
|
precision={3}
|
||||||
|
/>
|
||||||
|
<Text type="secondary">x</Text>
|
||||||
|
<InputNumber
|
||||||
|
style={{ width: 80 }}
|
||||||
|
controls={false}
|
||||||
|
placeholder="Цена"
|
||||||
|
min={0}
|
||||||
|
value={localPrice}
|
||||||
|
onChange={(val) => handleValueChange("price", val)}
|
||||||
|
onBlur={() => handleBlur("price")}
|
||||||
|
precision={2}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{ display: "flex", alignItems: "center", gap: 4 }}
|
||||||
display: "flex",
|
>
|
||||||
alignItems: "center",
|
<Text type="secondary">=</Text>
|
||||||
justifyContent: "space-between",
|
<InputNumber
|
||||||
background: "#fafafa",
|
style={{ width: 90, fontWeight: "bold" }}
|
||||||
margin: "0 -12px -12px -12px",
|
controls={false}
|
||||||
padding: "8px 12px",
|
placeholder="Сумма"
|
||||||
borderTop: "1px solid #f0f0f0",
|
min={0}
|
||||||
borderBottomLeftRadius: 8,
|
value={localSum}
|
||||||
borderBottomRightRadius: 8,
|
onChange={(val) => handleValueChange("sum", val)}
|
||||||
}}
|
onBlur={() => handleBlur("sum")}
|
||||||
>
|
precision={2}
|
||||||
<div
|
/>
|
||||||
style={{ display: "flex", gap: 8, alignItems: "center", flex: 1 }}
|
</div>
|
||||||
>
|
</div>
|
||||||
<InputNumber
|
</Flex>
|
||||||
style={{ width: 70 }}
|
</Card>
|
||||||
controls={false}
|
|
||||||
placeholder="Кол"
|
|
||||||
min={0}
|
|
||||||
value={localQty}
|
|
||||||
onChange={(val) => handleValueChange("quantity", val)}
|
|
||||||
onBlur={() => handleBlur("quantity")}
|
|
||||||
precision={3}
|
|
||||||
/>
|
|
||||||
<Text type="secondary">x</Text>
|
|
||||||
<InputNumber
|
|
||||||
style={{ width: 80 }}
|
|
||||||
controls={false}
|
|
||||||
placeholder="Цена"
|
|
||||||
min={0}
|
|
||||||
value={localPrice}
|
|
||||||
onChange={(val) => handleValueChange("price", val)}
|
|
||||||
onBlur={() => handleBlur("price")}
|
|
||||||
precision={2}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
|
);
|
||||||
<div style={{ display: "flex", alignItems: "center", gap: 4 }}>
|
}}
|
||||||
<Text type="secondary">=</Text>
|
</Draggable>
|
||||||
<InputNumber
|
|
||||||
style={{ width: 90, fontWeight: "bold" }}
|
|
||||||
controls={false}
|
|
||||||
placeholder="Сумма"
|
|
||||||
min={0}
|
|
||||||
value={localSum}
|
|
||||||
onChange={(val) => handleValueChange("sum", val)}
|
|
||||||
onBlur={() => handleBlur("sum")}
|
|
||||||
precision={2}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Flex>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
{activeProduct && (
|
{activeProduct && (
|
||||||
<CreateContainerModal
|
<CreateContainerModal
|
||||||
visible={isModalOpen}
|
visible={isModalOpen}
|
||||||
|
|||||||
@@ -33,7 +33,9 @@ import { DraftItemRow } from "../components/invoices/DraftItemRow";
|
|||||||
import type {
|
import type {
|
||||||
UpdateDraftItemRequest,
|
UpdateDraftItemRequest,
|
||||||
CommitDraftRequest,
|
CommitDraftRequest,
|
||||||
|
ReorderDraftItemsRequest,
|
||||||
} from "../services/types";
|
} from "../services/types";
|
||||||
|
import { DragDropContext, Droppable, type DropResult } from "@hello-pangea/dnd";
|
||||||
|
|
||||||
const { Text } = Typography;
|
const { Text } = Typography;
|
||||||
const { TextArea } = Input;
|
const { TextArea } = Input;
|
||||||
@@ -46,6 +48,9 @@ export const InvoiceDraftPage: React.FC = () => {
|
|||||||
const [form] = Form.useForm();
|
const [form] = Form.useForm();
|
||||||
|
|
||||||
const [updatingItems, setUpdatingItems] = useState<Set<string>>(new Set());
|
const [updatingItems, setUpdatingItems] = useState<Set<string>>(new Set());
|
||||||
|
const [itemsOrder, setItemsOrder] = useState<Record<string, number>>({});
|
||||||
|
const [enabled, setEnabled] = useState(false);
|
||||||
|
const [isDragging, setIsDragging] = useState(false);
|
||||||
|
|
||||||
// Состояние для просмотра фото чека
|
// Состояние для просмотра фото чека
|
||||||
const [previewVisible, setPreviewVisible] = useState(false);
|
const [previewVisible, setPreviewVisible] = useState(false);
|
||||||
@@ -68,6 +73,7 @@ export const InvoiceDraftPage: React.FC = () => {
|
|||||||
queryFn: () => api.getDraft(id!),
|
queryFn: () => api.getDraft(id!),
|
||||||
enabled: !!id,
|
enabled: !!id,
|
||||||
refetchInterval: (query) => {
|
refetchInterval: (query) => {
|
||||||
|
if (isDragging) return false;
|
||||||
const status = query.state.data?.status;
|
const status = query.state.data?.status;
|
||||||
return status === "PROCESSING" ? 3000 : false;
|
return status === "PROCESSING" ? 3000 : false;
|
||||||
},
|
},
|
||||||
@@ -150,7 +156,25 @@ export const InvoiceDraftPage: React.FC = () => {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const reorderItemsMutation = useMutation({
|
||||||
|
mutationFn: ({
|
||||||
|
draftId,
|
||||||
|
payload,
|
||||||
|
}: {
|
||||||
|
draftId: string;
|
||||||
|
payload: ReorderDraftItemsRequest;
|
||||||
|
}) => api.reorderDraftItems(draftId, payload),
|
||||||
|
onError: (error) => {
|
||||||
|
message.error("Не удалось изменить порядок элементов");
|
||||||
|
console.error("Reorder error:", error);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
// --- ЭФФЕКТЫ ---
|
// --- ЭФФЕКТЫ ---
|
||||||
|
useEffect(() => {
|
||||||
|
setEnabled(true);
|
||||||
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (draft) {
|
if (draft) {
|
||||||
const currentValues = form.getFieldsValue();
|
const currentValues = form.getFieldsValue();
|
||||||
@@ -241,6 +265,64 @@ export const InvoiceDraftPage: React.FC = () => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleDragStart = () => {
|
||||||
|
setIsDragging(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDragEnd = async (result: DropResult) => {
|
||||||
|
setIsDragging(false);
|
||||||
|
const { source, destination } = result;
|
||||||
|
|
||||||
|
// Если нет назначения или позиция не изменилась
|
||||||
|
if (
|
||||||
|
!destination ||
|
||||||
|
(source.droppableId === destination.droppableId &&
|
||||||
|
source.index === destination.index)
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!draft) return;
|
||||||
|
|
||||||
|
// Сохраняем предыдущее состояние для отката
|
||||||
|
const previousItems = [...draft.items];
|
||||||
|
const previousOrder = { ...itemsOrder };
|
||||||
|
|
||||||
|
// Создаём новый массив с изменённым порядком
|
||||||
|
const newItems = [...draft.items];
|
||||||
|
const [removed] = newItems.splice(source.index, 1);
|
||||||
|
newItems.splice(destination.index, 0, removed);
|
||||||
|
|
||||||
|
// Обновляем локальное состояние немедленно для быстрого UI
|
||||||
|
queryClient.setQueryData(["draft", id], {
|
||||||
|
...draft,
|
||||||
|
items: newItems,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Подготавливаем payload для API
|
||||||
|
const reorderPayload: ReorderDraftItemsRequest = {
|
||||||
|
items: newItems.map((item, index) => ({
|
||||||
|
id: item.id,
|
||||||
|
order: index,
|
||||||
|
})),
|
||||||
|
};
|
||||||
|
|
||||||
|
// Отправляем запрос на сервер
|
||||||
|
try {
|
||||||
|
await reorderItemsMutation.mutateAsync({
|
||||||
|
draftId: draft.id,
|
||||||
|
payload: reorderPayload,
|
||||||
|
});
|
||||||
|
} catch {
|
||||||
|
// При ошибке откатываем локальное состояние
|
||||||
|
queryClient.setQueryData(["draft", id], {
|
||||||
|
...draft,
|
||||||
|
items: previousItems,
|
||||||
|
});
|
||||||
|
setItemsOrder(previousOrder);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// --- RENDER ---
|
// --- RENDER ---
|
||||||
const showSpinner =
|
const showSpinner =
|
||||||
draftQuery.isLoading ||
|
draftQuery.isLoading ||
|
||||||
@@ -444,19 +526,65 @@ export const InvoiceDraftPage: React.FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Items List */}
|
{/* Items List */}
|
||||||
<div style={{ display: "flex", flexDirection: "column", gap: 8 }}>
|
{enabled ? (
|
||||||
{draft.items.map((item) => (
|
<DragDropContext
|
||||||
<DraftItemRow
|
onDragStart={handleDragStart}
|
||||||
key={item.id}
|
onDragEnd={handleDragEnd}
|
||||||
item={item}
|
>
|
||||||
onUpdate={handleItemUpdate}
|
<Droppable droppableId="draft-items">
|
||||||
// Передаем обработчик удаления
|
{(provided, snapshot) => (
|
||||||
onDelete={(itemId) => deleteItemMutation.mutate(itemId)}
|
<div
|
||||||
isUpdating={updatingItems.has(item.id)}
|
{...provided.droppableProps}
|
||||||
recommendations={recommendationsQuery.data || []}
|
ref={provided.innerRef}
|
||||||
/>
|
style={{
|
||||||
))}
|
display: "flex",
|
||||||
</div>
|
flexDirection: "column",
|
||||||
|
gap: 8,
|
||||||
|
backgroundColor: snapshot.isDraggingOver
|
||||||
|
? "#f0f0f0"
|
||||||
|
: "transparent",
|
||||||
|
borderRadius: "4px",
|
||||||
|
padding: snapshot.isDraggingOver ? "8px" : "0",
|
||||||
|
transition: "background-color 0.2s ease",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{draft.items.map((item, index) => (
|
||||||
|
<DraftItemRow
|
||||||
|
key={item.id}
|
||||||
|
item={item}
|
||||||
|
index={index}
|
||||||
|
onUpdate={handleItemUpdate}
|
||||||
|
onDelete={(itemId) => deleteItemMutation.mutate(itemId)}
|
||||||
|
isUpdating={updatingItems.has(item.id)}
|
||||||
|
recommendations={recommendationsQuery.data || []}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
{provided.placeholder}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</Droppable>
|
||||||
|
</DragDropContext>
|
||||||
|
) : (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
gap: 8,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{draft.items.map((item, index) => (
|
||||||
|
<DraftItemRow
|
||||||
|
key={item.id}
|
||||||
|
item={item}
|
||||||
|
index={index}
|
||||||
|
onUpdate={handleItemUpdate}
|
||||||
|
onDelete={(itemId) => deleteItemMutation.mutate(itemId)}
|
||||||
|
isUpdating={updatingItems.has(item.id)}
|
||||||
|
recommendations={recommendationsQuery.data || []}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Кнопка добавления позиции */}
|
{/* Кнопка добавления позиции */}
|
||||||
<Button
|
<Button
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ import type {
|
|||||||
DraftItem,
|
DraftItem,
|
||||||
UpdateDraftItemRequest,
|
UpdateDraftItemRequest,
|
||||||
CommitDraftRequest,
|
CommitDraftRequest,
|
||||||
|
ReorderDraftItemsRequest,
|
||||||
ProductSearchResult,
|
ProductSearchResult,
|
||||||
AddContainerRequest,
|
AddContainerRequest,
|
||||||
AddContainerResponse,
|
AddContainerResponse,
|
||||||
@@ -208,6 +209,10 @@ export const api = {
|
|||||||
await apiClient.delete(`/drafts/${draftId}/items/${itemId}`);
|
await apiClient.delete(`/drafts/${draftId}/items/${itemId}`);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
reorderDraftItems: async (draftId: string, payload: ReorderDraftItemsRequest): Promise<void> => {
|
||||||
|
await apiClient.post(`/drafts/${draftId}/reorder`, payload);
|
||||||
|
},
|
||||||
|
|
||||||
commitDraft: async (draftId: string, payload: CommitDraftRequest): Promise<{ document_number: string }> => {
|
commitDraft: async (draftId: string, payload: CommitDraftRequest): Promise<{ document_number: string }> => {
|
||||||
const { data } = await apiClient.post<{ document_number: string }>(`/drafts/${draftId}/commit`, payload);
|
const { data } = await apiClient.post<{ document_number: string }>(`/drafts/${draftId}/commit`, payload);
|
||||||
return data;
|
return data;
|
||||||
|
|||||||
@@ -237,6 +237,14 @@ export interface CommitDraftRequest {
|
|||||||
comment: string;
|
comment: string;
|
||||||
incoming_document_number?: string;
|
incoming_document_number?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ReorderDraftItemsRequest {
|
||||||
|
items: Array<{
|
||||||
|
id: UUID;
|
||||||
|
order: number;
|
||||||
|
}>;
|
||||||
|
}
|
||||||
|
|
||||||
export interface MainUnit {
|
export interface MainUnit {
|
||||||
id: UUID;
|
id: UUID;
|
||||||
name: string; // "кг"
|
name: string; // "кг"
|
||||||
|
|||||||
Reference in New Issue
Block a user