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.DELETE("/drafts/:id/items/:itemId", draftsHandler.DeleteDraftItem)
|
||||
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/container", draftsHandler.AddContainer)
|
||||
|
||||
|
||||
@@ -4,8 +4,8 @@ app:
|
||||
drop_tables: false
|
||||
storage_path: "./uploads"
|
||||
public_url: "https://rmser.serty.top"
|
||||
maintenance_mode: true
|
||||
dev_ids: [665599275,2126923472] # Укажите здесь ваш ID и ID тестировщиков
|
||||
maintenance_mode: false
|
||||
dev_ids: [] # Укажите здесь ваш ID и ID тестировщиков
|
||||
|
||||
db:
|
||||
dsn: "host=postgres user=rmser password=mhrcadmin994525 dbname=rmser_db port=5432 sslmode=disable TimeZone=Europe/Moscow"
|
||||
@@ -15,11 +15,6 @@ redis:
|
||||
password: ""
|
||||
db: 0
|
||||
|
||||
rms:
|
||||
base_url: "https://rest-mesto-vstrechi.iiko.it" # Например http://95.12.34.56:8080
|
||||
login: "MH"
|
||||
password: "MhLevfqkexit632597" # Пароль в открытом виде (приложение само хеширует)
|
||||
|
||||
ocr:
|
||||
service_url: "http://ocr-service:5005"
|
||||
|
||||
|
||||
@@ -91,6 +91,10 @@ type Repository interface {
|
||||
Update(draft *DraftInvoice) error
|
||||
UpdateItem(itemID uuid.UUID, updates map[string]interface{}) error
|
||||
UpdateItemOrder(itemID uuid.UUID, newOrder int) error
|
||||
ReorderItems(draftID uuid.UUID, items []struct {
|
||||
ID uuid.UUID
|
||||
Order int
|
||||
}) error
|
||||
CreateItem(item *DraftInvoiceItem) error
|
||||
DeleteItem(itemID uuid.UUID) error
|
||||
Delete(id uuid.UUID) error
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
package drafts
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"rmser/internal/domain/drafts"
|
||||
|
||||
"github.com/google/uuid"
|
||||
@@ -105,6 +107,35 @@ func (r *pgRepository) UpdateItemOrder(itemID uuid.UUID, newOrder int) 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 {
|
||||
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)
|
||||
}
|
||||
|
||||
// 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
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
@@ -73,6 +74,14 @@ type UpdateItemDTO struct {
|
||||
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 {
|
||||
@@ -303,14 +312,9 @@ func (h *DraftsHandler) DeleteDraft(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, gin.H{"status": newStatus, "id": id.String()})
|
||||
}
|
||||
|
||||
// ReorderItems изменяет порядок позиции в черновике
|
||||
// ReorderItems изменяет порядок нескольких позиций в черновике
|
||||
func (h *DraftsHandler) ReorderItems(c *gin.Context) {
|
||||
type ReorderRequest struct {
|
||||
ItemID uuid.UUID `json:"item_id" binding:"required"`
|
||||
NewOrder int `json:"new_order" binding:"required"`
|
||||
}
|
||||
|
||||
var req ReorderRequest
|
||||
var req ReorderItemsRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
@@ -323,7 +327,27 @@ func (h *DraftsHandler) ReorderItems(c *gin.Context) {
|
||||
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()})
|
||||
return
|
||||
}
|
||||
|
||||
95
rmser-view/package-lock.json
generated
95
rmser-view/package-lock.json
generated
@@ -8,11 +8,13 @@
|
||||
"name": "rmser-view",
|
||||
"version": "0.0.0",
|
||||
"dependencies": {
|
||||
"@hello-pangea/dnd": "^18.0.1",
|
||||
"@tanstack/react-query": "^5.90.12",
|
||||
"@twa-dev/sdk": "^8.0.2",
|
||||
"antd": "^6.1.0",
|
||||
"axios": "^1.13.2",
|
||||
"clsx": "^2.1.1",
|
||||
"lucide-react": "^0.563.0",
|
||||
"react": "^19.2.0",
|
||||
"react-dom": "^19.2.0",
|
||||
"react-router-dom": "^7.10.1",
|
||||
@@ -1029,6 +1031,23 @@
|
||||
"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": {
|
||||
"version": "0.19.1",
|
||||
"resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz",
|
||||
@@ -2294,6 +2313,12 @@
|
||||
"@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": {
|
||||
"version": "8.49.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.49.0.tgz",
|
||||
@@ -2943,6 +2968,15 @@
|
||||
"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": {
|
||||
"version": "3.2.3",
|
||||
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
|
||||
@@ -3813,6 +3847,15 @@
|
||||
"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": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
|
||||
@@ -4055,12 +4098,17 @@
|
||||
"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": {
|
||||
"version": "19.2.1",
|
||||
"resolved": "https://registry.npmjs.org/react/-/react-19.2.1.tgz",
|
||||
"integrity": "sha512-DGrYcCWK7tvYMnWh79yrPHt+vdx9tY+1gPZa7nJQtO/p8bLTDaHp4dzwEhQB7pZ4Xe3ok4XKuEPrVuc+wlpkmw==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
@@ -4084,6 +4132,29 @@
|
||||
"integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==",
|
||||
"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": {
|
||||
"version": "0.18.0",
|
||||
"resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.18.0.tgz",
|
||||
@@ -4132,6 +4203,13 @@
|
||||
"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": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz",
|
||||
@@ -4295,6 +4373,12 @@
|
||||
"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": {
|
||||
"version": "0.2.15",
|
||||
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz",
|
||||
@@ -4425,6 +4509,15 @@
|
||||
"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": {
|
||||
"version": "7.2.7",
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-7.2.7.tgz",
|
||||
|
||||
@@ -10,11 +10,13 @@
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"@hello-pangea/dnd": "^18.0.1",
|
||||
"@tanstack/react-query": "^5.90.12",
|
||||
"@twa-dev/sdk": "^8.0.2",
|
||||
"antd": "^6.1.0",
|
||||
"axios": "^1.13.2",
|
||||
"clsx": "^2.1.1",
|
||||
"lucide-react": "^0.563.0",
|
||||
"react": "^19.2.0",
|
||||
"react-dom": "^19.2.0",
|
||||
"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 { Draggable } from "@hello-pangea/dnd";
|
||||
import {
|
||||
Card,
|
||||
Flex,
|
||||
@@ -17,6 +18,7 @@ import {
|
||||
WarningFilled,
|
||||
DeleteOutlined,
|
||||
} from "@ant-design/icons";
|
||||
import { GripVertical } from "lucide-react";
|
||||
import { CatalogSelect } from "../ocr/CatalogSelect";
|
||||
import { CreateContainerModal } from "./CreateContainerModal";
|
||||
import type {
|
||||
@@ -31,6 +33,7 @@ const { Text } = Typography;
|
||||
|
||||
interface Props {
|
||||
item: DraftItem;
|
||||
index: number;
|
||||
onUpdate: (itemId: string, changes: UpdateDraftItemRequest) => void;
|
||||
onDelete: (itemId: string) => void;
|
||||
isUpdating: boolean;
|
||||
@@ -41,6 +44,7 @@ type FieldType = "quantity" | "price" | "sum";
|
||||
|
||||
export const DraftItemRow: React.FC<Props> = ({
|
||||
item,
|
||||
index,
|
||||
onUpdate,
|
||||
onDelete,
|
||||
isUpdating,
|
||||
@@ -287,21 +291,71 @@ export const DraftItemRow: React.FC<Props> = ({
|
||||
|
||||
return (
|
||||
<>
|
||||
<Draggable draggableId={item.id} index={index}>
|
||||
{(provided, snapshot) => {
|
||||
const style = {
|
||||
marginBottom: "8px",
|
||||
backgroundColor: snapshot.isDragging ? "#e6f7ff" : "transparent",
|
||||
boxShadow: snapshot.isDragging
|
||||
? "0 4px 12px rgba(0, 0, 0, 0.15)"
|
||||
: "none",
|
||||
borderRadius: "4px",
|
||||
transition: "background-color 0.2s ease, box-shadow 0.2s ease",
|
||||
...provided.draggableProps.style,
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={provided.innerRef}
|
||||
{...provided.draggableProps}
|
||||
style={style}
|
||||
>
|
||||
<Card
|
||||
size="small"
|
||||
style={{
|
||||
marginBottom: 8,
|
||||
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: 12 }}
|
||||
bodyStyle={{ padding: 0 }}
|
||||
>
|
||||
<Flex vertical gap={10}>
|
||||
{/* Drag handle - иконка для перетаскивания */}
|
||||
<div
|
||||
{...provided.dragHandleProps}
|
||||
style={{
|
||||
cursor: "grab",
|
||||
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";
|
||||
}}
|
||||
>
|
||||
<GripVertical size={20} />
|
||||
</div>
|
||||
|
||||
<Flex vertical gap={10} style={{ flex: 1 }}>
|
||||
<Flex justify="space-between" align="start">
|
||||
<div style={{ flex: 1 }}>
|
||||
<Text
|
||||
type="secondary"
|
||||
style={{ fontSize: 12, lineHeight: 1.2, display: "block" }}
|
||||
style={{
|
||||
fontSize: 12,
|
||||
lineHeight: 1.2,
|
||||
display: "block",
|
||||
}}
|
||||
>
|
||||
{item.raw_name || "Новая позиция"}
|
||||
</Text>
|
||||
@@ -322,11 +376,17 @@ export const DraftItemRow: React.FC<Props> = ({
|
||||
gap: 6,
|
||||
}}
|
||||
>
|
||||
{isUpdating && <SyncOutlined spin style={{ color: "#1890ff" }} />}
|
||||
{isUpdating && (
|
||||
<SyncOutlined spin style={{ color: "#1890ff" }} />
|
||||
)}
|
||||
|
||||
{activeWarning && (
|
||||
<WarningFilled
|
||||
style={{ color: "#faad14", fontSize: 16, cursor: "pointer" }}
|
||||
style={{
|
||||
color: "#faad14",
|
||||
fontSize: 16,
|
||||
cursor: "pointer",
|
||||
}}
|
||||
onClick={showWarningModal}
|
||||
/>
|
||||
)}
|
||||
@@ -400,7 +460,12 @@ export const DraftItemRow: React.FC<Props> = ({
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{ display: "flex", gap: 8, alignItems: "center", flex: 1 }}
|
||||
style={{
|
||||
display: "flex",
|
||||
gap: 8,
|
||||
alignItems: "center",
|
||||
flex: 1,
|
||||
}}
|
||||
>
|
||||
<InputNumber
|
||||
style={{ width: 70 }}
|
||||
@@ -425,7 +490,9 @@ export const DraftItemRow: React.FC<Props> = ({
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div style={{ display: "flex", alignItems: "center", gap: 4 }}>
|
||||
<div
|
||||
style={{ display: "flex", alignItems: "center", gap: 4 }}
|
||||
>
|
||||
<Text type="secondary">=</Text>
|
||||
<InputNumber
|
||||
style={{ width: 90, fontWeight: "bold" }}
|
||||
@@ -441,7 +508,10 @@ export const DraftItemRow: React.FC<Props> = ({
|
||||
</div>
|
||||
</Flex>
|
||||
</Card>
|
||||
|
||||
</div>
|
||||
);
|
||||
}}
|
||||
</Draggable>
|
||||
{activeProduct && (
|
||||
<CreateContainerModal
|
||||
visible={isModalOpen}
|
||||
|
||||
@@ -33,7 +33,9 @@ import { DraftItemRow } from "../components/invoices/DraftItemRow";
|
||||
import type {
|
||||
UpdateDraftItemRequest,
|
||||
CommitDraftRequest,
|
||||
ReorderDraftItemsRequest,
|
||||
} from "../services/types";
|
||||
import { DragDropContext, Droppable, type DropResult } from "@hello-pangea/dnd";
|
||||
|
||||
const { Text } = Typography;
|
||||
const { TextArea } = Input;
|
||||
@@ -46,6 +48,9 @@ export const InvoiceDraftPage: React.FC = () => {
|
||||
const [form] = Form.useForm();
|
||||
|
||||
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);
|
||||
@@ -68,6 +73,7 @@ export const InvoiceDraftPage: React.FC = () => {
|
||||
queryFn: () => api.getDraft(id!),
|
||||
enabled: !!id,
|
||||
refetchInterval: (query) => {
|
||||
if (isDragging) return false;
|
||||
const status = query.state.data?.status;
|
||||
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(() => {
|
||||
if (draft) {
|
||||
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 ---
|
||||
const showSpinner =
|
||||
draftQuery.isLoading ||
|
||||
@@ -444,19 +526,65 @@ export const InvoiceDraftPage: React.FC = () => {
|
||||
</div>
|
||||
|
||||
{/* Items List */}
|
||||
<div style={{ display: "flex", flexDirection: "column", gap: 8 }}>
|
||||
{draft.items.map((item) => (
|
||||
{enabled ? (
|
||||
<DragDropContext
|
||||
onDragStart={handleDragStart}
|
||||
onDragEnd={handleDragEnd}
|
||||
>
|
||||
<Droppable droppableId="draft-items">
|
||||
{(provided, snapshot) => (
|
||||
<div
|
||||
{...provided.droppableProps}
|
||||
ref={provided.innerRef}
|
||||
style={{
|
||||
display: "flex",
|
||||
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
|
||||
|
||||
@@ -18,6 +18,7 @@ import type {
|
||||
DraftItem,
|
||||
UpdateDraftItemRequest,
|
||||
CommitDraftRequest,
|
||||
ReorderDraftItemsRequest,
|
||||
ProductSearchResult,
|
||||
AddContainerRequest,
|
||||
AddContainerResponse,
|
||||
@@ -208,6 +209,10 @@ export const api = {
|
||||
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 }> => {
|
||||
const { data } = await apiClient.post<{ document_number: string }>(`/drafts/${draftId}/commit`, payload);
|
||||
return data;
|
||||
|
||||
@@ -237,6 +237,14 @@ export interface CommitDraftRequest {
|
||||
comment: string;
|
||||
incoming_document_number?: string;
|
||||
}
|
||||
|
||||
export interface ReorderDraftItemsRequest {
|
||||
items: Array<{
|
||||
id: UUID;
|
||||
order: number;
|
||||
}>;
|
||||
}
|
||||
|
||||
export interface MainUnit {
|
||||
id: UUID;
|
||||
name: string; // "кг"
|
||||
|
||||
Reference in New Issue
Block a user